├── README.md ├── stricttests.py └── strict.py /README.md: -------------------------------------------------------------------------------- 1 | # strict 2 | ## Implement strict checking at (sort of) compile time for Python3 3 | 4 | Normally, in Python, you can write code like the following 5 | 6 | ``` 7 | def print_apples(apples): 8 | if apples < 2: 9 | print("1 apple") 10 | else: 11 | print(aples,"apples") 12 | 13 | print_apples(1) 14 | ``` 15 | 16 | It will run without any errors. The typo, where "aples" will 17 | go unnoticed until someone calls print_apples(2). Wouldn't it 18 | be nice to be able to detect this type of mistake? The strict 19 | package does this for you: 20 | 21 | ``` 22 | from strict import * 23 | 24 | @strict 25 | def print_apples(apples): 26 | if apples < 2: 27 | print("1 apple") 28 | else: 29 | print(aples,"apples") 30 | 31 | print_apples(1) 32 | ``` 33 | 34 | Now the program above will die with an UndefException. At 35 | the time the decorator is applied, the Abstract Syntax 36 | Tree (AST) for the function is generated and examined by 37 | the decorator. If it finds symbols that are (at decorator 38 | time) undefined, it will raise an exception. 39 | 40 | Example 2: 41 | 42 | ``` 43 | verbose = True 44 | 45 | def do_calculation(a,b): 46 | c = a + b 47 | 48 | if a + b > 100 and verbose: 49 | print("a+b=",c) 50 | if a + b == 0: 51 | verbose = False 52 | 53 | do_calculation(1,1) 54 | ``` 55 | 56 | The problem here, of course, is that because of the assignment 57 | of verbose when a+b==0, the other reference to verbose will not 58 | be to the global. Our @strict decorator detects this, too. 59 | 60 | Fixing the above code, either by getting rid of the explicit 61 | setting of verbose, or by declaring verbose to be global, will 62 | satisfy strict. 63 | 64 | Example 3: 65 | 66 | ``` 67 | def option(a): 68 | if a == 0: 69 | b = 2 70 | else: 71 | c = 1 72 | b += 1 73 | return b 74 | 75 | option(0) 76 | ``` 77 | 78 | As long as option is called with zero, no error will be detected. 79 | Variable "b", however, will be undefined if we cal option(1). 80 | The strict package will detect this error as well. However, if 81 | we define variable b on all branches of the if statement, strict 82 | will be satisfied. 83 | ``` 84 | from strict import * 85 | 86 | @strict 87 | def option(a): 88 | if a == 0: 89 | b = 2 90 | else: 91 | b = 1 92 | b += 1 93 | 94 | option(0) 95 | ``` 96 | -------------------------------------------------------------------------------- /stricttests.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Steven R. Brandt 2 | # 3 | # Distributed under the Boost Software License, Version 1.0. (See accompanying 4 | # file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | # To run these tests, simply run "python3 stricttests.py". There should be no output. 7 | 8 | from strict import * 9 | 10 | import math 11 | 12 | @strict 13 | def foo1(a): 14 | a += 1 15 | 16 | class foo2: 17 | def __init__(self): 18 | self.a = 1 19 | self.b = 2 20 | 21 | f = foo2() 22 | 23 | @strict 24 | def foo3(): 25 | f.a = 1 26 | foo3() 27 | 28 | try: 29 | @strict 30 | def foo4(): 31 | c += 1 32 | assert False 33 | except UndefException: 34 | pass 35 | 36 | @strict 37 | def foo4(): 38 | c = 0 39 | c += 1 40 | 41 | try: 42 | @strict 43 | def foo6(): 44 | print(math.sine(3)) 45 | assert False 46 | except UndefException: 47 | pass 48 | 49 | @strict 50 | def foo7(): 51 | print(math.sin(3)) 52 | 53 | h = 1 54 | 55 | @strict 56 | def foo8(): 57 | print(h) 58 | 59 | try: 60 | @strict 61 | def foo9(): 62 | print(h) 63 | h = 1 64 | assert False 65 | except UndefException: 66 | pass 67 | 68 | @strict 69 | def foo10(a): 70 | if a > 1: 71 | b = 1 72 | else: 73 | b = 2 74 | b += 1 75 | 76 | try: 77 | @strict 78 | def foo11(a): 79 | if a > 1: 80 | b = 1 81 | else: 82 | c = 2 83 | b += 1 84 | assert False 85 | except UndefException: 86 | pass 87 | 88 | try: 89 | @strict 90 | def foo12(): 91 | for a in range(1,1): 92 | b = 0 93 | b += 1 94 | a += 1 95 | assert False 96 | except UndefException: 97 | pass 98 | 99 | @strict 100 | def foo13(): 101 | b = 0 102 | for a in range(1,1): 103 | b = 0 104 | b += 1 105 | a += 1 106 | 107 | try: 108 | @strict 109 | def foo14(apple): 110 | if apple < 2: 111 | print(aple) 112 | else: 113 | print("Otherwise") 114 | assert False 115 | except UndefException: 116 | pass 117 | 118 | @strict 119 | def foo15(): 120 | def shark(): 121 | pass 122 | shark() 123 | 124 | try: 125 | @strict 126 | def foo15(): 127 | shark2() 128 | assert False 129 | except UndefException: 130 | pass 131 | 132 | try: 133 | def foo16(): 134 | @strict 135 | def shark(): 136 | a = c 137 | foo16() 138 | assert False 139 | except UndefException: 140 | pass 141 | 142 | def foo17(): 143 | spind = 1 144 | @strict 145 | def shark(): 146 | a = spind 147 | foo17() 148 | -------------------------------------------------------------------------------- /strict.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Steven R. Brandt 2 | # 3 | # Distributed under the Boost Software License, Version 1.0. (See accompanying 4 | # file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) 5 | import ast, re, inspect 6 | 7 | class UndefException(Exception): 8 | def __init__(self,s): 9 | Exception.__init__(self,s) 10 | 11 | class defined(object): 12 | def __init__(self,b): 13 | self.b = b 14 | def __str__(self): 15 | if self.b: 16 | return "Def" 17 | else: 18 | return "Udef" 19 | 20 | Undefined = defined(False) 21 | Defined = defined(True) 22 | 23 | class Info(object): 24 | def __init__(self): 25 | self.gl = {} 26 | self.fl = None 27 | self.fn = None 28 | self.defs = {} 29 | 30 | def dupinfo(info): 31 | ninfo = Info() 32 | for k in ["fl","fn","defs","gl"]: 33 | setattr(ninfo,k,getattr(info,k)) 34 | return ninfo 35 | 36 | def get_info(a,depth=0): 37 | "Print detailed information about an AST" 38 | nm = a.__class__.__name__ 39 | print(" "*depth,end="") 40 | iter_children = True 41 | if nm == "Num": 42 | if type(a.n)==int: 43 | print("%s=%d" % (nm,a.n)) 44 | else: 45 | print("%s=%f" % (nm,a.n)) 46 | elif nm == "Global": 47 | print("Global:",dir(a)) 48 | elif nm == "Str": 49 | print("%s='%s'" % (nm,a.s)) 50 | elif nm == "Name": 51 | print("%s='%s'" %(nm,a.id)) 52 | elif nm == "arg": 53 | print("%s='%s'" %(nm,a.arg)) 54 | elif nm == "If": 55 | iter_children = False 56 | print(nm) 57 | get_info(a.test,depth) 58 | for n in a.body: 59 | get_info(n,depth+1) 60 | if len(a.orelse)>0: 61 | print(" "*depth,end="") 62 | print("Else") 63 | for n in a.orelse: 64 | get_info(n,depth+1) 65 | else: 66 | print(nm) 67 | for (f,v) in ast.iter_fields(a): 68 | if type(f) == str and type(v) == str: 69 | print("%s:attr[%s]=%s" % (" "*(depth+1),f,v)) 70 | if iter_children: 71 | for n in ast.iter_child_nodes(a): 72 | get_info(n,depth+1) 73 | 74 | def dupdefs(d): 75 | "Create a copy of a dict" 76 | r = {} 77 | for k in d: 78 | r[k] = d[k] 79 | return r 80 | 81 | builtins = {"long":1,"file":1,"KeyError":1} 82 | for b in dir(globals()["__builtins__"]): 83 | builtins[b] = Defined 84 | 85 | def getline(a,info): 86 | "Print out file and line information" 87 | line = info.fl + a.lineno - 1 88 | return "%s:%d" % (info.fn,line) 89 | 90 | def check_nm(n,a,info): 91 | "Check to see if a name is currently defined" 92 | defs = info.defs 93 | fl = info.fl 94 | gl = info.gl 95 | if n in defs: 96 | if defs[n] == Defined: 97 | pass 98 | elif defs[n] == Undefined: 99 | raise UndefException("Undefined variable %s, line=%s" % (n,getline(a,info))) 100 | elif n in gl or n in builtins: 101 | pass 102 | else: 103 | raise UndefException("Undefined variable %s, line=%s" % (n,getline(a,info))) 104 | 105 | check_depth = 0 106 | def check_vars(a,info): 107 | "Check a class to see if it meets the strict definition" 108 | global check_depth 109 | nm = a.__class__.__name__ 110 | if nm == "Str" or nm == "Num" or nm == "Lt" or nm == "Pass": 111 | return 112 | args = [arg for arg in ast.iter_child_nodes(a)] 113 | if nm == "Name": 114 | nm2 = args[0].__class__.__name__ 115 | if nm2 == "Load": 116 | check_nm(a.id,a,info) 117 | elif nm2 == "Store": 118 | info.defs[a.id] = Defined 119 | elif nm == "FunctionDef": 120 | check_depth += 1 121 | try: 122 | if check_depth < 2: 123 | for arg in args: 124 | check_vars(arg,info) 125 | else: 126 | info.defs[a.name] = Defined 127 | finally: 128 | check_depth -= 1 129 | elif nm == "BinOp" or nm == "Compare": 130 | check_vars(args[0],info) 131 | opname = args[1].__class__.__name__ 132 | if opname == "Add": 133 | c = ast.Call(ast.Attribute(args[0],"__add__","ctx"),args[0],ast.Num()) 134 | elif opname == "Sub": 135 | c = ast.Call(ast.Name("__sub__","ctx"),args[0],ast.Num()) 136 | elif opname == "Mul": 137 | c = ast.Call(ast.Name("__mul__","ctx"),args[0],ast.Num()) 138 | else: 139 | c = None 140 | if c != None: 141 | c.lineno = a.lineno 142 | check_vars(c,info) 143 | check_vars(args[2],info) 144 | elif nm == "Call": 145 | nm2 = args[0].__class__.__name__ 146 | if nm2 == "Name": 147 | check_nm(args[0].id,a,info) 148 | elif nm2 == "Attribute": 149 | attr = args[0].attr 150 | args2 = [arg for arg in ast.iter_child_nodes(args[0])] 151 | nm3 = args2[0].__class__.__name__ 152 | if nm3 == "Name": 153 | pkg_nm = args2[0].id 154 | if pkg_nm in info.gl or pkg_nm in locals(): 155 | pkg = info.gl[pkg_nm] 156 | if not hasattr(pkg,attr): 157 | raise UndefException("%s is not in %s, line=%s" % (attr,pkg_nm,getline(a,info))) 158 | for arg in args[1:]: 159 | check_vars(arg,info) 160 | elif nm == "Assign": 161 | nm2 = args[0].__class__.__name__ 162 | if nm2 == "Name": 163 | info.defs[args[0].id] = Defined 164 | check_vars(args[1],info) 165 | elif nm == "Slice": 166 | print(nm,a.lower,a.upper,dir(a)) 167 | elif nm == "AugAssign": 168 | check_nm(args[0].id,a,info) 169 | check_vars(args[2],info) 170 | elif nm == "For": 171 | d = dupdefs(info.defs) 172 | check_vars(args[0],info) 173 | for arg in args[1:]: 174 | check_vars(args[0],info) 175 | elif nm == "While": 176 | d = dupdefs(info.defs) 177 | for arg in args: 178 | check_vars(args[0],info) 179 | elif nm == "If": 180 | check_vars(a.test,info) 181 | d2 = dupdefs(info.defs) 182 | info2 = dupinfo(info) 183 | info2.defs = d2 184 | for b in a.body: 185 | check_vars(b,info2) 186 | d3 = dupdefs(info.defs) 187 | info3 = dupinfo(info) 188 | info3.defs = d3 189 | for b in a.orelse: 190 | check_vars(b,info3) 191 | for d in d2: 192 | if d in d3: 193 | if d2[d] == Defined and d3[d] == Defined: 194 | info.defs[d] = Defined 195 | elif nm == "Attribute": 196 | if args[0].id in gl: 197 | obj = gl[args[0].id] 198 | if not hasattr(obj,a.attr): 199 | line = getline(a,info) 200 | raise UndefException("%s not in %s, line=%s" % (a.attr,args[0].id,line)) 201 | elif nm == "arg": 202 | info.defs[a.arg] = Defined 203 | else: 204 | for arg in args: 205 | check_vars(arg,info) 206 | 207 | def strict(f): 208 | fr = inspect.currentframe() 209 | gl = f.__globals__ 210 | fl = f.__code__.co_firstlineno 211 | fn = f.__code__.co_filename 212 | for b in dir(f.__globals__["__builtins__"]): 213 | builtins[b] = Defined 214 | n = 0 215 | while fr: 216 | fr = fr.f_back 217 | n += 1 218 | if hasattr(fr,"f_locals"): 219 | for b in fr.f_locals: 220 | builtins[b] = Defined 221 | 222 | # Get the source code 223 | src = inspect.getsource(f) 224 | 225 | # Create the AST 226 | src = re.sub('^(?=\s+)','if True:\n',src) 227 | tree = ast.parse(src) 228 | defs = {} 229 | for v in f.__code__.co_varnames: 230 | defs[v] = Undefined 231 | info = Info() 232 | info.gl = gl 233 | info.fl = fl 234 | info.fn = fn 235 | info.defs = defs 236 | check_vars(tree,info) 237 | return f 238 | --------------------------------------------------------------------------------