diff --git a/.cruft.json b/.cruft.json index 090f177..4569f3c 100644 --- a/.cruft.json +++ b/.cruft.json @@ -6,7 +6,7 @@ "cookiecutter": { "author_name": "Nikita Zavadin", "author_email": "nikita.zavadin@wolt.com", - "github_username": "RB387", + "github_username": "woltapp", "project_name": "magic-di", "project_slug": "magic-di", "package_name": "magic_di", diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 05e14ab..a9d26fb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,6 @@ repos: types: [ python ] - id: ruff name: ruff - # Add --fix, in case you want it to autofix when this hook runs entry: poetry run ruff check --fix --force-exclude require_serial: true language: system diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f436d9..e39b33f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,3 +4,5 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +### Changed +- Initial version diff --git a/LICENCE b/LICENCE index 690a230..ccaa05d 100644 --- a/LICENCE +++ b/LICENCE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2021 Nikita Zavadin +Copyright (c) 2024 Wolt Enterprises Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README.md b/README.md index f8d1f93..070a010 100644 --- a/README.md +++ b/README.md @@ -16,19 +16,9 @@ --- -Dependency Injector that makes your life easier with built-in support of FastAPI, Celery (and it can be integrated with everything) - -What are the problems with FastAPI’s dependency injector? -1) It forces you to use global variables. -2) You need to write an endless number of fabrics with startup logic -3) It makes your project highly dependent on FastAPI’s injector by using “Depends” everywhere. - -To solve these problems, you can use this dead-simple Dependency Injector that will make development so much easier. - -__Q: But why not to use [python-dependency-injector](https://github.com/ets-labs/python-dependency-injector) or other libs?__ - -A: The goal of this Dependency Injector is to __reduce the amount of code as much as possible__ and get rid of enterprise code with millions of configs, containers, and fabrics. That’s why python-dependency-injector and similar libraries are overkill. The philosophy of this injector is that clients know how to configure themselves and perform all startup routines. +Dependency Injector with minimal boilerplate code, built-in support for FastAPI and Celery, and seamless integration to basically anything. +## Contents * [Install](#install) * [Getting Started](#getting-started) * [Clients Configuration](#clients-configuration) @@ -41,14 +31,25 @@ A: The goal of this Dependency Injector is to __reduce the amount of code as muc * [Testing](#testing) * [Default simple mock](#default-simple-mock) * [Custom mocks](#custom-mocks) +* [Alternatives](#alternatives) * [Development](#development) ## Install ```bash +pip install magic-di +``` + +With FastAPI integration: +```bash pip install 'magic-di[fastapi]' ``` +With Celery integration: +```bash +pip install 'magic-di[celery]' +``` + ## Getting Started ```python @@ -323,16 +324,32 @@ def client(injector: DependencyInjector, service_mock: Service): yield client ``` +## Alternatives + +### [FastAPI's built-in dependency injection](https://fastapi.tiangolo.com/tutorial/dependencies/) + +FastAPI's built-in DI is great, but it makes the project (and its business logic) dependent on FastAPI, `fastapi.Depends` specifically. + +`magic-di` decouples DI from other dependencies while still offering seamless integration to FastAPI, for example. + +### [python-dependency-injector](https://github.com/ets-labs/python-dependency-injector) + +[python-dependency-injector](https://github.com/ets-labs/python-dependency-injector) is great, but it requires a notable amount of boilerplate code. + +The goal of `magic-di` is to __reduce the amount of code as much as possible__ and get rid of enterprise code with countless configs, containers, and fabrics. +The philosophy of `magic-di` is that clients know how to configure themselves and perform all startup routines. + + ## Development * Clone this repository * Requirements: * [Poetry](https://python-poetry.org/) - * Python 3.8+ + * Python 3.10+ * Create a virtual environment and install the dependencies ```sh -poetry install +poetry install --all-extras ``` * Activate the virtual environment @@ -349,17 +366,17 @@ pytest ### Documentation -The documentation is automatically generated from the content of the [docs directory](https://github.com/RB387/magic-di/tree/master/docs) and from the docstrings +The documentation is automatically generated from the content of the [docs directory](https://github.com/woltapp/magic-di/tree/master/docs) and from the docstrings of the public signatures of the source code. The documentation is updated and published as a [Github Pages page](https://pages.github.com/) automatically as part each release. ### Releasing -Trigger the [Draft release workflow](https://github.com/RB387/magic-di/actions/workflows/draft_release.yml) +Trigger the [Draft release workflow](https://github.com/woltapp/magic-di/actions/workflows/draft_release.yml) (press _Run workflow_). This will update the changelog & version and create a GitHub release which is in _Draft_ state. Find the draft release from the -[GitHub releases](https://github.com/RB387/magic-di/releases) and publish it. When - a release is published, it'll trigger [release](https://github.com/RB387/magic-di/blob/master/.github/workflows/release.yml) workflow which creates PyPI +[GitHub releases](https://github.com/woltapp/magic-di/releases) and publish it. When + a release is published, it'll trigger [release](https://github.com/woltapp/magic-di/blob/master/.github/workflows/release.yml) workflow which creates PyPI release and deploys updated documentation. ### Pre-commit diff --git a/pyproject.toml b/pyproject.toml index 69bd7cf..38beb6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,9 +8,9 @@ authors = [ license = "MIT" readme = "README.md" -documentation = "https://RB387.github.io/magic-di" -homepage = "https://RB387.github.io/magic-di" -repository = "https://github.com/RB387/magic-di" +documentation = "https://woltapp.github.io/magic-di" +homepage = "https://woltapp.github.io/magic-di" +repository = "https://github.com/woltapp/magic-di" classifiers = [ "Development Status :: 4 - Beta", @@ -61,7 +61,7 @@ requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.ruff] -target-version = "py38" # The lowest supported version +target-version = "py310" # The lowest supported version line-length = 100 [tool.ruff.lint] diff --git a/src/magic_di/_container.py b/src/magic_di/_container.py index 3cce013..ddc8843 100644 --- a/src/magic_di/_container.py +++ b/src/magic_di/_container.py @@ -4,9 +4,11 @@ import inspect from dataclasses import dataclass from threading import Lock -from typing import TYPE_CHECKING, Any, Generic, Iterable, TypeVar, cast +from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast if TYPE_CHECKING: + from collections.abc import Iterable + from magic_di import ConnectableProtocol T = TypeVar("T") diff --git a/src/magic_di/_injector.py b/src/magic_di/_injector.py index 4dbe6dc..59878e0 100644 --- a/src/magic_di/_injector.py +++ b/src/magic_di/_injector.py @@ -8,9 +8,6 @@ TYPE_CHECKING, Annotated, Any, - Callable, - Iterable, - Iterator, TypeVar, cast, get_origin, @@ -27,6 +24,8 @@ from magic_di.exceptions import InjectionError, InspectionError if TYPE_CHECKING: + from collections.abc import Callable, Iterable, Iterator + from magic_di._connectable import ConnectableProtocol # flag to use in typing.Annotated diff --git a/src/magic_di/_utils.py b/src/magic_di/_utils.py index a4b8e56..63825f5 100644 --- a/src/magic_di/_utils.py +++ b/src/magic_di/_utils.py @@ -1,16 +1,16 @@ from __future__ import annotations -from typing import Any, TypeVar, Union, cast, get_args +from typing import Any, TypeVar, cast, get_args from typing import get_type_hints as _get_type_hints from magic_di import ConnectableProtocol -LegacyUnionType = type(Union[object, None]) +LegacyUnionType = type(object | None) try: from types import UnionType # type: ignore[import-error,unused-ignore] except ImportError: - UnionType = LegacyUnionType # type: ignore[misc, assignment] + UnionType = LegacyUnionType # type: ignore[misc] T = TypeVar("T") @@ -40,7 +40,7 @@ def get_cls_from_optional(cls: T) -> T: Returns: T: Extracted class """ - if not isinstance(cls, (UnionType, LegacyUnionType)): + if not isinstance(cls, UnionType | LegacyUnionType): return cls args = get_args(cls) diff --git a/src/magic_di/celery/_async_utils.py b/src/magic_di/celery/_async_utils.py index 80f5f0e..1a7fc22 100644 --- a/src/magic_di/celery/_async_utils.py +++ b/src/magic_di/celery/_async_utils.py @@ -3,7 +3,10 @@ import asyncio import threading from dataclasses import dataclass -from typing import Any, Coroutine, TypeVar +from typing import TYPE_CHECKING, Any, TypeVar + +if TYPE_CHECKING: + from collections.abc import Coroutine R = TypeVar("R") diff --git a/src/magic_di/celery/_loader.py b/src/magic_di/celery/_loader.py index 472fedb..68e95a4 100644 --- a/src/magic_di/celery/_loader.py +++ b/src/magic_di/celery/_loader.py @@ -3,7 +3,7 @@ import asyncio import os import threading -from typing import TYPE_CHECKING, Any, Callable, Protocol, runtime_checkable +from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable from celery import signals from celery.loaders.app import AppLoader # type: ignore[import-untyped] @@ -12,6 +12,8 @@ from magic_di.celery._async_utils import EventLoop, EventLoopGetter, run_in_event_loop if TYPE_CHECKING: + from collections.abc import Callable + from celery.loaders.base import BaseLoader diff --git a/src/magic_di/celery/_task.py b/src/magic_di/celery/_task.py index e039e92..1ec1d3f 100644 --- a/src/magic_di/celery/_task.py +++ b/src/magic_di/celery/_task.py @@ -2,7 +2,7 @@ import inspect from functools import wraps -from typing import Any, Callable, cast, get_type_hints +from typing import TYPE_CHECKING, Any, cast, get_type_hints from celery.app.task import Task @@ -10,6 +10,9 @@ from magic_di.celery._async_utils import EventLoop, run_in_event_loop from magic_di.celery._loader import InjectedCeleryLoaderProtocol +if TYPE_CHECKING: + from collections.abc import Callable + class BaseCeleryConnectableDeps(Connectable): ... diff --git a/src/magic_di/exceptions.py b/src/magic_di/exceptions.py index aa7d13f..a82db10 100644 --- a/src/magic_di/exceptions.py +++ b/src/magic_di/exceptions.py @@ -1,5 +1,6 @@ import inspect -from typing import Any, Callable +from collections.abc import Callable +from typing import Any from magic_di._signature import Signature from magic_di._utils import get_type_hints diff --git a/src/magic_di/fastapi/_app.py b/src/magic_di/fastapi/_app.py index 4cefd8c..cd20d92 100644 --- a/src/magic_di/fastapi/_app.py +++ b/src/magic_di/fastapi/_app.py @@ -6,9 +6,6 @@ TYPE_CHECKING, Annotated, Any, - AsyncIterator, - Callable, - Iterator, Protocol, get_origin, runtime_checkable, @@ -19,6 +16,8 @@ from magic_di._injector import DependencyInjector if TYPE_CHECKING: + from collections.abc import AsyncIterator, Callable, Iterator + from fastapi import FastAPI, routing diff --git a/src/magic_di/utils.py b/src/magic_di/utils.py index 0c87902..72c481f 100644 --- a/src/magic_di/utils.py +++ b/src/magic_di/utils.py @@ -2,12 +2,12 @@ import asyncio import inspect -from typing import TYPE_CHECKING, Awaitable, TypeVar, overload +from typing import TYPE_CHECKING, TypeVar, overload from magic_di import DependencyInjector if TYPE_CHECKING: - from collections.abc import Callable + from collections.abc import Awaitable, Callable T = TypeVar("T") diff --git a/tests/test_app.py b/tests/test_app.py index 3bbaa55..f21f637 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -14,7 +14,7 @@ def test_app_injection(injector: DependencyInjector) -> None: app = inject_app(FastAPI(), injector=injector) @app.get(path="/hello-world") - def hello_world(service: Provide[Service], some_query: str) -> dict[str, str | bool]: # noqa: FA102 + def hello_world(service: Provide[Service], some_query: str) -> dict[str, str | bool]: assert isinstance(service, Service) return {"query": some_query, "is_alive": service.is_alive()} @@ -42,7 +42,7 @@ def global_dependency(dep: Provide[GlobalConnect]) -> None: class MiddlewareNonConnectable: creds: str = "secret_creds" - def get_creds(self, value: str | None = None) -> str: # noqa: FA102 + def get_creds(self, value: str | None = None) -> str: return value or self.creds class AnotherDatabase(Database): ... @@ -65,7 +65,7 @@ def get_creds( def hello_world( service: Provide[Service], creds: Annotated[str, Depends(get_creds)], - ) -> dict[str, str | bool]: # noqa: FA102 + ) -> dict[str, str | bool]: assert isinstance(service, Service) return {"creds": creds, "is_alive": service.is_alive()} @@ -90,7 +90,7 @@ def test_app_injection_clients_connect( router = APIRouter() @router.get(path="/hello-world") - def hello_world(service: Provide[Service]) -> dict[str, bool]: # noqa: FA102 + def hello_world(service: Provide[Service]) -> dict[str, bool]: assert service.workers return { diff --git a/tests/test_celery.py b/tests/test_celery.py index 20430b4..a309f87 100644 --- a/tests/test_celery.py +++ b/tests/test_celery.py @@ -1,9 +1,10 @@ import contextlib import tempfile +from collections.abc import Iterator from contextlib import contextmanager from dataclasses import dataclass from threading import Thread -from typing import Any, Iterator, cast +from typing import Any, cast from unittest.mock import MagicMock, call import pytest @@ -57,7 +58,7 @@ async def service_ping( arg1: int, arg2: str, service: Service = PROVIDE, - ) -> tuple[int, str, bool]: # noqa: FA102 + ) -> tuple[int, str, bool]: return arg1, arg2, service.is_alive() return cast(InjectableCeleryTask, service_ping) @@ -70,7 +71,7 @@ def service_ping_sync( arg1: int, arg2: str, service: Service = PROVIDE, - ) -> tuple[int, str, bool]: # noqa: FA102 + ) -> tuple[int, str, bool]: return arg1, arg2, service.is_alive() return cast(InjectableCeleryTask, service_ping_sync) @@ -85,7 +86,7 @@ class Deps(BaseCeleryConnectableDeps): class SyncServicePingTask(InjectableCeleryTask): deps: Deps - async def run(self, arg1: int, arg2: str) -> tuple[int, str, bool]: # noqa: FA102 + async def run(self, arg1: int, arg2: str) -> tuple[int, str, bool]: return arg1, arg2, self.deps.db.connected return SyncServicePingTask() @@ -100,7 +101,7 @@ class Deps(BaseCeleryConnectableDeps): class ServicePingTask(InjectableCeleryTask): deps: Deps - def run(self, arg1: int, arg2: str) -> tuple[int, str, bool]: # noqa: FA102 + def run(self, arg1: int, arg2: str) -> tuple[int, str, bool]: return arg1, arg2, self.deps.db.connected return ServicePingTask() @@ -266,7 +267,7 @@ async def test_async_function_based_tasks_inside_event_loop( *, task_always_eager: bool, use_broker_and_backend: bool, - expected_mock_calls: list[Any], # noqa: FA102 + expected_mock_calls: list[Any], ) -> None: injector = DependencyInjector() @@ -280,14 +281,14 @@ async def ping_task( arg1: int, arg2: str, service: Service = PROVIDE, - ) -> tuple[int, str, bool]: # noqa: FA102 + ) -> tuple[int, str, bool]: mock() return arg1, arg2, service.is_alive() fastapi_app = FastAPI() @fastapi_app.get("/") - async def handler() -> dict[str, bool]: # noqa: FA102 + async def handler() -> dict[str, bool]: ping_task.apply_async(args=(1337, "leet")) ping_task.apply(args=(1337, "leet-2")) return {"ok": True}