Created
February 18, 2015 18:29
-
-
Save groner/240fe5faab51a0e3b20a to your computer and use it in GitHub Desktop.
Binding classes and instances with jeni injectors
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
'''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 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
'''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