Skip to main content

Collections for the Injector Package

Project description

Injector Collections

This package adds collections to the Injector python package. A collection is an injectable class containing several other classes, which were tagged using the @CollectionItem keyword and a designated Collection-class.

Installation

pip install injector_collections

Setup

To be able to use this package, You must create a new module-directory inside your python project. The name of this module-directory does not matter, let's name it my_collections here. Inside this module-directory You must create a file (module) named collections.py (and of course an __init__.py).

Provided your root-directory is named src, the following file-tree will be the result:

src
└── my_collections
    ├── __init__.py
    └── collections.py

All files can be initially empty.

Usage / Example

Be sure to have done everything described in Setup.

Let's say You have an application and want to add several plugins which are all used in the app afterwards, but of course this list of plugins must be easily extensible. You already use injector to instantiate your App:

# app.py

from injector import inject

class App:
    @inject
     def __init__(self, '''plugins shall be injected here''', '''some other injected classes'''):
         # here comes some code

         def run(self):
             # plugins should be executed here
             # runs the app

from injector import Injector

injector = Injector()
outer = injector.get(App)

Now the first step is to create a collection class for your plugins:

# my_collections/stub.py

from injector_collections import Collection

class PluginCollection(Collection):
    pass

Note: The collection class (here PluginCollection) should not have any implementation. Currently any implementation will just be ignored and cannot be used after the actual class was generated from the stub.

Next add some Plugins as an example. And Tag them with @PluginCollection and your previously defined PluginCollection as argument:

# plugins.py

from injector_collections import CollectionItem
# The @CollectionItem decorator needs the collection class as argument
from my_collections.collections import PluginCollection

@CollectionItem(PluginCollection)
class HelloPlugin:
    def run(self):
        print("Hello Friends!")

@CollectionItem(PluginCollection)
class GoodbyPlugin:
    def run(self):
        print("Goodby Friends!")

Important: Currently you need to import CollectionItem literally, as the code will be scanned for files containing the @CollectionItem string, which will then be imported to generate the collections!

Now we're almost done. The last thing to do, before we can use our actual collections is to generate them, of course! Create a small script under your projects root-directory:

# generate.py
from injector import inject
from injector_collections import generateCollections
# First argument of generateCollections is your my_collections-module name, the
# second a list of modules containing any collection items (in this case your
# plugins-module).
generateCollections(inject, "my_collections", ['plugins'])

And execute it:

python ./generate.py

Now you just need to import the PluginCollection to your App and use it:

# app.py

from my_collections.collections import PluginCollection

from plugins import HelloPlugin

from injector import inject

class App:
    @inject
     def __init__(self, plugins: PluginCollection, '''some other injected classes'''):
         # here comes some code
         self.plugins = plugins

         def run(self):
             # plugins.items contains a dict containing the plugins:
             for plugin in self.plugins.items.values():
                 plugin.run() # prints "Hello Friends!" and "Goodby Friends!"
             # Or just call a single plugin from the collection:
             self.plugins[HelloPlugin].run()
             # also getting plugins simply by class name (if unambigous in this
             # collection) is possible
             self.plugins.byClassname['HelloPlugin'].run()
...

Type Hinting for polymorphic Items in a Collection

If all items in a collection e.g. implement a common Interface, the generated Collections may make use of type-Hinting. The Interface-Type can then be provided as generic argument when deriving from Collection.

# my_collections/stub.py

from plugins import PluginInterface
from injector_collections import Collection

class PluginCollection(Collection[PluginInterface]):
    pass

After that the PluginCollection.items, PluginCollection.__get__, PluginCollection.__set__ und PluginCollection.byClassname attributes/methods have proper type hints on PluginItemInterface.

Items with Assisted Injection

If some of the items in the created collection have non-injectable parameters, one can use the assisted-parameter for the @CollectionItem-decorator, e.g.:

