├── MANIFEST.in ├── Makefile ├── setup.cfg ├── CHANGES.rst ├── LICENSE ├── setup.py ├── tests.py ├── README.rst └── bottle_inject.py /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGES.rst 2 | include README.rst 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | clean: 2 | -rm -r *.pyc *.egg *.egg-info MANIFEST 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | detailed-errors=1 3 | with-coverage=1 4 | cover-package=bottle_inject 5 | debug=nose.loader 6 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changes 2 | ======= 3 | 4 | Roadmap 5 | ------- 6 | 7 | * Do stuff ... 8 | 9 | Changes 0.1 10 | ----------- 11 | 12 | * Initial version 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Marcel Hellkamp. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from setuptools import setup 4 | 5 | here = os.path.abspath(os.path.dirname(__file__)) 6 | 7 | with open(os.path.join(here, 'README.rst')) as f: 8 | README = f.read() 9 | 10 | with open(os.path.join(here, 'CHANGES.rst')) as f: 11 | CHANGES = f.read() 12 | 13 | with open(os.path.join(here, 'bottle_inject.py')) as f: 14 | for line in f: 15 | if line.startswith("__version__"): 16 | VERSION = line.strip().split('"')[-2] 17 | break 18 | else: 19 | raise RuntimeError("Could not find version string in module file.") 20 | 21 | extra = { 22 | 'install_requires': [ 23 | 'distribute', 24 | 'bottle>=0.11', 25 | ], 26 | 'tests_require': [ 27 | 'coverage', 28 | ], 29 | 'test_suite': 'nose.collector', 30 | 'setup_requires': ['wheel', 'nose>=1.0'] 31 | } 32 | 33 | if sys.version_info >= (3,): 34 | extra['use_2to3'] = True 35 | 36 | setup( 37 | name='Bottle-Inject', 38 | version=VERSION, 39 | url='http://github.com/bottlepy/bottle-inject/', 40 | description='Dependency injection for Bottle.', 41 | long_description=README.strip() + '\n'*4 + CHANGES.strip(), 42 | author='Marcel Hellkamp', 43 | author_email='marc@gsites.de', 44 | license='MIT', 45 | platforms='any', 46 | zip_safe=True, 47 | py_modules=[ 48 | 'bottle_inject' 49 | ], 50 | classifiers=[ 51 | 'Environment :: Web Environment', 52 | 'Intended Audience :: Developers', 53 | 'License :: OSI Approved :: MIT License', 54 | 'Operating System :: OS Independent', 55 | 'Programming Language :: Python', 56 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 57 | 'Topic :: Software Development :: Libraries :: Python Modules' 58 | ], 59 | **extra 60 | ) 61 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | from bottle_inject import inject, Injector, InjectError, Plugin 3 | import unittest 4 | import functools 5 | 6 | def as_implicit(ip): 7 | ip.implicit = True 8 | return ip 9 | 10 | class TestInjector(unittest.TestCase): 11 | 12 | def setUp(self): 13 | self.seq = range(10) 14 | 15 | def test_inject_compare(self): 16 | self.assertEqual(inject('x'), inject('x')) 17 | self.assertEqual(inject('x', bar=6, foo=5), inject('x', foo=5, bar=6)) 18 | self.assertNotEqual(inject('x', foo=5), inject('x')) 19 | self.assertNotEqual(inject('y'), inject('x')) 20 | self.assertNotEqual(inject('x'), inject('x')) 21 | self.assertNotEqual(inject('x'), 'x') 22 | 23 | def _common_checks(self, results): 24 | self.assertEqual(as_implicit(inject('a')), results['a']) 25 | self.assertEqual(as_implicit(inject('_b')), results['_b']) 26 | self.assertEqual(None, results.get('c')) 27 | self.assertEqual(inject('x'), results['d']) 28 | self.assertFalse(results['d'].implicit) 29 | self.assertEqual(inject('x2', foo='foo', bar="baz"), results['e']) 30 | self.assertFalse(results['e'].implicit) 31 | self.assertEqual(None, results.get('f')) 32 | self.assertEqual(None, results.get('g')) 33 | 34 | def test_inspection(self): 35 | def test(a, _b, c=5, d=inject('x'), e=inject('x2', foo='foo', bar="baz"), *f, **g): pass 36 | self._common_checks(Injector().inspect(test)) 37 | 38 | def test_inspect_class(self): 39 | class Foo: 40 | def __init__(self, a, _b, c=5, d=inject('x'), e=inject('x2', foo='foo', bar="baz"), *f, **g): 41 | pass 42 | self._common_checks(Injector().inspect(Foo)) 43 | 44 | def test_inspect_blacklist(self): 45 | def test(self, a): pass 46 | self.assertEquals(['a'], Injector().inspect(test).keys()) 47 | 48 | def test_inspect_wrapped(self): 49 | def test(a, _b, c=5, d=inject('x'), e=inject('x2', foo='foo', bar="baz"), *f, **g): pass 50 | @functools.wraps(test) 51 | def wrapped(): pass 52 | 53 | if not hasattr(wrapped, '__wrapped__'): 54 | # Python 3.2 added this. Without it we cannot unwrap. 55 | # This is just to satisfy the coverage in unsupported python versions 56 | wrapped.__wrapped__ = test 57 | 58 | self._common_checks(Injector().inspect(wrapped)) 59 | 60 | def test_inject_value(self): 61 | ij = Injector() 62 | value = [] 63 | ij.add_value('val', value) 64 | def test(val, other=inject('val')): 65 | self.assertTrue(val is other) 66 | self.assertTrue(val is value) 67 | val.append(5) 68 | ij.call_inject(test) 69 | self.assertEqual([5], value) 70 | 71 | def test_inject_provider(self): 72 | def provider(): 73 | counter['provider_called'] += 1 74 | return counter 75 | 76 | def test(c, other=inject('c')): 77 | self.assertTrue(other is c) 78 | c['counter_used'] += 1 79 | 80 | counter = Counter() 81 | ij = Injector() 82 | ij.add_provider('c', provider) 83 | 84 | ij.call_inject(test) 85 | self.assertEqual(2, counter['provider_called']) 86 | self.assertEqual(1, counter['counter_used']) 87 | 88 | def test_inject_provider_decorator(self): 89 | counter = Counter() 90 | ij = Injector() 91 | 92 | @ij.provider('c') 93 | def provider(): 94 | counter['provider_called'] += 1 95 | return counter 96 | 97 | def test(c, other=inject('c')): 98 | self.assertTrue(other is c) 99 | c['counter_used'] += 1 100 | 101 | ij.call_inject(test) 102 | self.assertEqual(2, counter['provider_called']) 103 | self.assertEqual(1, counter['counter_used']) 104 | 105 | def test_inject_resolver(self): 106 | counter = Counter() 107 | 108 | def resolver(keyname='provider_called', increment=1): 109 | counter['resolver_called'] += 1 110 | def provider(): 111 | counter[keyname] += increment 112 | return counter 113 | return provider 114 | 115 | def test(c, other=inject('c', keyname='special_called', increment=10)): 116 | self.assertTrue(other is c) 117 | c['counter_used'] += 1 118 | 119 | ij = Injector() 120 | ij.add_resolver('c', resolver) 121 | 122 | ij.call_inject(test) 123 | self.assertEqual(2, counter['resolver_called']) 124 | self.assertEqual(1, counter['provider_called']) 125 | self.assertEqual(10, counter['special_called']) 126 | self.assertEqual(1, counter['counter_used']) 127 | 128 | ij.call_inject(test) 129 | self.assertEqual(2, counter['resolver_called']) # !!! Should be cached and not called again 130 | self.assertEqual(2, counter['provider_called']) 131 | self.assertEqual(20, counter['special_called']) 132 | self.assertEqual(2, counter['counter_used']) 133 | 134 | def test_inject_resolver_decorator(self): 135 | counter = Counter() 136 | 137 | ij = Injector() 138 | @ij.resolver('c') 139 | def resolver(keyname='provider_called', increment=1): 140 | counter['resolver_called'] += 1 141 | def provider(): 142 | counter[keyname] += increment 143 | return counter 144 | return provider 145 | 146 | def test(c, other=inject('c', keyname='special_called', increment=10)): 147 | self.assertTrue(other is c) 148 | c['counter_used'] += 1 149 | 150 | ij.call_inject(test) 151 | self.assertEqual(2, counter['resolver_called']) 152 | self.assertEqual(1, counter['provider_called']) 153 | self.assertEqual(10, counter['special_called']) 154 | self.assertEqual(1, counter['counter_used']) 155 | 156 | ij.call_inject(test) 157 | self.assertEqual(2, counter['resolver_called']) # !!! Should be cached and not called again 158 | self.assertEqual(2, counter['provider_called']) 159 | self.assertEqual(20, counter['special_called']) 160 | self.assertEqual(2, counter['counter_used']) 161 | 162 | def test_remove_provider(self): 163 | ij = Injector() 164 | ij.add_value('val', 5) 165 | ij.remove('val') 166 | def test(val): pass 167 | with self.assertRaises(InjectError): 168 | ij.call_inject(test) 169 | 170 | def test_resolver_alias(self): 171 | counter = Counter() 172 | 173 | def resolver(keyname='provider_called', increment=1): 174 | counter['resolver_called'] += 1 175 | def provider(): 176 | counter[keyname] += increment 177 | return counter 178 | return provider 179 | 180 | def test(a, b, c, d, e, f): 181 | self.assertTrue(a is b) 182 | self.assertTrue(a is c) 183 | self.assertTrue(a is d) 184 | self.assertTrue(a is e) 185 | self.assertTrue(a is f) 186 | a['counter_used'] += 1 187 | 188 | ij = Injector() 189 | ij.add_resolver('a', resolver, alias="b") 190 | ij.add_resolver('c', resolver, alias=("d", "e", "f")) 191 | 192 | ij.call_inject(test) 193 | self.assertEqual(6, counter['resolver_called']) 194 | self.assertEqual(6, counter['provider_called']) 195 | self.assertEqual(1, counter['counter_used']) 196 | 197 | def test_wrap_decorator(self): 198 | ij = Injector() 199 | 200 | @ij.wrap 201 | def test(a): 202 | return a 203 | 204 | with self.assertRaises(InjectError): 205 | test() 206 | 207 | ij.add_value('a', 5) 208 | self.assertEquals(5, test()) 209 | self.assertEquals(6, test(a=6)) 210 | 211 | 212 | import bottle 213 | class TestBottlePlugin(unittest.TestCase): 214 | def test_autoinject(self): 215 | app = bottle.Bottle() 216 | ij = app.install(Plugin()) 217 | @app.get('/') 218 | def get_route(req, res, injector): 219 | self.assertEquals(bottle.reqest, req) 220 | self.assertEquals(bottle.response, res) 221 | self.assertEquals(ij, injector) 222 | 223 | if __name__ == '__main__': 224 | unittest.main() 225 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Bottle Dependency Injection 2 | =========================== 3 | 4 | The Bottle framework already does dependency injection in some regard: The URL parameters of your routes are injected into your handler functions as keyword arguments. Some other Plugins (actually, most plugins) do it, too: They inject database connections, authentication contexts, session objects and much more. This Plugin makes the concept available to you without the need to write a new plugin for every single dependency you want to inject. It also can change the way you use Bottle and write applications in a funcamental way, if you let it. If done right, dependency injection can reduce the complexity and increase testability and readability of your application a lot. But let us start easy, with a simple example:: 5 | 6 | app = Bottle() 7 | injector = app.install(bottle.ext.inject.Plugin()) 8 | 9 | @injector.provider('db') 10 | def get_db_handle(): 11 | return database_connection_pool.get_connection() 12 | 13 | @app.route('/random_quote') 14 | def random_quote(db): 15 | with db.cursor() as cursor: 16 | row = cursor.execute('SELECT quote FROM quotes ORDER BY RANDOM() LIMIT 1').fetchone() 17 | return row['quote'] 18 | 19 | The first two lines are nothing new. We just create a bottle application and install this plugin to it. The next block is more interesting. Similar to how bottle binds handler functions to URL paths, the injector binds providers to injection points. In this case, we bind the provider 'get_db_handle' to the injection point named 'db'. Whenever a function is called through our injector and has an argument with the same name, it recieves a fresh database connection from our provider. You can see that in the next few lines. Because all handler callbacks are managed by our injector plugin, you just need to accept a 'db' argument and it is automatically injected for us by the plugin. If you define a route that does not accept a 'db' argument, then nothing happens. No database connection is ever created for that route. 20 | 21 | That little example shows the benefits of dependency injection very well: 22 | 23 | * You can unit-test the 'random_quote()' function directly by passing it a fake- or test-database object. No need to set-up the whole application just for testing. 24 | * No global variables or global state used. The function can be used again in a different context without hassle. 25 | * You don't have to import `get_db_handle` into every module that defines bottle application routes. 26 | * You can change the implementation of 'get_db_handle' and it affects every route of your application. No need to search/replace your codebase. 27 | * Less typing. Be lazy where it counts. 28 | 29 | Usage without bottle 30 | -------------------- 31 | 32 | This library does not depend on bottle and can be used without it:: 33 | 34 | from bottle_inject import Injector 35 | injector = Injector() 36 | 37 | @injector.provider('db') 38 | def get_db_handle(): 39 | return database_connection_pool.get_connection() 40 | 41 | @injector.wrap 42 | def random_quote(db): 43 | with db.cursor() as cursor: 44 | row = cursor.execute('SELECT quote FROM quotes ORDER BY RANDOM() LIMIT 1').fetchone() 45 | return row['quote'] 46 | 47 | Advanced usage 48 | ============== 49 | 50 | Dependencies, Providers and Resolvers 51 | ------------------------------- 52 | 53 | The *dependency* is re-used for every injection and treated as a singleton. 54 | 55 | A *provider* returns the requested dependency when called. The provider is called with no arguments every time the dependency is needed. 56 | 57 | A *resolver* returns a cache-able *provider* and may accept injection-point specific configuration. The resolver is usually called only once per injection point and the return value is cached. It must return a (callable) provider. 58 | 59 | Injection Points 60 | ---------------- 61 | 62 | TODO: Describe the inject() function and how it is used. 63 | 64 | :: 65 | 66 | def my_func( 67 | db # Positional arguments are always recognized as injection points with the same name. 68 | a = 'value' # Keyword arguments with default values are not recognized. 69 | b = inject('db') # Here we explicitly define an injection point. The name of the argument is no longer 70 | # important. 71 | c: inject('db') # In Python 3 you can use the annotation syntax. (recommended) 72 | ): 73 | pass 74 | 75 | :: 76 | 77 | # Python 2 78 | def func(name = inject('param', key='value'), 79 | file = inject('file', field='upload')): 80 | pass 81 | 82 | # Python 3 83 | def func(name: inject('param', key='name'), 84 | file: inject('file', field='upload')): 85 | pass 86 | 87 | TODO: Describe the difference between implicit (non-annotated) and explicit (annotated with ``inject()``) injection points. In short: If a resolver is missing, explicit injection points will fail immediately while implcit injection points only fail if they are actually resolved. 88 | 89 | TODO: You can disable the injection into unannotized arguments (maybe). 90 | 91 | Recursive Dependency Injection 92 | ------------------------------ 93 | TODO: Describe recursive injection (injecting stuff into providers and resolvers), which already works. 94 | 95 | Default injection points 96 | ------------------------ 97 | 98 | The plugin comes with a set of pre-defined providers. You can use them right away, or unregister them if you don't want them. 99 | 100 | ================= ========================= ===== =============================================== 101 | Injection Points Type Scope Description 102 | ================= ========================= ===== =============================================== 103 | request, req, rq `bottle.Request` local 104 | response, res, rs `bottle.Response` local 105 | injector `Injector` app The injector itself. Can be used for runtime 106 | inspection of injectable values, e.g. by other 107 | plugins. 108 | params `bottle.FormsDict` local Not implenented. 109 | param[name] `str` local Not implenented. 110 | ================= ========================= ===== =============================================== 111 | 112 | Lifecycle of injected values 113 | ---------------------------- 114 | 115 | TODO: Explain that the injection framework does not close objects returned by providers. If you want to inject values that need to be closed after usage, either close them explicitly in your code, or inject a context manager instead. Example for an SQLAlchemy session:: 116 | 117 | @injector.provider('db') 118 | @contextmanager 119 | def session_scope(): 120 | session = Session() 121 | try: 122 | yield session 123 | session.commit() 124 | except: 125 | session.rollback() 126 | raise 127 | finally: 128 | session.close() 129 | 130 | @app.route('/random_quote') 131 | def random_quote(db): 132 | with db as session: 133 | quote = session.query(models.Quote)... 134 | return quote.text 135 | 136 | 137 | What is "Dependency Injection"? 138 | =============================== 139 | 140 | The term "Dependency Injection" is just a fancy name for a simple concept: The *caller* of a piece of code should *provide* all *dependencies* the code needs to run. In other words: A function or object should not need to *reach out*, but be *provided* with everything it needs. 141 | 142 | A small example probably helps best. The following code does *not* follow dependency injection paradigm:: 143 | 144 | db = my_database_connection.cursor() 145 | 146 | def do_stuff(): 147 | db.execute('...') 148 | 149 | do_stuff() 150 | 151 | And now, with dependency injection:: 152 | 153 | def do_stuff(db): 154 | db.execute('...') 155 | 156 | do_stuff(my_database_connection.cursor()) 157 | 158 | The only difference is that we now pass the database connction handle to the function explicitly, instead of letting the function fetch it from the global namespace. That's basically it. Now you can easily test `do_stuff` by passing it a fake database connection or a connection to a test database, re-use it in other contexts with different darabases, and the possible side-effects are no longer hidden within the code. 159 | 160 | On the downside, you'd have to type more and pass around a lot of stuff, but that is exactly what this plugin does for you: It manages the dependencies and injects them where needed. 161 | 162 | Glossary 163 | -------- 164 | 165 | Injector 166 | An object that manages *Dependencies*, *Providers* and *Resolvers* and can be asked to inject the required 167 | dependencies into a function call. 168 | 169 | Injection Point 170 | A place to inject dependencies into. This plugin injects into function call arguments most of the time. 171 | 172 | Consumer 173 | A function or callable that has injection points in its call signature so that the injector can inject dependencies. 174 | 175 | Dependency 176 | An object or resource that can be injected. 177 | 178 | Provider 179 | A function or callable that creates dependencies on demand, or otherwise provides the dependencies for when they are needed. 180 | 181 | Resolver 182 | A function or callable that creates individual providers based on injection-point specific configuration. (Yes, you could call it a dependency-provoder-provider but that sounds weird) 183 | -------------------------------------------------------------------------------- /bottle_inject.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import inspect 3 | import sys 4 | 5 | __version__ = "0.1.3" 6 | __all__ = "Plugin Injector inject".split() 7 | 8 | py32 = sys.version_info >= (3, 2, 0) 9 | 10 | 11 | def _makelist(data): 12 | if isinstance(data, (tuple, list, set)): 13 | return list(data) 14 | elif data: 15 | return [data] 16 | else: 17 | return [] 18 | 19 | 20 | class InjectError(RuntimeError): 21 | pass 22 | 23 | 24 | class _InjectionPoint(object): 25 | """ The object returned by :func:`inject`. """ 26 | 27 | def __init__(self, name, config=None, implicit=False): 28 | self.name = name 29 | self.config = config or {} 30 | self.implicit = implicit 31 | 32 | def __eq__(self, other): 33 | if isinstance(other, _InjectionPoint): 34 | return self.__dict__ == other.__dict__ 35 | return False 36 | 37 | 38 | class _ProviderCache(dict): 39 | """ A self-filling cache for :meth:`Injector.resolve` results. """ 40 | 41 | def __init__(self, injector): 42 | super(_ProviderCache, self).__init__() 43 | self.injector = injector 44 | 45 | def __missing__(self, func): 46 | self[func] = value = list(self.injector._resolve(func).items()) 47 | return value 48 | 49 | 50 | def _unwrap(func): 51 | if inspect.isclass(func): 52 | func = func.__init__ 53 | while hasattr(func, '__wrapped__'): 54 | func = func.__wrapped__ 55 | return func 56 | 57 | 58 | def _make_null_resolver(name, provider): 59 | msg = "The dependency provider for %r does not accept configuration (it is"\ 60 | " not a resolver)." % name 61 | def null_resolver(*a, **ka): 62 | if a or ka: 63 | raise InjectError(msg) 64 | return provider 65 | return null_resolver 66 | 67 | 68 | class Injector(object): 69 | def __init__(self): 70 | self.__cache = _ProviderCache(self) 71 | self._resolvers = {} 72 | self._never_inject = set(('self', )) 73 | 74 | def add_value(self, name, value, alias=()): 75 | """ 76 | Register a dependency value. 77 | 78 | The dependency value is re-used for every injection and treated as a 79 | singleton. 80 | 81 | :param name: Name of the injection point. 82 | :param value: The singleton to provide. 83 | :param alias: A list of alternative injection points. 84 | :return: None 85 | """ 86 | self.add_provider(name, lambda: value, alias=alias) 87 | 88 | def add_provider(self, name, func, alias=()): 89 | """ 90 | Register a dependency provider. 91 | 92 | A *provider* returns the requested dependency when called. The 93 | provider is called with no arguments every time the dependency is 94 | needed. It is possible to inject other dependencies into the call 95 | signature of a provider. 96 | 97 | :param name: Name of the injection point. 98 | :param func: The provider callable. 99 | :param alias: A list of alternative injection points. 100 | :return: None 101 | """ 102 | self.add_resolver(name, _make_null_resolver(name, func), alias=alias) 103 | 104 | def add_resolver(self, name, func, alias=()): 105 | """ 106 | Register a dependency provider resolver. 107 | 108 | A *resolver* returns a cache-able *provider* and may accept 109 | injection-point specific configuration. The resolver is usually 110 | called only once per injection point and the return value is cached. 111 | It must return a (callable) provider. It is possible to inject other 112 | dependencies into the call signature of a resolver. 113 | 114 | :param name: Name of the injection point. 115 | :param func: The resolver callable. 116 | :param alias: A list of alternative injection points. 117 | :return: None 118 | """ 119 | self._resolvers[name] = func 120 | for name in _makelist(alias): 121 | self._resolvers[name] = func 122 | self.__cache.clear() 123 | 124 | def remove(self, name): 125 | """ 126 | Remove any dependency, provider or resolver bound to the named 127 | injection point. 128 | 129 | :param name: Name of the injection point to clear. 130 | :return: None 131 | """ 132 | if self._resolvers.pop(name): 133 | self.__cache.clear() 134 | 135 | def provider(self, name, alias=()): 136 | """ 137 | Decorator to register a dependency provider. 138 | See :func:`add_provider` for a description. 139 | 140 | :param name: Name of the injection point. 141 | :param alias: A list of alias names for this injection point. 142 | :return: Decorator that registers the provider function to the 143 | injector. 144 | """ 145 | assert isinstance(name, str) 146 | 147 | def decorator(func): 148 | self.add_provider(name, func, alias=alias) 149 | return func 150 | 151 | return decorator 152 | 153 | def resolver(self, name, alias=()): 154 | """ 155 | Decorator to register a dependency provider resolver. See 156 | :func:`add_resolver` for a description. 157 | 158 | :param name: Name of the injection point. 159 | :param alias: A list of alias names for this injection point. 160 | :return: Decorator that registers the resolver to the injector. 161 | """ 162 | 163 | def decorator(func): 164 | self.add_resolver(name, func, alias=alias) 165 | return func 166 | 167 | return decorator 168 | 169 | def inspect(self, func): 170 | """ 171 | Return a dict that maps parameter names to injection points for the 172 | provided callable. 173 | """ 174 | func = _unwrap(func) 175 | 176 | if py32: 177 | args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations = inspect.getfullargspec(func) 178 | else: 179 | args, varargs, keywords, defaults = inspect.getargspec(func) 180 | kwonlyargs, kwonlydefaults, annotations = [], {}, {} 181 | 182 | defaults = defaults or () 183 | kwonlydefaults = kwonlydefaults or {} 184 | 185 | injection_points = {} 186 | 187 | # Positional arguments without default value are potential injection points, 188 | # but marked as 'implicit'. 189 | for arg in args[:len(args) - len(defaults or [])]: 190 | if arg not in self._never_inject: 191 | injection_points[arg] = _InjectionPoint(arg, implicit=True) 192 | 193 | for arg, value in zip(args[::-1], defaults[::-1]): 194 | if isinstance(value, _InjectionPoint): 195 | injection_points[arg] = value 196 | 197 | for arg, value in kwonlydefaults.items(): 198 | if isinstance(value, _InjectionPoint): 199 | injection_points[arg] = value 200 | 201 | for arg, value in annotations.items(): 202 | if isinstance(value, _InjectionPoint): 203 | injection_points[arg] = value 204 | 205 | return injection_points 206 | 207 | def _resolve(self, func): 208 | """ 209 | Given a callable, return a dict that maps argument names to provider 210 | callables. The providers are resolved and wrapped already and should 211 | be called with no arguments to receive the injectable. 212 | 213 | This is called by __ProviderCache.__missing__ and should not be used 214 | in other situations. 215 | """ 216 | results = {} 217 | for arg, ip in self.inspect(func).items(): 218 | results[arg] = self._prime(ip) 219 | return results 220 | 221 | def _prime(self, ip): 222 | """ 223 | Prepare a named resolver for a given injection point. 224 | 225 | Internal use only. See _resolve() 226 | """ 227 | try: 228 | provider_resolver = self._resolvers[ip.name] 229 | except KeyError: 230 | err = InjectError("No provider for injection point %r" % ip.name) 231 | if not ip.implicit: 232 | raise err 233 | def fail_if_injected(): 234 | raise err 235 | return fail_if_injected 236 | 237 | provider = self.call_inject(provider_resolver, **ip.config) 238 | return self.wrap(provider) 239 | 240 | def call_inject(self, func, **ka): 241 | """ 242 | Call a function and inject missing dependencies. If you want to call 243 | the same function multiple times, consider :method:`wrap`ing it. 244 | """ 245 | for key, producer in self.__cache[func]: 246 | if key not in ka: 247 | ka[key] = producer() 248 | return func(**ka) 249 | 250 | def wrap(self, func): 251 | """ 252 | Turn a function into a dependency managed callable. 253 | 254 | Usage:: 255 | @injector.wrap 256 | def my_func(db: inject('database')): 257 | pass 258 | 259 | or:: 260 | managed_callable = injector.wrap(my_callable) 261 | 262 | :param func: A callable with at least one injectable parameter. 263 | :return: A wrapped function that calls :method:`call_inject` 264 | internally. 265 | 266 | If the provided function does not accept any injectable parameters, 267 | it is returned unchanged. 268 | """ 269 | cache = self.__cache # Avoid dot lookup in hot path 270 | 271 | # Skip wrapping for functions with no injection points 272 | if not self.inspect(func): 273 | return func 274 | 275 | @functools.wraps(func) 276 | def wrapper(**ka): 277 | # PERF: Inlined call_inject call. 278 | # Keep in sync with the implementation above. 279 | for key, producer in cache[func]: 280 | if key not in ka: 281 | ka[key] = producer() 282 | return func(**ka) 283 | 284 | wrapper.__injector__ = self 285 | return wrapper 286 | 287 | 288 | class Plugin(Injector): 289 | api = 2 290 | 291 | def __init__(self): 292 | super(Plugin, self).__init__() 293 | self.app = None 294 | 295 | def setup(self, app): 296 | from bottle import request, response 297 | 298 | self.app = app 299 | self.add_value('injector', self) 300 | self.add_value('config', app.config) 301 | self.add_value('app', app) 302 | self.add_value('request', request, alias=['req', 'rq']) 303 | self.add_value('response', response, alias=['res', 'rs']) 304 | 305 | def apply(self, callback, route): 306 | if self.inspect(callback): 307 | return self.wrap(callback) 308 | return callback 309 | 310 | 311 | def inject(name, **kwargs): 312 | """ 313 | Mark an argument in a function signature as an injection point. 314 | 315 | The return value can be used as an annotation (Python 3) or default 316 | value (Python 2) for parameters that should be recognized by dependency 317 | injection. 318 | 319 | Usage:: 320 | def my_func(a: inject('name'), 321 | b: inject('name', conf="val") # Resolvers only. 322 | ): 323 | pass 324 | 325 | :param name: Name of the dependency to inject. 326 | :param kwargs: Additional keyword arguments passed to the dependency 327 | provider resolver. 328 | :return: 329 | """ 330 | return _InjectionPoint(name, config=kwargs) 331 | 332 | --------------------------------------------------------------------------------