Source code for haskpy.utils

"""Utilities

.. autosummary::
   :toctree:

   immutable

"""
import functools
import inspect
import attr
import hypothesis.strategies as st


[docs]def immutable(maybe_cls=None, eq=False, repr=False, **kwargs): return attr.s( maybe_cls=maybe_cls, frozen=True, eq=eq, order=False, hash=False, str=False, repr=repr, **kwargs )
def singleton(C): return C() class decorator(): """Base class for various decorators""" def __init__(self, f): self.f = f self.__doc__ = f.__doc__ self.__name__ = f.__name__ self.__module__ = f.__module__ self.__defaults__ = f.__defaults__ self.__kwdefaults__ = f.__kwdefaults__ self.__annotations__ = f.__annotations__ return def __call__(self, *args, **kwargs): return self.f(*args, **kwargs) @property def __code__(self): return self.f.__code__ @property def __signature__(self): return inspect.signature(self.f) # The following properties are needed so that Sphinx recognizes (class) # methods. Note that these properties don't exist for normal functions. @property def __self__(self): return self.f.__self__ @property def __func__(self): return self.f.__func__ class class_function(decorator): """Class method that isn't a method of the instances""" def __get__(self, obj, cls): if obj is None: return self.f.__get__(cls, type(cls)) else: raise AttributeError( "'{0}' object has no attribute '{1}'".format( cls.__name__, self.f.__name__, ) ) class class_property(decorator): """Class attribute that isn't an attribute of the instances To access the docstring, use ``__dict__`` as ``SomeClass.__dict__["some_attribute"].__doc__`` """ def __get__(self, obj, cls): if obj is None: return self.f.__get__(obj, cls)(cls) else: raise AttributeError( "'{0}' object has no attribute '{1}'".format( cls.__name__, self.f.__name__, ) ) class abstract_function(decorator): """Function that has no implementation yet""" def __call__(self, *args, **kwargs): raise NotImplementedError( "'{0}' function is abstract".format(self.f.__name__) ) def __get__(self, obj, cls): return abstract_function(self.f.__get__(obj, cls)) @property def __code__(self): return self.f.__code__ @property def __signature__(self): return inspect.signature(self.f) class abstract_property(decorator): """Property that has no implementation yet To access the property ``abstract_property`` object without raising ``NotImplementedError``, use ``__dict__``. For instance, to access the docstring: .. code-block:: python class Foo(): @abstract_property def bar(self): '''My docstring''' Foo.__dict__["bar"].__doc__ isinstance(Foo.__dict__["bar"], abstract_property) """ def __get__(self, obj, cls): self.f.__get__(obj, cls) raise NotImplementedError( "'{0}' attribute of type object '{1}' is abstract".format( self.f.__name__, cls.__name__, ) if obj is None else "'{0}' attribute of object '{1}' is abstract".format( self.f.__name__, cls.__name__, ) ) def abstract_class_property(f): return abstract_property(class_function(f)) def abstract_class_function(f): # Wrap the result gain with class_function so that we can recognize the # result as a class function when building the documentation.. A bit ugly # hack.. Probably there's a better way.. return abstract_function(class_function(f)) @immutable class nonexisting_function(): """Mark method non-existing This is a workaround for Python forcefully creating some methods. One cannot create objects that don't have ``__eq__``, ``__ge__``, ``__gt__`` and many other methods. They are there and it's not possible to delete them. With this wrapper you can override those methods so that they won't show up in ``__dir__`` listing and if accessed in any way, ``AttributeError`` is raised. Note that it just hides the methods, one can still access them as ``object.__getattribute__(obj, "__eq__")``. """ method = attr.ib() cls = attr.ib(default=None) def __call__(self, *args, **kwargs): name = self.method.__name__ # The method doesn't exist raise TypeError( "No {0} function".format(name) if self.cls is None else "Class {0} has no {1} method".format(self.cls.__name__, name) ) def __get__(self, obj, objtype): # Bind the method to a class return nonexisting_function(self.method, cls=objtype) def update_argspec(spec, args, kwargs): # TODO: Instead of running getfullargspec after every partial evaluation, # it might be faster to use the existing argspec and update that based on # args and kwargs. However, the speed gains might be quite small and one # needs to be very careful to implement exactly the same logic that Python # itself uses. It is possible that this logic changes from one Python # version to another, so it might become a maintenance nightmare. Still, # perhaps worth at least checking. # # Below is just some sketching. no_varargs = spec.varargs is None nargs_takes = len(spec.args) nargs_given = len(args) nargs_remain = nargs_takes - nargs_given if no_varargs and nargs_remain < 0: raise TypeError( "function takes {takes} positional argument but {given} were given" .format( name=name, takes=nargs_takes, given=nargs_given, ) ) new_args = spec.args[nargs_given:] new_defaults = spec.defaults[-nargs_remain:] if nargs_remain > 0 else None # FIXME: new_kwonlyargs = spec.kwonlyargs new_kwonlydefaults = spec.kwonlydefaults return inspect.FullArgSpec( args=new_args, varargs=spec.varargs, varkw=spec.varkw, defaults=new_defaults, kwonlyargs=new_kwonlyargs, kwonlydefaults=new_kwonlydefaults, # FIXME: What to do with this? annotations=spec.annotations, ) pass def count_required_arguments(argspec): # Positional arguments without defaults provided n_args = len(argspec.args) - ( 0 if argspec.defaults is None else len(argspec.defaults) ) # Positional required arguments may get into required keyword # argument position if some positional arguments before them are # given as keyword arguments. For instance: # # curry(lambda x, y: x - y)(x=5) # # Now, y becomes a keyword argument but it's still required as it # doesn't have any default value. Handle this by looking at # kwonlyargs that don't have a value in kwonlydefaults. defaults = ( set() if argspec.kwonlydefaults is None else set(argspec.kwonlydefaults.keys()) ) kws = set(argspec.kwonlyargs) n_kw = len(kws.difference(defaults)) return n_args + n_kw @immutable class Wrapped(): """Original function that provides metainformation""" __unwrapped = attr.ib() """Wrapped function that is actually called""" __wrapped = attr.ib() def __call__(self, *args, **kwargs): return self.__wrapped(*args, **kwargs) def __repr__(self): return repr(self.__wrapped) @property def __module__(self): return self.__unwrapped.__module__ @property def __signature__(self): return inspect.signature(self.__unwrapped) @property def __doc__(self): return self.__unwrapped.__doc__ def wraps(f): """Simple wrapping function similar to functools.wraps Aims to be a bit simpler and faster, but not sure about it. Experimenting at the moment. """ def wrap(g): return Wrapped(f, g) return wrap def identity(x): """a -> a""" return x class PerformanceWarning(Warning): pass @st.composite def draw_args(draw, f, *args): return f(*(draw(a) for a in args)) @st.composite def sample_type(draw, types, types1=[], types2=[]): if len(types) == 0: raise ValueError("Must provide at least one concrete type") arg = st.deferred(lambda: sample_type(types, types1, types2)) return draw( st.one_of( # Concrete types *[st.just(t) for t in types], # One-argument type constructors *[ draw_args(t1, arg) for t1 in types1 ], # Two-argument type constructors *[ draw_args(t2, arg, arg) for t2 in types2 ], ) ) def sample_sized(e, size=None): return ( e if size is None else st.tuples(*(size * [e])) ) def eq_test(x, y, data, **kwargs): eq = getattr(x, "__eq_test__", lambda v, *_, **__: x == v) return eq(y, data, **kwargs) def assert_output(f): """Assert that the output pair elements are equal""" @functools.wraps(f) def wrapped(*args, **kwargs): xs = f(*args) x0 = xs[0] for xi in xs[1:]: assert eq_test(x0, xi, **kwargs) return return wrapped @singleton @immutable class universal_set(): """Universal set is a set that contains all objects""" def __contains__(self, _): return True def make_test_class(C, typeclasses=universal_set): """Create a PyTest-compatible test class In order to test only some typeclass laws, give the set of typeclasses as ``typeclasses`` argument. When PyTest runs tests on a class, it creates an instance of the class and tests the instance. The class shouldn't have __init__ method. However, we want to test class methods, so there's no need to create an instance in the first place and it's ok to have any kind of __init__ method. To work around this PyTest limitation, we crete a class which doesn't have __init__ and when you call it's constructor, you actually don't get an instance of that class but instead the class that we wanted to test in the first place. PyTest thinks it got an instance of the class but actually we just gave it a class. So, to run the class method tests for a class SomeClass, add the following to some ``test_`` prefixed module: .. code-block:: python TestSomeClass = make_test_class(SomeClass) """ classes = {cls for cls in C.mro() if cls in typeclasses} test_methods = { method for cls in classes for method in cls.__dict__.keys() if method.startswith("test_") } dct = { name: getattr(C, name) for name in dir(C) if name in test_methods } class MetaTestClass(type): __dict__ = dct class TestClass(metaclass=MetaTestClass): __dict__ = dct def __getattr__(self, name): return getattr(C, name) return TestClass