from injector_collections import CollectionItem
from my_collections.collections import PluginCollection

@CollectionItem(PluginCollection, assisted=True)
class HelloPlugin:
    def __init__(someNotInjectableParameter: SomeClass):
        self.param = someNotInjectableParameter 

    def run(self):
        print("Hello Friends!")

Now the item will not be directly injected into the Collection, but instead a ClassAssitedBuilder instance for that item. Don't forget to adjust the type hinting.

FAQ or Why does it not work?

The process of creating the collections is quite clumsy and assumes that the developer sets quite a lot of things right. If it does not work, it is quite likely, that You just forgot one of these awful details:

  • Is there a collection class for the collection?
  • Are all items annotated with @CollectionItem?
  • Is the Type-Argument of @CollectionItem the correct collection class?
  • Are the items all inside a proper module hierarchy (the directory and all parent directories of the items should contain an __init__.py).
  • Have You generated the collections using generateCollections?
  • Did You add the module or a parent module of the items to the module list of generateCollections?

When the complexity of the project increases, it is quite likely, that You will encounter recursive import problems. Some things You can do:

  • If You just need typehinting for a class, which may cause import recursion. You can import it only for type-checking like this:
    from abc import abstractmethod
    from typing import TYPE_CHECKING
    
    # avoid circular imports, we only need PipelineContext for the LSP (type check)
    if TYPE_CHECKING:
        from xyz import Someclass
    
    class SomeInterface:
        @abstractmethod
        def hello() -> 'SomeClass':
            pass
    
  • There are some really strange cases, where a certain collection name causes an error with injector. I can't say, if it's a bug in injector or injector_collections. Just try another collection name.
  • If the project grows, there may be circular dependencies, because all collections are in a single file. That can be circumvented by changing the file structure:
    src
    └── my_collections
        ├── __init__.py
        └── collections
            ├── __init__.py
            ├── PluginCollection.py
            └── SomeOtherCollection.py
    
    __init__.py must have the following contents:
    from my_collections.collections.PluginCollection import *
    from my_collections.collections.SomeOtherCollection import *
    ... # other collection definitions not included in a seperate file
    
    Now import from my_collections.collections.PluginCollection and my_collections.collections.SomeOtherCollection seperately in your code when using @CollectionItem(PluginCollection) or @CollectionItem(SomeOtherCollection).

Developement

git tag # show all existing tags (versions)
VERSION=0.0.25 # use a not yet existing version number
# edit pyproject.toml and insert new version
git add .
git commit
git tag v$VERSION # create a tag for latest commit
git push origin master --tags # push commit + tag
hatchling build
twine upload dist/*-$VERSION-*.whl dist/*-$VERSION.tar.gz

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

injector_collections-0.1.0.tar.gz (13.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

injector_collections-0.1.0-py3-none-any.whl (8.9 kB view details)

Uploaded Python 3

File details

Details for the file injector_collections-0.1.0.tar.gz.

File metadata

  • Download URL: injector_collections-0.1.0.tar.gz
  • Upload date:
  • Size: 13.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.0.1 CPython/3.13.2

File hashes

Hashes for injector_collections-0.1.0.tar.gz
Algorithm Hash digest
SHA256 80e3bbb82c2795bb25d9a8f343749d0e64875d1c4639436398ef0f461c51b7d8
MD5 aff0ba516d3059e03f27aa81d61e8e79
BLAKE2b-256 c980fe3b613c4f7ef7f2b01c72325874d8c2d08dd0036a4c53e6bc4727b87e14

See more details on using hashes here.

File details

Details for the file injector_collections-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for injector_collections-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0c39855bea4ad7e652ac1f1b172a7988eef8e446d1d47be4fce8cc348e92be40
MD5 76c1aeda6610b8470495188beacb9aa2
BLAKE2b-256 17e7526c3dd2529501862a968b2f35ec28650c616aa91bc0d33f15fca317df12

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page