Last active
August 9, 2017 20:03
-
-
Save urigoren/6ceda834174657d5396bd047addaddba to your computer and use it in GitHub Desktop.
This module tests whether a function is pure
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import ast, inspect, textwrap | |
whitelist = {'math', 'itertools', 'collections', 'functools', 'operator', | |
'json', 'pickle', 'string', 'types', 'statistics', 'fractions', 'decimal'} | |
def pure(f): | |
"""pure decorator""" | |
f.pure = True | |
return f | |
def impure(f): | |
"""impure decorator""" | |
f.pure = False | |
return f | |
class PureVisitor(ast.NodeVisitor): | |
def __init__(self, visited): | |
super().__init__() | |
self.pure = True | |
self.visited = visited | |
def visit_Name(self, node): | |
return node.id | |
def visit_Attribute(self, node): | |
name = [node.attr] | |
child = node.value | |
while child is not None: | |
if isinstance(child, ast.Attribute): | |
name.append(child.attr) | |
child = child.value | |
else: | |
name.append(child.id) | |
break | |
name = ".".join(reversed(name)) | |
return name | |
def visit_Call(self, node): | |
if not self.pure: | |
return | |
name = self.visit(node.func) | |
if name not in self.visited: | |
self.visited.append(name) | |
try: | |
callee = eval(name) | |
if not is_pure(callee, self.visited): | |
self.pure = False | |
except NameError: | |
self.pure = False | |
def is_pure(f, _visited=None): | |
"""Returns True if f is declared pure or if it calls only pure functions""" | |
try: | |
return f.pure | |
except AttributeError: | |
pass | |
try: | |
module = inspect.getmodule(f).__name__ | |
except AttributeError: | |
module = '' | |
if module in whitelist: | |
return True | |
try: | |
code = inspect.getsource(f.__code__) | |
except AttributeError: | |
return False | |
code = textwrap.dedent(code) | |
node = compile(code, "<unknown>", "exec", ast.PyCF_ONLY_AST) | |
if _visited is None: | |
_visited = [] | |
visitor = PureVisitor(_visited) | |
visitor.visit(node) | |
return visitor.pure | |
def immutable_args(): | |
"""Forces only immutable types at runtime""" | |
def decorator(function): | |
signature= inspect.signature(function) | |
def wrapper(*args, **kwargs): | |
bound_args= signature.bind(*args, **kwargs) | |
for variable, value in bound_args.arguments.items(): | |
if not immutable_value(value): | |
raise SyntaxError(variable + " is immutable, {v} is illegal".format(v=value)) | |
return function(*args, **kwargs) | |
return wrapper | |
return decorator | |
def immutable_value(x): | |
"""checks if x is immutable""" | |
return isinstance(x, tuple) or isinstance(x, int) or isinstance(x, float) or isinstance(x, str) | |
if __name__ == "__main__": | |
import math, datetime | |
@pure | |
def rectangle_area(a, b): | |
return a * b | |
@pure | |
def triangle_area(a, b, c): | |
return ((a + (b + c))(c - (a - b))(c + (a - b))(a + (b - c))) ** 0.5 / 4 | |
def house_area(a, b, c): | |
return rectangle_area(a, b) + triangle_area(a, b, c) | |
assert is_pure(house_area) == True | |
assert is_pure(math.sin) == True | |
assert is_pure(datetime.date.today) == False | |
assert is_pure(map) == False |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment