Skip to main content

An opinionated dependency injection library for Python

Project description

PySyringe

Tests Coverage PyPI version Docs

An opinionated dependency injection library for Python.

A container that does not rely on adding decorators to your domain classes. It only wraps views in the infrastructure layer to keep your domain and app layer decoupled from the framework and the container.

Read the full documentation

✨ Features

  • 🚀 Zero-decorator DI: keep your domain clean; inject only at call sites.
  • 🏭 Factory-based wiring: resolve by return type annotations on your factory.
  • 🧩 Inference-based construction: auto-wire constructor dependencies by type hints.
  • 🧪 Test-friendly mocks: replace any dependency per test with use_mock(...).
  • 🔒 Thread-safe mocks: mocks are stored per-thread; aliases and blacklists are global.
  • 🧰 Aliases & blacklists: map interfaces to implementations and mark types as non-creatable.
  • Resolution cache: caches factory lookups and constructor introspection (not instances).
  • 🔐 Thread-safe: singleton creation uses locking; per-thread singletons via thread_local_singleton.
  • High test coverage: comprehensive test suite ensuring reliability and correctness.

📦 Installation

pip install pysyringe

pysyringe on PyPI

🚀 Usage

1) Define a factory with return-type annotations

Your factory can be any class. The container inspects public methods and uses the return-type annotation to know what it can provide.

from myapp.domain import EmailSenderInterface
from myapp.infra import LoggingEmailSender, SmtpEmailSender


class Factory:
    def __init__(self, environment: str) -> None:
        self.environment = environment

    def get_mailer(self) -> EmailSenderInterface:
        if self.environment == "production":
            return SmtpEmailSender("mta.example.org", 25)
        return LoggingEmailSender()

2) Create the container

from os import getenv
from pysyringe.container import Container

factory = Factory(getenv("ENVIRONMENT", "development"))
container = Container(factory)

3) Configure blacklist and aliases

  • never_provide(type) blacklists types you don't want the container to try to resolve when processing functions decorated with @container.inject. This is helpful to prevent the container from attempting to build framework types (e.g., Django/Starlette requests) via constructor inference.

  • alias(interface, implementation) is a simple way to map an interface to a concrete class without having to provide a factory method. The container will build the implementation using constructor introspection, recursively resolving its dependencies.

from django.core.http import HttpRequest, HttpResponse
from myapp.domain import CalendarInterface
from myapp.infra import Calendar

# Never try to construct these framework types during injection
container.never_provide(HttpRequest)
container.never_provide(HttpResponse)

# Map an interface to a concrete implementation (no factory needed)
container.alias(CalendarInterface, Calendar)

4) Inject at the call site

Use @container.inject to supply arguments by type. Only parameters that can be resolved will be injected; the rest stay as normal function parameters. The decorator returns a partial function with all the dependencies wired; you are responsible for passing the remaining parameters to your methods.

A complete Flask example that prints the current time using an alias:

# app.py
from datetime import datetime, timezone
from flask import Flask
from pysyringe.container import Container


# 1) Define an interface and an implementation in your app
class CalendarInterface:
    def now(self) -> datetime:
        raise NotImplementedError


class Calendar(CalendarInterface):
    def now(self) -> datetime:
        return datetime.now(timezone.utc)


# 2) Create the container and configure an alias
container = Container()                       # No factory needed for this example
container.alias(CalendarInterface, Calendar)  # resolve CalendarInterface -> Calendar


# 3) Write your application and leverage the container.inject to provide dependencies
app = Flask(__name__)

@app.get("/now")
@container.inject
def get_now(calendar: CalendarInterface):
    return {"now": calendar.now().isoformat()}

if __name__ == "__main__":
    app.run(debug=True)

5.1) Test with mocks (new syntax)

Mocks are thread-local. Configure them per-test and clear afterwards.

import pytest
from pysyringe.container import Container
from myapp.domain import UserRepository
from myapp.usecases import SignupUserService
from myapp.infra.testing import InMemoryUserRepository


def test_create_user():
    user_repository = InMemoryUserRepository()
    with container.override(UserRepository, user_repository):
        service = container.provide(SignupUserService)

    service.signup("John Doe", "john.doe@example.org")

    assert user_repository.get_by_email("john.doe@example.org")

5.2) Test with mocks (old syntax)

Mocks are thread-local. Configure them per-test and clear afterwards.

import pytest
from pysyringe.container import Container
from myapp.domain import UserRepository
from myapp.usecases import SignupUserService
from myapp.infra.testing import InMemoryUserRepository


@pytest.fixture(autouse=True)
def clear_container_mocks_after_each_test():
    yield
    container.clear_mocks()


def test_create_user():
    user_repository = InMemoryUserRepository()
    container.use_mock(UserRepository, user_repository)
    service = container.provide(SignupUserService)

    service.signup("John Doe", "john.doe@example.org")

    assert user_repository.get_by_email("john.doe@example.org")

⚡ Resolution cache

PySyringe includes a lightweight resolution cache to speed up dependency resolution without caching instances.

What is cached:

  • A precomputed map of factory methods keyed by their return type (built once at Container initialization) for O(1) lookups.
  • Constructor parameter introspection is LRU-cached to avoid repeated signature parsing and type disambiguation.

What is NOT cached:

  • Resolved instances. The cache accelerates how dependencies are located and wired, not the objects produced.

This means singleton semantics or any custom sharing strategy you define remain unchanged. The cache only reduces overhead during resolution.

🧷 Singleton helpers

PySyringe provides two singleton helpers for use inside your factory methods. Both cache instances keyed by the class and its constructor arguments.

Helper Scope Use case
singleton() Global (shared across threads) Thread-safe resources like connection pools or HTTP clients
thread_local_singleton() Per-thread Resources that are not safe to share, like database sessions

singleton() — shared across all threads

from pysyringe.singleton import singleton
from pysyringe.container import Container


class DatabaseClient:
    def __init__(self, connection_string: str) -> None:
        self.connection_string = connection_string


class Factory:
    def get_database_client(self) -> DatabaseClient:
        return singleton(DatabaseClient, "postgresql://localhost:5432/mydb")


container = Container(Factory())

client1 = container.provide(DatabaseClient)
client2 = container.provide(DatabaseClient)
assert client1 is client2  # Same instance, even across threads

Creation is thread-safe: concurrent threads calling singleton() for the same key will never produce duplicate instances (double-checked locking).

thread_local_singleton() — one instance per thread

from pysyringe.singleton import thread_local_singleton


class Factory:
    def get_session(self) -> DatabaseSession:
        return thread_local_singleton(DatabaseSession, "postgresql://localhost:5432/mydb")

Each thread gets its own DatabaseSession instance. Within the same thread, repeated calls return the same object. This is useful for resources that are not thread-safe, such as database sessions or request-scoped state.

Notes

  • The cache key includes: the class, positional args, and keyword args (order-independent for keywords).
  • Perfect for database connections, HTTP clients, or any resource that should be shared per configuration.

🔒 Thread safety

The Container is thread-safe with respect to mocks. Mocks configured after the container has been created are stored in thread-local storage, so changes made to mocks in one thread do not affect other threads.

  • Shared across all threads: alias(...), never_provide(...), and the factory configuration (methods on your factory used for resolution).
  • Thread-local: use_mock(...) and clear_mocks() operate only on the calling thread's mock store.

Implications:

  • Using use_mock(SomeType, mock_instance) in one thread will not change what another thread receives for SomeType.
  • Calling clear_mocks() clears only the current thread's mocks.
  • To share a behavior globally across threads, prefer alias(...) or implement a factory method instead of relying on mocks.

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

pysyringe-1.4.2.tar.gz (58.6 kB view details)

Uploaded Source

Built Distribution

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

pysyringe-1.4.2-py3-none-any.whl (9.0 kB view details)

Uploaded Python 3

File details

Details for the file pysyringe-1.4.2.tar.gz.

File metadata

  • Download URL: pysyringe-1.4.2.tar.gz
  • Upload date:
  • Size: 58.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.8

File hashes

Hashes for pysyringe-1.4.2.tar.gz
Algorithm Hash digest
SHA256 c9a3045d356a1773e65c9a645d5a1f7cadca5ad1a9e4b0af102281a3a0f5d661
MD5 ac93e0c17c2daa943a8929a8e84780d8
BLAKE2b-256 56fa32d33c9e86e140ebcfc90a15b6846780fe433104105bc96d65e9ec9f5c8c

See more details on using hashes here.

File details

Details for the file pysyringe-1.4.2-py3-none-any.whl.

File metadata

  • Download URL: pysyringe-1.4.2-py3-none-any.whl
  • Upload date:
  • Size: 9.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.8

File hashes

Hashes for pysyringe-1.4.2-py3-none-any.whl
Algorithm Hash digest
SHA256 3f6526a62f6eaca1db73be73964b1dbe5b9f019c25643ed873e67c614c29b418
MD5 814f9b74522d17c29d9dc7baa75f8b26
BLAKE2b-256 549864b325a9a4f4be409e342845259e6213dc119bbf2ff430b6e0ad887e950b

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