diff --git a/docs/conf.py b/docs/conf.py index 346b860..b083f8d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,12 +16,12 @@ import injector # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. @@ -34,7 +34,7 @@ templates_path = ['_templates'] source_suffix = '.rst' # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' @@ -54,40 +54,40 @@ release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # -- Options for HTML output --------------------------------------------------- @@ -99,26 +99,26 @@ html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -127,48 +127,47 @@ html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. html_sidebars = { - 'index': ('sidebar.html', 'sourcelink.html', 'searchbox.html'), - '**' : ('localtoc.html', 'relations.html', - 'sourcelink.html', 'searchbox.html'), + 'index': ('sidebar.html', 'sourcelink.html', 'searchbox.html'), + '**': ('localtoc.html', 'relations.html', 'sourcelink.html', 'searchbox.html'), } # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'Injectordoc' @@ -177,55 +176,47 @@ htmlhelp_basename = 'Injectordoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). -latex_documents = [ - ('index', 'Injector.tex', u'Injector Documentation', - u'Alec Thomas', 'manual'), -] +latex_documents = [('index', 'Injector.tex', u'Injector Documentation', u'Alec Thomas', 'manual')] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'injector', u'Injector Documentation', - [u'Alec Thomas'], 1) -] +man_pages = [('index', 'injector', u'Injector Documentation', [u'Alec Thomas'], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -234,22 +225,28 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'Injector', u'Injector Documentation', - u'Alec Thomas', 'Injector', 'One line description of project.', - 'Miscellaneous'), + ( + 'index', + 'Injector', + u'Injector Documentation', + u'Alec Thomas', + 'Injector', + 'One line description of project.', + 'Miscellaneous', + ) ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False # Example configuration for intersphinx: refer to the Python standard library. @@ -257,13 +254,13 @@ intersphinx_mapping = {'http://docs.python.org/': None} def setup(app): - app.connect('autodoc-skip-member', skip_member) + app.connect('autodoc-skip-member', skip_member) def skip_member(app, what, name, obj, skip, options): - return ( - skip or - getattr(obj, '__doc__', None) is None or - getattr(obj, '__private__', False) is True or - getattr(getattr(obj, '__func__', None), '__private__', False) is True - ) + return ( + skip + or getattr(obj, '__doc__', None) is None + or getattr(obj, '__private__', False) is True + or getattr(getattr(obj, '__func__', None), '__private__', False) is True + ) diff --git a/injector/__init__.py b/injector/__init__.py index 61aee51..a7ba146 100644 --- a/injector/__init__.py +++ b/injector/__init__.py @@ -53,9 +53,12 @@ def synchronized(lock): def wrapper(*args, **kwargs): with lock: return function(*args, **kwargs) + return wrapper + return outside_wrapper + lock = threading.RLock() @@ -76,8 +79,7 @@ class UnsatisfiedRequirement(Error): def __str__(self): on = '%s has an ' % _describe(self.args[0]) if self.args[0] else '' - return '%sunsatisfied requirement on %s' % ( - on, _describe(self.args[1].interface),) + return '%sunsatisfied requirement on %s' % (on, _describe(self.args[1].interface)) class CallError(Error): @@ -98,12 +100,17 @@ class CallError(Error): full_method = '.'.join((cls, method_name)).strip('.') - parameters = ', '.join(itertools.chain( - (repr(arg) for arg in args), - ('%s=%r' % (key, value) for (key, value) in kwargs.items()) - )) + parameters = ', '.join( + itertools.chain( + (repr(arg) for arg in args), ('%s=%r' % (key, value) for (key, value) in kwargs.items()) + ) + ) return 'Call to %s(%s) failed: %s (injection stack: %r)' % ( - full_method, parameters, original_error, [level[0] for level in stack]) + full_method, + parameters, + original_error, + [level[0] for level in stack], + ) class CircularDependency(Error): @@ -197,7 +204,6 @@ class InstanceProvider(Provider): return '%s(%r)' % (type(self).__name__, self._instance) - @private class ListOfProviders(Provider): """Provide a list of instances via other Providers.""" @@ -241,13 +247,11 @@ class BindingKey(tuple): def create(cls, what: Any) -> 'BindingKey': if isinstance(what, list): if len(what) != 1: - raise Error('list bindings must have a single interface ' - 'element') + raise Error('list bindings must have a single interface ' 'element') what = (list, BindingKey.create(what[0])) elif isinstance(what, dict): if len(what) != 1: - raise Error('dictionary bindings must have a single interface ' - 'key and value') + raise Error('dictionary bindings must have a single interface ' 'key and value') what = (dict, BindingKey.create(list(what.items())[0])) return tuple.__new__(cls, (what,)) @@ -295,8 +299,7 @@ class Binder: if type(interface) is type and issubclass(interface, (BaseMappingKey, BaseSequenceKey)): return self.multibind(interface, to, scope=scope) key = BindingKey.create(interface) - self._bindings[key] = \ - self.create_binding(interface, to, scope) + self._bindings[key] = self.create_binding(interface, to, scope) def bind_scope(self, scope): """Bind a Scope. @@ -326,9 +329,7 @@ class Binder: """ key = BindingKey.create(interface) if key not in self._bindings: - if ( - isinstance(interface, dict) or - isinstance(interface, type) and issubclass(interface, dict)): + if isinstance(interface, dict) or isinstance(interface, type) and issubclass(interface, dict): provider = MapBindProvider() else: provider = MultiBindProvider() @@ -372,17 +373,18 @@ class Binder: binder.install(MyModule) """ if ( - hasattr(module, '__bindings__') or - hasattr(getattr(module, 'configure', None), '__bindings__') or - hasattr(getattr(module, '__init__', None), '__bindings__') + hasattr(module, '__bindings__') + or hasattr(getattr(module, 'configure', None), '__bindings__') + or hasattr(getattr(module, '__init__', None), '__bindings__') ): warnings.warn( 'Injector Modules (ie. %s) constructors and their configure methods should ' 'not have injectable parameters. This can result in non-deterministic ' 'initialization. If you need injectable objects in order to provide something ' ' you can use @provider-decorated methods. \n' - ' This feature will be removed in next Injector release.' % - module.__name__ if hasattr(module, '__name__') else module.__class__.__name__, + ' This feature will be removed in next Injector release.' % module.__name__ + if hasattr(module, '__name__') + else module.__class__.__name__, RuntimeWarning, stacklevel=3, ) @@ -390,11 +392,7 @@ class Binder: instance = self.injector.create_object(module) instance(self) else: - self.injector.call_with_injection( - callable=module, - self_=None, - args=(self,), - ) + self.injector.call_with_injection(callable=module, self_=None, args=(self,)) def create_binding(self, interface, to=None, scope=None): provider = self.provider_for(interface, to) @@ -420,13 +418,21 @@ class Binder: return to elif isinstance(interface, Provider): return interface - elif isinstance(to, (types.FunctionType, types.LambdaType, - types.MethodType, types.BuiltinFunctionType, - types.BuiltinMethodType)): + elif isinstance( + to, + ( + types.FunctionType, + types.LambdaType, + types.MethodType, + types.BuiltinFunctionType, + types.BuiltinMethodType, + ), + ): return CallableProvider(to) elif issubclass(type(to), type): return ClassProvider(to) elif isinstance(interface, BoundKey): + def proxy(**kwargs): return interface.interface(**kwargs) @@ -436,11 +442,7 @@ class Binder: (target,) = interface.__args__ builder = interface(self.injector, target) return InstanceProvider(builder) - elif ( - isinstance(base_type, (tuple, type)) and - interface is not Any and - isinstance(to, base_type) - ): + elif isinstance(base_type, (tuple, type)) and interface is not Any and isinstance(to, base_type): return InstanceProvider(to) elif issubclass(type(interface), type) or isinstance(interface, (tuple, list)): if to is not None: @@ -450,8 +452,7 @@ class Binder: elif hasattr(interface, '__call__'): raise TypeError('Injecting partially applied functions is no longer supported.') else: - raise UnknownProvider('couldn\'t determine provider for %r to %r' % - (interface, to)) + raise UnknownProvider('couldn\'t determine provider for %r to %r' % (interface, to)) def _get_binding(self, key, *, only_this_binder: bool = False): binding = self._bindings.get(key) @@ -484,13 +485,11 @@ class Binder: # "Special" interfaces are ones that you cannot bind yourself but # you can request them (for example you cannot bind ProviderOf(SomeClass) # to anything but you can inject ProviderOf(SomeClass) just fine - return any( - _is_specialization(interface, cls) - for cls in [AssistedBuilder, ProviderOf] - ) + return any(_is_specialization(interface, cls) for cls in [AssistedBuilder, ProviderOf]) if TYPING353: + def _is_specialization(cls, generic_class): # Starting with typing 3.5.3/Python 3.6 it is no longer necessarily true that # issubclass(SomeGeneric[X], SomeGeneric) so we need some other way to @@ -508,6 +507,7 @@ if TYPING353: # by design). return origin is generic_class or issubclass(origin, generic_class) + else: # To maintain compatibility we fall back to an issubclass check. def _is_specialization(cls, generic_class): @@ -549,9 +549,7 @@ class ScopeDecorator: cls.__scope__ = self.scope binding = getattr(cls, '__binding__', None) if binding: - new_binding = Binding(interface=binding.interface, - provider=binding.provider, - scope=self.scope) + new_binding = Binding(interface=binding.interface, provider=binding.provider, scope=self.scope) setattr(cls, '__binding__', new_binding) return cls @@ -561,6 +559,7 @@ class ScopeDecorator: class NoScope(Scope): """An unscoped provider.""" + def __init__(self, injector=None): super(NoScope, self).__init__(injector) @@ -585,6 +584,7 @@ class SingletonScope(Scope): >>> a is b True """ + def configure(self): self._context = {} @@ -603,6 +603,7 @@ singleton = ScopeDecorator(SingletonScope) class ThreadLocalScope(Scope): """A :class:`Scope` that returns a per-thread instance for a key.""" + def configure(self): self._locals = threading.local() @@ -629,9 +630,7 @@ class Module: if hasattr(function, '__binding__'): binding = function.__binding__ binder.bind( - binding.interface, - to=types.MethodType(binding.provider, self), - scope=binding.scope, + binding.interface, to=types.MethodType(binding.provider, self), scope=binding.scope ) self.configure(binder) @@ -725,8 +724,9 @@ class Injector: scope_binding, _ = binder.get_binding(None, scope_key) scope_instance = scope_binding.provider.get(self) - log.debug('%sInjector.get(%r, scope=%r) using %r', - self._log_prefix, interface, scope, binding.provider) + log.debug( + '%sInjector.get(%r, scope=%r) using %r', self._log_prefix, interface, scope, binding.provider + ) result = scope_instance.get(key, binding.provider).get(self) log.debug('%s -> %r', self._log_prefix, result) return result @@ -742,19 +742,21 @@ class Injector: try: instance = cls.__new__(cls) except TypeError as e: - reraise(e, CallError( - cls, - getattr(cls.__new__, '__func__', cls.__new__), - (), {}, e, self._stack,), - maximum_frames=2) + reraise( + e, + CallError(cls, getattr(cls.__new__, '__func__', cls.__new__), (), {}, e, self._stack), + maximum_frames=2, + ) try: - self.install_into(instance, _internal = True) + self.install_into(instance, _internal=True) installed = True except AttributeError: installed = False if hasattr(instance, '__slots__'): - raise Error('Can\'t create an instance of type %r due to presence of __slots__, ' - 'remove __slots__ to fix that' % (cls,)) + raise Error( + 'Can\'t create an instance of type %r due to presence of __slots__, ' + 'remove __slots__ to fix that' % (cls,) + ) # Else do nothing - some builtin types can not be modified. try: @@ -764,17 +766,23 @@ class Injector: except TypeError as e: # The reason why getattr() fallback is used here is that # __init__.__func__ apparently doesn't exist for Key-type objects - reraise(e, CallError( - instance, - getattr(instance.__init__, '__func__', instance.__init__), - (), additional_kwargs, e, self._stack,) + reraise( + e, + CallError( + instance, + getattr(instance.__init__, '__func__', instance.__init__), + (), + additional_kwargs, + e, + self._stack, + ), ) return instance finally: if installed: self._uninstall_from(instance) - def install_into(self, instance, *, _internal = False): + def install_into(self, instance, *, _internal=False): """Put injector reference in given object. This method has, in general, two applications: @@ -836,6 +844,7 @@ class Injector: :type kwargs: dict of string -> object :return: Value returned by callable. """ + def _get_callable_bindings(callable): if not hasattr(callable, '__bindings__'): return {} @@ -847,23 +856,18 @@ class Injector: bindings = _get_callable_bindings(callable) noninjectables = getattr(callable, '__noninjectables__', set()) - needed = dict( - (k, v) for (k, v) in bindings.items() - if k not in kwargs and k not in noninjectables - ) + needed = dict((k, v) for (k, v) in bindings.items() if k not in kwargs and k not in noninjectables) dependencies = self.args_to_inject( function=callable, bindings=needed, owner_key=self_.__class__ if self_ is not None else callable.__module__, - ) + ) dependencies.update(kwargs) try: - return callable( - *((self_,) if self_ is not None else ()) + tuple(args), - **dependencies) + return callable(*((self_,) if self_ is not None else ()) + tuple(args), **dependencies) except TypeError as e: reraise(e, CallError(self_, callable, args, dependencies, e, self._stack)) @@ -890,8 +894,8 @@ class Injector: if key in self._stack: raise CircularDependency( - 'circular dependency detected: %s -> %s' % - (' -> '.join(map(repr_key, self._stack)), repr_key(key)) + 'circular dependency detected: %s -> %s' + % (' -> '.join(map(repr_key, self._stack)), repr_key(key)) ) self._stack += (key,) @@ -964,7 +968,7 @@ def with_injector(*injector_args, **injector_kwargs): @functools.wraps(f) def setup(self_, *args, **kwargs): injector = Injector(*injector_args, **injector_kwargs) - injector.install_into(self_, _internal = True) + injector.install_into(self_, _internal=True) return f(self_, *args, **kwargs) return setup @@ -1039,13 +1043,12 @@ def inject(function=None, **bindings): raise AssertionError( 'Passing keyword arguments to inject is no longer supported. ' 'Use inject in combination with parameter annotations to declare dependencies. ' - 'See documentation for details', + 'See documentation for details' ) if not function: raise AssertionError( - 'No function being decorated, make sure you decorate your function with ' - '@inject, not @inject()', + 'No function being decorated, make sure you decorate your function with ' '@inject, not @inject()' ) try: @@ -1078,23 +1081,25 @@ def noninjectable(*args): :func:`inject` and :func:`noninjectable` doesn't matter. """ + def decorator(function): argspec = inspect.getfullargspec(inspect.unwrap(function)) for arg in args: if arg not in argspec.args and arg not in argspec.kwonlyargs: - raise UnknownArgument('Unable to mark unknown argument %s ' - 'as non-injectable.' % arg) + raise UnknownArgument('Unable to mark unknown argument %s ' 'as non-injectable.' % arg) existing = getattr(function, '__noninjectables__', set()) merged = existing | set(args) function.__noninjectables__ = merged return function + return decorator def method_wrapper(f, bindings): argspec = inspect.getfullargspec(f) if argspec.args and argspec.args[0] == 'self': + @functools.wraps(f) def inject(self_, *args, **kwargs): try: @@ -1102,12 +1107,7 @@ def method_wrapper(f, bindings): except RuntimeError: injector = None if injector: - return injector.call_with_injection( - callable=f, - self_=self_, - args=args, - kwargs=kwargs - ) + return injector.call_with_injection(callable=f, self_=self_, args=args, kwargs=kwargs) else: return f(self_, *args, **kwargs) @@ -1141,8 +1141,10 @@ class BaseKey: """Base type for binding keys.""" def __init__(self): - raise Exception('Instantiation of %s prohibited - it is derived from BaseKey ' - 'so most likely you should bind it to something.' % (self.__class__,)) + raise Exception( + 'Instantiation of %s prohibited - it is derived from BaseKey ' + 'so most likely you should bind it to something.' % (self.__class__,) + ) def Key(name): @@ -1160,9 +1162,12 @@ def Key(name): @private class BaseMappingKey(dict): """Base type for mapping binding keys.""" + def __init__(self): - raise Exception('Instantiation of %s prohibited - it is derived from BaseMappingKey ' - 'so most likely you should bind it to something.' % (self.__class__,)) + raise Exception( + 'Instantiation of %s prohibited - it is derived from BaseMappingKey ' + 'so most likely you should bind it to something.' % (self.__class__,) + ) def MappingKey(name): @@ -1173,9 +1178,12 @@ def MappingKey(name): @private class BaseSequenceKey(list): """Base type for mapping sequence keys.""" + def __init__(self): - raise Exception('Instantiation of %s prohibited - it is derived from BaseSequenceKey ' - 'so most likely you should bind it to something.' % (self.__class__,)) + raise Exception( + 'Instantiation of %s prohibited - it is derived from BaseSequenceKey ' + 'so most likely you should bind it to something.' % (self.__class__,) + ) def SequenceKey(name): @@ -1211,7 +1219,6 @@ class BoundKey(tuple): class AssistedBuilder(Generic[T]): - def __init__(self, injector, target): self._injector = injector self._target = target @@ -1224,7 +1231,8 @@ class AssistedBuilder(Generic[T]): if not isinstance(provider, ClassProvider): raise Error( 'Assisted interface building works only with ClassProviders, ' - 'got %r for %r' % (provider, binding.interface)) + 'got %r for %r' % (provider, binding.interface) + ) return self._build_class(provider._cls, **kwargs) @@ -1268,8 +1276,7 @@ class ProviderOf(Generic[T]): self._interface = interface def __repr__(self): - return '%s(%r, %r)' % ( - type(self).__name__, self._injector, self._interface) + return '%s(%r, %r)' % (type(self).__name__, self._injector, self._interface) def get(self) -> T: """Get an implementation for the specified interface.""" diff --git a/injector_test.py b/injector_test.py index 4ff46b7..c19e504 100644 --- a/injector_test.py +++ b/injector_test.py @@ -20,13 +20,33 @@ import warnings import pytest from injector import ( - Binder, CallError, Injector, Scope, InstanceProvider, ClassProvider, - inject, noninjectable, singleton, threadlocal, UnsatisfiedRequirement, - CircularDependency, Module, Key, SingletonScope, - ScopeDecorator, with_injector, AssistedBuilder, BindingKey, - SequenceKey, MappingKey, provider, ProviderOf, ClassAssistedBuilder, - Error, UnknownArgument, - ) + Binder, + CallError, + Injector, + Scope, + InstanceProvider, + ClassProvider, + inject, + noninjectable, + singleton, + threadlocal, + UnsatisfiedRequirement, + CircularDependency, + Module, + Key, + SingletonScope, + ScopeDecorator, + with_injector, + AssistedBuilder, + BindingKey, + SequenceKey, + MappingKey, + provider, + ProviderOf, + ClassAssistedBuilder, + Error, + UnknownArgument, +) def prepare_basic_injection(): @@ -55,21 +75,23 @@ def check_exception_contains_stuff(exception, stuff): stringified = str(exception) for thing in stuff: - assert thing in stringified, ( - '%r should be present in the exception representation: %s' % ( - thing, stringified)) + assert thing in stringified, '%r should be present in the exception representation: %s' % ( + thing, + stringified, + ) def test_child_injector_inherits_parent_bindings(): parent, child = prepare_nested_injectors() - assert (child.get(str) == parent.get(str)) + assert child.get(str) == parent.get(str) def test_child_injector_overrides_parent_bindings(): parent, child = prepare_nested_injectors() child.binder.bind(str, to='qwe') - assert ((parent.get(str), child.get(str)) == ('asd', 'qwe')) + assert (parent.get(str), child.get(str)) == ('asd', 'qwe') + def test_child_injector_rebinds_arguments_for_parent_scope(): I = Key("interface") @@ -88,9 +110,10 @@ def test_child_injector_rebinds_arguments_for_parent_scope(): binder.bind(I, to="Child") parent = Injector(configure_parent) - assert (parent.get(Cls).val == "Parent") + assert parent.get(Cls).val == "Parent" child = parent.create_child_injector(configure_child) - assert (child.get(Cls).val == "Child") + assert child.get(Cls).val == "Child" + def test_scopes_are_only_bound_to_root_injector(): parent, child = prepare_nested_injectors() @@ -99,7 +122,7 @@ def test_scopes_are_only_bound_to_root_injector(): pass parent.binder.bind(A, to=A, scope=singleton) - assert (parent.get(A) is child.get(A)) + assert parent.get(A) is child.get(A) def test_key_cannot_be_instantiated(): @@ -120,20 +143,20 @@ def test_get_default_injected_instances(): binder.bind(B) injector = Injector(configure) - assert (injector.get(Injector) is injector) - assert (injector.get(Binder) is injector.binder) + assert injector.get(Injector) is injector + assert injector.get(Binder) is injector.binder def test_instantiate_injected_method(): A, _ = prepare_basic_injection() a = A('Bob') - assert (a.b == 'Bob') + assert a.b == 'Bob' def test_method_decorator_is_wrapped(): A, _ = prepare_basic_injection() - assert (A.__init__.__doc__ == 'Construct a new A.') - assert (A.__init__.__name__ == '__init__') + assert A.__init__.__doc__ == 'Construct a new A.' + assert A.__init__.__name__ == '__init__' def test_decorator_works_for_function_with_no_args(): @@ -169,8 +192,8 @@ def test_inject_direct(): injector = Injector(configure) a = injector.get(A) - assert (isinstance(a, A)) - assert (isinstance(a.b, B)) + assert isinstance(a, A) + assert isinstance(a.b, B) def test_configure_multiple_modules(): @@ -184,8 +207,8 @@ def test_configure_multiple_modules(): injector = Injector([configure_a, configure_b]) a = injector.get(A) - assert (isinstance(a, A)) - assert (isinstance(a.b, B)) + assert isinstance(a, A) + assert isinstance(a.b, B) def test_inject_with_missing_dependency(): @@ -214,8 +237,8 @@ def test_inject_named_interface(): injector = Injector(configure) a = injector.get(A) - assert (isinstance(a, A)) - assert (isinstance(a.b, B)) + assert isinstance(a, A) + assert isinstance(a.b, B) def prepare_transitive_injection(): @@ -245,9 +268,9 @@ def test_transitive_injection(): injector = Injector(configure) a = injector.get(A) - assert (isinstance(a, A)) - assert (isinstance(a.b, B)) - assert (isinstance(a.b.c, C)) + assert isinstance(a, A) + assert isinstance(a.b, B) + assert isinstance(a.b.c, C) def test_transitive_injection_with_missing_dependency(): @@ -280,7 +303,7 @@ def test_inject_singleton(): injector1 = Injector(configure) a1 = injector1.get(A) a2 = injector1.get(A) - assert (a1.b is a2.b) + assert a1.b is a2.b def test_inject_decorated_singleton_class(): @@ -300,7 +323,7 @@ def test_inject_decorated_singleton_class(): injector1 = Injector(configure) a1 = injector1.get(A) a2 = injector1.get(A) - assert (a1.b is a2.b) + assert a1.b is a2.b def test_threadlocal(): @@ -316,7 +339,7 @@ def test_threadlocal(): a1 = injector.get(A) a2 = injector.get(A) - assert (a1 is a2) + assert a1 is a2 a3 = [None] ready = threading.Event() @@ -328,7 +351,7 @@ def test_threadlocal(): threading.Thread(target=inject_a3).start() ready.wait(1.0) - assert (a2 is not a3[0] and a3[0] is not None) + assert a2 is not a3[0] and a3[0] is not None def test_injecting_interface_implementation(): @@ -349,7 +372,7 @@ def test_injecting_interface_implementation(): injector = Injector(configure) a = injector.get(A) - assert (isinstance(a.i, Implementation)) + assert isinstance(a.i, Implementation) def test_cyclic_dependencies(): @@ -421,7 +444,7 @@ def test_that_injection_is_lazy(): injector = Injector(configure) assert not (Interface.constructed) injector.get(A) - assert (Interface.constructed) + assert Interface.constructed def test_module_provider(): @@ -443,7 +466,7 @@ def test_module_class_gets_instantiated(): binder.bind(str, to=name) injector = Injector(MyModule) - assert (injector.get(str) == name) + assert injector.get(str) == name def test_with_injector_works(): @@ -459,7 +482,7 @@ def test_with_injector_works(): self.username = username aaa = Aaa() - assert (aaa.username == name) + assert aaa.username == name def test_bind_using_key(): @@ -475,8 +498,8 @@ def test_bind_using_key(): binder.bind(Age, to=25) injector = Injector(MyModule()) - assert (injector.get(Age) == 25) - assert (injector.get(Name) == 'Bob') + assert injector.get(Age) == 25 + assert injector.get(Name) == 'Bob' def test_inject_using_key(): @@ -493,7 +516,7 @@ def test_inject_using_key(): def provide_description(self, name: Name) -> Description: return '%s is cool!' % name - assert (Injector(MyModule()).get(Description) == 'Bob is cool!') + assert Injector(MyModule()).get(Description) == 'Bob is cool!' def test_inject_and_provide_coexist_happily(): @@ -512,7 +535,7 @@ def test_inject_and_provide_coexist_happily(): def provide_description(self, age: int, weight: float) -> str: return 'Bob is %d and weighs %0.1fkg' % (age, weight) - assert (Injector(MyModule()).get(str) == 'Bob is 25 and weighs 50.0kg') + assert Injector(MyModule()).get(str) == 'Bob is 25 and weighs 50.0kg' def test_multibind(): @@ -524,7 +547,7 @@ def test_multibind(): def configure_two(binder): binder.multibind(Names, to=['Tom']) - assert (Injector([configure_one, configure_two]).get(Names) == ['Bob', 'Tom']) + assert Injector([configure_one, configure_two]).get(Names) == ['Bob', 'Tom'] def test_provider_sequence_decorator(): @@ -539,16 +562,15 @@ def test_provider_sequence_decorator(): def tom(self) -> Names: return ['Tom'] - assert (Injector(MyModule()).get(Names) == ['Bob', 'Tom']) + assert Injector(MyModule()).get(Names) == ['Bob', 'Tom'] def test_auto_bind(): - class A: pass injector = Injector() - assert (isinstance(injector.get(A), A)) + assert isinstance(injector.get(A), A) def test_custom_scope(): @@ -604,20 +626,19 @@ def test_custom_scope(): with scope(request): handler = injector.get(Handler) - assert (handler.request is request) + assert handler.request is request with pytest.raises(UnsatisfiedRequirement): injector.get(Handler) def test_bind_interface_of_list_of_types(): - def configure(binder): binder.multibind([int], to=[1, 2, 3]) binder.multibind([int], to=[4, 5, 6]) injector = Injector(configure) - assert (injector.get([int]) == [1, 2, 3, 4, 5, 6]) + assert injector.get([int]) == [1, 2, 3, 4, 5, 6] def test_provider_mapping(): @@ -638,7 +659,7 @@ def test_provider_mapping(): return {'four': 4} injector = Injector([configure, MyModule()]) - assert (injector.get(StrInt) == {'one': 1, 'two': 2, 'three': 3, 'four': 4}) + assert injector.get(StrInt) == {'one': 1, 'two': 2, 'three': 3, 'four': 4} def test_binder_install(): @@ -651,31 +672,31 @@ def test_binder_install(): binder.install(ModuleA()) injector = Injector([ModuleB()]) - assert (injector.get(str) == 'hello world') + assert injector.get(str) == 'hello world' def test_binder_provider_for_method_with_explicit_provider(): injector = Injector() binder = injector.binder provider = binder.provider_for(int, to=InstanceProvider(1)) - assert (type(provider) is InstanceProvider) - assert (provider.get(injector) == 1) + assert type(provider) is InstanceProvider + assert provider.get(injector) == 1 def test_binder_provider_for_method_with_instance(): injector = Injector() binder = injector.binder provider = binder.provider_for(int, to=1) - assert (type(provider) is InstanceProvider) - assert (provider.get(injector) == 1) + assert type(provider) is InstanceProvider + assert provider.get(injector) == 1 def test_binder_provider_for_method_with_class(): injector = Injector() binder = injector.binder provider = binder.provider_for(int) - assert (type(provider) is ClassProvider) - assert (provider.get(injector) == 0) + assert type(provider) is ClassProvider + assert provider.get(injector) == 0 def test_binder_provider_for_method_with_class_to_specific_subclass(): @@ -688,8 +709,8 @@ def test_binder_provider_for_method_with_class_to_specific_subclass(): injector = Injector() binder = injector.binder provider = binder.provider_for(A, B) - assert (type(provider) is ClassProvider) - assert (isinstance(provider.get(injector), B)) + assert type(provider) is ClassProvider + assert isinstance(provider.get(injector), B) def test_binder_provider_for_type_with_metaclass(): @@ -697,11 +718,11 @@ def test_binder_provider_for_type_with_metaclass(): # otherwise should be: # class A(object, metaclass=abc.ABCMeta): # passa - A = abc.ABCMeta('A', (object, ), {}) + A = abc.ABCMeta('A', (object,), {}) injector = Injector() binder = injector.binder - assert (isinstance(binder.provider_for(A, None).get(injector), A)) + assert isinstance(binder.provider_for(A, None).get(injector), A) def test_injecting_undecorated_class_with_missing_dependencies_raises_the_right_error(): @@ -747,7 +768,7 @@ def test_assisted_builder_works_when_got_directly_from_injector(): injector = Injector() builder = injector.get(AssistedBuilder[NeedsAssistance]) obj = builder.build(b=123) - assert ((obj.a, obj.b) == (str(), 123)) + assert (obj.a, obj.b) == (str(), 123) def test_assisted_builder_works_when_injected(): @@ -758,7 +779,7 @@ def test_assisted_builder_works_when_injected(): injector = Injector() x = injector.get(X) - assert ((x.obj.a, x.obj.b) == (str(), 234)) + assert (x.obj.a, x.obj.b) == (str(), 234) def test_assisted_builder_uses_bindings(): @@ -770,7 +791,7 @@ def test_assisted_builder_uses_bindings(): injector = Injector(configure) builder = injector.get(AssistedBuilder[Interface]) x = builder.build(b=333) - assert ((type(x), x.b) == (NeedsAssistance, 333)) + assert (type(x), x.b) == (NeedsAssistance, 333) def test_assisted_builder_uses_concrete_class_when_specified(): @@ -795,7 +816,7 @@ def test_assisted_builder_injection_is_safe_to_use_with_multiple_injectors(): i1, i2 = Injector(), Injector() b1 = i1.get(X).builder b2 = i2.get(X).builder - assert ((b1._injector, b2._injector) == (i1, i2)) + assert (b1._injector, b2._injector) == (i1, i2) def test_assisted_builder_injection_uses_the_same_binding_key_every_time(): @@ -842,12 +863,12 @@ class TestThreadSafety: def test_injection_is_thread_safe(self): objects = self.gather_results(2) - assert (len(objects) == 2) + assert len(objects) == 2 def test_singleton_scope_is_thread_safe(self): self.injector.binder.bind(self.cls, scope=singleton) a, b = self.gather_results(2) - assert (a is b) + assert a is b def test_provider_and_scope_decorator_collaboration(): @@ -879,6 +900,7 @@ def test_injecting_into_method_of_object_that_is_falseish_works(): def test_injection_fails_when_injector_cant_install_itself_into_an_object_with_slots(): try: + class ClassName: __slots__ = () @@ -1008,6 +1030,7 @@ def test_special_interfaces_work_with_auto_bind_disabled(): def test_binding_an_instance_regression(): text = b'hello'.decode() + def configure(binder): # Yes, this binding doesn't make sense strictly speaking but # it's just a sample case. @@ -1046,12 +1069,12 @@ def test_implicit_injection_for_python3(): class B: @inject - def __init__(self, a:A): + def __init__(self, a: A): self.a = a class C: @inject - def __init__(self, b:B): + def __init__(self, b: B): self.b = b injector = Injector() @@ -1323,7 +1346,6 @@ def test_custom_scopes_work_as_expected_with_child_injectors(): # Test for https://github.com/alecthomas/injector/issues/75 def test_inject_decorator_does_not_break_manual_construction_of_pyqt_objects(): class PyQtFake: - @inject def __init__(self): pass @@ -1333,7 +1355,8 @@ def test_inject_decorator_does_not_break_manual_construction_of_pyqt_objects(): raise RuntimeError( 'A PyQt class would raise this exception if getting ' 'self.__injector__ before __init__ is called and ' - 'self.__injector__ has not been set by Injector.') + 'self.__injector__ has not been set by Injector.' + ) return object.__getattribute__(self, item) instance = PyQtFake() # This used to raise the exception diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..23fca96 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,4 @@ +[tool.black] +line-length = 110 +target_version = ['py36', 'py37'] +skip_string_normalization = true diff --git a/runtest.py b/runtest.py index baab46a..62f7a36 100644 --- a/runtest.py +++ b/runtest.py @@ -2413,6 +2413,7 @@ import base64 import zlib import imp + class DictImporter: def __init__(self, sources): self.sources = sources @@ -2427,6 +2428,7 @@ class DictImporter: def load_module(self, fullname): # print "load_module:", fullname from types import ModuleType + try: s = self.sources[fullname] is_pkg = False @@ -2451,14 +2453,17 @@ class DictImporter: res = self.sources.get(name + '.__init__') return res + if __name__ == "__main__": if sys.version_info >= (3, 0): exec("def do_exec(co, loc): exec(co, loc)\n") import pickle - sources = sources.encode("ascii") # ensure bytes + + sources = sources.encode("ascii") # ensure bytes sources = pickle.loads(zlib.decompress(base64.decodebytes(sources))) else: import pickle as pickle + exec("def do_exec(co, loc): exec co in loc\n") sources = pickle.loads(zlib.decompress(base64.decodestring(sources))) diff --git a/setup.py b/setup.py index 07751e6..ffe4e63 100644 --- a/setup.py +++ b/setup.py @@ -17,6 +17,7 @@ class PyTest(Command): def run(self): import subprocess + errno = subprocess.call([sys.executable, 'runtest.py']) raise SystemExit(errno) @@ -35,10 +36,10 @@ version_tag = read_injector_variable('__version_tag__') try: import pypandoc + long_description = pypandoc.convert('README.md', 'rst') except ImportError: - warnings.warn('Could not locate pandoc, using Markdown long_description.', - ImportWarning) + warnings.warn('Could not locate pandoc, using Markdown long_description.', ImportWarning) with open('README.md') as f: long_description = f.read() @@ -56,12 +57,16 @@ setup( license='BSD', platforms=['any'], packages=['injector'], - package_data = {'injector': ['py.typed']}, + package_data={'injector': ['py.typed']}, author='Alec Thomas', author_email='alec@swapoff.org', cmdclass={'test': PyTest}, keywords=[ - 'Dependency Injection', 'DI', 'Dependency Injection framework', - 'Inversion of Control', 'IoC', 'Inversion of Control container', + 'Dependency Injection', + 'DI', + 'Dependency Injection framework', + 'Inversion of Control', + 'IoC', + 'Inversion of Control container', ], )