Skip to content

Instantly share code, notes, and snippets.

@schlarpc
Created December 4, 2017 07:46
Show Gist options
  • Save schlarpc/223f5053fa5d49bc14cd77cfa71da399 to your computer and use it in GitHub Desktop.
Save schlarpc/223f5053fa5d49bc14cd77cfa71da399 to your computer and use it in GitHub Desktop.
Bad ideas in Python: something like pattern matching by abusing context managers and stack frames
import contextlib
import ctypes
import inspect
import mock
import collections
import re
import attr
import six
class MatchError(Exception):
pass
@attr.s(frozen=True)
class Placeholder(object):
name = attr.ib()
@attr.s(frozen=True)
class Pinned(object):
name = attr.ib()
value = attr.ib()
@attr.s(frozen=True)
class BoundResult(object):
name = attr.ib()
value = attr.ib()
class FrozenSetDict(frozenset):
pass
class PatternStorage(object):
def __init__(self):
self.patterns = collections.OrderedDict()
@classmethod
def freeze(cls, obj):
if isinstance(obj, dict):
return FrozenSetDict({k: cls.freeze(v) for k, v in obj.items()}.items())
if isinstance(obj, list):
return tuple([cls.freeze(v) for v in obj])
return obj
def __setitem__(self, key, value):
# self.patterns.append(key, value)
# print('setitem', key, value)
self.patterns[self.freeze(key)] = value
class Context(object):
def __init__(self, matcher):
self.matcher = matcher
self.injections = set()
self.frames = []
self.data = []
def __call__(self, data):
self.data.append(data)
return self
def __enter__(self):
calling_frame = inspect.stack()[3][0]
original_globals = calling_frame.f_globals.copy()
for injection in self.injections:
calling_frame.f_globals[injection] = Placeholder(name=injection)
ctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(calling_frame), ctypes.c_int(0))
return self
def __exit__(self, type, value, traceback):
if type:
return
data = self.data.pop()
for pattern, handler in self.pattern.patterns.items():
print(pattern, data, 'matches:', self.recursive_match(pattern, data))
print()
def recursive_match(self, pattern, data):
if isinstance(pattern, Placeholder):
return BoundResult(pattern.name, data)
if isinstance(pattern, FrozenSetDict):
pattern = dict(pattern)
if hasattr(pattern, 'items') and hasattr(data, 'items'):
keys_all_common = set(obj1.keys()) == set(data.keys())
if not keys_all_common:
raise MatchError()
# TODO handle abbreviated dict matches (ellipsis doesn't work in a dict... :( )
values_matched = [self.recursive_match(pattern[k], data[k]) for k in pattern.keys()]
if
return values_all_common
if isinstance(pattern, six.string_types) and isinstance(data, six.string_types):
if pattern == data:
return []
else:
raise MatchError()
if isinstance(pattern, collections.Iterable) and isinstance(data, collections.Iterable):
if len(pattern) != len(data):
# todo iterables without lengths
raise MatchError()
return all(self.recursive_match(v1, v2) for v1, v2 in zip(pattern, data))
raise MatchError()
def match(self, data):
try:
return self.matcher(self, data)
except NameError as ex:
match = re.match("^(global )?name '(.+)' is not defined$", str(ex))
if not match:
raise
undefined_name = match.group(2)
self.injections.add(undefined_name)
return self.match(data)
def pin(self, var):
pass
pattern = PatternStorage()
def dostuff():
def matcher(ctx, data):
existing_local = 6
with ctx(data):
ctx.pattern[{'match': b}] = lambda b: b
ctx.pattern[{'match_1': a, 'match_2': 80}] = lambda a: a
ctx.pattern[{'match_1': a, 'match_2': b}] = lambda b: b
ctx.pattern[{'pinned': ctx.pin(existing_local), 'matched': a}] = lambda a: a
ctx.pattern[_] = lambda: 'failed'
ctx = Context(matcher)
ctx.match({'match': 80}) == 80
ctx.match({'match_1': 40, 'match_2': 80}) == 40
ctx.match({'match_1': 40, 'match_2': 70}) == 70
ctx.match({'pinned': 40, 'matched': 80}) == 'failed'
ctx.match({'pinned': 6, 'matched': 80}) == 80
ctx.match({'nothing': 'at all'}) == 'failed'
if __name__ == '__main__':
dostuff()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment