Metadata-Version: 2.4
Name: withdico
Version: 0.1.0
Summary: Lightweight dependency injection container for Python with Convention over Configuration
Project-URL: Homepage, https://github.com/amnz/withdico
Project-URL: Repository, https://github.com/amnz/withdico
Project-URL: Issues, https://github.com/amnz/withdico/issues
Author-email: amnz <amnz.is@gmail.com>
License: MIT
License-File: LICENSE
Keywords: container,dependency injection,di,ioc
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.12
Description-Content-Type: text/markdown

# withdico

Python 向けの軽量な依存性注入（DI）コンテナです。
Convention over Configuration（CoC）により、設定ファイル不要でクラスを自動解決します。

## インストール

```bash
pip install withdico
# または
uv add withdico
```

## 基本的な使い方

### 1. 抽象クラスと実装クラスを定義する

命名規則：抽象クラス `Xxx` に対して、実装クラスは `DefaultXxx` とします。
抽象クラスと実装クラスは**同じモジュール**に配置します。

```python
from abc import ABC, abstractmethod
from withdico import resolve

class Greeter(ABC):
    @abstractmethod
    def greet(self, name: str) -> str: ...

class DefaultGreeter(Greeter):
    def greet(self, name: str) -> str:
        return f"Hello, {name}!"

class App:
    def __init__(self, greeter: Greeter) -> None:
        self.greeter = greeter

# Greeter → DefaultGreeter を自動解決し、App のコンストラクタに注入
app = resolve(App)
app.greeter.greet("World")  # "Hello, World!"
```

コンストラクタの引数は型アノテーションを元に再帰的に解決されます（オートワイヤリング）。
型アノテーションが付いていないコンストラクタ引数がある場合は `TypeError` が発生します。

### 2. シングルトン

同じ型を複数回 resolve しても、同一インスタンスが返ります。

```python
app1 = resolve(App)
app2 = resolve(App)
assert app1 is app2  # True
```

### 3. ファクトリー登録

構築ロジックが複雑な場合や、起動時の環境変数に依存する場合は `register_factory` を使います。
ファクトリーは最初の `resolve` 時に一度だけ呼ばれ、結果はシングルトンとしてキャッシュされます。

```python
import os
from withdico import Withdico, resolve

Withdico.register_factory(Config, lambda: Config(path=os.environ["CONFIG_PATH"]))

config = resolve(Config)  # このときはじめてファクトリーが呼ばれる
resolve(Config) is config  # True（以降はキャッシュを返す）
```

### 4. テスト時のモック差し替え

```python
from withdico import Withdico

class MockGreeter(Greeter):
    def greet(self, name: str) -> str:
        return "mocked!"

Withdico.register(Greeter, MockGreeter())
Withdico.unregister(App)  # App のキャッシュをクリアして再生成させる

app = resolve(App)
app.greeter.greet("World")  # "mocked!"
```

テスト間でシングルトンをすべてリセットしたい場合：

```python
Withdico.reset()
```

### 5. 複数インスタンスの管理（name 引数）

同じ型に対して複数のインスタンスを名前で使い分けることができます。

```python
Withdico.register(Greeter, JaGreeter(), name="ja")
Withdico.register(Greeter, EnGreeter(), name="en")

ja = Withdico.resolve(Greeter, name="ja")
en = Withdico.resolve(Greeter, name="en")
```

---

## @package デコレーター

抽象クラスに `@package` デコレーターを付けることで、実装クラスの**検索先モジュール**を指定できます。
同一モジュールより優先して検索されます。

```python
from withdico import package

@package('myproject.api')
class Greeter(ABC):
    @abstractmethod
    def greet(self, name: str) -> str: ...
```

`myproject.api` モジュール内の `DefaultGreeter` が優先して使用されます。

### 複数指定

複数の `@package` を重ねて指定でき、**記述順（上が優先）**に検索されます。

```python
@package('myproject.api')   # 1番目に検索
@package('myproject.impl')  # 2番目に検索
class Greeter(ABC):
    ...
# 検索順: myproject.api → myproject.impl → 同一モジュール（フォールバック）
```

---

## Environment 環境変数による環境別切り替え

環境変数 `Environment` を設定すると、各検索先パッケージの**サブパッケージ**が優先して検索されます。
開発・ステージング・本番など環境ごとに実装を切り替えるために使用します。

```bash
Environment=staging python main.py
```

`@package('tool.config') @package('config')` が指定されていて `Environment=staging` の場合、
以下の順で `DefaultXxx` クラスを検索します：

```
1. tool.config.staging   ← 環境別サブパッケージを優先
2. config.staging
3. (同一モジュール).staging
4. tool.config           ← 環境変数なしと同じ検索
5. config
6. (同一モジュール)
```

### 使用例

```python
# myproject/config/staging/greeter.py
class DefaultGreeter(Greeter):
    def greet(self, name: str) -> str:
        return "Staging environment!"
```

```python
# myproject/service.py
@package('myproject.config')
class Greeter(ABC):
    @abstractmethod
    def greet(self, name: str) -> str: ...
```

```bash
# ステージング環境で起動
Environment=staging python main.py
# → myproject.config.staging.DefaultGreeter が使用される
```

---

## テスト分離（TEST_TOKEN）

環境変数 `TEST_TOKEN` を設定すると、シングルトンのキーにトークンが付加されます。
並列テストなど、テスト間でシングルトンを分離したい場合に使用します。

```python
# pytest の場合
def test_a(monkeypatch):
    monkeypatch.setenv("TEST_TOKEN", "test_a")
    svc = resolve(MyService)  # test_a 専用のインスタンス

def test_b(monkeypatch):
    monkeypatch.setenv("TEST_TOKEN", "test_b")
    svc = resolve(MyService)  # test_b 専用のインスタンス（test_a とは別）
```

---

## API リファレンス

| API | 説明 |
|---|---|
| `resolve(Type)` | シングルトン取得（CoC で自動生成） |
| `Withdico.resolve(Type, name)` | `resolve()` と同じ（name 指定可） |
| `Withdico.register(Type, instance, name)` | インスタンスを事前登録 |
| `Withdico.register_factory(Type, factory, name)` | ファクトリー関数を登録（初回 resolve 時に呼び出し） |
| `Withdico.unregister(Type, name)` | シングルトン／ファクトリーの登録を解除 |
| `Withdico.is_registered(Type, name)` | シングルトンまたはファクトリーとして登録済みか確認 |
| `Withdico.try_resolve(Type, name, if_not_registered)` | 未登録なら `None` またはフォールバック |
| `Withdico.reset()` | 全シングルトン／ファクトリーをリセット |
| `@package(module_path)` | 実装クラスの検索先モジュールを指定（複数可） |

## resolve の優先順位

```
register() で登録済み  →  register_factory() で登録済み  →  CoC（DefaultXxx 自動解決）
```

## Convention over Configuration

抽象クラスを `resolve()` すると、実装クラスを自動的に探して生成します。

### 検索ルール

| 検索先 | 探すクラス名 |
|---|---|
| 外部モジュール（`@package` 指定 / env サブパッケージ） | `ClassName` → `DefaultClassName` の順 |
| 同一モジュール（フォールバック） | `DefaultClassName` のみ |

同一モジュールで `ClassName` を探さないのは、そのクラス自身が今まさに解決しようとしている抽象クラスだからです。

### 完全な検索順の例

`@package('tool.config') @package('config')` が指定された `common.Database`（抽象）を `Environment=staging` で resolve する場合：

```
 1. tool.config.staging.Database      ← 外部モジュール × env × 同名
 2. tool.config.staging.DefaultDatabase
 3. config.staging.Database
 4. config.staging.DefaultDatabase
 5. common.staging.Database
 6. common.staging.DefaultDatabase
 7. tool.config.Database              ← 外部モジュール × 同名
 8. tool.config.DefaultDatabase
 9. config.Database
10. config.DefaultDatabase
11. common.DefaultDatabase            ← 同一モジュールは DefaultXxx のみ
```

見つかったクラスが抽象クラスの場合はスキップして次の候補へ進みます。

明示的に `register()` または `register_factory()` した場合はすべての CoC より優先されます。

## エラー

| 例外 | 発生条件 |
|---|---|
| `WithdicoImplementationException` | 抽象クラスに対応する `DefaultXxx` が見つからない |
| `WithdicoCircularDependencyException` | 循環依存（A→B→A など）を検出した |
| `TypeError` | コンストラクタ引数に型アノテーションがない |

## ライセンス

MIT License
