Skip to main content

No project description provided

Project description

ditto: dependency injection tool

ditto is a simple and lightweight dependency injection tool for Python, with support for nullable arguments and parent class registration.

Features

  • Easy-to-use decorators for dependency injection
  • Supports class and instance-based services
  • Sync and Async support
  • Nullable argument handling
  • Parent class registration

Installation

pip install pyditto

Usage

Basic Usage

Declare your dependencies to ditto on application start:

import ditto as di

class Pokemon:
    def attack(self) -> str:
        pass

class FirePokemon(Pokemon):
    def attack(self) -> str:
        return 'Ember ! 🔥'

di.register(FirePokemon)  # register as a class
di.register(FirePokemon())  # or as an object

Then you can inject your dependency into your class:

@di.inject
class Team:
    charmander: FirePokemon

    def battle(self):
        self.charmander.attack()

team = Team()
team.battle()  # 'Ember ! 🔥'

Or into an isolated function within your class:

class Team:
    @di.inject
    def battle(self, charmander: FirePokemon):
        charmander.attack()

team = Team()
team.battle()  # 'Ember ! 🔥'

Or just within a bare function:

@di.inject
def battle(charmander: FirePokemon):
    charmander.attack()

battle()  # 'Ember ! 🔥'

Advanced Usage

Type Lookup in Registry

ditto uses the lowercase name of the class as the key in its registry:

di.register(Charmander)

# These will all retrieve the Charmander instance:
fire_pokemon = di.ServiceRegistry.get_instance().get('firepokemon')
charmander = di.ServiceRegistry.get_instance().get('charmander')

This is particularly useful to understand when working with optional dependencies or when overriding parent class implementations.

Asynchronous Support

ditto supports dependency injection for asynchronous functions:

import asyncio

class AsyncPokemon:
    async def attack(self) -> str:
        await asyncio.sleep(1)
        return "Async attack!"

di.register(AsyncPokemon)

@di.inject
async def async_battle(pokemon: AsyncPokemon):
    result = await pokemon.attack()
    print(result)

asyncio.run(async_battle())  # Output: Async attack!

Nullable Argument Handling

ditto supports nullable arguments, allowing you to specify optional dependencies:

from typing import Optional

@di.inject
def train_pokemon(charmander: FirePokemon, optional_pokemon: Optional[Pokemon] = None):
    charmander.attack()
    if optional_pokemon:
        optional_pokemon.attack()
    else:
        print("No optional Pokemon available")

# This will work even if Pokemon is not registered
train_pokemon()

# You can also explicitly pass None for optional arguments
train_pokemon(optional_pokemon=None)

In this example:

  • charmander is a required dependency and must be registered.
  • optional_pokemon is an optional dependency. If it's not registered, None will be injected.

Note on Type Registration for Optional Parameters: For both Pokemon and Optional[Pokemon], ditto uses the same key ('pokemon') in its registry. The Optional wrapper doesn't affect the registration key; it only changes how ditto handles the case when the service isn't found.

Parent Class Registration

When you register a service class, ditto automatically registers it under its immediate parent class as well:

class Pokemon:
    pass

class FirePokemon(Pokemon):
    pass

class Charmander(FirePokemon):
    pass

di.register(Charmander)

@di.inject
def train(fire_pokemon: FirePokemon, charmander: Charmander):
    print(f"Training: {fire_pokemon.__class__.__name__}, {charmander.__class__.__name__}")

train()  # Output: Training: Charmander, Charmander

@di.inject
def catch(pokemon: Pokemon):
    print(f"Trying to catch: {pokemon.__class__.__name__}")

catch()  # This will raise a ValueError: Service 'pokemon' not found.

In this example:

  • Registering Charmander also makes it available when FirePokemon (its immediate parent class) is requested.
  • However, Pokemon (the grandparent class) is not automatically registered.
  • This allows for one level of inheritance in dependency injection.

Error Handling and Common Pitfalls

  1. Missing dependencies: If a required dependency is not registered, ditto will raise a ValueError:

    @di.inject
    def use_unregistered(service: NotInPokedexPokemon):
        pass
    
    use_unregistered()  # Raises ValueError: Service 'notinpokedexpokemon' not found.
    
  2. Circular dependencies: ditto doesn't automatically resolve circular dependencies. Be cautious when designing your dependency graph.

Multiple Registrations

When multiple classes are registered for the same type, ditto will use the most recently registered one:

class Pikachu(Pokemon):
    pass

class Charmander(Pokemon):
    pass

di.register(Pikachu)
di.register(Charmander)

@di.inject
def catch(pokemon: Pokemon):
    print(f"Caught a {pokemon.__class__.__name__}")

catch()  # Output: Caught a Charmander

Best Practices

  1. Register dependencies at the application's entry point.
  2. Use interfaces (abstract base classes) for better decoupling.
  3. Avoid registering too many concrete implementations to keep your dependency graph simple.

Managing Registered Services

To unregister a service:

# Unregister a service
ServiceRegistry.get_instance()._services.pop('pokemon', None)

Limitations

  • ditto doesn't support automatic constructor injection. Parameters must be explicitly annotated.
  • Circular dependencies are not automatically resolved and may cause issues if not carefully managed.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License - see the LICENSE file for details.

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

pyditto-0.1.3.tar.gz (4.8 kB view details)

Uploaded Source

Built Distribution

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

pyditto-0.1.3-py3-none-any.whl (5.2 kB view details)

Uploaded Python 3

File details

Details for the file pyditto-0.1.3.tar.gz.

File metadata

  • Download URL: pyditto-0.1.3.tar.gz
  • Upload date:
  • Size: 4.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.8.3 CPython/3.12.0 Linux/5.15.146.1-microsoft-standard-WSL2

File hashes

Hashes for pyditto-0.1.3.tar.gz
Algorithm Hash digest
SHA256 58a75a0cca5ef4efd84e7c7e7d04a628685253d8cf844b97d6e376ce963e5aab
MD5 3c32fa61f79ee09e06d80869b249e279
BLAKE2b-256 59a807374045eda7d4fc019b520381c6fc323b8adfda2263bfe36ef433151b08

See more details on using hashes here.

File details

Details for the file pyditto-0.1.3-py3-none-any.whl.

File metadata

  • Download URL: pyditto-0.1.3-py3-none-any.whl
  • Upload date:
  • Size: 5.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.8.3 CPython/3.12.0 Linux/5.15.146.1-microsoft-standard-WSL2

File hashes

Hashes for pyditto-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 cb3a7fc633e737ca61f15e5df86a88cd97113d9c846d3eb695358a7841b3257a
MD5 afe2e5a92614191867b5fdc41d39c896
BLAKE2b-256 17a144c4b13c126ce67fdeb4915711554e04c08353db803be89a59a0ea859fee

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