is one of"
26 | @echo " html to make standalone HTML files"
27 | @echo " dirhtml to make HTML files named index.html in directories"
28 | @echo " singlehtml to make a single large HTML file"
29 | @echo " pickle to make pickle files"
30 | @echo " json to make JSON files"
31 | @echo " htmlhelp to make HTML files and a HTML help project"
32 | @echo " qthelp to make HTML files and a qthelp project"
33 | @echo " devhelp to make HTML files and a Devhelp project"
34 | @echo " epub to make an epub"
35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
36 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
38 | @echo " text to make text files"
39 | @echo " man to make manual pages"
40 | @echo " texinfo to make Texinfo files"
41 | @echo " info to make Texinfo files and run them through makeinfo"
42 | @echo " gettext to make PO message catalogs"
43 | @echo " changes to make an overview of all changed/added/deprecated items"
44 | @echo " xml to make Docutils-native XML files"
45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes"
46 | @echo " linkcheck to check all external links for integrity"
47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
48 |
49 | clean:
50 | rm -rf $(BUILDDIR)/*
51 |
52 | html:
53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
54 | @echo
55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
56 |
57 | dirhtml:
58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
59 | @echo
60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
61 |
62 | singlehtml:
63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
64 | @echo
65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
66 |
67 | pickle:
68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
69 | @echo
70 | @echo "Build finished; now you can process the pickle files."
71 |
72 | json:
73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
74 | @echo
75 | @echo "Build finished; now you can process the JSON files."
76 |
77 | htmlhelp:
78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
79 | @echo
80 | @echo "Build finished; now you can run HTML Help Workshop with the" \
81 | ".hhp project file in $(BUILDDIR)/htmlhelp."
82 |
83 | qthelp:
84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
85 | @echo
86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Injector.qhcp"
89 | @echo "To view the help file:"
90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Injector.qhc"
91 |
92 | devhelp:
93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
94 | @echo
95 | @echo "Build finished."
96 | @echo "To view the help file:"
97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Injector"
98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Injector"
99 | @echo "# devhelp"
100 |
101 | epub:
102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
103 | @echo
104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
105 |
106 | latex:
107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
108 | @echo
109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
111 | "(use \`make latexpdf' here to do that automatically)."
112 |
113 | latexpdf:
114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
115 | @echo "Running LaTeX files through pdflatex..."
116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
118 |
119 | latexpdfja:
120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
121 | @echo "Running LaTeX files through platex and dvipdfmx..."
122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
124 |
125 | text:
126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
127 | @echo
128 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
129 |
130 | man:
131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
132 | @echo
133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
134 |
135 | texinfo:
136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
137 | @echo
138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
139 | @echo "Run \`make' in that directory to run these through makeinfo" \
140 | "(use \`make info' here to do that automatically)."
141 |
142 | info:
143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
144 | @echo "Running Texinfo files through makeinfo..."
145 | make -C $(BUILDDIR)/texinfo info
146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
147 |
148 | gettext:
149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
150 | @echo
151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
152 |
153 | changes:
154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
155 | @echo
156 | @echo "The overview file is in $(BUILDDIR)/changes."
157 |
158 | linkcheck:
159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
160 | @echo
161 | @echo "Link check complete; look for any errors in the above output " \
162 | "or in $(BUILDDIR)/linkcheck/output.txt."
163 |
164 | doctest:
165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
166 | @echo "Testing of doctests in the sources finished, look at the " \
167 | "results in $(BUILDDIR)/doctest/output.txt."
168 |
169 | xml:
170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
171 | @echo
172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
173 |
174 | pseudoxml:
175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
176 | @echo
177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
178 |
--------------------------------------------------------------------------------
/docs/_templates/sidebar.html:
--------------------------------------------------------------------------------
1 | Injector
2 | Dependency Injection Framework
3 |
8 |
--------------------------------------------------------------------------------
/docs/api.rst:
--------------------------------------------------------------------------------
1 | Injector API reference
2 | ======================
3 |
4 | .. note::
5 |
6 | Unless specified otherwise, instance methods are **not** thread safe.
7 |
8 | The following functions are thread safe:
9 |
10 | * :meth:`Injector.get`
11 | * injection provided by :func:`inject` decorator (please note, however, that it doesn't say anything about decorated function thread safety)
12 |
13 |
14 | .. automodule:: injector
15 | :members:
16 | :undoc-members:
17 | :show-inheritance:
18 |
--------------------------------------------------------------------------------
/docs/changelog.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../CHANGES
2 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Injector documentation build configuration file, created by
4 | # sphinx-quickstart on Mon Sep 16 02:58:17 2013.
5 | #
6 | # This file is execfile()d with the current directory set to its containing dir.
7 | #
8 | # Note that not all possible configuration values are present in this
9 | # autogenerated file.
10 | #
11 | # All configuration values have a default; values that are commented out
12 | # serve to show the default.
13 |
14 | import injector
15 |
16 | # If extensions (or modules to document with autodoc) are in another directory,
17 | # add these directories to sys.path here. If the directory is relative to the
18 | # documentation root, use os.path.abspath to make it absolute, like shown here.
19 | # sys.path.insert(0, os.path.abspath('.'))
20 |
21 | # -- General configuration -----------------------------------------------------
22 |
23 | # If your documentation needs a minimal Sphinx version, state it here.
24 | # needs_sphinx = '1.0'
25 |
26 | # Add any Sphinx extension module names here, as strings. They can be extensions
27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
28 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx']
29 |
30 | # Add any paths that contain templates here, relative to this directory.
31 | templates_path = ['_templates']
32 |
33 | # The suffix of source filenames.
34 | source_suffix = '.rst'
35 |
36 | # The encoding of source files.
37 | # source_encoding = 'utf-8-sig'
38 |
39 | # The master toctree document.
40 | master_doc = 'index'
41 |
42 | # General information about the project.
43 | project = u'Injector'
44 | copyright = u'2013, Alec Thomas'
45 |
46 | # The version info for the project you're documenting, acts as replacement for
47 | # |version| and |release|, also used in various other places throughout the
48 | # built documents.
49 | #
50 | # The short X.Y version.
51 | version = injector.__version__
52 | # The full version, including alpha/beta/rc tags.
53 | release = version
54 |
55 | # The language for content autogenerated by Sphinx. Refer to documentation
56 | # for a list of supported languages.
57 | # language = None
58 |
59 | # There are two options for replacing |today|: either, you set today to some
60 | # non-false value, then it is used:
61 | # today = ''
62 | # Else, today_fmt is used as the format for a strftime call.
63 | # today_fmt = '%B %d, %Y'
64 |
65 | # List of patterns, relative to source directory, that match files and
66 | # directories to ignore when looking for source files.
67 | exclude_patterns = ['_build']
68 |
69 | # The reST default role (used for this markup: `text`) to use for all documents.
70 | # default_role = None
71 |
72 | # If true, '()' will be appended to :func: etc. cross-reference text.
73 | # add_function_parentheses = True
74 |
75 | # If true, the current module name will be prepended to all description
76 | # unit titles (such as .. function::).
77 | # add_module_names = True
78 |
79 | # If true, sectionauthor and moduleauthor directives will be shown in the
80 | # output. They are ignored by default.
81 | # show_authors = False
82 |
83 | # The name of the Pygments (syntax highlighting) style to use.
84 | pygments_style = 'sphinx'
85 |
86 | # A list of ignored prefixes for module index sorting.
87 | # modindex_common_prefix = []
88 |
89 | # If true, keep warnings as "system message" paragraphs in the built documents.
90 | # keep_warnings = False
91 |
92 |
93 | # -- Options for HTML output ---------------------------------------------------
94 |
95 | # The theme to use for HTML and HTML Help pages. See the documentation for
96 | # a list of builtin themes.
97 | html_theme = 'furo'
98 |
99 | # Theme options are theme-specific and customize the look and feel of a theme
100 | # further. For a list of options available for each theme, see the
101 | # documentation.
102 | # html_theme_options = {}
103 |
104 | # Add any paths that contain custom themes here, relative to this directory.
105 | # html_theme_path = []
106 |
107 | # The name for this set of Sphinx documents. If None, it defaults to
108 | # " v documentation".
109 | # html_title = None
110 |
111 | # A shorter title for the navigation bar. Default is the same as html_title.
112 | # html_short_title = None
113 |
114 | # The name of an image file (relative to this directory) to place at the top
115 | # of the sidebar.
116 | # html_logo = None
117 |
118 | # The name of an image file (within the static path) to use as favicon of the
119 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
120 | # pixels large.
121 | # html_favicon = None
122 |
123 | # Add any paths that contain custom static files (such as style sheets) here,
124 | # relative to this directory. They are copied after the builtin static files,
125 | # so a file named "default.css" will overwrite the builtin "default.css".
126 | html_static_path = ['_static']
127 |
128 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
129 | # using the given strftime format.
130 | # html_last_updated_fmt = '%b %d, %Y'
131 |
132 | # If true, SmartyPants will be used to convert quotes and dashes to
133 | # typographically correct entities.
134 | # html_use_smartypants = True
135 |
136 | # Additional templates that should be rendered to pages, maps page names to
137 | # template names.
138 | # html_additional_pages = {}
139 |
140 | # If false, no module index is generated.
141 | # html_domain_indices = True
142 |
143 | # If false, no index is generated.
144 | # html_use_index = True
145 |
146 | # If true, the index is split into individual pages for each letter.
147 | # html_split_index = False
148 |
149 | # If true, links to the reST sources are added to the pages.
150 | # html_show_sourcelink = True
151 |
152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
153 | # html_show_sphinx = True
154 |
155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
156 | # html_show_copyright = True
157 |
158 | # If true, an OpenSearch description file will be output, and all pages will
159 | # contain a tag referring to it. The value of this option must be the
160 | # base URL from which the finished HTML is served.
161 | # html_use_opensearch = ''
162 |
163 | # This is the file name suffix for HTML files (e.g. ".xhtml").
164 | # html_file_suffix = None
165 |
166 | # Output file base name for HTML help builder.
167 | htmlhelp_basename = 'Injectordoc'
168 |
169 |
170 | # -- Options for LaTeX output --------------------------------------------------
171 |
172 | latex_elements = {
173 | # The paper size ('letterpaper' or 'a4paper').
174 | #'papersize': 'letterpaper',
175 | # The font size ('10pt', '11pt' or '12pt').
176 | #'pointsize': '10pt',
177 | # Additional stuff for the LaTeX preamble.
178 | #'preamble': '',
179 | }
180 |
181 | # Grouping the document tree into LaTeX files. List of tuples
182 | # (source start file, target name, title, author, documentclass [howto/manual]).
183 | latex_documents = [('index', 'Injector.tex', u'Injector Documentation', u'Alec Thomas', 'manual')]
184 |
185 | # The name of an image file (relative to this directory) to place at the top of
186 | # the title page.
187 | # latex_logo = None
188 |
189 | # For "manual" documents, if this is true, then toplevel headings are parts,
190 | # not chapters.
191 | # latex_use_parts = False
192 |
193 | # If true, show page references after internal links.
194 | # latex_show_pagerefs = False
195 |
196 | # If true, show URL addresses after external links.
197 | # latex_show_urls = False
198 |
199 | # Documents to append as an appendix to all manuals.
200 | # latex_appendices = []
201 |
202 | # If false, no module index is generated.
203 | # latex_domain_indices = True
204 |
205 |
206 | # -- Options for manual page output --------------------------------------------
207 |
208 | # One entry per manual page. List of tuples
209 | # (source start file, name, description, authors, manual section).
210 | man_pages = [('index', 'injector', u'Injector Documentation', [u'Alec Thomas'], 1)]
211 |
212 | # If true, show URL addresses after external links.
213 | # man_show_urls = False
214 |
215 |
216 | # -- Options for Texinfo output ------------------------------------------------
217 |
218 | # Grouping the document tree into Texinfo files. List of tuples
219 | # (source start file, target name, title, author,
220 | # dir menu entry, description, category)
221 | texinfo_documents = [
222 | (
223 | 'index',
224 | 'Injector',
225 | u'Injector Documentation',
226 | u'Alec Thomas',
227 | 'Injector',
228 | 'One line description of project.',
229 | 'Miscellaneous',
230 | )
231 | ]
232 |
233 | # Documents to append as an appendix to all manuals.
234 | # texinfo_appendices = []
235 |
236 | # If false, no module index is generated.
237 | # texinfo_domain_indices = True
238 |
239 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
240 | # texinfo_show_urls = 'footnote'
241 |
242 | # If true, do not generate a @detailmenu in the "Top" node's menu.
243 | # texinfo_no_detailmenu = False
244 |
245 |
246 | # Example configuration for intersphinx: refer to the Python standard library.
247 | intersphinx_mapping = {'http://docs.python.org/': None}
248 |
249 |
250 | def setup(app):
251 | app.connect('autodoc-skip-member', skip_member)
252 |
253 |
254 | def skip_member(app, what, name, obj, skip, options):
255 | return (
256 | skip
257 | or getattr(obj, '__doc__', None) is None
258 | or getattr(obj, '__private__', False) is True
259 | or getattr(getattr(obj, '__func__', None), '__private__', False) is True
260 | )
261 |
--------------------------------------------------------------------------------
/docs/faq.rst:
--------------------------------------------------------------------------------
1 | .. _faq:
2 |
3 | Frequently Asked Questions
4 | ==========================
5 |
6 | If I use :func:`~injector.inject` or scope decorators on my classess will I be able to create instances of them without using Injector?
7 | ---------------------------------------------------------------------------------------------------------------------------------------
8 |
9 | Yes. Scope decorators don't change the way you can construct your class
10 | instances without Injector interaction.
11 |
12 | I'm calling this method (/function/class) but I'm getting "TypeError: XXX() takes exactly X arguments (Y given)"
13 | ----------------------------------------------------------------------------------------------------------------
14 |
15 | Example code:
16 |
17 | .. code-block:: python
18 |
19 | class X:
20 | @inject
21 | def __init__(self, s: str):
22 | self.s = s
23 |
24 | def configure(binder):
25 | binder.bind(s, to='some string')
26 |
27 | injector = Injector(configure)
28 | x = X()
29 |
30 | Result?
31 |
32 | ::
33 |
34 | TypeError: __init__() takes exactly 2 arguments (1 given)
35 |
36 | Reason? There's *no* global state that :class:`Injector` modifies when
37 | it's instantiated and configured. Its whole knowledge about bindings etc.
38 | is stored in itself. Moreover :func:`inject` will *not* make
39 | dependencies appear out of thin air when you for example attempt to create
40 | an instance of a class manually (without ``Injector``'s help) - there's no
41 | global state ``@inject`` decorated methods can access.
42 |
43 | In order for ``X`` to be able to use bindings defined in ``@inject``
44 | decoration :class:`Injector` needs to be used (directly or indirectly)
45 | to create an instance of ``X``. This means most of the time you want to just
46 | inject ``X`` where you need it, you can also use :meth:`Injector.get` to obtain
47 | an instance of the class (see its documentation for usage notes).
48 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. Injector documentation master file, created by
2 | sphinx-quickstart on Mon Sep 16 02:58:17 2013.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Welcome to Injector's documentation!
7 | ====================================
8 |
9 | .. image:: https://github.com/alecthomas/injector/workflows/CI/badge.svg
10 | :alt: Build status
11 | :target: https://github.com/alecthomas/injector/actions?query=workflow%3ACI+branch%3Amaster
12 |
13 | .. image:: https://codecov.io/gh/alecthomas/injector/branch/master/graph/badge.svg
14 | :alt: Covergage status
15 | :target: https://codecov.io/gh/alecthomas/injector
16 |
17 |
18 | GitHub (code repository, issues): https://github.com/alecthomas/injector
19 |
20 | PyPI (installable, stable distributions): https://pypi.org/project/injector. You can install Injector using pip::
21 |
22 | pip install injector
23 |
24 | Injector works with CPython 3.6+ and PyPy 3 implementing Python 3.6+.
25 |
26 | Introduction
27 | ------------
28 |
29 | While dependency injection is easy to do in Python due to its support for keyword arguments, the ease with which objects can be mocked and its dynamic natura, a framework for assisting in this process can remove a lot of boiler-plate from larger applications. That's where Injector can help. It automatically and transitively provides dependencies for you. As an added benefit, Injector encourages nicely compartmentalised code through the use of :ref:`modules `.
30 |
31 | If you're not sure what dependency injection is or you'd like to learn more about it see:
32 |
33 | * `The Clean Code Talks - Don't Look For Things! (a talk by Miško Hevery)
34 | `_
35 | * `Inversion of Control Containers and the Dependency Injection pattern (an article by Martin Fowler)
36 | `_
37 |
38 | The core values of Injector are:
39 |
40 | * Simplicity - while being inspired by Guice, Injector does not slavishly replicate its API.
41 | Providing a Pythonic API trumps faithfulness. Additionally some features are ommitted
42 | because supporting them would be cumbersome and introduce a little bit too much "magic"
43 | (member injection, method injection).
44 |
45 | Connected to this, Injector tries to be as nonintrusive as possible. For example while you may
46 | declare a class' constructor to expect some injectable parameters, the class' constructor
47 | remains a standard constructor – you may instaniate the class just the same manually, if you want.
48 |
49 | * No global state – you can have as many :class:`Injector` instances as you like, each with
50 | a different configuration and each with different objects in different scopes. Code like this
51 | won't work for this very reason::
52 |
53 | # This will NOT work:
54 |
55 | class MyClass:
56 | @inject
57 | def __init__(self, t: SomeType):
58 | # ...
59 |
60 | MyClass()
61 |
62 | This is simply because there's no global :class:`Injector` to use. You need to be explicit and use
63 | :meth:`Injector.get `,
64 | :meth:`Injector.create_object ` or inject `MyClass` into the place
65 | that needs it.
66 |
67 | * Cooperation with static type checking infrastructure – the API provides as much static type safety
68 | as possible and only breaks it where there's no other option. For example the
69 | :meth:`Injector.get ` method is typed such that `injector.get(SomeType)`
70 | is statically declared to return an instance of `SomeType`, therefore making it possible for tools
71 | such as `mypy `_ to type-check correctly the code using it.
72 |
73 | Quick start
74 | -----------
75 |
76 | See `the project's README `_ for an
77 | example of Injector use.
78 |
79 | Contents
80 | --------
81 |
82 | .. toctree::
83 | :maxdepth: 1
84 |
85 | changelog
86 | terminology
87 | testing
88 | scopes
89 | logging
90 | api
91 | faq
92 | practices
93 |
--------------------------------------------------------------------------------
/docs/logging.rst:
--------------------------------------------------------------------------------
1 | Logging
2 | =======
3 |
4 | Injector uses standard :mod:`logging` module, the logger name is ``injector``.
5 |
6 | By default ``injector`` logger is not configured to print logs anywhere.
7 |
8 | To enable ``get()`` tracing (and some other useful information) you need to set ``injector`` logger level to ``DEBUG``. You can do that by executing::
9 |
10 | import logging
11 |
12 | logging.getLogger('injector').setLevel(logging.DEBUG)
13 |
14 | or by configuring :mod:`logging` module in any other way.
15 |
--------------------------------------------------------------------------------
/docs/practices.rst:
--------------------------------------------------------------------------------
1 | .. _practices:
2 |
3 | Good and bad practices
4 | ======================
5 |
6 | Side effects
7 | ````````````
8 |
9 | You should avoid creating side effects in your modules for two reasons:
10 |
11 | * Side effects will make it more difficult to test a module if you want to do it
12 | * Modules expose a way to acquire some resource but they don't provide any way
13 | to release it. If, for example, your module connects to a remote server while
14 | creating a service you have no way of closing that connection unless the
15 | service exposes it.
16 |
17 |
18 | Injecting into constructors vs injecting into other methods
19 | ```````````````````````````````````````````````````````````
20 |
21 | .. note::
22 |
23 | Injector 0.11+ doesn't support injecting into non-constructor methods,
24 | this section is kept for historical reasons.
25 |
26 | .. note::
27 |
28 | Injector 0.11 deprecates using @inject with keyword arguments to declare
29 | bindings, this section remains unchanged for historical reasons.
30 |
31 | In general you should prefer injecting into constructors to injecting into
32 | other methods because:
33 |
34 | * it can expose potential issues earlier (at object construction time rather
35 | than at the method call)
36 | * it exposes class' dependencies more openly. Constructor injection:
37 |
38 | .. code-block:: python
39 |
40 | class Service1(object):
41 | @inject(http_client=HTTP)
42 | def __init__(self, http_client):
43 | self.http_client = http_client
44 | # some other code
45 |
46 | # tens or hundreds lines of code
47 |
48 | def method(self):
49 | # do something
50 | pass
51 |
52 | Regular method injection:
53 |
54 | .. code-block:: python
55 |
56 | class Service2(object):
57 | def __init__(self):
58 | # some other code
59 |
60 | # tens or hundreds lines of code
61 |
62 | @inject(http_client=HTTP)
63 | def method(self, http_client):
64 | # do something
65 | pass
66 |
67 |
68 | In first case you know all the dependencies by looking at the class'
69 | constructor, in the second you don't know about ``HTTP`` dependency until
70 | you see the method definition.
71 |
72 | Slightly different approach is suggested when it comes to Injector modules -
73 | in this case injecting into their constructors (or ``configure`` methods)
74 | would make the injection process dependent on the order of passing modules
75 | to Injector and therefore quite fragile. See this code sample:
76 |
77 | .. code-block:: python
78 |
79 | A = Key('A')
80 | B = Key('B')
81 |
82 | class ModuleA(Module):
83 | @inject(a=A)
84 | def configure(self, binder, a):
85 | pass
86 |
87 | class ModuleB(Module):
88 | @inject(b=B)
89 | def __init__(self, b):
90 | pass
91 |
92 | class ModuleC(Module):
93 | def configure(self, binder):
94 | binder.bind(A, to='a')
95 | binder.bind(B, to='b')
96 |
97 |
98 | # error, at the time of ModuleA processing A is unbound
99 | Injector([ModuleA, ModuleC])
100 |
101 | # error, at the time of ModuleB processing B is unbound
102 | Injector([ModuleB, ModuleC])
103 |
104 | # no error this time
105 | Injector([ModuleC, ModuleA, ModuleB])
106 |
107 |
108 | Doing too much in modules and/or providers
109 | ``````````````````````````````````````````
110 |
111 | An implementation detail of Injector: Injector and accompanying classes are
112 | protected by a lock to make them thread safe. This has a downside though:
113 | in general only one thread can use dependency injection at any given moment.
114 |
115 | In best case scenario you "only" slow other threads' dependency injection
116 | down. In worst case scenario (performing blocking calls without timeouts) you
117 | can **deadlock** whole application.
118 |
119 | **It is advised to avoid performing any IO, particularly without a timeout
120 | set, inside modules code.**
121 |
122 | As an illustration:
123 |
124 | .. code-block:: python
125 |
126 | from threading import Thread
127 | from time import sleep
128 |
129 | from injector import inject, Injector, Module, provider
130 |
131 | class A: pass
132 | class SubA(A): pass
133 | class B: pass
134 |
135 |
136 | class BadModule(Module):
137 | @provider
138 | def provide_a(self, suba: SubA) -> A:
139 | return suba
140 |
141 | @provider
142 | def provide_suba(self) -> SubA:
143 | print('Providing SubA...')
144 | while True:
145 | print('Sleeping...')
146 | sleep(1)
147 |
148 | # This never executes
149 | return SubA()
150 |
151 | @provider
152 | def provide_b(self) -> B:
153 | return B()
154 |
155 |
156 | injector = Injector([BadModule])
157 |
158 | thread = Thread(target=lambda: injector.get(A))
159 |
160 | # to make sure the thread doesn't keep the application alive
161 | thread.daemon = True
162 | thread.start()
163 |
164 | # This will never finish
165 | injector.get(B)
166 | print('Got B')
167 |
168 |
169 | Here's the output of the application::
170 |
171 | Providing SubA...
172 | Sleeping...
173 | Sleeping...
174 | Sleeping...
175 | (...)
176 |
177 |
178 | Injecting Injector and abusing Injector.get
179 | ```````````````````````````````````````````
180 |
181 | Sometimes code like this is written:
182 |
183 | .. code-block:: python
184 |
185 | class A:
186 | pass
187 |
188 | class B:
189 | pass
190 |
191 | class C:
192 | @inject
193 | def __init__(self, injector: Injector):
194 | self.a = injector.get(A)
195 | self.b = injector.get(B)
196 |
197 |
198 | It is advised to use the following pattern instead:
199 |
200 | .. code-block:: python
201 |
202 | class A:
203 | pass
204 |
205 | class B:
206 | pass
207 |
208 | class C:
209 | @inject
210 | def __init__(self, a: A, b: B):
211 | self.a = a
212 | self.b = b
213 |
214 |
215 | The second form has the benefits of:
216 |
217 | * expressing clearly what the dependencies of ``C`` are
218 | * making testing of the ``C`` class easier - you can provide the dependencies
219 | (whether they are mocks or not) directly, instead of having to mock
220 | :class:`Injector` and make the mock handle :meth:`Injector.get` calls
221 | * following the common practice and being easier to understand
222 |
223 |
224 | Injecting dependencies only to pass them somewhere else
225 | ```````````````````````````````````````````````````````
226 |
227 | A pattern similar to the one below can emerge:
228 |
229 | .. code-block:: python
230 |
231 | class A:
232 | pass
233 |
234 | class B:
235 | def __init__(self, a):
236 | self.a = a
237 |
238 | class C:
239 | @inject
240 | def __init__(self, a: A):
241 | self.b = B(a)
242 |
243 | Class ``C`` in this example has the responsibility of gathering dependencies of
244 | class ``B`` and constructing an object of type ``B``, there may be a valid reason
245 | for it but in general it defeats the purpose of using ``Injector`` and should
246 | be avoided.
247 |
248 | The appropriate pattern is:
249 |
250 | .. code-block:: python
251 |
252 | class A:
253 | pass
254 |
255 | class B:
256 | @inject
257 | def __init__(self, a: A):
258 | self.a = a
259 |
260 | class C:
261 | @inject
262 | def __init__(self, b: B):
263 | self.b = b
264 |
--------------------------------------------------------------------------------
/docs/scopes.rst:
--------------------------------------------------------------------------------
1 | .. _scopes:
2 |
3 | Scopes
4 | ======
5 |
6 | Singletons
7 | ``````````
8 |
9 | Singletons are declared by binding them in the SingletonScope. This can be done in three ways:
10 |
11 | 1. Decorating the class with `@singleton`.
12 | 2. Decorating a `@provider` decorated Module method with `@singleton`.
13 | 3. Explicitly calling `binder.bind(X, scope=singleton)`.
14 |
15 | A (redundant) example showing all three methods::
16 |
17 | @singleton
18 | class Thing: pass
19 | class ThingModule(Module):
20 | def configure(self, binder):
21 | binder.bind(Thing, scope=singleton)
22 | @singleton
23 | @provider
24 | def provide_thing(self) -> Thing:
25 | return Thing()
26 |
27 | If using hierarchies of injectors, classes decorated with `@singleton` will be created by and bound to the parent/ancestor injector closest to the root that can provide all of its dependencies.
28 |
29 | Implementing new Scopes
30 | ```````````````````````
31 |
32 | In the above description of scopes, we glossed over a lot of detail. In particular, how one would go about implementing our own scopes.
33 |
34 | Basically, there are two steps. First, subclass `Scope` and implement `Scope.get`::
35 |
36 | from injector import Scope
37 | class CustomScope(Scope):
38 | def get(self, key, provider):
39 | return provider
40 |
41 | Then create a global instance of :class:`ScopeDecorator` to allow classes to be easily annotated with your scope::
42 |
43 | from injector import ScopeDecorator
44 | customscope = ScopeDecorator(CustomScope)
45 |
46 | This can be used like so::
47 |
48 | @customscope
49 | class MyClass:
50 | pass
51 |
52 | Scopes are bound in modules with the :meth:`Binder.install` method::
53 |
54 | class MyModule(Module):
55 | def configure(self, binder):
56 | binder.install(CustomScope)
57 |
58 | Scopes can be retrieved from the injector, as with any other instance. They are singletons across the life of the injector::
59 |
60 | >>> injector = Injector([MyModule()])
61 | >>> injector.get(CustomScope) is injector.get(CustomScope)
62 | True
63 |
64 | For scopes with a transient lifetime, such as those tied to HTTP requests, the usual solution is to use a thread or greenlet-local cache inside the scope. The scope is "entered" in some low-level code by calling a method on the scope instance that creates this cache. Once the request is complete, the scope is "left" and the cache cleared.
65 |
--------------------------------------------------------------------------------
/docs/terminology.rst:
--------------------------------------------------------------------------------
1 | Terminology
2 | ===========
3 |
4 | At its heart, Injector is simply a dictionary for mapping types to things that create instances of those types. This could be as simple as::
5 |
6 | {str: 'an instance of a string'}
7 |
8 | For those new to dependency-injection and/or Guice, though, some of the terminology used may not be obvious.
9 |
10 | Provider
11 | ````````
12 |
13 | A means of providing an instance of a type. Built-in providers include:
14 |
15 | * :class:`~injector.ClassProvider` - creates a new instance from a class
16 | * :class:`~injector.InstanceProvider` - returns an existing instance directly
17 | * :class:`~injector.CallableProvider` - provides an instance by calling a function
18 |
19 | In order to create custom provider you need to subclass :class:`~injector.Provider` and override its :meth:`~injector.Provider.get` method.
20 |
21 | Scope
22 | `````
23 |
24 | By default, providers are executed each time an instance is required. Scopes allow this behaviour to be customised. For example, `SingletonScope` (typically used through the class decorator `singleton`), can be used to always provide the same instance of a class.
25 |
26 | Other examples of where scopes might be a threading scope, where instances are provided per-thread, or a request scope, where instances are provided per-HTTP-request.
27 |
28 | The default scope is :class:`NoScope`.
29 |
30 | .. seealso:: :ref:`scopes`
31 |
32 | Binding
33 | ```````
34 |
35 | A binding is the mapping of a unique binding key to a corresponding provider. For example::
36 |
37 | >>> from injector import InstanceProvider
38 | >>> bindings = {
39 | ... (Name, None): InstanceProvider('Sherlock'),
40 | ... (Description, None): InstanceProvider('A man of astounding insight'),
41 | ... }
42 |
43 |
44 | Binder
45 | ``````
46 |
47 | The `Binder` is simply a convenient wrapper around the dictionary that maps types to providers. It provides methods that make declaring bindings easier.
48 |
49 |
50 | .. _module:
51 |
52 | Module
53 | ``````
54 |
55 | A `Module` configures bindings. It provides methods that simplify the process of binding a key to a provider. For example the above bindings would be created with::
56 |
57 | >>> from injector import Module
58 | >>> class MyModule(Module):
59 | ... def configure(self, binder):
60 | ... binder.bind(Name, to='Sherlock')
61 | ... binder.bind(Description, to='A man of astounding insight')
62 |
63 | For more complex instance construction, methods decorated with `@provider` will be called to resolve binding keys::
64 |
65 | >>> from injector import provider
66 | >>> class MyModule(Module):
67 | ... def configure(self, binder):
68 | ... binder.bind(Name, to='Sherlock')
69 | ...
70 | ... @provider
71 | ... def describe(self) -> Description:
72 | ... return 'A man of astounding insight (at %s)' % time.time()
73 |
74 | Injection
75 | `````````
76 |
77 | Injection is the process of providing an instance of a type, to a method that uses that instance. It is achieved with the `inject` decorator. Keyword arguments to inject define which arguments in its decorated method should be injected, and with what.
78 |
79 | Here is an example of injection on a module provider method, and on the constructor of a normal class::
80 |
81 | from typing import NewType
82 |
83 | from injector import Binder, Module, inject, provider
84 |
85 | Name = NewType("Name", str)
86 | Description = NewType("Description", str)
87 |
88 | class User:
89 | @inject
90 | def __init__(self, name: Name, description: Description):
91 | self.name = name
92 | self.description = description
93 |
94 | class UserModule(Module):
95 | def configure(self, binder: Binder):
96 | binder.bind(User)
97 |
98 | class UserAttributeModule(Module):
99 | def configure(self, binder: Binder):
100 | binder.bind(Name, to='Sherlock')
101 |
102 | @provider
103 | def describe(self, name: Name) -> Description:
104 | return '%s is a man of astounding insight' % name
105 |
106 |
107 | Injector
108 | ````````
109 |
110 | The `Injector` brings everything together. It takes a list of `Module` s, and configures them with a binder, effectively creating a dependency graph::
111 |
112 | from injector import Injector
113 | injector = Injector([UserModule(), UserAttributeModule()])
114 |
115 | You can also pass classes instead of instances to `Injector`, it will instantiate them for you::
116 |
117 | injector = Injector([UserModule, UserAttributeModule])
118 |
119 | The injector can then be used to acquire instances of a type, either directly::
120 |
121 | >>> injector.get(Name)
122 | 'Sherlock'
123 | >>> injector.get(Description)
124 | 'Sherlock is a man of astounding insight'
125 |
126 | Or transitively::
127 |
128 | >>> user = injector.get(User)
129 | >>> isinstance(user, User)
130 | True
131 | >>> user.name
132 | 'Sherlock'
133 | >>> user.description
134 | 'Sherlock is a man of astounding insight'
135 |
136 | Assisted injection
137 | ``````````````````
138 |
139 | Sometimes there are classes that have injectable and non-injectable parameters in their constructors. Let's have for example::
140 |
141 | class Database: pass
142 |
143 |
144 | class User:
145 | def __init__(self, name):
146 | self.name = name
147 |
148 |
149 | class UserUpdater:
150 | def __init__(self, db: Database, user):
151 | pass
152 |
153 | You may want to have database connection `db` injected into `UserUpdater` constructor, but in the same time provide `user` object by yourself, and assuming that `user` object is a value object and there's many users in your application it doesn't make much sense to inject objects of class `User`.
154 |
155 | In this situation there's technique called Assisted injection::
156 |
157 | from injector import ClassAssistedBuilder
158 | injector = Injector()
159 | builder = injector.get(ClassAssistedBuilder[UserUpdater])
160 | user = User('John')
161 | user_updater = builder.build(user=user)
162 |
163 | This way we don't get `UserUpdater` directly but rather a builder object. Such builder has `build(**kwargs)` method which takes non-injectable parameters, combines them with injectable dependencies of `UserUpdater` and calls `UserUpdater` initializer using all of them.
164 |
165 | `AssistedBuilder[T]` and `ClassAssistedBuilder[T]` are injectable just as anything
166 | else, if you need instance of it you just ask for it like that::
167 |
168 | class NeedsUserUpdater:
169 | @inject
170 | def __init__(self, builder: ClassAssistedBuilder[UserUpdater]):
171 | self.updater_builder = builder
172 |
173 | def method(self):
174 | updater = self.updater_builder.build(user=None)
175 |
176 | `ClassAssistedBuilder` means it'll construct a concrete class and no bindings will be used.
177 |
178 | If you want to follow bindings and construct class pointed to by a key you use `AssistedBuilder` and can do it like this::
179 |
180 | >>> DB = Key('DB')
181 | >>> class DBImplementation:
182 | ... def __init__(self, uri):
183 | ... pass
184 | ...
185 | >>> def configure(binder):
186 | ... binder.bind(DB, to=DBImplementation)
187 | ...
188 | >>> injector = Injector(configure)
189 | >>> builder = injector.get(AssistedBuilder[DB])
190 | >>> isinstance(builder.build(uri='x'), DBImplementation)
191 | True
192 |
193 | More information on this topic:
194 |
195 | - `"How to use Google Guice to create objects that require parameters?" on Stack Overflow `_
196 | - `Google Guice assisted injection `_
197 |
198 |
199 | Child injectors
200 | ```````````````
201 |
202 | Concept similar to Guice's child injectors is supported by `Injector`. This way you can have one injector that inherits bindings from other injector (parent) but these bindings can be overriden in it and it doesn't affect parent injector bindings::
203 |
204 | >>> def configure_parent(binder):
205 | ... binder.bind(str, to='asd')
206 | ... binder.bind(int, to=42)
207 | ...
208 | >>> def configure_child(binder):
209 | ... binder.bind(str, to='qwe')
210 | ...
211 | >>> parent = Injector(configure_parent)
212 | >>> child = parent.create_child_injector(configure_child)
213 | >>> parent.get(str), parent.get(int)
214 | ('asd', 42)
215 | >>> child.get(str), child.get(int)
216 | ('qwe', 42)
217 |
218 | **Note**: Default scopes are bound only to root injector. Binding them manually to child injectors will result in unexpected behaviour. **Note 2**: Once a binding key is present in parent injector scope (like `singleton` scope), provider saved there takes predecence when binding is overridden in child injector in the same scope. This behaviour is subject to change::
219 |
220 |
221 | >>> def configure_parent(binder):
222 | ... binder.bind(str, to='asd', scope=singleton)
223 | ...
224 | >>> def configure_child(binder):
225 | ... binder.bind(str, to='qwe', scope=singleton)
226 | ...
227 | >>> parent = Injector(configure_parent)
228 | >>> child = parent.create_child_injector(configure_child)
229 | >>> child.get(str) # this behaves as expected
230 | 'qwe'
231 | >>> parent.get(str) # wat
232 | 'qwe'
233 |
--------------------------------------------------------------------------------
/docs/testing.rst:
--------------------------------------------------------------------------------
1 | Testing with Injector
2 | =====================
3 |
4 | When you use unit test framework such as `unittest2` or `nose` you can also profit from `injector`. ::
5 |
6 | import unittest
7 | from injector import Injector, Module
8 |
9 |
10 | class UsernameModule(Module):
11 | def configure(self, binder):
12 | binder.bind(str, 'Maria')
13 |
14 |
15 | class TestSomethingClass(unittest.TestCase):
16 |
17 | def setUp(self):
18 | self.__injector = Injector(UsernameModule())
19 |
20 | def test_username(self):
21 | username = self.__injector.get(str)
22 | self.assertEqual(username, 'Maria')
23 |
24 |
--------------------------------------------------------------------------------
/injector/__init__.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | #
3 | # Copyright (C) 2010 Alec Thomas
4 | # All rights reserved.
5 | #
6 | # This software is licensed as described in the file COPYING, which
7 | # you should have received as part of this distribution.
8 | #
9 | # Author: Alec Thomas
10 |
11 | """Injector - Python dependency injection framework, inspired by Guice
12 |
13 | :copyright: (c) 2012 by Alec Thomas
14 | :license: BSD
15 | """
16 |
17 | import functools
18 | import inspect
19 | import itertools
20 | import logging
21 | import sys
22 | import threading
23 | import types
24 | from abc import ABCMeta, abstractmethod
25 | from collections import namedtuple
26 | from typing import (
27 | Any,
28 | Callable,
29 | cast,
30 | Dict,
31 | Generic,
32 | Iterable,
33 | List,
34 | Optional,
35 | overload,
36 | Set,
37 | Tuple,
38 | Type,
39 | TypeVar,
40 | TYPE_CHECKING,
41 | Union,
42 | )
43 |
44 | try:
45 | from typing import NoReturn
46 | except ImportError:
47 | from typing_extensions import NoReturn
48 |
49 | # This is a messy, type-wise, because we not only have two potentially conflicting imports here
50 | # The easiest way to make mypy happy here is to tell it the versions from typing_extensions are
51 | # canonical. Since this typing_extensions import is only for mypy it'll work even without
52 | # typing_extensions actually installed so all's good.
53 | if TYPE_CHECKING:
54 | from typing_extensions import _AnnotatedAlias, Annotated, get_type_hints
55 | else:
56 | # Ignoring errors here as typing_extensions stub doesn't know about those things yet
57 | try:
58 | from typing import _AnnotatedAlias, Annotated, get_type_hints
59 | except ImportError:
60 | from typing_extensions import _AnnotatedAlias, Annotated, get_type_hints
61 |
62 |
63 | __author__ = 'Alec Thomas '
64 | __version__ = '0.22.0'
65 | __version_tag__ = ''
66 |
67 | log = logging.getLogger('injector')
68 | log.addHandler(logging.NullHandler())
69 |
70 | if log.level == logging.NOTSET:
71 | log.setLevel(logging.WARN)
72 |
73 | T = TypeVar('T')
74 | K = TypeVar('K')
75 | V = TypeVar('V')
76 |
77 |
78 | def private(something: T) -> T:
79 | something.__private__ = True # type: ignore
80 | return something
81 |
82 |
83 | CallableT = TypeVar('CallableT', bound=Callable)
84 |
85 |
86 | def synchronized(lock: threading.RLock) -> Callable[[CallableT], CallableT]:
87 | def outside_wrapper(function: CallableT) -> CallableT:
88 | @functools.wraps(function)
89 | def wrapper(*args: Any, **kwargs: Any) -> Any:
90 | with lock:
91 | return function(*args, **kwargs)
92 |
93 | return cast(CallableT, wrapper)
94 |
95 | return outside_wrapper
96 |
97 |
98 | lock = threading.RLock()
99 |
100 |
101 | _inject_marker = object()
102 | _noinject_marker = object()
103 |
104 | InjectT = TypeVar('InjectT')
105 | Inject = Annotated[InjectT, _inject_marker]
106 | """An experimental way to declare injectable dependencies utilizing a `PEP 593`_ implementation
107 | in Python 3.9 and backported to Python 3.7+ in `typing_extensions`.
108 |
109 | Those two declarations are equivalent::
110 |
111 | @inject
112 | def fun(t: SomeType) -> None:
113 | pass
114 |
115 | def fun(t: Inject[SomeType]) -> None:
116 | pass
117 |
118 | The advantage over using :func:`inject` is that if you have some noninjectable parameters
119 | it may be easier to spot what are they. Those two are equivalent::
120 |
121 | @inject
122 | @noninjectable('s')
123 | def fun(t: SomeType, s: SomeOtherType) -> None:
124 | pass
125 |
126 | def fun(t: Inject[SomeType], s: SomeOtherType) -> None:
127 | pass
128 |
129 | .. seealso::
130 |
131 | Function :func:`get_bindings`
132 | A way to inspect how various injection declarations interact with each other.
133 |
134 | .. versionadded:: 0.18.0
135 | .. note:: Requires Python 3.7+.
136 | .. note::
137 |
138 | If you're using mypy you need the version 0.750 or newer to fully type-check code using this
139 | construct.
140 |
141 | .. _PEP 593: https://www.python.org/dev/peps/pep-0593/
142 | .. _typing_extensions: https://pypi.org/project/typing-extensions/
143 | """
144 |
145 | NoInject = Annotated[InjectT, _noinject_marker]
146 | """An experimental way to declare noninjectable dependencies utilizing a `PEP 593`_ implementation
147 | in Python 3.9 and backported to Python 3.7+ in `typing_extensions`.
148 |
149 | Since :func:`inject` declares all function's parameters to be injectable there needs to be a way
150 | to opt out of it. This has been provided by :func:`noninjectable` but `noninjectable` suffers from
151 | two issues:
152 |
153 | * You need to repeat the parameter name
154 | * The declaration may be relatively distance in space from the actual parameter declaration, thus
155 | hindering readability
156 |
157 | `NoInject` solves both of those concerns, for example (those two declarations are equivalent)::
158 |
159 | @inject
160 | @noninjectable('b')
161 | def fun(a: TypeA, b: TypeB) -> None:
162 | pass
163 |
164 | @inject
165 | def fun(a: TypeA, b: NoInject[TypeB]) -> None:
166 | pass
167 |
168 | .. seealso::
169 |
170 | Function :func:`get_bindings`
171 | A way to inspect how various injection declarations interact with each other.
172 |
173 | .. versionadded:: 0.18.0
174 | .. note:: Requires Python 3.7+.
175 | .. note::
176 |
177 | If you're using mypy you need the version 0.750 or newer to fully type-check code using this
178 | construct.
179 |
180 | .. _PEP 593: https://www.python.org/dev/peps/pep-0593/
181 | .. _typing_extensions: https://pypi.org/project/typing-extensions/
182 | """
183 |
184 |
185 | def reraise(original: Exception, exception: Exception, maximum_frames: int = 1) -> NoReturn:
186 | prev_cls, prev, tb = sys.exc_info()
187 | frames = inspect.getinnerframes(cast(types.TracebackType, tb))
188 | if len(frames) > maximum_frames:
189 | exception = original
190 | raise exception.with_traceback(tb)
191 |
192 |
193 | class Error(Exception):
194 | """Base exception."""
195 |
196 |
197 | class UnsatisfiedRequirement(Error):
198 | """Requirement could not be satisfied."""
199 |
200 | def __init__(self, owner: Optional[object], interface: type) -> None:
201 | super().__init__(owner, interface)
202 | self.owner = owner
203 | self.interface = interface
204 |
205 | def __str__(self) -> str:
206 | on = '%s has an ' % _describe(self.owner) if self.owner else ''
207 | return '%sunsatisfied requirement on %s' % (on, _describe(self.interface))
208 |
209 |
210 | class CallError(Error):
211 | """Call to callable object fails."""
212 |
213 | def __str__(self) -> str:
214 | if len(self.args) == 1:
215 | return self.args[0]
216 |
217 | instance, method, args, kwargs, original_error, stack = self.args
218 | cls = instance.__class__.__name__ if instance is not None else ''
219 |
220 | full_method = '.'.join((cls, method.__name__)).strip('.')
221 |
222 | parameters = ', '.join(
223 | itertools.chain(
224 | (repr(arg) for arg in args), ('%s=%r' % (key, value) for (key, value) in kwargs.items())
225 | )
226 | )
227 | return 'Call to %s(%s) failed: %s (injection stack: %r)' % (
228 | full_method,
229 | parameters,
230 | original_error,
231 | [level[0] for level in stack],
232 | )
233 |
234 |
235 | class CircularDependency(Error):
236 | """Circular dependency detected."""
237 |
238 |
239 | class UnknownProvider(Error):
240 | """Tried to bind to a type whose provider couldn't be determined."""
241 |
242 |
243 | class UnknownArgument(Error):
244 | """Tried to mark an unknown argument as noninjectable."""
245 |
246 |
247 | class Provider(Generic[T]):
248 | """Provides class instances."""
249 |
250 | __metaclass__ = ABCMeta
251 |
252 | @abstractmethod
253 | def get(self, injector: 'Injector') -> T:
254 | raise NotImplementedError # pragma: no cover
255 |
256 |
257 | class ClassProvider(Provider, Generic[T]):
258 | """Provides instances from a given class, created using an Injector."""
259 |
260 | def __init__(self, cls: Type[T]) -> None:
261 | self._cls = cls
262 |
263 | def get(self, injector: 'Injector') -> T:
264 | return injector.create_object(self._cls)
265 |
266 |
267 | class CallableProvider(Provider, Generic[T]):
268 | """Provides something using a callable.
269 |
270 | The callable is called every time new value is requested from the provider.
271 |
272 | There's no need to explicitly use :func:`inject` or :data:`Inject` with the callable as it's
273 | assumed that, if the callable has annotated parameters, they're meant to be provided
274 | automatically. It wouldn't make sense any other way, as there's no mechanism to provide
275 | parameters to the callable at a later time, so either they'll be injected or there'll be
276 | a `CallError`.
277 |
278 | ::
279 |
280 | >>> class MyClass:
281 | ... def __init__(self, value: int) -> None:
282 | ... self.value = value
283 | ...
284 | >>> def factory():
285 | ... print('providing')
286 | ... return MyClass(42)
287 | ...
288 | >>> def configure(binder):
289 | ... binder.bind(MyClass, to=CallableProvider(factory))
290 | ...
291 | >>> injector = Injector(configure)
292 | >>> injector.get(MyClass) is injector.get(MyClass)
293 | providing
294 | providing
295 | False
296 | """
297 |
298 | def __init__(self, callable: Callable[..., T]):
299 | self._callable = callable
300 |
301 | def get(self, injector: 'Injector') -> T:
302 | return injector.call_with_injection(self._callable)
303 |
304 | def __repr__(self) -> str:
305 | return '%s(%r)' % (type(self).__name__, self._callable)
306 |
307 |
308 | class InstanceProvider(Provider, Generic[T]):
309 | """Provide a specific instance.
310 |
311 | ::
312 |
313 | >>> class MyType:
314 | ... def __init__(self):
315 | ... self.contents = []
316 | >>> def configure(binder):
317 | ... binder.bind(MyType, to=InstanceProvider(MyType()))
318 | ...
319 | >>> injector = Injector(configure)
320 | >>> injector.get(MyType) is injector.get(MyType)
321 | True
322 | >>> injector.get(MyType).contents.append('x')
323 | >>> injector.get(MyType).contents
324 | ['x']
325 | """
326 |
327 | def __init__(self, instance: T) -> None:
328 | self._instance = instance
329 |
330 | def get(self, injector: 'Injector') -> T:
331 | return self._instance
332 |
333 | def __repr__(self) -> str:
334 | return '%s(%r)' % (type(self).__name__, self._instance)
335 |
336 |
337 | @private
338 | class ListOfProviders(Provider, Generic[T]):
339 | """Provide a list of instances via other Providers."""
340 |
341 | _providers: List[Provider[T]]
342 |
343 | def __init__(self) -> None:
344 | self._providers = []
345 |
346 | def append(self, provider: Provider[T]) -> None:
347 | self._providers.append(provider)
348 |
349 | def __repr__(self) -> str:
350 | return '%s(%r)' % (type(self).__name__, self._providers)
351 |
352 |
353 | class MultiBindProvider(ListOfProviders[List[T]]):
354 | """Used by :meth:`Binder.multibind` to flatten results of providers that
355 | return sequences."""
356 |
357 | def get(self, injector: 'Injector') -> List[T]:
358 | return [i for provider in self._providers for i in provider.get(injector)]
359 |
360 |
361 | class MapBindProvider(ListOfProviders[Dict[str, T]]):
362 | """A provider for map bindings."""
363 |
364 | def get(self, injector: 'Injector') -> Dict[str, T]:
365 | map: Dict[str, T] = {}
366 | for provider in self._providers:
367 | map.update(provider.get(injector))
368 | return map
369 |
370 |
371 | _BindingBase = namedtuple('_BindingBase', 'interface provider scope')
372 |
373 |
374 | @private
375 | class Binding(_BindingBase):
376 | """A binding from an (interface,) to a provider in a scope."""
377 |
378 | def is_multibinding(self) -> bool:
379 | return _get_origin(_punch_through_alias(self.interface)) in {dict, list}
380 |
381 |
382 | @private
383 | class ImplicitBinding(Binding):
384 | """A binding that was created implicitly by auto-binding."""
385 |
386 | pass
387 |
388 |
389 | _InstallableModuleType = Union[Callable[['Binder'], None], 'Module', Type['Module']]
390 |
391 |
392 | class Binder:
393 | """Bind interfaces to implementations.
394 |
395 | .. note:: This class is instantiated internally for you and there's no need
396 | to instantiate it on your own.
397 | """
398 |
399 | _bindings: Dict[type, Binding]
400 |
401 | @private
402 | def __init__(
403 | self, injector: 'Injector', auto_bind: bool = True, parent: Optional['Binder'] = None
404 | ) -> None:
405 | """Create a new Binder.
406 |
407 | :param injector: Injector we are binding for.
408 | :param auto_bind: Whether to automatically bind missing types.
409 | :param parent: Parent binder.
410 | """
411 | self.injector = injector
412 | self._auto_bind = auto_bind
413 | self._bindings = {}
414 | self.parent = parent
415 |
416 | def bind(
417 | self,
418 | interface: Type[T],
419 | to: Union[None, T, Callable[..., T], Provider[T]] = None,
420 | scope: Union[None, Type['Scope'], 'ScopeDecorator'] = None,
421 | ) -> None:
422 | """Bind an interface to an implementation.
423 |
424 | Binding `T` to an instance of `T` like
425 |
426 | ::
427 |
428 | binder.bind(A, to=A('some', 'thing'))
429 |
430 | is, for convenience, a shortcut for
431 |
432 | ::
433 |
434 | binder.bind(A, to=InstanceProvider(A('some', 'thing'))).
435 |
436 | Likewise, binding to a callable like
437 |
438 | ::
439 |
440 | binder.bind(A, to=some_callable)
441 |
442 | is a shortcut for
443 |
444 | ::
445 |
446 | binder.bind(A, to=CallableProvider(some_callable))
447 |
448 | and, as such, if `some_callable` there has any annotated parameters they'll be provided
449 | automatically without having to use :func:`inject` or :data:`Inject` with the callable.
450 |
451 | `typing.List` and `typing.Dict` instances are reserved for multibindings and trying to bind them
452 | here will result in an error (use :meth:`multibind` instead)::
453 |
454 | binder.bind(List[str], to=['hello', 'there']) # Error
455 |
456 | :param interface: Type to bind.
457 | :param to: Instance or class to bind to, or an instance of
458 | :class:`Provider` subclass.
459 | :param scope: Optional :class:`Scope` in which to bind.
460 | """
461 | if _get_origin(_punch_through_alias(interface)) in {dict, list}:
462 | raise Error(
463 | 'Type %s is reserved for multibindings. Use multibind instead of bind.' % (interface,)
464 | )
465 | self._bindings[interface] = self.create_binding(interface, to, scope)
466 |
467 | @overload
468 | def multibind(
469 | self,
470 | interface: Type[List[T]],
471 | to: Union[List[T], Callable[..., List[T]], Provider[List[T]]],
472 | scope: Union[Type['Scope'], 'ScopeDecorator', None] = None,
473 | ) -> None: # pragma: no cover
474 | pass
475 |
476 | @overload
477 | def multibind(
478 | self,
479 | interface: Type[Dict[K, V]],
480 | to: Union[Dict[K, V], Callable[..., Dict[K, V]], Provider[Dict[K, V]]],
481 | scope: Union[Type['Scope'], 'ScopeDecorator', None] = None,
482 | ) -> None: # pragma: no cover
483 | pass
484 |
485 | def multibind(
486 | self, interface: type, to: Any, scope: Union['ScopeDecorator', Type['Scope'], None] = None
487 | ) -> None:
488 | """Creates or extends a multi-binding.
489 |
490 | A multi-binding contributes values to a list or to a dictionary. For example::
491 |
492 | binder.multibind(List[str], to=['some', 'strings'])
493 | binder.multibind(List[str], to=['other', 'strings'])
494 | injector.get(List[str]) # ['some', 'strings', 'other', 'strings']
495 |
496 | binder.multibind(Dict[str, int], to={'key': 11})
497 | binder.multibind(Dict[str, int], to={'other_key': 33})
498 | injector.get(Dict[str, int]) # {'key': 11, 'other_key': 33}
499 |
500 | .. versionchanged:: 0.17.0
501 | Added support for using `typing.Dict` and `typing.List` instances as interfaces.
502 | Deprecated support for `MappingKey`, `SequenceKey` and single-item lists and
503 | dictionaries as interfaces.
504 |
505 | :param interface: typing.Dict or typing.List instance to bind to.
506 | :param to: Instance, class to bind to, or an explicit :class:`Provider`
507 | subclass. Must provide a list or a dictionary, depending on the interface.
508 | :param scope: Optional Scope in which to bind.
509 | """
510 | if interface not in self._bindings:
511 | provider: ListOfProviders
512 | if (
513 | isinstance(interface, dict)
514 | or isinstance(interface, type)
515 | and issubclass(interface, dict)
516 | or _get_origin(_punch_through_alias(interface)) is dict
517 | ):
518 | provider = MapBindProvider()
519 | else:
520 | provider = MultiBindProvider()
521 | binding = self.create_binding(interface, provider, scope)
522 | self._bindings[interface] = binding
523 | else:
524 | binding = self._bindings[interface]
525 | provider = binding.provider
526 | assert isinstance(provider, ListOfProviders)
527 | provider.append(self.provider_for(interface, to))
528 |
529 | def install(self, module: _InstallableModuleType) -> None:
530 | """Install a module into this binder.
531 |
532 | In this context the module is one of the following:
533 |
534 | * function taking the :class:`Binder` as its only parameter
535 |
536 | ::
537 |
538 | def configure(binder):
539 | bind(str, to='s')
540 |
541 | binder.install(configure)
542 |
543 | * instance of :class:`Module` (instance of its subclass counts)
544 |
545 | ::
546 |
547 | class MyModule(Module):
548 | def configure(self, binder):
549 | binder.bind(str, to='s')
550 |
551 | binder.install(MyModule())
552 |
553 | * subclass of :class:`Module` - the subclass needs to be instantiable so if it
554 | expects any parameters they need to be injected
555 |
556 | ::
557 |
558 | binder.install(MyModule)
559 | """
560 | if type(module) is type and issubclass(cast(type, module), Module):
561 | instance = cast(type, module)()
562 | else:
563 | instance = module
564 | instance(self)
565 |
566 | def create_binding(
567 | self, interface: type, to: Any = None, scope: Union['ScopeDecorator', Type['Scope'], None] = None
568 | ) -> Binding:
569 | provider = self.provider_for(interface, to)
570 | scope = scope or getattr(to or interface, '__scope__', NoScope)
571 | if isinstance(scope, ScopeDecorator):
572 | scope = scope.scope
573 | return Binding(interface, provider, scope)
574 |
575 | def provider_for(self, interface: Any, to: Any = None) -> Provider:
576 | base_type = _punch_through_alias(interface)
577 | origin = _get_origin(base_type)
578 |
579 | if interface is Any:
580 | raise TypeError('Injecting Any is not supported')
581 | elif _is_specialization(interface, ProviderOf):
582 | (target,) = interface.__args__
583 | if to is not None:
584 | raise Exception('ProviderOf cannot be bound to anything')
585 | return InstanceProvider(ProviderOf(self.injector, target))
586 | elif isinstance(to, Provider):
587 | return to
588 | elif isinstance(
589 | to,
590 | (
591 | types.FunctionType,
592 | types.LambdaType,
593 | types.MethodType,
594 | types.BuiltinFunctionType,
595 | types.BuiltinMethodType,
596 | ),
597 | ):
598 | return CallableProvider(to)
599 | elif issubclass(type(to), type):
600 | return ClassProvider(cast(type, to))
601 | elif isinstance(interface, BoundKey):
602 |
603 | def proxy(injector: Injector) -> Any:
604 | binder = injector.binder
605 | kwarg_providers = {
606 | name: binder.provider_for(None, provider) for (name, provider) in interface.kwargs.items()
607 | }
608 | kwargs = {name: provider.get(injector) for (name, provider) in kwarg_providers.items()}
609 | return interface.interface(**kwargs)
610 |
611 | return CallableProvider(inject(proxy))
612 | elif _is_specialization(interface, AssistedBuilder):
613 | (target,) = interface.__args__
614 | builder = interface(self.injector, target)
615 | return InstanceProvider(builder)
616 | elif (
617 | origin is None
618 | and isinstance(base_type, (tuple, type))
619 | and interface is not Any
620 | and isinstance(to, base_type)
621 | or origin in {dict, list}
622 | and isinstance(to, origin)
623 | ):
624 | return InstanceProvider(to)
625 | elif issubclass(type(base_type), type) or isinstance(base_type, (tuple, list)):
626 | if to is not None:
627 | return InstanceProvider(to)
628 | return ClassProvider(base_type)
629 |
630 | else:
631 | raise UnknownProvider('couldn\'t determine provider for %r to %r' % (interface, to))
632 |
633 | def _get_binding(self, key: type, *, only_this_binder: bool = False) -> Tuple[Binding, 'Binder']:
634 | binding = self._bindings.get(key)
635 | if binding:
636 | return binding, self
637 | if self.parent and not only_this_binder:
638 | return self.parent._get_binding(key)
639 |
640 | raise KeyError
641 |
642 | def get_binding(self, interface: type) -> Tuple[Binding, 'Binder']:
643 | is_scope = isinstance(interface, type) and issubclass(interface, Scope)
644 | is_assisted_builder = _is_specialization(interface, AssistedBuilder)
645 | try:
646 | return self._get_binding(interface, only_this_binder=is_scope or is_assisted_builder)
647 | except (KeyError, UnsatisfiedRequirement):
648 | if is_scope:
649 | scope = interface
650 | self.bind(scope, to=scope(self.injector))
651 | return self._get_binding(interface)
652 | # The special interface is added here so that requesting a special
653 | # interface with auto_bind disabled works
654 | if self._auto_bind or self._is_special_interface(interface):
655 | binding = ImplicitBinding(*self.create_binding(interface))
656 | self._bindings[interface] = binding
657 | return binding, self
658 |
659 | raise UnsatisfiedRequirement(None, interface)
660 |
661 | def has_binding_for(self, interface: type) -> bool:
662 | return interface in self._bindings
663 |
664 | def has_explicit_binding_for(self, interface: type) -> bool:
665 | return self.has_binding_for(interface) and not isinstance(self._bindings[interface], ImplicitBinding)
666 |
667 | def _is_special_interface(self, interface: type) -> bool:
668 | # "Special" interfaces are ones that you cannot bind yourself but
669 | # you can request them (for example you cannot bind ProviderOf(SomeClass)
670 | # to anything but you can inject ProviderOf(SomeClass) just fine
671 | return any(_is_specialization(interface, cls) for cls in [AssistedBuilder, ProviderOf])
672 |
673 |
674 | def _is_specialization(cls: type, generic_class: Any) -> bool:
675 | # Starting with typing 3.5.3/Python 3.6 it is no longer necessarily true that
676 | # issubclass(SomeGeneric[X], SomeGeneric) so we need some other way to
677 | # determine whether a particular object is a generic class with type parameters
678 | # provided. Fortunately there seems to be __origin__ attribute that's useful here.
679 |
680 | # We need to special-case Annotated as its __origin__ behaves differently than
681 | # other typing generic classes. See https://github.com/python/typing/pull/635
682 | # for some details.
683 | if generic_class is Annotated and isinstance(cls, _AnnotatedAlias):
684 | return True
685 |
686 | if not hasattr(cls, '__origin__'):
687 | return False
688 | origin = cast(Any, cls).__origin__
689 | if not inspect.isclass(generic_class):
690 | generic_class = type(generic_class)
691 | if not inspect.isclass(origin):
692 | origin = type(origin)
693 | # __origin__ is generic_class is a special case to handle Union as
694 | # Union cannot be used in issubclass() check (it raises an exception
695 | # by design).
696 | return origin is generic_class or issubclass(origin, generic_class)
697 |
698 |
699 | def _punch_through_alias(type_: Any) -> type:
700 | if (
701 | sys.version_info < (3, 10)
702 | and getattr(type_, '__qualname__', '') == 'NewType..new_type'
703 | or sys.version_info >= (3, 10)
704 | and type(type_).__module__ == 'typing'
705 | and type(type_).__name__ == 'NewType'
706 | ):
707 | return type_.__supertype__
708 | elif isinstance(type_, _AnnotatedAlias) and getattr(type_, '__metadata__', None) is not None:
709 | return type_.__origin__
710 | else:
711 | return type_
712 |
713 |
714 | def _get_origin(type_: type) -> Optional[type]:
715 | origin = getattr(type_, '__origin__', None)
716 | # Older typing behaves differently there and stores Dict and List as origin, we need to be flexible.
717 | if origin is List:
718 | return list
719 | elif origin is Dict:
720 | return dict
721 | return origin
722 |
723 |
724 | class Scope:
725 | """A Scope looks up the Provider for a binding.
726 |
727 | By default (ie. :class:`NoScope` ) this simply returns the default
728 | :class:`Provider` .
729 | """
730 |
731 | __metaclass__ = ABCMeta
732 |
733 | def __init__(self, injector: 'Injector') -> None:
734 | self.injector = injector
735 | self.configure()
736 |
737 | def configure(self) -> None:
738 | """Configure the scope."""
739 |
740 | @abstractmethod
741 | def get(self, key: Type[T], provider: Provider[T]) -> Provider[T]:
742 | """Get a :class:`Provider` for a key.
743 |
744 | :param key: The key to return a provider for.
745 | :param provider: The default Provider associated with the key.
746 | :returns: A Provider instance that can provide an instance of key.
747 | """
748 | raise NotImplementedError # pragma: no cover
749 |
750 |
751 | class ScopeDecorator:
752 | def __init__(self, scope: Type[Scope]) -> None:
753 | self.scope = scope
754 |
755 | def __call__(self, cls: T) -> T:
756 | cast(Any, cls).__scope__ = self.scope
757 | binding = getattr(cls, '__binding__', None)
758 | if binding:
759 | new_binding = Binding(interface=binding.interface, provider=binding.provider, scope=self.scope)
760 | setattr(cls, '__binding__', new_binding)
761 | return cls
762 |
763 | def __repr__(self) -> str:
764 | return 'ScopeDecorator(%s)' % self.scope.__name__
765 |
766 |
767 | class NoScope(Scope):
768 | """An unscoped provider."""
769 |
770 | def get(self, unused_key: Type[T], provider: Provider[T]) -> Provider[T]:
771 | return provider
772 |
773 |
774 | noscope = ScopeDecorator(NoScope)
775 |
776 |
777 | class SingletonScope(Scope):
778 | """A :class:`Scope` that returns a per-Injector instance for a key.
779 |
780 | :data:`singleton` can be used as a convenience class decorator.
781 |
782 | >>> class A: pass
783 | >>> injector = Injector()
784 | >>> provider = ClassProvider(A)
785 | >>> singleton = SingletonScope(injector)
786 | >>> a = singleton.get(A, provider)
787 | >>> b = singleton.get(A, provider)
788 | >>> a is b
789 | True
790 | """
791 |
792 | _context: Dict[type, Provider]
793 |
794 | def configure(self) -> None:
795 | self._context = {}
796 |
797 | @synchronized(lock)
798 | def get(self, key: Type[T], provider: Provider[T]) -> Provider[T]:
799 | try:
800 | return self._context[key]
801 | except KeyError:
802 | instance = self._get_instance(key, provider, self.injector)
803 | provider = InstanceProvider(instance)
804 | self._context[key] = provider
805 | return provider
806 |
807 | def _get_instance(self, key: Type[T], provider: Provider[T], injector: 'Injector') -> T:
808 | if injector.parent and not injector.binder.has_explicit_binding_for(key):
809 | try:
810 | return self._get_instance_from_parent(key, provider, injector.parent)
811 | except (CallError, UnsatisfiedRequirement):
812 | pass
813 | return provider.get(injector)
814 |
815 | def _get_instance_from_parent(self, key: Type[T], provider: Provider[T], parent: 'Injector') -> T:
816 | singleton_scope_binding, _ = parent.binder.get_binding(type(self))
817 | singleton_scope = singleton_scope_binding.provider.get(parent)
818 | provider = singleton_scope.get(key, provider)
819 | return provider.get(parent)
820 |
821 |
822 | singleton = ScopeDecorator(SingletonScope)
823 |
824 |
825 | class ThreadLocalScope(Scope):
826 | """A :class:`Scope` that returns a per-thread instance for a key."""
827 |
828 | def configure(self) -> None:
829 | self._locals = threading.local()
830 |
831 | def get(self, key: Type[T], provider: Provider[T]) -> Provider[T]:
832 | try:
833 | return getattr(self._locals, repr(key))
834 | except AttributeError:
835 | provider = InstanceProvider(provider.get(self.injector))
836 | setattr(self._locals, repr(key), provider)
837 | return provider
838 |
839 |
840 | threadlocal = ScopeDecorator(ThreadLocalScope)
841 |
842 |
843 | class Module:
844 | """Configures injector and providers."""
845 |
846 | def __call__(self, binder: Binder) -> None:
847 | """Configure the binder."""
848 | self.__injector__ = binder.injector
849 | for unused_name, function in inspect.getmembers(self, inspect.ismethod):
850 | binding = None
851 | if hasattr(function, '__binding__'):
852 | binding = function.__binding__
853 | if binding.interface == '__deferred__':
854 | # We could not evaluate a forward reference at @provider-decoration time, we need to
855 | # try again now.
856 | try:
857 | annotations = get_type_hints(function)
858 | except NameError as e:
859 | raise NameError(
860 | 'Cannot avaluate forward reference annotation(s) in method %r belonging to %r: %s'
861 | % (function.__name__, type(self), e)
862 | ) from e
863 | return_type = annotations['return']
864 | binding = cast(Any, function.__func__).__binding__ = Binding(
865 | interface=return_type, provider=binding.provider, scope=binding.scope
866 | )
867 | bind_method = binder.multibind if binding.is_multibinding() else binder.bind
868 | bind_method( # type: ignore
869 | binding.interface, to=types.MethodType(binding.provider, self), scope=binding.scope
870 | )
871 | self.configure(binder)
872 |
873 | def configure(self, binder: Binder) -> None:
874 | """Override to configure bindings."""
875 |
876 |
877 | class Injector:
878 | """
879 | :param modules: Optional - a configuration module or iterable of configuration modules.
880 | Each module will be installed in current :class:`Binder` using :meth:`Binder.install`.
881 |
882 | Consult :meth:`Binder.install` documentation for the details.
883 |
884 | :param auto_bind: Whether to automatically bind missing types.
885 | :param parent: Parent injector.
886 |
887 | .. versionadded:: 0.7.5
888 | ``use_annotations`` parameter
889 |
890 | .. versionchanged:: 0.13.0
891 | ``use_annotations`` parameter is removed
892 | """
893 |
894 | _stack: Tuple[Tuple[object, Callable, Tuple[Tuple[str, type], ...]], ...]
895 | binder: Binder
896 |
897 | def __init__(
898 | self,
899 | modules: Union[_InstallableModuleType, Iterable[_InstallableModuleType], None] = None,
900 | auto_bind: bool = True,
901 | parent: Optional['Injector'] = None,
902 | ) -> None:
903 | # Stack of keys currently being injected. Used to detect circular
904 | # dependencies.
905 | self._stack = ()
906 |
907 | self.parent = parent
908 |
909 | # Binder
910 | self.binder = Binder(self, auto_bind=auto_bind, parent=parent.binder if parent is not None else None)
911 |
912 | if not modules:
913 | modules = []
914 | elif not hasattr(modules, '__iter__'):
915 | modules = [cast(_InstallableModuleType, modules)]
916 | # This line is needed to pelase mypy. We know we have Iteable of modules here.
917 | modules = cast(Iterable[_InstallableModuleType], modules)
918 |
919 | # Bind some useful types
920 | self.binder.bind(Injector, to=self)
921 | self.binder.bind(Binder, to=self.binder)
922 |
923 | # Initialise modules
924 | for module in modules:
925 | self.binder.install(module)
926 |
927 | @property
928 | def _log_prefix(self) -> str:
929 | return '>' * (len(self._stack) + 1) + ' '
930 |
931 | @synchronized(lock)
932 | def get(self, interface: Type[T], scope: Union[ScopeDecorator, Type[Scope], None] = None) -> T:
933 | """Get an instance of the given interface.
934 |
935 | .. note::
936 |
937 | Although this method is part of :class:`Injector`'s public interface
938 | it's meant to be used in limited set of circumstances.
939 |
940 | For example, to create some kind of root object (application object)
941 | of your application (note that only one `get` call is needed,
942 | inside the `Application` class and any of its dependencies
943 | :func:`inject` can and should be used):
944 |
945 | .. code-block:: python
946 |
947 | class Application:
948 |
949 | @inject
950 | def __init__(self, dep1: Dep1, dep2: Dep2):
951 | self.dep1 = dep1
952 | self.dep2 = dep2
953 |
954 | def run(self):
955 | self.dep1.something()
956 |
957 | injector = Injector(configuration)
958 | application = injector.get(Application)
959 | application.run()
960 |
961 | :param interface: Interface whose implementation we want.
962 | :param scope: Class of the Scope in which to resolve.
963 | :returns: An implementation of interface.
964 | """
965 | binding, binder = self.binder.get_binding(interface)
966 | scope = scope or binding.scope
967 | if isinstance(scope, ScopeDecorator):
968 | scope = scope.scope
969 | # Fetch the corresponding Scope instance from the Binder.
970 | scope_binding, _ = binder.get_binding(scope)
971 | scope_instance = scope_binding.provider.get(self)
972 |
973 | log.debug(
974 | '%sInjector.get(%r, scope=%r) using %r', self._log_prefix, interface, scope, binding.provider
975 | )
976 | provider_instance = scope_instance.get(interface, binding.provider)
977 | result = provider_instance.get(self)
978 | log.debug('%s -> %r', self._log_prefix, result)
979 | return result
980 |
981 | def create_child_injector(self, *args: Any, **kwargs: Any) -> 'Injector':
982 | kwargs['parent'] = self
983 | return Injector(*args, **kwargs)
984 |
985 | def create_object(self, cls: Type[T], additional_kwargs: Any = None) -> T:
986 | """Create a new instance, satisfying any dependencies on cls."""
987 | additional_kwargs = additional_kwargs or {}
988 | log.debug('%sCreating %r object with %r', self._log_prefix, cls, additional_kwargs)
989 |
990 | try:
991 | instance = cls.__new__(cls)
992 | except TypeError as e:
993 | reraise(
994 | e,
995 | CallError(cls, getattr(cls.__new__, '__func__', cls.__new__), (), {}, e, self._stack),
996 | maximum_frames=2,
997 | )
998 | init = cls.__init__
999 | try:
1000 | self.call_with_injection(init, self_=instance, kwargs=additional_kwargs)
1001 | except TypeError as e:
1002 | # Mypy says "Cannot access "__init__" directly"
1003 | init_function = instance.__init__.__func__ # type: ignore
1004 | reraise(e, CallError(instance, init_function, (), additional_kwargs, e, self._stack))
1005 | return instance
1006 |
1007 | def call_with_injection(
1008 | self, callable: Callable[..., T], self_: Any = None, args: Any = (), kwargs: Any = {}
1009 | ) -> T:
1010 | """Call a callable and provide its dependencies if needed.
1011 |
1012 | Dependencies are provided when the callable is decorated with :func:`@inject `
1013 | or some individual parameters are wrapped in :data:`Inject` – otherwise
1014 | ``call_with_injection()`` is equivalent to just calling the callable directly.
1015 |
1016 | If there is an overlap between arguments provided in ``args`` and ``kwargs``
1017 | and injectable dependencies the provided values take precedence and no dependency
1018 | injection process will take place for the corresponding parameters.
1019 |
1020 | :param self_: Instance of a class callable belongs to if it's a method,
1021 | None otherwise.
1022 | :param args: Arguments to pass to callable.
1023 | :param kwargs: Keyword arguments to pass to callable.
1024 | :type callable: callable
1025 | :type args: tuple of objects
1026 | :type kwargs: dict of string -> object
1027 | :return: Value returned by callable.
1028 | """
1029 |
1030 | bindings = get_bindings(callable)
1031 | signature = inspect.signature(callable)
1032 | full_args = args
1033 | if self_ is not None:
1034 | full_args = (self_,) + full_args
1035 | bound_arguments = signature.bind_partial(*full_args)
1036 |
1037 | needed = dict(
1038 | (k, v) for (k, v) in bindings.items() if k not in kwargs and k not in bound_arguments.arguments
1039 | )
1040 |
1041 | dependencies = self.args_to_inject(
1042 | function=callable,
1043 | bindings=needed,
1044 | owner_key=self_.__class__ if self_ is not None else callable.__module__,
1045 | )
1046 |
1047 | dependencies.update(kwargs)
1048 |
1049 | try:
1050 | return callable(*full_args, **dependencies)
1051 | except TypeError as e:
1052 | reraise(e, CallError(self_, callable, args, dependencies, e, self._stack))
1053 | # Needed because of a mypy-related issue (https://github.com/python/mypy/issues/8129).
1054 | assert False, "unreachable" # pragma: no cover
1055 |
1056 | @private
1057 | @synchronized(lock)
1058 | def args_to_inject(
1059 | self, function: Callable, bindings: Dict[str, type], owner_key: object
1060 | ) -> Dict[str, Any]:
1061 | """Inject arguments into a function.
1062 |
1063 | :param function: The function.
1064 | :param bindings: Map of argument name to binding key to inject.
1065 | :param owner_key: A key uniquely identifying the *scope* of this function.
1066 | For a method this will be the owning class.
1067 | :returns: Dictionary of resolved arguments.
1068 | """
1069 | dependencies = {}
1070 |
1071 | key = (owner_key, function, tuple(sorted(bindings.items())))
1072 |
1073 | def repr_key(k: Tuple[object, Callable, Tuple[Tuple[str, type], ...]]) -> str:
1074 | owner_key, function, bindings = k
1075 | return '%s.%s(injecting %s)' % (tuple(map(_describe, k[:2])) + (dict(k[2]),))
1076 |
1077 | log.debug('%sProviding %r for %r', self._log_prefix, bindings, function)
1078 |
1079 | if key in self._stack:
1080 | raise CircularDependency(
1081 | 'circular dependency detected: %s -> %s'
1082 | % (' -> '.join(map(repr_key, self._stack)), repr_key(key))
1083 | )
1084 |
1085 | self._stack += (key,)
1086 | try:
1087 | for arg, interface in bindings.items():
1088 | try:
1089 | instance: Any = self.get(interface)
1090 | except UnsatisfiedRequirement as e:
1091 | if not e.owner:
1092 | e = UnsatisfiedRequirement(owner_key, e.interface)
1093 | raise e
1094 | dependencies[arg] = instance
1095 | finally:
1096 | self._stack = tuple(self._stack[:-1])
1097 |
1098 | return dependencies
1099 |
1100 |
1101 | def get_bindings(callable: Callable) -> Dict[str, type]:
1102 | """Get bindings of injectable parameters from a callable.
1103 |
1104 | If the callable is not decorated with :func:`inject` and does not have any of its
1105 | parameters declared as injectable using :data:`Inject` an empty dictionary will
1106 | be returned. Otherwise the returned dictionary will contain a mapping
1107 | between parameter names and their types with the exception of parameters
1108 | excluded from dependency injection (either with :func:`noninjectable`, :data:`NoInject`
1109 | or only explicit injection with :data:`Inject` being used). For example::
1110 |
1111 | >>> def function1(a: int) -> None:
1112 | ... pass
1113 | ...
1114 | >>> get_bindings(function1)
1115 | {}
1116 |
1117 | >>> @inject
1118 | ... def function2(a: int) -> None:
1119 | ... pass
1120 | ...
1121 | >>> get_bindings(function2)
1122 | {'a': }
1123 |
1124 | >>> @inject
1125 | ... @noninjectable('b')
1126 | ... def function3(a: int, b: str) -> None:
1127 | ... pass
1128 | ...
1129 | >>> get_bindings(function3)
1130 | {'a': }
1131 |
1132 | >>> # The simple case of no @inject but injection requested with Inject[...]
1133 | >>> def function4(a: Inject[int], b: str) -> None:
1134 | ... pass
1135 | ...
1136 | >>> get_bindings(function4)
1137 | {'a': }
1138 |
1139 | >>> # Using @inject with Inject is redundant but it should not break anything
1140 | >>> @inject
1141 | ... def function5(a: Inject[int], b: str) -> None:
1142 | ... pass
1143 | ...
1144 | >>> get_bindings(function5)
1145 | {'a': , 'b': }
1146 |
1147 | >>> # We need to be able to exclude a parameter from injection with NoInject
1148 | >>> @inject
1149 | ... def function6(a: int, b: NoInject[str]) -> None:
1150 | ... pass
1151 | ...
1152 | >>> get_bindings(function6)
1153 | {'a': }
1154 |
1155 | >>> # The presence of NoInject should not trigger anything on its own
1156 | >>> def function7(a: int, b: NoInject[str]) -> None:
1157 | ... pass
1158 | ...
1159 | >>> get_bindings(function7)
1160 | {}
1161 |
1162 | This function is used internally so by calling it you can learn what exactly
1163 | Injector is going to try to provide to a callable.
1164 | """
1165 | look_for_explicit_bindings = False
1166 | if not hasattr(callable, '__bindings__'):
1167 | type_hints = get_type_hints(callable, include_extras=True)
1168 | has_injectable_parameters = any(
1169 | _is_specialization(v, Annotated) and _inject_marker in v.__metadata__ for v in type_hints.values()
1170 | )
1171 |
1172 | if not has_injectable_parameters:
1173 | return {}
1174 | else:
1175 | look_for_explicit_bindings = True
1176 |
1177 | if look_for_explicit_bindings or cast(Any, callable).__bindings__ == 'deferred':
1178 | read_and_store_bindings(
1179 | callable, _infer_injected_bindings(callable, only_explicit_bindings=look_for_explicit_bindings)
1180 | )
1181 | noninjectables: Set[str] = getattr(callable, '__noninjectables__', set())
1182 | return {k: v for k, v in cast(Any, callable).__bindings__.items() if k not in noninjectables}
1183 |
1184 |
1185 | class _BindingNotYetAvailable(Exception):
1186 | pass
1187 |
1188 |
1189 | # See a comment in _infer_injected_bindings() for why this is useful.
1190 | class _NoReturnAnnotationProxy:
1191 | def __init__(self, callable: Callable) -> None:
1192 | self.callable = callable
1193 |
1194 | def __getattribute__(self, name: str) -> Any:
1195 | # get_type_hints() uses quite complex logic to determine the namespaces using which
1196 | # any forward references should be resolved. Instead of mirroring this logic here
1197 | # let's just take the easy way out and forward all attribute access to the original
1198 | # callable except for the annotations – we want to filter them.
1199 | callable = object.__getattribute__(self, 'callable')
1200 | if name == '__annotations__':
1201 | annotations = callable.__annotations__
1202 | return {name: value for (name, value) in annotations.items() if name != 'return'}
1203 | return getattr(callable, name)
1204 |
1205 |
1206 | def _infer_injected_bindings(callable: Callable, only_explicit_bindings: bool) -> Dict[str, type]:
1207 | def _is_new_union_type(instance: Any) -> bool:
1208 | new_union_type = getattr(types, 'UnionType', None)
1209 | return new_union_type is not None and isinstance(instance, new_union_type)
1210 |
1211 | spec = inspect.getfullargspec(callable)
1212 |
1213 | try:
1214 | # Return types don't matter for the purpose of dependency injection so instead of
1215 | # obtaining type hints of the callable directly let's wrap it in _NoReturnAnnotationProxy.
1216 | # The proxy removes the return type annotation (if present) from the annotations so that
1217 | # get_type_hints() works even if the return type is a forward reference that can't be
1218 | # resolved.
1219 | bindings = get_type_hints(cast(Callable, _NoReturnAnnotationProxy(callable)), include_extras=True)
1220 | except NameError as e:
1221 | raise _BindingNotYetAvailable(e)
1222 |
1223 | # We don't care about the return value annotation as it doesn't matter
1224 | # injection-wise.
1225 | bindings.pop('return', None)
1226 |
1227 | # If we're dealing with a bound method get_type_hints will still return `self` annotation even though
1228 | # it's already provided and we're not really interested in its type. So – drop it.
1229 | if isinstance(callable, types.MethodType):
1230 | self_name = spec.args[0]
1231 | bindings.pop(self_name, None)
1232 |
1233 | # variadic arguments aren't supported at the moment (this may change
1234 | # in the future if someone has a good idea how to utilize them)
1235 | if spec.varargs:
1236 | bindings.pop(spec.varargs, None)
1237 | if spec.varkw:
1238 | bindings.pop(spec.varkw, None)
1239 |
1240 | for k, v in list(bindings.items()):
1241 | if _is_specialization(v, Annotated):
1242 | v, metadata = v.__origin__, v.__metadata__
1243 | bindings[k] = v
1244 | else:
1245 | metadata = tuple()
1246 |
1247 | if only_explicit_bindings and _inject_marker not in metadata or _noinject_marker in metadata:
1248 | del bindings[k]
1249 | elif _is_specialization(v, Union) or _is_new_union_type(v):
1250 | # We don't treat Optional parameters in any special way at the moment.
1251 | union_members = v.__args__
1252 | new_members = tuple(set(union_members) - {type(None)})
1253 | # mypy stared complaining about this line for some reason:
1254 | # error: Variable "new_members" is not valid as a type
1255 | new_union = Union[new_members] # type: ignore
1256 | # mypy complains about this construct:
1257 | # error: The type alias is invalid in runtime context
1258 | # See: https://github.com/python/mypy/issues/5354
1259 | union_metadata = {
1260 | metadata
1261 | for member in new_members
1262 | for metadata in getattr(member, '__metadata__', tuple())
1263 | if _is_specialization(member, Annotated)
1264 | }
1265 | if (
1266 | only_explicit_bindings
1267 | and _inject_marker not in union_metadata
1268 | or _noinject_marker in union_metadata
1269 | ):
1270 | del bindings[k]
1271 | else:
1272 | bindings[k] = new_union # type: ignore
1273 |
1274 | return bindings
1275 |
1276 |
1277 | def provider(function: CallableT) -> CallableT:
1278 | """Decorator for :class:`Module` methods, registering a provider of a type.
1279 |
1280 | >>> class MyModule(Module):
1281 | ... @provider
1282 | ... def provide_name(self) -> str:
1283 | ... return 'Bob'
1284 |
1285 | @provider-decoration implies @inject so you can omit it and things will
1286 | work just the same:
1287 |
1288 | >>> class MyModule2(Module):
1289 | ... def configure(self, binder):
1290 | ... binder.bind(int, to=654)
1291 | ...
1292 | ... @provider
1293 | ... def provide_str(self, i: int) -> str:
1294 | ... return str(i)
1295 | ...
1296 | >>> injector = Injector(MyModule2)
1297 | >>> injector.get(str)
1298 | '654'
1299 | """
1300 | _mark_provider_function(function, allow_multi=False)
1301 | return function
1302 |
1303 |
1304 | def multiprovider(function: CallableT) -> CallableT:
1305 | """Like :func:`provider`, but for multibindings. Example usage::
1306 |
1307 | class MyModule(Module):
1308 | @multiprovider
1309 | def provide_strs(self) -> List[str]:
1310 | return ['str1']
1311 |
1312 | class OtherModule(Module):
1313 | @multiprovider
1314 | def provide_strs_also(self) -> List[str]:
1315 | return ['str2']
1316 |
1317 | Injector([MyModule, OtherModule]).get(List[str]) # ['str1', 'str2']
1318 |
1319 | See also: :meth:`Binder.multibind`."""
1320 | _mark_provider_function(function, allow_multi=True)
1321 | return function
1322 |
1323 |
1324 | def _mark_provider_function(function: Callable, *, allow_multi: bool) -> None:
1325 | scope_ = getattr(function, '__scope__', None)
1326 | try:
1327 | annotations = get_type_hints(function)
1328 | except NameError:
1329 | return_type = '__deferred__'
1330 | else:
1331 | return_type = annotations['return']
1332 | _validate_provider_return_type(function, cast(type, return_type), allow_multi)
1333 | function.__binding__ = Binding(return_type, inject(function), scope_) # type: ignore
1334 |
1335 |
1336 | def _validate_provider_return_type(function: Callable, return_type: type, allow_multi: bool) -> None:
1337 | origin = _get_origin(_punch_through_alias(return_type))
1338 | if origin in {dict, list} and not allow_multi:
1339 | raise Error(
1340 | 'Function %s needs to be decorated with multiprovider instead of provider if it is to '
1341 | 'provide values to a multibinding of type %s' % (function.__name__, return_type)
1342 | )
1343 |
1344 |
1345 | ConstructorOrClassT = TypeVar('ConstructorOrClassT', bound=Union[Callable, Type])
1346 |
1347 |
1348 | @overload
1349 | def inject(constructor_or_class: CallableT) -> CallableT: # pragma: no cover
1350 | pass
1351 |
1352 |
1353 | @overload
1354 | def inject(constructor_or_class: Type[T]) -> Type[T]: # pragma: no cover
1355 | pass
1356 |
1357 |
1358 | def inject(constructor_or_class: ConstructorOrClassT) -> ConstructorOrClassT:
1359 | """Decorator declaring parameters to be injected.
1360 |
1361 | eg.
1362 |
1363 | >>> class A:
1364 | ... @inject
1365 | ... def __init__(self, number: int, name: str):
1366 | ... print([number, name])
1367 | ...
1368 | >>> def configure(binder):
1369 | ... binder.bind(A)
1370 | ... binder.bind(int, to=123)
1371 | ... binder.bind(str, to='Bob')
1372 |
1373 | Use the Injector to get a new instance of A:
1374 |
1375 | >>> a = Injector(configure).get(A)
1376 | [123, 'Bob']
1377 |
1378 | As a convenience one can decorate a class itself::
1379 |
1380 | @inject
1381 | class B:
1382 | def __init__(self, dependency: Dependency):
1383 | self.dependency = dependency
1384 |
1385 | This is equivalent to decorating its constructor. In particular this provides integration with
1386 | `dataclasses `_ (the order of decorator
1387 | application is important here)::
1388 |
1389 | @inject
1390 | @dataclass
1391 | class C:
1392 | dependency: Dependency
1393 |
1394 | .. note::
1395 |
1396 | This decorator is to be used on class constructors (or, as a convenience, on classes).
1397 | Using it on non-constructor methods worked in the past but it was an implementation
1398 | detail rather than a design decision.
1399 |
1400 | Third party libraries may, however, provide support for injecting dependencies
1401 | into non-constructor methods or free functions in one form or another.
1402 |
1403 | .. seealso::
1404 |
1405 | Generic type :data:`Inject`
1406 | A more explicit way to declare parameters as injectable.
1407 |
1408 | Function :func:`get_bindings`
1409 | A way to inspect how various injection declarations interact with each other.
1410 |
1411 | .. versionchanged:: 0.16.2
1412 |
1413 | (Re)added support for decorating classes with @inject.
1414 | """
1415 | if isinstance(constructor_or_class, type) and hasattr(constructor_or_class, '__init__'):
1416 | inject(cast(Any, constructor_or_class).__init__)
1417 | else:
1418 | function = constructor_or_class
1419 | try:
1420 | bindings = _infer_injected_bindings(function, only_explicit_bindings=False)
1421 | read_and_store_bindings(function, bindings)
1422 | except _BindingNotYetAvailable:
1423 | cast(Any, function).__bindings__ = 'deferred'
1424 | return constructor_or_class
1425 |
1426 |
1427 | def noninjectable(*args: str) -> Callable[[CallableT], CallableT]:
1428 | """Mark some parameters as not injectable.
1429 |
1430 | This serves as documentation for people reading the code and will prevent
1431 | Injector from ever attempting to provide the parameters.
1432 |
1433 | For example:
1434 |
1435 | >>> class Service:
1436 | ... pass
1437 | ...
1438 | >>> class SomeClass:
1439 | ... @inject
1440 | ... @noninjectable('user_id')
1441 | ... def __init__(self, service: Service, user_id: int):
1442 | ... # ...
1443 | ... pass
1444 |
1445 | :func:`noninjectable` decorations can be stacked on top of
1446 | each other and the order in which a function is decorated with
1447 | :func:`inject` and :func:`noninjectable`
1448 | doesn't matter.
1449 |
1450 | .. seealso::
1451 |
1452 | Generic type :data:`NoInject`
1453 | A nicer way to declare parameters as noninjectable.
1454 |
1455 | Function :func:`get_bindings`
1456 | A way to inspect how various injection declarations interact with each other.
1457 |
1458 | """
1459 |
1460 | def decorator(function: CallableT) -> CallableT:
1461 | argspec = inspect.getfullargspec(inspect.unwrap(function))
1462 | for arg in args:
1463 | if arg not in argspec.args and arg not in argspec.kwonlyargs:
1464 | raise UnknownArgument('Unable to mark unknown argument %s ' 'as non-injectable.' % arg)
1465 |
1466 | existing: Set[str] = getattr(function, '__noninjectables__', set())
1467 | merged = existing | set(args)
1468 | cast(Any, function).__noninjectables__ = merged
1469 | return function
1470 |
1471 | return decorator
1472 |
1473 |
1474 | @private
1475 | def read_and_store_bindings(f: Callable, bindings: Dict[str, type]) -> None:
1476 | function_bindings = getattr(f, '__bindings__', None) or {}
1477 | if function_bindings == 'deferred':
1478 | function_bindings = {}
1479 | merged_bindings = dict(function_bindings, **bindings)
1480 |
1481 | if hasattr(f, '__func__'):
1482 | f = cast(Any, f).__func__
1483 | cast(Any, f).__bindings__ = merged_bindings
1484 |
1485 |
1486 | class BoundKey(tuple):
1487 | """A BoundKey provides a key to a type with pre-injected arguments.
1488 |
1489 | >>> class A:
1490 | ... def __init__(self, a, b):
1491 | ... self.a = a
1492 | ... self.b = b
1493 | >>> InjectedA = BoundKey(A, a=InstanceProvider(1), b=InstanceProvider(2))
1494 | >>> injector = Injector()
1495 | >>> a = injector.get(InjectedA)
1496 | >>> a.a, a.b
1497 | (1, 2)
1498 | """
1499 |
1500 | def __new__(cls, interface: Type[T], **kwargs: Any) -> 'BoundKey':
1501 | kwargs_tuple = tuple(sorted(kwargs.items()))
1502 | return super(BoundKey, cls).__new__(cls, (interface, kwargs_tuple)) # type: ignore
1503 |
1504 | @property
1505 | def interface(self) -> Type[T]:
1506 | return self[0]
1507 |
1508 | @property
1509 | def kwargs(self) -> Dict[str, Any]:
1510 | return dict(self[1])
1511 |
1512 |
1513 | class AssistedBuilder(Generic[T]):
1514 | def __init__(self, injector: Injector, target: Type[T]) -> None:
1515 | self._injector = injector
1516 | self._target = target
1517 |
1518 | def build(self, **kwargs: Any) -> T:
1519 | binder = self._injector.binder
1520 | binding, _ = binder.get_binding(self._target)
1521 | provider = binding.provider
1522 | if not isinstance(provider, ClassProvider):
1523 | raise Error(
1524 | 'Assisted interface building works only with ClassProviders, '
1525 | 'got %r for %r' % (provider, binding.interface)
1526 | )
1527 |
1528 | return self._build_class(cast(Type[T], provider._cls), **kwargs)
1529 |
1530 | def _build_class(self, cls: Type[T], **kwargs: Any) -> T:
1531 | return self._injector.create_object(cls, additional_kwargs=kwargs)
1532 |
1533 |
1534 | class ClassAssistedBuilder(AssistedBuilder[T]):
1535 | def build(self, **kwargs: Any) -> T:
1536 | return self._build_class(self._target, **kwargs)
1537 |
1538 |
1539 | def _describe(c: Any) -> str:
1540 | if hasattr(c, '__name__'):
1541 | return cast(str, c.__name__)
1542 | if type(c) in (tuple, list):
1543 | return '[%s]' % c[0].__name__
1544 | return str(c)
1545 |
1546 |
1547 | class ProviderOf(Generic[T]):
1548 | """Can be used to get a provider of an interface, for example:
1549 |
1550 | >>> def provide_int():
1551 | ... print('providing')
1552 | ... return 123
1553 | >>>
1554 | >>> def configure(binder):
1555 | ... binder.bind(int, to=provide_int)
1556 | >>>
1557 | >>> injector = Injector(configure)
1558 | >>> provider = injector.get(ProviderOf[int])
1559 | >>> value = provider.get()
1560 | providing
1561 | >>> value
1562 | 123
1563 | """
1564 |
1565 | def __init__(self, injector: Injector, interface: Type[T]):
1566 | self._injector = injector
1567 | self._interface = interface
1568 |
1569 | def __repr__(self) -> str:
1570 | return '%s(%r, %r)' % (type(self).__name__, self._injector, self._interface)
1571 |
1572 | def get(self) -> T:
1573 | """Get an implementation for the specified interface."""
1574 | return self._injector.get(self._interface)
1575 |
1576 |
1577 | def is_decorated_with_inject(function: Callable[..., Any]) -> bool:
1578 | """See if given callable is declared to want some dependencies injected.
1579 |
1580 | Example use:
1581 |
1582 | >>> def fun(i: int) -> str:
1583 | ... return str(i)
1584 |
1585 | >>> is_decorated_with_inject(fun)
1586 | False
1587 | >>>
1588 | >>> @inject
1589 | ... def fun2(i: int) -> str:
1590 | ... return str(i)
1591 |
1592 | >>> is_decorated_with_inject(fun2)
1593 | True
1594 | """
1595 | return hasattr(function, '__bindings__')
1596 |
--------------------------------------------------------------------------------
/injector/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/python-injector/injector/a7f0bc4deec968f4ba40dad55458f585a99c6adf/injector/py.typed
--------------------------------------------------------------------------------
/injector_test.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | #
3 | # Copyright (C) 2010 Alec Thomas
4 | # All rights reserved.
5 | #
6 | # This software is licensed as described in the file COPYING, which
7 | # you should have received as part of this distribution.
8 | #
9 | # Author: Alec Thomas
10 |
11 | """Functional tests for the "Injector" dependency injection framework."""
12 |
13 | from contextlib import contextmanager
14 | from typing import Any, NewType, Optional, Union
15 | import abc
16 | import sys
17 | import threading
18 | import traceback
19 | import warnings
20 |
21 | if sys.version_info >= (3, 9):
22 | from typing import Annotated
23 | else:
24 | from typing_extensions import Annotated
25 |
26 | from typing import Dict, List, NewType
27 |
28 | import pytest
29 |
30 | from injector import (
31 | Binder,
32 | CallError,
33 | Inject,
34 | Injector,
35 | NoInject,
36 | Scope,
37 | InstanceProvider,
38 | ClassProvider,
39 | get_bindings,
40 | inject,
41 | multiprovider,
42 | noninjectable,
43 | singleton,
44 | threadlocal,
45 | UnsatisfiedRequirement,
46 | CircularDependency,
47 | Module,
48 | SingletonScope,
49 | ScopeDecorator,
50 | AssistedBuilder,
51 | provider,
52 | ProviderOf,
53 | ClassAssistedBuilder,
54 | Error,
55 | UnknownArgument,
56 | )
57 |
58 |
59 | class EmptyClass:
60 | pass
61 |
62 |
63 | class DependsOnEmptyClass:
64 | @inject
65 | def __init__(self, b: EmptyClass):
66 | """Construct a new DependsOnEmptyClass."""
67 | self.b = b
68 |
69 |
70 | def prepare_nested_injectors():
71 | def configure(binder):
72 | binder.bind(str, to='asd')
73 |
74 | parent = Injector(configure)
75 | child = parent.create_child_injector()
76 | return parent, child
77 |
78 |
79 | def check_exception_contains_stuff(exception, stuff):
80 | stringified = str(exception)
81 |
82 | for thing in stuff:
83 | assert thing in stringified, '%r should be present in the exception representation: %s' % (
84 | thing,
85 | stringified,
86 | )
87 |
88 |
89 | def test_child_injector_inherits_parent_bindings():
90 | parent, child = prepare_nested_injectors()
91 | assert child.get(str) == parent.get(str)
92 |
93 |
94 | def test_child_injector_overrides_parent_bindings():
95 | parent, child = prepare_nested_injectors()
96 | child.binder.bind(str, to='qwe')
97 |
98 | assert (parent.get(str), child.get(str)) == ('asd', 'qwe')
99 |
100 |
101 | def test_child_injector_rebinds_arguments_for_parent_scope():
102 | class Cls:
103 | val = ""
104 |
105 | class A(Cls):
106 | @inject
107 | def __init__(self, val: str):
108 | self.val = val
109 |
110 | def configure_parent(binder):
111 | binder.bind(Cls, to=A)
112 | binder.bind(str, to="Parent")
113 |
114 | def configure_child(binder):
115 | binder.bind(str, to="Child")
116 |
117 | parent = Injector(configure_parent)
118 | assert parent.get(Cls).val == "Parent"
119 | child = parent.create_child_injector(configure_child)
120 | assert child.get(Cls).val == "Child"
121 |
122 |
123 | def test_scopes_are_only_bound_to_root_injector():
124 | parent, child = prepare_nested_injectors()
125 |
126 | class A:
127 | pass
128 |
129 | parent.binder.bind(A, to=A, scope=singleton)
130 | assert parent.get(A) is child.get(A)
131 |
132 |
133 | def test_get_default_injected_instances():
134 | def configure(binder):
135 | binder.bind(DependsOnEmptyClass)
136 | binder.bind(EmptyClass)
137 |
138 | injector = Injector(configure)
139 | assert injector.get(Injector) is injector
140 | assert injector.get(Binder) is injector.binder
141 |
142 |
143 | def test_instantiate_injected_method():
144 | a = DependsOnEmptyClass('Bob')
145 | assert a.b == 'Bob'
146 |
147 |
148 | def test_method_decorator_is_wrapped():
149 | assert DependsOnEmptyClass.__init__.__doc__ == 'Construct a new DependsOnEmptyClass.'
150 | assert DependsOnEmptyClass.__init__.__name__ == '__init__'
151 |
152 |
153 | def test_decorator_works_for_function_with_no_args():
154 | @inject
155 | def wrapped(*args, **kwargs):
156 | pass
157 |
158 |
159 | def test_providers_arent_called_for_dependencies_that_are_already_provided():
160 | def configure(binder):
161 | binder.bind(int, to=lambda: 1 / 0)
162 |
163 | class A:
164 | @inject
165 | def __init__(self, i: int):
166 | pass
167 |
168 | injector = Injector(configure)
169 | builder = injector.get(AssistedBuilder[A])
170 |
171 | with pytest.raises(ZeroDivisionError):
172 | builder.build()
173 |
174 | builder.build(i=3)
175 |
176 |
177 | def test_inject_direct():
178 | def configure(binder):
179 | binder.bind(DependsOnEmptyClass)
180 | binder.bind(EmptyClass)
181 |
182 | injector = Injector(configure)
183 | a = injector.get(DependsOnEmptyClass)
184 | assert isinstance(a, DependsOnEmptyClass)
185 | assert isinstance(a.b, EmptyClass)
186 |
187 |
188 | def test_configure_multiple_modules():
189 | def configure_a(binder):
190 | binder.bind(DependsOnEmptyClass)
191 |
192 | def configure_b(binder):
193 | binder.bind(EmptyClass)
194 |
195 | injector = Injector([configure_a, configure_b])
196 | a = injector.get(DependsOnEmptyClass)
197 | assert isinstance(a, DependsOnEmptyClass)
198 | assert isinstance(a.b, EmptyClass)
199 |
200 |
201 | def test_inject_with_missing_dependency():
202 | def configure(binder):
203 | binder.bind(DependsOnEmptyClass)
204 |
205 | injector = Injector(configure, auto_bind=False)
206 | with pytest.raises(UnsatisfiedRequirement):
207 | injector.get(EmptyClass)
208 |
209 |
210 | def test_inject_named_interface():
211 | class A:
212 | @inject
213 | def __init__(self, b: EmptyClass):
214 | self.b = b
215 |
216 | def configure(binder):
217 | binder.bind(A)
218 | binder.bind(EmptyClass)
219 |
220 | injector = Injector(configure)
221 | a = injector.get(A)
222 | assert isinstance(a, A)
223 | assert isinstance(a.b, EmptyClass)
224 |
225 |
226 | class TransitiveC:
227 | pass
228 |
229 |
230 | class TransitiveB:
231 | @inject
232 | def __init__(self, c: TransitiveC):
233 | self.c = c
234 |
235 |
236 | class TransitiveA:
237 | @inject
238 | def __init__(self, b: TransitiveB):
239 | self.b = b
240 |
241 |
242 | def test_transitive_injection():
243 | def configure(binder):
244 | binder.bind(TransitiveA)
245 | binder.bind(TransitiveB)
246 | binder.bind(TransitiveC)
247 |
248 | injector = Injector(configure)
249 | a = injector.get(TransitiveA)
250 | assert isinstance(a, TransitiveA)
251 | assert isinstance(a.b, TransitiveB)
252 | assert isinstance(a.b.c, TransitiveC)
253 |
254 |
255 | def test_transitive_injection_with_missing_dependency():
256 | def configure(binder):
257 | binder.bind(TransitiveA)
258 | binder.bind(TransitiveB)
259 |
260 | injector = Injector(configure, auto_bind=False)
261 | with pytest.raises(UnsatisfiedRequirement):
262 | injector.get(TransitiveA)
263 | with pytest.raises(UnsatisfiedRequirement):
264 | injector.get(TransitiveB)
265 |
266 |
267 | def test_inject_singleton():
268 | class A:
269 | @inject
270 | def __init__(self, b: EmptyClass):
271 | self.b = b
272 |
273 | def configure(binder):
274 | binder.bind(A)
275 | binder.bind(EmptyClass, scope=SingletonScope)
276 |
277 | injector1 = Injector(configure)
278 | a1 = injector1.get(A)
279 | a2 = injector1.get(A)
280 | assert a1.b is a2.b
281 |
282 |
283 | @singleton
284 | class SingletonB:
285 | pass
286 |
287 |
288 | def test_inject_decorated_singleton_class():
289 | class A:
290 | @inject
291 | def __init__(self, b: SingletonB):
292 | self.b = b
293 |
294 | def configure(binder):
295 | binder.bind(A)
296 | binder.bind(SingletonB)
297 |
298 | injector1 = Injector(configure)
299 | a1 = injector1.get(A)
300 | a2 = injector1.get(A)
301 | assert a1.b is a2.b
302 | assert a1 is not a2
303 |
304 |
305 | def test_injecting_an_auto_bound_decorated_singleton_class():
306 | class A:
307 | @inject
308 | def __init__(self, b: SingletonB):
309 | self.b = b
310 |
311 | injector1 = Injector()
312 | a1 = injector1.get(A)
313 | a2 = injector1.get(A)
314 | assert a1.b is a2.b
315 | assert a1 is not a2
316 |
317 |
318 | def test_a_decorated_singleton_is_shared_between_parent_and_child_injectors_when_parent_creates_it_first():
319 | parent_injector = Injector()
320 |
321 | child_injector = parent_injector.create_child_injector()
322 |
323 | assert parent_injector.get(SingletonB) is child_injector.get(SingletonB)
324 |
325 |
326 | def test_a_decorated_singleton_is_shared_between_parent_and_child_injectors_when_child_creates_it_first():
327 | parent_injector = Injector()
328 |
329 | child_injector = parent_injector.create_child_injector()
330 |
331 | assert child_injector.get(SingletonB) is parent_injector.get(SingletonB)
332 |
333 |
334 | # Test for https://github.com/python-injector/injector/issues/207
335 | def test_a_decorated_singleton_is_shared_among_child_injectors():
336 | parent_injector = Injector()
337 |
338 | child_injector_1 = parent_injector.create_child_injector()
339 | child_injector_2 = parent_injector.create_child_injector()
340 |
341 | assert child_injector_1.get(SingletonB) is child_injector_2.get(SingletonB)
342 |
343 |
344 | def test_a_decorated_singleton_should_not_override_explicit_binds():
345 | parent_injector = Injector()
346 |
347 | child_injector = parent_injector.create_child_injector()
348 | grand_child_injector = child_injector.create_child_injector()
349 |
350 | bound_singleton = SingletonB()
351 | child_injector.binder.bind(SingletonB, to=bound_singleton)
352 |
353 | assert parent_injector.get(SingletonB) is not bound_singleton
354 | assert child_injector.get(SingletonB) is bound_singleton
355 | assert grand_child_injector.get(SingletonB) is bound_singleton
356 |
357 |
358 | def test_binding_a_singleton_to_a_child_injector_does_not_affect_the_parent_injector():
359 | parent_injector = Injector()
360 |
361 | child_injector = parent_injector.create_child_injector()
362 | child_injector.binder.bind(EmptyClass, scope=singleton)
363 |
364 | assert child_injector.get(EmptyClass) is child_injector.get(EmptyClass)
365 | assert child_injector.get(EmptyClass) is not parent_injector.get(EmptyClass)
366 | assert parent_injector.get(EmptyClass) is not parent_injector.get(EmptyClass)
367 |
368 |
369 | def test_a_decorated_singleton_should_not_override_a_child_provider():
370 | parent_injector = Injector()
371 |
372 | provided_instance = SingletonB()
373 |
374 | class MyModule(Module):
375 | @provider
376 | def provide_name(self) -> SingletonB:
377 | return provided_instance
378 |
379 | child_injector = parent_injector.create_child_injector([MyModule])
380 |
381 | assert child_injector.get(SingletonB) is provided_instance
382 | assert parent_injector.get(SingletonB) is not provided_instance
383 | assert parent_injector.get(SingletonB) is parent_injector.get(SingletonB)
384 |
385 |
386 | # Test for https://github.com/python-injector/injector/issues/207
387 | def test_a_decorated_singleton_is_created_as_close_to_the_root_where_dependencies_fulfilled():
388 | class NonInjectableD:
389 | @inject
390 | def __init__(self, required) -> None:
391 | self.required = required
392 |
393 | @singleton
394 | class SingletonC:
395 | @inject
396 | def __init__(self, d: NonInjectableD):
397 | self.d = d
398 |
399 | parent_injector = Injector()
400 |
401 | child_injector_1 = parent_injector.create_child_injector()
402 |
403 | child_injector_2 = parent_injector.create_child_injector()
404 | child_injector_2_1 = child_injector_2.create_child_injector()
405 |
406 | provided_d = NonInjectableD(required=True)
407 | child_injector_2.binder.bind(NonInjectableD, to=provided_d)
408 |
409 | assert child_injector_2_1.get(SingletonC) is child_injector_2.get(SingletonC)
410 | assert child_injector_2.get(SingletonC).d is provided_d
411 |
412 | with pytest.raises(CallError):
413 | parent_injector.get(SingletonC)
414 |
415 | with pytest.raises(CallError):
416 | child_injector_1.get(SingletonC)
417 |
418 |
419 | def test_a_bound_decorated_singleton_is_created_as_close_to_the_root_where_it_exists_when_auto_bind_is_disabled():
420 | parent_injector = Injector(auto_bind=False)
421 |
422 | child_injector_1 = parent_injector.create_child_injector(auto_bind=False)
423 |
424 | child_injector_2 = parent_injector.create_child_injector(auto_bind=False)
425 | child_injector_2_1 = child_injector_2.create_child_injector(auto_bind=False)
426 |
427 | child_injector_2.binder.bind(SingletonB)
428 |
429 | assert child_injector_2_1.get(SingletonB) is child_injector_2_1.get(SingletonB)
430 | assert child_injector_2_1.get(SingletonB) is child_injector_2.get(SingletonB)
431 |
432 | with pytest.raises(UnsatisfiedRequirement):
433 | parent_injector.get(SingletonB)
434 |
435 | with pytest.raises(UnsatisfiedRequirement):
436 | child_injector_1.get(SingletonB)
437 |
438 |
439 | def test_threadlocal():
440 | @threadlocal
441 | class A:
442 | def __init__(self):
443 | pass
444 |
445 | def configure(binder):
446 | binder.bind(A)
447 |
448 | injector = Injector(configure)
449 | a1 = injector.get(A)
450 | a2 = injector.get(A)
451 |
452 | assert a1 is a2
453 |
454 | a3 = [None]
455 | ready = threading.Event()
456 |
457 | def inject_a3():
458 | a3[0] = injector.get(A)
459 | ready.set()
460 |
461 | threading.Thread(target=inject_a3).start()
462 | ready.wait(1.0)
463 |
464 | assert a2 is not a3[0] and a3[0] is not None
465 |
466 |
467 | class Interface2:
468 | pass
469 |
470 |
471 | def test_injecting_interface_implementation():
472 | class Implementation:
473 | pass
474 |
475 | class A:
476 | @inject
477 | def __init__(self, i: Interface2):
478 | self.i = i
479 |
480 | def configure(binder):
481 | binder.bind(A)
482 | binder.bind(Interface2, to=Implementation)
483 |
484 | injector = Injector(configure)
485 | a = injector.get(A)
486 | assert isinstance(a.i, Implementation)
487 |
488 |
489 | class CyclicInterface:
490 | pass
491 |
492 |
493 | class CyclicA:
494 | @inject
495 | def __init__(self, i: CyclicInterface):
496 | self.i = i
497 |
498 |
499 | class CyclicB:
500 | @inject
501 | def __init__(self, a: CyclicA):
502 | self.a = a
503 |
504 |
505 | def test_cyclic_dependencies():
506 | def configure(binder):
507 | binder.bind(CyclicInterface, to=CyclicB)
508 | binder.bind(CyclicA)
509 |
510 | injector = Injector(configure)
511 | with pytest.raises(CircularDependency):
512 | injector.get(CyclicA)
513 |
514 |
515 | class CyclicInterface2:
516 | pass
517 |
518 |
519 | class CyclicA2:
520 | @inject
521 | def __init__(self, i: CyclicInterface2):
522 | self.i = i
523 |
524 |
525 | class CyclicB2:
526 | @inject
527 | def __init__(self, a_builder: AssistedBuilder[CyclicA2]):
528 | self.a = a_builder.build(i=self)
529 |
530 |
531 | def test_dependency_cycle_can_be_worked_broken_by_assisted_building():
532 | def configure(binder):
533 | binder.bind(CyclicInterface2, to=CyclicB2)
534 | binder.bind(CyclicA2)
535 |
536 | injector = Injector(configure)
537 |
538 | # Previously it'd detect a circular dependency here:
539 | # 1. Constructing CyclicA2 requires CyclicInterface2 (bound to CyclicB2)
540 | # 2. Constructing CyclicB2 requires assisted build of CyclicA2
541 | # 3. Constructing CyclicA2 triggers circular dependency check
542 | assert isinstance(injector.get(CyclicA2), CyclicA2)
543 |
544 |
545 | class Interface5:
546 | constructed = False
547 |
548 | def __init__(self):
549 | Interface5.constructed = True
550 |
551 |
552 | def test_that_injection_is_lazy():
553 | class A:
554 | @inject
555 | def __init__(self, i: Interface5):
556 | self.i = i
557 |
558 | def configure(binder):
559 | binder.bind(Interface5)
560 | binder.bind(A)
561 |
562 | injector = Injector(configure)
563 | assert not (Interface5.constructed)
564 | injector.get(A)
565 | assert Interface5.constructed
566 |
567 |
568 | def test_module_provider():
569 | class MyModule(Module):
570 | @provider
571 | def provide_name(self) -> str:
572 | return 'Bob'
573 |
574 | module = MyModule()
575 | injector = Injector(module)
576 | assert injector.get(str) == 'Bob'
577 |
578 |
579 | def test_module_class_gets_instantiated():
580 | name = 'Meg'
581 |
582 | class MyModule(Module):
583 | def configure(self, binder):
584 | binder.bind(str, to=name)
585 |
586 | injector = Injector(MyModule)
587 | assert injector.get(str) == name
588 |
589 |
590 | def test_inject_and_provide_coexist_happily():
591 | class MyModule(Module):
592 | @provider
593 | def provide_weight(self) -> float:
594 | return 50.0
595 |
596 | @provider
597 | def provide_age(self) -> int:
598 | return 25
599 |
600 | # TODO(alec) Make provider/inject order independent.
601 | @provider
602 | @inject
603 | def provide_description(self, age: int, weight: float) -> str:
604 | return 'Bob is %d and weighs %0.1fkg' % (age, weight)
605 |
606 | assert Injector(MyModule()).get(str) == 'Bob is 25 and weighs 50.0kg'
607 |
608 |
609 | Names = NewType('Names', List[str])
610 | Passwords = NewType('Ages', Dict[str, str])
611 |
612 |
613 | def test_multibind():
614 | # First let's have some explicit multibindings
615 | def configure(binder):
616 | binder.multibind(List[str], to=['not a name'])
617 | binder.multibind(Dict[str, str], to={'asd': 'qwe'})
618 | # To make sure Lists and Dicts of different subtypes are treated distinctly
619 | binder.multibind(List[int], to=[1, 2, 3])
620 | binder.multibind(Dict[str, int], to={'weight': 12})
621 | # To see that NewTypes are treated distinctly
622 | binder.multibind(Names, to=['Bob'])
623 | binder.multibind(Passwords, to={'Bob': 'password1'})
624 |
625 | # Then @multiprovider-decorated Module methods
626 | class CustomModule(Module):
627 | @multiprovider
628 | def provide_some_ints(self) -> List[int]:
629 | return [4, 5, 6]
630 |
631 | @multiprovider
632 | def provide_some_strs(self) -> List[str]:
633 | return ['not a name either']
634 |
635 | @multiprovider
636 | def provide_str_to_str_mapping(self) -> Dict[str, str]:
637 | return {'xxx': 'yyy'}
638 |
639 | @multiprovider
640 | def provide_str_to_int_mapping(self) -> Dict[str, int]:
641 | return {'height': 33}
642 |
643 | @multiprovider
644 | def provide_names(self) -> Names:
645 | return ['Alice', 'Clarice']
646 |
647 | @multiprovider
648 | def provide_passwords(self) -> Passwords:
649 | return {'Alice': 'aojrioeg3', 'Clarice': 'clarice30'}
650 |
651 | injector = Injector([configure, CustomModule])
652 | assert injector.get(List[str]) == ['not a name', 'not a name either']
653 | assert injector.get(List[int]) == [1, 2, 3, 4, 5, 6]
654 | assert injector.get(Dict[str, str]) == {'asd': 'qwe', 'xxx': 'yyy'}
655 | assert injector.get(Dict[str, int]) == {'weight': 12, 'height': 33}
656 | assert injector.get(Names) == ['Bob', 'Alice', 'Clarice']
657 | assert injector.get(Passwords) == {'Bob': 'password1', 'Alice': 'aojrioeg3', 'Clarice': 'clarice30'}
658 |
659 |
660 | def test_regular_bind_and_provider_dont_work_with_multibind():
661 | # We only want multibind and multiprovider to work to avoid confusion
662 |
663 | Names = NewType('Names', List[str])
664 | Passwords = NewType('Passwords', Dict[str, str])
665 |
666 | class MyModule(Module):
667 | with pytest.raises(Error):
668 |
669 | @provider
670 | def provide_strs(self) -> List[str]:
671 | return []
672 |
673 | with pytest.raises(Error):
674 |
675 | @provider
676 | def provide_names(self) -> Names:
677 | return []
678 |
679 | with pytest.raises(Error):
680 |
681 | @provider
682 | def provide_strs_in_dict(self) -> Dict[str, str]:
683 | return {}
684 |
685 | with pytest.raises(Error):
686 |
687 | @provider
688 | def provide_passwords(self) -> Passwords:
689 | return {}
690 |
691 | injector = Injector()
692 | binder = injector.binder
693 |
694 | with pytest.raises(Error):
695 | binder.bind(List[str], to=[])
696 |
697 | with pytest.raises(Error):
698 | binder.bind(Names, to=[])
699 |
700 | with pytest.raises(Error):
701 | binder.bind(Dict[str, str], to={})
702 |
703 | with pytest.raises(Error):
704 | binder.bind(Passwords, to={})
705 |
706 |
707 | def test_auto_bind():
708 | class A:
709 | pass
710 |
711 | injector = Injector()
712 | assert isinstance(injector.get(A), A)
713 |
714 |
715 | def test_auto_bind_with_newtype():
716 | # Reported in https://github.com/alecthomas/injector/issues/117
717 | class A:
718 | pass
719 |
720 | AliasOfA = NewType('AliasOfA', A)
721 | injector = Injector()
722 | assert isinstance(injector.get(AliasOfA), A)
723 |
724 |
725 | class Request:
726 | pass
727 |
728 |
729 | class RequestScope(Scope):
730 | def configure(self):
731 | self.context = None
732 |
733 | @contextmanager
734 | def __call__(self, request):
735 | assert self.context is None
736 | self.context = {}
737 | binder = self.injector.get(Binder)
738 | binder.bind(Request, to=request, scope=RequestScope)
739 | yield
740 | self.context = None
741 |
742 | def get(self, key, provider):
743 | if self.context is None:
744 | raise UnsatisfiedRequirement(None, key)
745 | try:
746 | return self.context[key]
747 | except KeyError:
748 | provider = InstanceProvider(provider.get(self.injector))
749 | self.context[key] = provider
750 | return provider
751 |
752 |
753 | request = ScopeDecorator(RequestScope)
754 |
755 |
756 | @request
757 | class Handler:
758 | def __init__(self, request):
759 | self.request = request
760 |
761 |
762 | class RequestModule(Module):
763 | @provider
764 | @inject
765 | def handler(self, request: Request) -> Handler:
766 | return Handler(request)
767 |
768 |
769 | def test_custom_scope():
770 | injector = Injector([RequestModule()], auto_bind=False)
771 |
772 | with pytest.raises(UnsatisfiedRequirement):
773 | injector.get(Handler)
774 |
775 | scope = injector.get(RequestScope)
776 | request = Request()
777 |
778 | with scope(request):
779 | handler = injector.get(Handler)
780 | assert handler.request is request
781 |
782 | with pytest.raises(UnsatisfiedRequirement):
783 | injector.get(Handler)
784 |
785 |
786 | def test_binder_install():
787 | class ModuleA(Module):
788 | def configure(self, binder):
789 | binder.bind(str, to='hello world')
790 |
791 | class ModuleB(Module):
792 | def configure(self, binder):
793 | binder.install(ModuleA())
794 |
795 | injector = Injector([ModuleB()])
796 | assert injector.get(str) == 'hello world'
797 |
798 |
799 | def test_binder_provider_for_method_with_explicit_provider():
800 | injector = Injector()
801 | binder = injector.binder
802 | provider = binder.provider_for(int, to=InstanceProvider(1))
803 | assert type(provider) is InstanceProvider
804 | assert provider.get(injector) == 1
805 |
806 |
807 | def test_binder_provider_for_method_with_instance():
808 | injector = Injector()
809 | binder = injector.binder
810 | provider = binder.provider_for(int, to=1)
811 | assert type(provider) is InstanceProvider
812 | assert provider.get(injector) == 1
813 |
814 |
815 | def test_binder_provider_for_method_with_class():
816 | injector = Injector()
817 | binder = injector.binder
818 | provider = binder.provider_for(int)
819 | assert type(provider) is ClassProvider
820 | assert provider.get(injector) == 0
821 |
822 |
823 | def test_binder_provider_for_method_with_class_to_specific_subclass():
824 | class A:
825 | pass
826 |
827 | class B(A):
828 | pass
829 |
830 | injector = Injector()
831 | binder = injector.binder
832 | provider = binder.provider_for(A, B)
833 | assert type(provider) is ClassProvider
834 | assert isinstance(provider.get(injector), B)
835 |
836 |
837 | def test_binder_provider_for_type_with_metaclass():
838 | # use a metaclass cross python2/3 way
839 | # otherwise should be:
840 | # class A(object, metaclass=abc.ABCMeta):
841 | # passa
842 | A = abc.ABCMeta('A', (object,), {})
843 |
844 | injector = Injector()
845 | binder = injector.binder
846 | assert isinstance(binder.provider_for(A, None).get(injector), A)
847 |
848 |
849 | class ClassA:
850 | def __init__(self, parameter):
851 | pass
852 |
853 |
854 | class ClassB:
855 | @inject
856 | def __init__(self, a: ClassA):
857 | pass
858 |
859 |
860 | def test_injecting_undecorated_class_with_missing_dependencies_raises_the_right_error():
861 | injector = Injector()
862 | try:
863 | injector.get(ClassB)
864 | except CallError as ce:
865 | check_exception_contains_stuff(ce, ('ClassA.__init__', 'ClassB'))
866 |
867 |
868 | def test_call_to_method_with_legitimate_call_error_raises_type_error():
869 | class A:
870 | def __init__(self):
871 | max()
872 |
873 | injector = Injector()
874 | with pytest.raises(TypeError):
875 | injector.get(A)
876 |
877 |
878 | def test_call_error_str_representation_handles_single_arg():
879 | ce = CallError('zxc')
880 | assert str(ce) == 'zxc'
881 |
882 |
883 | class NeedsAssistance:
884 | @inject
885 | def __init__(self, a: str, b):
886 | self.a = a
887 | self.b = b
888 |
889 |
890 | def test_assisted_builder_works_when_got_directly_from_injector():
891 | injector = Injector()
892 | builder = injector.get(AssistedBuilder[NeedsAssistance])
893 | obj = builder.build(b=123)
894 | assert (obj.a, obj.b) == (str(), 123)
895 |
896 |
897 | def test_assisted_builder_works_when_injected():
898 | class X:
899 | @inject
900 | def __init__(self, builder: AssistedBuilder[NeedsAssistance]):
901 | self.obj = builder.build(b=234)
902 |
903 | injector = Injector()
904 | x = injector.get(X)
905 | assert (x.obj.a, x.obj.b) == (str(), 234)
906 |
907 |
908 | class Interface:
909 | b = 0
910 |
911 |
912 | def test_assisted_builder_uses_bindings():
913 | def configure(binder):
914 | binder.bind(Interface, to=NeedsAssistance)
915 |
916 | injector = Injector(configure)
917 | builder = injector.get(AssistedBuilder[Interface])
918 | x = builder.build(b=333)
919 | assert (type(x), x.b) == (NeedsAssistance, 333)
920 |
921 |
922 | def test_assisted_builder_uses_concrete_class_when_specified():
923 | class X:
924 | pass
925 |
926 | def configure(binder):
927 | # meant only to show that provider isn't called
928 | binder.bind(X, to=lambda: 1 / 0)
929 |
930 | injector = Injector(configure)
931 | builder = injector.get(ClassAssistedBuilder[X])
932 | builder.build()
933 |
934 |
935 | def test_assisted_builder_injection_is_safe_to_use_with_multiple_injectors():
936 | class X:
937 | @inject
938 | def __init__(self, builder: AssistedBuilder[NeedsAssistance]):
939 | self.builder = builder
940 |
941 | i1, i2 = Injector(), Injector()
942 | b1 = i1.get(X).builder
943 | b2 = i2.get(X).builder
944 | assert (b1._injector, b2._injector) == (i1, i2)
945 |
946 |
947 | def test_assisted_builder_injection_is_safe_to_use_with_child_injectors():
948 | class X:
949 | @inject
950 | def __init__(self, builder: AssistedBuilder[NeedsAssistance]):
951 | self.builder = builder
952 |
953 | i1 = Injector()
954 | i2 = i1.create_child_injector()
955 | b1 = i1.get(X).builder
956 | b2 = i2.get(X).builder
957 | assert (b1._injector, b2._injector) == (i1, i2)
958 |
959 |
960 | class TestThreadSafety:
961 | def setup_method(self):
962 | self.event = threading.Event()
963 |
964 | def configure(binder):
965 | binder.bind(str, to=lambda: self.event.wait() and 'this is str')
966 |
967 | class XXX:
968 | @inject
969 | def __init__(self, s: str):
970 | pass
971 |
972 | self.injector = Injector(configure)
973 | self.cls = XXX
974 |
975 | def gather_results(self, count):
976 | objects = []
977 | lock = threading.Lock()
978 |
979 | def target():
980 | o = self.injector.get(self.cls)
981 | with lock:
982 | objects.append(o)
983 |
984 | threads = [threading.Thread(target=target) for i in range(count)]
985 |
986 | for t in threads:
987 | t.start()
988 |
989 | self.event.set()
990 |
991 | for t in threads:
992 | t.join()
993 |
994 | return objects
995 |
996 | def test_injection_is_thread_safe(self):
997 | objects = self.gather_results(2)
998 | assert len(objects) == 2
999 |
1000 | def test_singleton_scope_is_thread_safe(self):
1001 | self.injector.binder.bind(self.cls, scope=singleton)
1002 | a, b = self.gather_results(2)
1003 | assert a is b
1004 |
1005 |
1006 | def test_provider_and_scope_decorator_collaboration():
1007 | @provider
1008 | @singleton
1009 | def provider_singleton() -> int:
1010 | return 10
1011 |
1012 | @singleton
1013 | @provider
1014 | def singleton_provider() -> int:
1015 | return 10
1016 |
1017 | assert provider_singleton.__binding__.scope == SingletonScope
1018 | assert singleton_provider.__binding__.scope == SingletonScope
1019 |
1020 |
1021 | def test_injecting_into_method_of_object_that_is_falseish_works():
1022 | # regression test
1023 |
1024 | class X(dict):
1025 | @inject
1026 | def __init__(self, s: str):
1027 | pass
1028 |
1029 | injector = Injector()
1030 | injector.get(X)
1031 |
1032 |
1033 | Name = NewType("Name", str)
1034 | Message = NewType("Message", str)
1035 |
1036 |
1037 | def test_callable_provider_injection():
1038 | @inject
1039 | def create_message(name: Name):
1040 | return "Hello, " + name
1041 |
1042 | def configure(binder):
1043 | binder.bind(Name, to="John")
1044 | binder.bind(Message, to=create_message)
1045 |
1046 | injector = Injector([configure])
1047 | msg = injector.get(Message)
1048 | assert msg == "Hello, John"
1049 |
1050 |
1051 | def test_providerof():
1052 | counter = [0]
1053 |
1054 | def provide_str():
1055 | counter[0] += 1
1056 | return 'content'
1057 |
1058 | def configure(binder):
1059 | binder.bind(str, to=provide_str)
1060 |
1061 | injector = Injector(configure)
1062 |
1063 | assert counter[0] == 0
1064 |
1065 | provider = injector.get(ProviderOf[str])
1066 | assert counter[0] == 0
1067 |
1068 | assert provider.get() == 'content'
1069 | assert counter[0] == 1
1070 |
1071 | assert provider.get() == injector.get(str)
1072 | assert counter[0] == 3
1073 |
1074 |
1075 | def test_providerof_cannot_be_bound():
1076 | def configure(binder):
1077 | binder.bind(ProviderOf[int], to=InstanceProvider(None))
1078 |
1079 | with pytest.raises(Exception):
1080 | Injector(configure)
1081 |
1082 |
1083 | def test_providerof_is_safe_to_use_with_multiple_injectors():
1084 | def configure1(binder):
1085 | binder.bind(int, to=1)
1086 |
1087 | def configure2(binder):
1088 | binder.bind(int, to=2)
1089 |
1090 | injector1 = Injector(configure1)
1091 | injector2 = Injector(configure2)
1092 |
1093 | provider_of = ProviderOf[int]
1094 |
1095 | provider1 = injector1.get(provider_of)
1096 | provider2 = injector2.get(provider_of)
1097 |
1098 | assert provider1.get() == 1
1099 | assert provider2.get() == 2
1100 |
1101 |
1102 | def test_special_interfaces_work_with_auto_bind_disabled():
1103 | class InjectMe:
1104 | pass
1105 |
1106 | def configure(binder):
1107 | binder.bind(InjectMe, to=InstanceProvider(InjectMe()))
1108 |
1109 | injector = Injector(configure, auto_bind=False)
1110 |
1111 | # This line used to fail with:
1112 | # Traceback (most recent call last):
1113 | # File "/projects/injector/injector_test.py", line 1171,
1114 | # in test_auto_bind_disabled_regressions
1115 | # injector.get(ProviderOf(InjectMe))
1116 | # File "/projects/injector/injector.py", line 687, in get
1117 | # binding = self.binder.get_binding(None, key)
1118 | # File "/projects/injector/injector.py", line 459, in get_binding
1119 | # raise UnsatisfiedRequirement(cls, key)
1120 | # UnsatisfiedRequirement: unsatisfied requirement on
1121 | #
1122 | injector.get(ProviderOf[InjectMe])
1123 |
1124 | # This used to fail with an error similar to the ProviderOf one
1125 | injector.get(ClassAssistedBuilder[InjectMe])
1126 |
1127 |
1128 | def test_binding_an_instance_regression():
1129 | text = b'hello'.decode()
1130 |
1131 | def configure(binder):
1132 | # Yes, this binding doesn't make sense strictly speaking but
1133 | # it's just a sample case.
1134 | binder.bind(bytes, to=text)
1135 |
1136 | injector = Injector(configure)
1137 | # This used to return empty bytes instead of the expected string
1138 | assert injector.get(bytes) == text
1139 |
1140 |
1141 | class PartialB:
1142 | @inject
1143 | def __init__(self, a: EmptyClass, b: str):
1144 | self.a = a
1145 | self.b = b
1146 |
1147 |
1148 | def test_class_assisted_builder_of_partially_injected_class_old():
1149 | class C:
1150 | @inject
1151 | def __init__(self, a: EmptyClass, builder: ClassAssistedBuilder[PartialB]):
1152 | self.a = a
1153 | self.b = builder.build(b='C')
1154 |
1155 | c = Injector().get(C)
1156 | assert isinstance(c, C)
1157 | assert isinstance(c.b, PartialB)
1158 | assert isinstance(c.b.a, EmptyClass)
1159 |
1160 |
1161 | class ImplicitA:
1162 | pass
1163 |
1164 |
1165 | class ImplicitB:
1166 | @inject
1167 | def __init__(self, a: ImplicitA):
1168 | self.a = a
1169 |
1170 |
1171 | class ImplicitC:
1172 | @inject
1173 | def __init__(self, b: ImplicitB):
1174 | self.b = b
1175 |
1176 |
1177 | def test_implicit_injection_for_python3():
1178 | injector = Injector()
1179 | c = injector.get(ImplicitC)
1180 | assert isinstance(c, ImplicitC)
1181 | assert isinstance(c.b, ImplicitB)
1182 | assert isinstance(c.b.a, ImplicitA)
1183 |
1184 |
1185 | def test_annotation_based_injection_works_in_provider_methods():
1186 | class MyModule(Module):
1187 | def configure(self, binder):
1188 | binder.bind(int, to=42)
1189 |
1190 | @provider
1191 | def provide_str(self, i: int) -> str:
1192 | return str(i)
1193 |
1194 | @singleton
1195 | @provider
1196 | def provide_object(self) -> object:
1197 | return object()
1198 |
1199 | injector = Injector(MyModule)
1200 | assert injector.get(str) == '42'
1201 | assert injector.get(object) is injector.get(object)
1202 |
1203 |
1204 | class Fetcher:
1205 | def fetch(self, user_id):
1206 | assert user_id == 333
1207 | return {'name': 'John'}
1208 |
1209 |
1210 | class Processor:
1211 | @noninjectable('provider_id')
1212 | @inject
1213 | @noninjectable('user_id')
1214 | def __init__(self, fetcher: Fetcher, user_id: int, provider_id: str):
1215 | assert provider_id == 'not injected'
1216 | data = fetcher.fetch(user_id)
1217 | self.name = data['name']
1218 |
1219 |
1220 | def test_assisted_building_is_supported():
1221 | def configure(binder):
1222 | binder.bind(int, to=897)
1223 | binder.bind(str, to='injected')
1224 |
1225 | injector = Injector(configure)
1226 | processor_builder = injector.get(AssistedBuilder[Processor])
1227 |
1228 | with pytest.raises(CallError):
1229 | processor_builder.build()
1230 |
1231 | processor = processor_builder.build(user_id=333, provider_id='not injected')
1232 | assert processor.name == 'John'
1233 |
1234 |
1235 | def test_raises_when_noninjectable_arguments_defined_with_invalid_arguments():
1236 | with pytest.raises(UnknownArgument):
1237 |
1238 | class A:
1239 | @inject
1240 | @noninjectable('c')
1241 | def __init__(self, b: str):
1242 | self.b = b
1243 |
1244 |
1245 | def test_can_create_instance_with_untyped_noninjectable_argument():
1246 | class Parent:
1247 | @inject
1248 | @noninjectable('child1', 'child2')
1249 | def __init__(self, child1, *, child2):
1250 | self.child1 = child1
1251 | self.child2 = child2
1252 |
1253 | injector = Injector()
1254 | parent_builder = injector.get(AssistedBuilder[Parent])
1255 | parent = parent_builder.build(child1='injected1', child2='injected2')
1256 |
1257 | assert parent.child1 == 'injected1'
1258 | assert parent.child2 == 'injected2'
1259 |
1260 |
1261 | def test_implicit_injection_fails_when_annotations_are_missing():
1262 | class A:
1263 | def __init__(self, n):
1264 | self.n = n
1265 |
1266 | injector = Injector()
1267 | with pytest.raises(CallError):
1268 | injector.get(A)
1269 |
1270 |
1271 | def test_injection_works_in_presence_of_return_value_annotation():
1272 | # Code with PEP 484-compatible type hints will have __init__ methods
1273 | # annotated as returning None[1] and this didn't work well with Injector.
1274 | #
1275 | # [1] https://www.python.org/dev/peps/pep-0484/#the-meaning-of-annotations
1276 |
1277 | class A:
1278 | @inject
1279 | def __init__(self, s: str) -> None:
1280 | self.s = s
1281 |
1282 | def configure(binder):
1283 | binder.bind(str, to='this is string')
1284 |
1285 | injector = Injector([configure])
1286 |
1287 | # Used to fail with:
1288 | # injector.UnknownProvider: couldn't determine provider for None to None
1289 | a = injector.get(A)
1290 |
1291 | # Just a sanity check, if the code above worked we're almost certain
1292 | # we're good but just in case the return value annotation handling changed
1293 | # something:
1294 | assert a.s == 'this is string'
1295 |
1296 |
1297 | def test_things_dont_break_in_presence_of_args_or_kwargs():
1298 | class A:
1299 | @inject
1300 | def __init__(self, s: str, *args: int, **kwargs: str):
1301 | assert not args
1302 | assert not kwargs
1303 |
1304 | injector = Injector()
1305 |
1306 | # The following line used to fail with something like this:
1307 | # Traceback (most recent call last):
1308 | # File "/ve/injector/injector_test_py3.py", line 192,
1309 | # in test_things_dont_break_in_presence_of_args_or_kwargs
1310 | # injector.get(A)
1311 | # File "/ve/injector/injector.py", line 707, in get
1312 | # result = scope_instance.get(key, binding.provider).get(self)
1313 | # File "/ve/injector/injector.py", line 142, in get
1314 | # return injector.create_object(self._cls)
1315 | # File "/ve/injector/injector.py", line 744, in create_object
1316 | # init(instance, **additional_kwargs)
1317 | # File "/ve/injector/injector.py", line 1082, in inject
1318 | # kwargs=kwargs
1319 | # File "/ve/injector/injector.py", line 851, in call_with_injection
1320 | # **dependencies)
1321 | # File "/ve/injector/injector_test_py3.py", line 189, in __init__
1322 | # assert not kwargs
1323 | # AssertionError: assert not {'args': 0, 'kwargs': ''}
1324 | injector.get(A)
1325 |
1326 |
1327 | def test_forward_references_in_annotations_are_handled():
1328 | # See https://www.python.org/dev/peps/pep-0484/#forward-references for details
1329 |
1330 | class CustomModule(Module):
1331 | @provider
1332 | def provide_x(self) -> 'X':
1333 | return X('hello')
1334 |
1335 | @inject
1336 | def fun(s: 'X') -> 'X':
1337 | return s
1338 |
1339 | # The class needs to be module-global in order for the string -> object
1340 | # resolution mechanism to work. I could make it work with locals but it
1341 | # doesn't seem worth it.
1342 | global X
1343 |
1344 | class X:
1345 | def __init__(self, message: str) -> None:
1346 | self.message = message
1347 |
1348 | try:
1349 | injector = Injector(CustomModule)
1350 | assert injector.call_with_injection(fun).message == 'hello'
1351 | finally:
1352 | del X
1353 |
1354 |
1355 | def test_more_useful_exception_is_raised_when_parameters_type_is_any():
1356 | @inject
1357 | def fun(a: Any) -> None:
1358 | pass
1359 |
1360 | injector = Injector()
1361 |
1362 | # This was the exception before:
1363 | #
1364 | # TypeError: Cannot instantiate
1365 | #
1366 | # Now:
1367 | #
1368 | # injector.CallError: Call to AnyMeta.__new__() failed: Cannot instantiate
1369 | # (injection stack: ['injector_test_py3'])
1370 | #
1371 | # In this case the injection stack doesn't provide too much information but
1372 | # it quickly gets helpful when the stack gets deeper.
1373 | with pytest.raises((CallError, TypeError)):
1374 | injector.call_with_injection(fun)
1375 |
1376 |
1377 | def test_optionals_are_ignored_for_now():
1378 | @inject
1379 | def fun(s: str = None):
1380 | return s
1381 |
1382 | assert Injector().call_with_injection(fun) == ''
1383 |
1384 |
1385 | def test_explicitly_passed_parameters_override_injectable_values():
1386 | # The class needs to be defined globally for the 'X' forward reference to be able to be resolved.
1387 | global X
1388 |
1389 | # We test a method on top of regular function to exercise the code path that's
1390 | # responsible for handling methods.
1391 | class X:
1392 | @inject
1393 | def method(self, s: str) -> str:
1394 | return s
1395 |
1396 | @inject
1397 | def method_typed_self(self: 'X', s: str) -> str:
1398 | return s
1399 |
1400 | @inject
1401 | def function(s: str) -> str:
1402 | return s
1403 |
1404 | injection_counter = 0
1405 |
1406 | def provide_str() -> str:
1407 | nonlocal injection_counter
1408 | injection_counter += 1
1409 | return 'injected string'
1410 |
1411 | def configure(binder: Binder) -> None:
1412 | binder.bind(str, to=provide_str)
1413 |
1414 | injector = Injector([configure])
1415 | x = X()
1416 |
1417 | try:
1418 | assert injection_counter == 0
1419 |
1420 | assert injector.call_with_injection(x.method) == 'injected string'
1421 | assert injection_counter == 1
1422 | assert injector.call_with_injection(x.method_typed_self) == 'injected string'
1423 | assert injection_counter == 2
1424 | assert injector.call_with_injection(function) == 'injected string'
1425 | assert injection_counter == 3
1426 |
1427 | assert injector.call_with_injection(x.method, args=('passed string',)) == 'passed string'
1428 | assert injection_counter == 3
1429 | assert injector.call_with_injection(x.method_typed_self, args=('passed string',)) == 'passed string'
1430 | assert injection_counter == 3
1431 | assert injector.call_with_injection(function, args=('passed string',)) == 'passed string'
1432 | assert injection_counter == 3
1433 |
1434 | assert injector.call_with_injection(x.method, kwargs={'s': 'passed string'}) == 'passed string'
1435 | assert injection_counter == 3
1436 | assert (
1437 | injector.call_with_injection(x.method_typed_self, kwargs={'s': 'passed string'})
1438 | == 'passed string'
1439 | )
1440 | assert injection_counter == 3
1441 | assert injector.call_with_injection(function, kwargs={'s': 'passed string'}) == 'passed string'
1442 | assert injection_counter == 3
1443 | finally:
1444 | del X
1445 |
1446 |
1447 | class AssistedB:
1448 | @inject
1449 | def __init__(self, a: EmptyClass, b: str):
1450 | self.a = a
1451 | self.b = b
1452 |
1453 |
1454 | def test_class_assisted_builder_of_partially_injected_class():
1455 | class C:
1456 | @inject
1457 | def __init__(self, a: EmptyClass, builder: ClassAssistedBuilder[AssistedB]):
1458 | self.a = a
1459 | self.b = builder.build(b='C')
1460 |
1461 | c = Injector().get(C)
1462 | assert isinstance(c, C)
1463 | assert isinstance(c.b, AssistedB)
1464 | assert isinstance(c.b.a, EmptyClass)
1465 |
1466 |
1467 | # The test taken from Alec Thomas' pull request: https://github.com/alecthomas/injector/pull/73
1468 | def test_child_scope():
1469 | TestKey = NewType('TestKey', str)
1470 | TestKey2 = NewType('TestKey2', str)
1471 |
1472 | def parent_module(binder):
1473 | binder.bind(TestKey, to='in parent', scope=singleton)
1474 |
1475 | def first_child_module(binder):
1476 | binder.bind(TestKey2, to='in first child', scope=singleton)
1477 |
1478 | def second_child_module(binder):
1479 | binder.bind(TestKey2, to='in second child', scope=singleton)
1480 |
1481 | injector = Injector(modules=[parent_module])
1482 | first_child_injector = injector.create_child_injector(modules=[first_child_module])
1483 | second_child_injector = injector.create_child_injector(modules=[second_child_module])
1484 |
1485 | assert first_child_injector.get(TestKey) is first_child_injector.get(TestKey)
1486 | assert first_child_injector.get(TestKey) is second_child_injector.get(TestKey)
1487 | assert first_child_injector.get(TestKey2) is not second_child_injector.get(TestKey2)
1488 |
1489 |
1490 | def test_custom_scopes_work_as_expected_with_child_injectors():
1491 | class CustomSingletonScope(SingletonScope):
1492 | pass
1493 |
1494 | custom_singleton = ScopeDecorator(CustomSingletonScope)
1495 |
1496 | def parent_module(binder):
1497 | binder.bind(str, to='parent value', scope=custom_singleton)
1498 |
1499 | def child_module(binder):
1500 | binder.bind(str, to='child value', scope=custom_singleton)
1501 |
1502 | parent = Injector(modules=[parent_module])
1503 | child = parent.create_child_injector(modules=[child_module])
1504 | print('parent, child: %s, %s' % (parent, child))
1505 | assert parent.get(str) == 'parent value'
1506 | assert child.get(str) == 'child value'
1507 |
1508 |
1509 | # Test for https://github.com/alecthomas/injector/issues/75
1510 | def test_inject_decorator_does_not_break_manual_construction_of_pyqt_objects():
1511 | class PyQtFake:
1512 | @inject
1513 | def __init__(self):
1514 | pass
1515 |
1516 | def __getattribute__(self, item):
1517 | if item == '__injector__':
1518 | raise RuntimeError(
1519 | 'A PyQt class would raise this exception if getting '
1520 | 'self.__injector__ before __init__ is called and '
1521 | 'self.__injector__ has not been set by Injector.'
1522 | )
1523 | return object.__getattribute__(self, item)
1524 |
1525 | instance = PyQtFake() # This used to raise the exception
1526 |
1527 | assert isinstance(instance, PyQtFake)
1528 |
1529 |
1530 | def test_using_an_assisted_builder_with_a_provider_raises_an_injector_error():
1531 | class MyModule(Module):
1532 | @provider
1533 | def provide_a(self, builder: AssistedBuilder[EmptyClass]) -> EmptyClass:
1534 | return builder.build()
1535 |
1536 | injector = Injector(MyModule)
1537 |
1538 | with pytest.raises(Error):
1539 | injector.get(EmptyClass)
1540 |
1541 |
1542 | def test_newtype_integration_works():
1543 | UserID = NewType('UserID', int)
1544 |
1545 | def configure(binder):
1546 | binder.bind(UserID, to=123)
1547 |
1548 | injector = Injector([configure])
1549 | assert injector.get(UserID) == 123
1550 |
1551 |
1552 | def test_dataclass_integration_works():
1553 | import dataclasses
1554 |
1555 | @inject
1556 | @dataclasses.dataclass
1557 | class Data:
1558 | name: str
1559 |
1560 | def configure(binder):
1561 | binder.bind(str, to='data')
1562 |
1563 | injector = Injector([configure])
1564 | assert injector.get(Data).name == 'data'
1565 |
1566 |
1567 | def test_binder_does_not_have_a_binding_for_an_unbound_type():
1568 | injector = Injector()
1569 | assert not injector.binder.has_binding_for(int)
1570 | assert not injector.binder.has_explicit_binding_for(int)
1571 |
1572 |
1573 | def test_binder_has_binding_for_explicitly_bound_type():
1574 | def configure(binder):
1575 | binder.bind(int, to=123)
1576 |
1577 | injector = Injector([configure])
1578 | assert injector.binder.has_binding_for(int)
1579 | assert injector.binder.has_explicit_binding_for(int)
1580 |
1581 |
1582 | def test_binder_has_implicit_binding_for_implicitly_bound_type():
1583 | injector = Injector()
1584 |
1585 | injector.get(int)
1586 |
1587 | assert injector.binder.has_binding_for(int)
1588 | assert not injector.binder.has_explicit_binding_for(int)
1589 |
1590 |
1591 | def test_get_bindings():
1592 | def function1(a: int) -> None:
1593 | pass
1594 |
1595 | assert get_bindings(function1) == {}
1596 |
1597 | @inject
1598 | def function2(a: int) -> None:
1599 | pass
1600 |
1601 | assert get_bindings(function2) == {'a': int}
1602 |
1603 | @inject
1604 | @noninjectable('b')
1605 | def function3(a: int, b: str) -> None:
1606 | pass
1607 |
1608 | assert get_bindings(function3) == {'a': int}
1609 |
1610 | # Let's verify that the inject/noninjectable ordering doesn't matter
1611 | @noninjectable('b')
1612 | @inject
1613 | def function3b(a: int, b: str) -> None:
1614 | pass
1615 |
1616 | assert get_bindings(function3b) == {'a': int}
1617 |
1618 | # The simple case of no @inject but injection requested with Inject[...]
1619 | def function4(a: Inject[int], b: str) -> None:
1620 | pass
1621 |
1622 | assert get_bindings(function4) == {'a': int}
1623 |
1624 | # Using @inject with Inject is redundant but it should not break anything
1625 | @inject
1626 | def function5(a: Inject[int], b: str) -> None:
1627 | pass
1628 |
1629 | assert get_bindings(function5) == {'a': int, 'b': str}
1630 |
1631 | # We need to be able to exclude a parameter from injection with NoInject
1632 | @inject
1633 | def function6(a: int, b: NoInject[str]) -> None:
1634 | pass
1635 |
1636 | assert get_bindings(function6) == {'a': int}
1637 |
1638 | # The presence of NoInject should not trigger anything on its own
1639 | def function7(a: int, b: NoInject[str]) -> None:
1640 | pass
1641 |
1642 | assert get_bindings(function7) == {}
1643 |
1644 | # There was a bug where in case of multiple NoInject-decorated parameters only the first one was
1645 | # actually made noninjectable and we tried to inject something we couldn't possibly provide
1646 | # into the second one.
1647 | @inject
1648 | def function8(a: NoInject[int], b: NoInject[int]) -> None:
1649 | pass
1650 |
1651 | assert get_bindings(function8) == {}
1652 |
1653 | # Default arguments to NoInject annotations should behave the same as noninjectable decorator w.r.t 'None'
1654 | @inject
1655 | @noninjectable('b')
1656 | def function9(self, a: int, b: Optional[str] = None):
1657 | pass
1658 |
1659 | @inject
1660 | def function10(self, a: int, b: NoInject[Optional[str]] = None):
1661 | # b:s type is Union[NoInject[Union[str, None]], None]
1662 | pass
1663 |
1664 | assert get_bindings(function9) == {'a': int} == get_bindings(function10)
1665 |
1666 | # If there's a return type annottion that contains an a forward reference that can't be
1667 | # resolved (for whatever reason) we don't want that to break things for us – return types
1668 | # don't matter for the purpose of dependency injection.
1669 | @inject
1670 | def function11(a: int) -> 'InvalidForwardReference':
1671 | pass
1672 |
1673 | assert get_bindings(function11) == {'a': int}
1674 |
1675 |
1676 | # Tests https://github.com/alecthomas/injector/issues/202
1677 | @pytest.mark.skipif(sys.version_info < (3, 10), reason="Requires Python 3.10+")
1678 | def test_get_bindings_for_pep_604():
1679 | @inject
1680 | def function1(a: int | None) -> None:
1681 | pass
1682 |
1683 | assert get_bindings(function1) == {'a': int}
1684 |
1685 | @inject
1686 | def function1(a: int | str) -> None:
1687 | pass
1688 |
1689 | assert get_bindings(function1) == {'a': Union[int, str]}
1690 |
1691 |
1692 | # test for https://github.com/python-injector/injector/issues/217
1693 | def test_annotated_instance_integration_works():
1694 | UserID = Annotated[int, "user_id"]
1695 |
1696 | def configure(binder):
1697 | binder.bind(UserID, to=123)
1698 |
1699 | injector = Injector([configure])
1700 | assert injector.get(UserID) == 123
1701 |
1702 |
1703 | def test_annotated_class_integration_works():
1704 | class Shape(abc.ABC):
1705 | pass
1706 |
1707 | class Circle(Shape):
1708 | pass
1709 |
1710 | first = Annotated[Shape, "first"]
1711 |
1712 | def configure(binder):
1713 | binder.bind(first, to=Circle)
1714 |
1715 | injector = Injector([configure])
1716 | assert isinstance(injector.get(first), Circle)
1717 |
1718 |
1719 | def test_annotated_meta_separate_bindings():
1720 | first = Annotated[int, "first"]
1721 | second = Annotated[int, "second"]
1722 |
1723 | def configure(binder):
1724 | binder.bind(first, to=123)
1725 | binder.bind(second, to=456)
1726 |
1727 | injector = Injector([configure])
1728 | assert injector.get(first) == 123
1729 | assert injector.get(second) == 456
1730 | assert injector.get(first) != injector.get(second)
1731 |
1732 |
1733 | def test_annotated_origin_separate_bindings():
1734 | UserID = Annotated[int, "user_id"]
1735 |
1736 | def configure(binder):
1737 | binder.bind(UserID, to=123)
1738 | binder.bind(int, to=456)
1739 |
1740 | injector = Injector([configure])
1741 | assert injector.get(UserID) == 123
1742 | assert injector.get(int) == 456
1743 | assert injector.get(UserID) != injector.get(int)
1744 |
1745 |
1746 | def test_annotated_non_comparable_types():
1747 | foo = Annotated[int, float("nan")]
1748 | bar = Annotated[int, object()]
1749 |
1750 | def configure(binder):
1751 | binder.bind(foo, to=123)
1752 | binder.bind(bar, to=456)
1753 |
1754 | injector = Injector([configure])
1755 | assert injector.get(foo) == 123
1756 | assert injector.get(bar) == 456
1757 |
--------------------------------------------------------------------------------
/mypy.ini:
--------------------------------------------------------------------------------
1 | [mypy]
2 | ignore_missing_imports = true
3 | follow_imports = error
4 | warn_no_return = true
5 | warn_redundant_casts = true
6 | disallow_untyped_defs = true
7 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.black]
2 | line-length = 110
3 | target_version = ['py36', 'py37']
4 | skip_string_normalization = true
5 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | addopts = -v --tb=native --doctest-glob=*.md --doctest-modules --cov-report term --cov-report html --cov-report xml --cov=injector --cov-branch
3 | norecursedirs = __pycache__ *venv* .git build
4 |
--------------------------------------------------------------------------------
/requirements-dev.in:
--------------------------------------------------------------------------------
1 | # Our direct dependencies used in development/CI.
2 | #
3 | # We generate requirements-dev.txt from this file by running
4 | #
5 | # pip install -r requirements-dev.in && pip freeze > requirements-dev.txt
6 | #
7 | # and then modifying the file manually to restrict black and mypy to CPython
8 |
9 | pytest
10 | pytest-cov>=2.5.1
11 | mypy;implementation_name=="cpython"
12 | black;implementation_name=="cpython"
13 | check-manifest
14 | typing_extensions>=3.7.4;python_version<"3.9"
15 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | black==24.3.0 ; implementation_name == "cpython"
2 | build==1.0.3
3 | check-manifest==0.49
4 | click==8.1.7
5 | coverage[toml]==7.3.2
6 | exceptiongroup==1.2.0
7 | importlib-metadata==7.0.0
8 | iniconfig==2.0.0
9 | mypy==1.7.1 ; implementation_name == "cpython"
10 | mypy-extensions==1.0.0
11 | packaging==23.2
12 | pathspec==0.12.1
13 | platformdirs==4.1.0
14 | pluggy==1.3.0
15 | pyproject-hooks==1.0.0
16 | pytest==7.4.3
17 | pytest-cov==4.1.0
18 | tomli==2.0.1
19 | typing-extensions==4.9.0 ; python_version < "3.9"
20 | zipp==3.19.1
21 |
--------------------------------------------------------------------------------
/requirements-docs.in:
--------------------------------------------------------------------------------
1 | # The documentation-specific development dependencies.
2 | #
3 | # We generate requirements-dev.txt from this file by running
4 | #
5 | # pip install -r requirements-docs.in && pip freeze > requirements-docs.txt
6 | #
7 | # and then modifying the file manually to restrict black and mypy to CPython
8 | furo
9 | sphinx
10 |
--------------------------------------------------------------------------------
/requirements-docs.txt:
--------------------------------------------------------------------------------
1 | alabaster==0.7.13
2 | babel==2.14.0
3 | beautifulsoup4==4.12.3
4 | certifi==2024.7.4
5 | charset-normalizer==3.3.2
6 | docutils==0.20.1
7 | furo==2024.5.6
8 | idna==3.7
9 | imagesize==1.4.1
10 | jinja2==3.1.6
11 | markupsafe==2.1.3
12 | packaging==23.2
13 | pygments==2.17.2
14 | requests==2.32.2
15 | snowballstemmer==2.2.0
16 | soupsieve==2.5
17 | sphinx==7.1.2
18 | sphinx-basic-ng==1.0.0b2
19 | sphinxcontrib-applehelp==1.0.4
20 | sphinxcontrib-devhelp==1.0.2
21 | sphinxcontrib-htmlhelp==2.0.1
22 | sphinxcontrib-jsmath==1.0.1
23 | sphinxcontrib-qthelp==1.0.3
24 | sphinxcontrib-serializinghtml==1.1.5
25 | urllib3==2.2.2
26 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | typing_extensions>=3.7.4;python_version<"3.9"
2 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [wheel]
2 | universal = True
3 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, Command
2 | import sys
3 | import warnings
4 |
5 |
6 | warnings.filterwarnings("always", module=__name__)
7 |
8 |
9 | def obtain_requirements(file_name):
10 | with open(file_name) as fd_in:
11 | for line in fd_in:
12 | if '#' not in line:
13 | yield line.strip()
14 |
15 |
16 | class PyTest(Command):
17 | user_options = []
18 |
19 | def initialize_options(self):
20 | pass
21 |
22 | def finalize_options(self):
23 | pass
24 |
25 | def run(self):
26 | import subprocess
27 |
28 | errno = subprocess.call([sys.executable, '-m', 'pytest'])
29 | raise SystemExit(errno)
30 |
31 |
32 | def read_injector_variable(name):
33 | prefix = '%s = ' % (name,)
34 | with open('injector/__init__.py') as f:
35 | for line in f:
36 | if line.startswith(prefix):
37 | return line.replace(prefix, '').strip().strip("'")
38 | raise AssertionError('variable %s not found' % (name,))
39 |
40 |
41 | version = read_injector_variable('__version__')
42 | version_tag = read_injector_variable('__version_tag__')
43 |
44 |
45 | requirements = list(obtain_requirements('requirements.txt'))
46 | requirements_dev = list(obtain_requirements('requirements-dev.txt'))
47 |
48 |
49 | try:
50 | import pypandoc
51 |
52 | long_description = pypandoc.convert_file('README.md', 'rst')
53 | except ImportError:
54 | warnings.warn('Could not locate pandoc, using Markdown long_description.', ImportWarning)
55 | with open('README.md') as f:
56 | long_description = f.read()
57 |
58 | description = long_description.splitlines()[0].strip()
59 |
60 |
61 | setup(
62 | name='injector',
63 | url='https://github.com/alecthomas/injector',
64 | download_url='https://pypi.org/project/injector/',
65 | version=version,
66 | options=dict(egg_info=dict(tag_build=version_tag)),
67 | description=description,
68 | long_description=long_description,
69 | license='BSD',
70 | platforms=['any'],
71 | packages=['injector'],
72 | package_data={'injector': ['py.typed']},
73 | author='Alec Thomas',
74 | author_email='alec@swapoff.org',
75 | cmdclass={'test': PyTest},
76 | extras_require={'dev': requirements_dev},
77 | keywords=[
78 | 'Dependency Injection',
79 | 'DI',
80 | 'Dependency Injection framework',
81 | 'Inversion of Control',
82 | 'IoC',
83 | 'Inversion of Control container',
84 | ],
85 | install_requires=requirements,
86 | )
87 |
--------------------------------------------------------------------------------