diff --git a/snippets/undefined.py b/snippets/undefined.py new file mode 100644 index 0000000..baeb491 --- /dev/null +++ b/snippets/undefined.py @@ -0,0 +1,117 @@ +# Example of an undefined singleton, usable with eval() functions to limit +# possibility of NameError exceptions. +# +# Use with a namespace decorator like : +# +# class NoNameErrorNamespace: +# def __init__(self, ns): +# self.ns = ns +# +# def __getitem__(self, name): +# try: +# return self.ns[name] +# except KeyError: +# return Undefined() +# +# def __setitem__(self, name, value): +# self.ns[name] = value +# +# eval('x - 1') + + +class Undefined: + def __str__(self): + return 'Undefined' + + def __getattr__(self, name): + return self + + def __neq__(self, other): + return True + + self = lambda self, *args, **kwargs: self + true = lambda *args: True + false = lambda *args: False + + __eq__ = self + __lt__ = false + __gt__ = false + __le__ = false + __ge__ = false + + # Arithmetic operators + for op in ['__add__', '__sub__', '__mul__', '__matmul__', '__truediv__', + '__floordiv__', '__mod__', '__divmod__', '__pow__', + '__lshift__', '__rshift__', '__and__', '__xor__', '__or__']: + locals()[op] = self + locals()[op.replace('__', '__r', 1)] = self + locals()[op.replace('__', '__i', 1)] = self + + __call__ = self + __getitem__ = self + __bool__ = false + __abs__ = self + __neg__ = self + __pos__ = self + __invert__ = self + __contains__ = self + __reversed__ = self + __length_hint__ = lambda: 0 + __hash__ = lambda self: id(self) + + __instance = None + def __new__(cls, *args, **kwargs): + if cls.__instance is None: + cls.__instance = super().__new__(cls) + return cls.__instance + + +import operator +import inspect +import pytest + + +def test(): + assert Undefined().__lt__(1) is False + + assert Undefined() is Undefined() + assert Undefined().x is Undefined() + assert Undefined()['a'] is Undefined() + assert Undefined()() is Undefined() + assert Undefined().x('a', 1, x=3) is Undefined() + assert not Undefined() + assert not bool(Undefined()) + assert (Undefined() - 1) is Undefined() + assert (1 - Undefined()) is Undefined() + + +ops = [op for key, op in operator.__dict__.items() if callable(op) and key.startswith('__')] +@pytest.mark.parametrize('obj', [1, '', [], (), {}, dict()], ids=['integer', 'string', 'list', 'tuple', 'set', 'dict']) +@pytest.mark.parametrize('op', ops, ids=[op.__name__ for op in ops]) +def test_op(op, obj): + if isinstance(op, type): + pytest.skip() + name = op.__name__.strip('_').strip('0') + if name in ['index', 'concat', 'countOf', 'delitem', 'indexOf', 'setitem', 'attrgetter', 'iconcat']: + pytest.skip() + args_len = len([p for p in inspect.signature(op).parameters.values() if p.default is p.empty]) + if args_len == 0: + pytest.skip('no arg') + result = Undefined() + if name in ['lt', 'gt', 'le', 'ge', 'truth', 'is', 'contains']: + result = False + if name in ['ne', 'not', 'is_not']: + result = True + if name in ['length_hint']: + result = 0 + result_reversed = result + if name in ['imod', 'mod'] and obj == '': + result_reversed = '' + if args_len == 1: + assert op(Undefined()) is result, f'failed with {op}' + elif args_len == 2: + assert op(Undefined(), obj) is result, f'failed with {op}' + if name not in ['contains', 'getitem'] or (name == 'ctonains' and obj != '' and hasattr(obj, '__contains__')) or (name == 'getitem' and hasattr(obj, '__getitem__') and obj != '' and obj != [] and obj != () and obj != {}): + assert op(obj, Undefined()) is result_reversed, f'failed with reversed {op}' + else: + raise NotImplementedError(f'{op} {args_len}')