Skip to content

Instantly share code, notes, and snippets.

@groner
Created February 18, 2015 18:29
Show Gist options
  • Save groner/240fe5faab51a0e3b20a to your computer and use it in GitHub Desktop.
Save groner/240fe5faab51a0e3b20a to your computer and use it in GitHub Desktop.
Binding classes and instances with jeni injectors
'''This is my second attempt. It is much cleaner, except for the part that exposes the injector on bound classes and instances.
'''
from contextlib import suppress
from functools import wraps
from pprint import pprint
import jeni
class Annotator(jeni.Annotator):
'''Annotator with the ability to annotate classes.'''
@wraps(jeni.Annotator.__call__)
def __call__(self, *notes, **keyword_notes):
if not keyword_notes and len(notes) == 1 and isinstance(notes[0], type):
cls = notes[0]
if self.has_annotations(cls.__init__):
cls.__notes__ = cls.__init__.__notes__
return cls
return super().__call__(*notes, **keyword_notes)
def method(self, *notes, **keyword_notes):
'''Annotate a method as part of an annotated class.
When combining staticmethod or classmethod with these, it is necessary
to put them on the inside. This is because those wrappers don't
delegate to inner descriptors.
'''
# Do basic annotation, then wrap in AnnotatedMethod
if not keyword_notes and len(notes) == 1:
if callable(notes[0]):
fn = self(notes[0])
return AnnotatedMethod(fn)
if hasattr(notes[0], '__func__') and callable(notes[0].__func__):
method_descriptor = notes[0]
fn = self(method_descriptor.__func__)
return AnnotatedMethod(method_descriptor)
def decorator(fn):
self(*notes, **keyword_notes)(fn)
return AnnotatedMethod(fn)
return decorator
annotate = Annotator()
class AnnotatedMethod:
def __init__(self, method):
self.__inner_descriptor = method
method = getattr(method, '__func__', method)
def __get__(self, instance, cls):
method = self.__inner_descriptor.__get__(instance, cls)
if instance is None:
instance = cls
try:
injector = instance.__injector__
except AttributeError:
return method
return injector.partial(method)
class Injector(jeni.Injector):
# cls.__injector__?? Say it ain't so!
def partial_class(self, cls):
# TODO: Also handle annotated constructors
return cls.__class__(cls.__name__, (cls,), dict(
__injector__=self))
def partial(self, fn, *user_args, **user_kwargs):
if isinstance(fn, type):
return self.partial_class(fn)
return super().partial(fn, *user_args, **user_kwargs)
def apply_class(self, cls, *user_args, **user_kwargs):
# TODO: Also handle annotated constructors
instance = cls.__new__(cls)
instance.__injector__ = self
instance.__init__(*user_args, **user_kwargs)
return instance
def apply(self, fn, *a, **kw):
if isinstance(fn, type):
return self.apply_class(fn, *a, **kw)
return super().__apply(fn, *a, **kw)
def test():
@annotate
class Test:
@annotate.method
def foo(self, x: 'x'):
assert isinstance(self, Test)
assert x == 31
return 'ok'
@annotate.method
@staticmethod
def bar(x: 'x'):
assert x == 31
return 'ok'
@annotate.method
@classmethod
def quux(cls, x: 'x'):
assert issubclass(cls, Test)
assert x == 31
return 'ok'
class TestInjector(Injector):
pass
TestInjector.value('x', 31)
with TestInjector() as inj:
print('T = Test')
print('T', Test)
print('T.mro()', Test.mro())
print('T.foo', Test.foo)
print('T.bar', Test.bar)
print('T.bar(31)', Test.bar(31))
print('T.quux', Test.quux)
print('T.quux(31)', Test.quux(31))
print()
print('t = Test()')
t = Test()
print('t', t)
print('t.foo', t.foo)
print('t.foo(31)', t.foo(31))
print('t.bar', t.bar)
print('t.bar(31)', t.bar(31))
print('t.quux', t.quux)
print('t.quux(31)', t.quux(31))
print()
print('T = inj.partial(Test)')
T = inj.partial(Test)
print('T', T)
print('T.mro()', T.mro())
print('T.foo', T.foo)
print('T.bar', T.bar)
print('T.bar()', T.bar())
print('T.quux', T.quux)
print('T.quux()', T.quux())
print()
print('t = inj.partial(Test)()')
t = inj.partial(Test)()
print('t', t)
print('t.foo', t.foo)
print('t.foo()', t.foo())
print('t.bar', t.bar)
print('t.bar()', t.bar())
print('t.quux', t.quux)
print('t.quux()', t.quux())
print()
print('t = inj.apply(Test)')
t = inj.apply(Test)
print('t', t)
print('t.foo', t.foo)
print('t.foo()', t.foo())
print('t.bar', t.bar)
print('t.bar()', t.bar())
print('t.quux', t.quux)
print('t.quux()', t.quux())
print()
class debug:
def __init__(self, *exc_types):
self.exc_types = exc_types or Exception
def __enter__(self):
return self
def __exit__(self, exc_type, exc, exc_tb):
if exc is not None and isinstance(exc, self.exc_types):
import traceback; traceback.print_exc()
import pdb; pdb.post_mortem()
if __name__ == '__main__':
with debug():
test()
'''This was my initial exploration, it does some stuff in the base class constructor wrapper that really belongs in Injector. I'm including it as a reference for discussion.
'''
from contextlib import suppress
from functools import wraps
from pprint import pprint
import jeni
class Annotator(jeni.Annotator):
'''Annotator with the ability to annotate classes.'''
@wraps(jeni.Annotator.__call__)
def __call__(self, *notes, **keyword_notes):
if not keyword_notes and len(notes) == 1 and isinstance(notes[0], type):
cls = notes[0]
if self.has_annotations(cls.__init__):
cls.__notes__ = cls.__init__.__notes__
return cls
return super().__call__(*notes, **keyword_notes)
annotate = Annotator()
class AutoPartialsMethodsMeta(type):
def __init__(cls, name, bases, d):
# Augment __init__ to bind annotated methods to the instance.
obnoxious_prefix = '__autopartial_method_'
mangle = lambda k: obnoxious_prefix+k
unmangle = lambda k: k[len(obnoxious_prefix):]
__init__ = d.get('__init__')
@wraps(cls.__init__)
def wrapper_init(them, *a, **kw):
for name in method_notes:
injector_bound_method = kw.pop(name)
# TODO: static methods, class methods
method = injector_bound_method.__get__(them)
setattr(them, unmangle(name), method)
if __init__ is not None:
them.__init__(*a, **kw)
else:
super(cls, them).__init__(*a, **kw)
with suppress(AttributeError):
del wrapper_init.__notes__
# FIXME: This can gather occluded methods which might then shadow non
# annotated methods in further up the inheritance chain.
method_notes = {
mangle(name): annotate.partial(method)
for name,method in cls.gather_annotated_methods() }
if method_notes:
annotate.set_annotations(wrapper_init, **method_notes)
cls.__init__ = d['__init__'] = wrapper_init
super().__init__(name, bases, d)
def gather_annotated_methods(cls):
for base in cls.__bases__:
if isinstance(base, AutoPartialsMethodsMeta):
yield from base.gather_annotated_methods()
for k,v in vars(cls).items():
if callable(v) and annotate.has_annotations(v):
yield k,v
class AutoPartialsMethods(metaclass=AutoPartialsMethodsMeta):
pass
def test():
@annotate
class TestWithAnAnnotatedMethod(AutoPartialsMethods):
@annotate
def foo(self, x: 'x'):
print('{.__class__.__name__}.foo got x = {!r}'.format(self, x))
@annotate
class TestWithAnAnnotatedInit(AutoPartialsMethods):
@annotate
def __init__(self, x: 'x'):
print('{.__class__.__name__}.__init__ got x = {!r}'.format(self, x))
class Injector(jeni.Injector):
pass
Injector.value('x', 3)
with Injector() as inj:
t = inj.apply(TestWithAnAnnotatedMethod)
print(t)
pprint(vars(t))
t.foo()
t = inj.apply(TestWithAnAnnotatedInit)
print(t)
pprint(vars(t))
class debug:
def __init__(self, *exc_types):
self.exc_types = exc_types or Exception
def __enter__(self):
return self
def __exit__(self, exc_type, exc, exc_tb):
if exc is not None and isinstance(exc, self.exc_types):
import traceback; traceback.print_exc()
import pdb; pdb.post_mortem()
if __name__ == '__main__':
with debug():
test()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment