abcdi-config 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- abcdi_config-0.0.1/LICENSE +21 -0
- abcdi_config-0.0.1/PKG-INFO +128 -0
- abcdi_config-0.0.1/README.md +92 -0
- abcdi_config-0.0.1/pyproject.toml +50 -0
- abcdi_config-0.0.1/requirements.txt +25 -0
- abcdi_config-0.0.1/setup.cfg +4 -0
- abcdi_config-0.0.1/src/abcdi_config/__init__.py +107 -0
- abcdi_config-0.0.1/src/abcdi_config/exceptions.py +32 -0
- abcdi_config-0.0.1/src/abcdi_config/py.typed +1 -0
- abcdi_config-0.0.1/src/abcdi_config/schema/__init__.py +14 -0
- abcdi_config-0.0.1/src/abcdi_config.egg-info/PKG-INFO +128 -0
- abcdi_config-0.0.1/src/abcdi_config.egg-info/SOURCES.txt +19 -0
- abcdi_config-0.0.1/src/abcdi_config.egg-info/dependency_links.txt +1 -0
- abcdi_config-0.0.1/src/abcdi_config.egg-info/requires.txt +16 -0
- abcdi_config-0.0.1/src/abcdi_config.egg-info/top_level.txt +1 -0
- abcdi_config-0.0.1/tests/test_edge_cases.py +50 -0
- abcdi_config-0.0.1/tests/test_factory_usage.py +200 -0
- abcdi_config-0.0.1/tests/test_integration.py +86 -0
- abcdi_config-0.0.1/tests/test_main.py +33 -0
- abcdi_config-0.0.1/tests/test_schema.py +72 -0
- abcdi_config-0.0.1/version +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Julian Kenwood
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: abcdi-config
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A Python library for configuration-based dependency injection using the abcdi library
|
|
5
|
+
Author-email: Julian Kenwood <julian.kenwood@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/nomad010/abcdi-config
|
|
8
|
+
Project-URL: Repository, https://github.com/nomad010/abcdi-config
|
|
9
|
+
Project-URL: Issues, https://github.com/nomad010/abcdi-config/issues
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.13
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: abcdi==0.7.0
|
|
21
|
+
Requires-Dist: annotated-types==0.8.0
|
|
22
|
+
Requires-Dist: pydantic==2.13.5
|
|
23
|
+
Requires-Dist: pydantic-core==2.46.5
|
|
24
|
+
Requires-Dist: pydantic-settings==2.15.0
|
|
25
|
+
Requires-Dist: python-dotenv==1.2.4
|
|
26
|
+
Requires-Dist: pyyaml==6.0.3
|
|
27
|
+
Requires-Dist: typing-extensions==4.16.0
|
|
28
|
+
Requires-Dist: typing-inspection==0.4.4
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: build>=0.10.0; extra == "dev"
|
|
31
|
+
Requires-Dist: twine>=4.0.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
33
|
+
Requires-Dist: pylint>=2.15.0; extra == "dev"
|
|
34
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# abcdi-config
|
|
38
|
+
|
|
39
|
+
A Python library for configuration-based dependency injection using the abcdi library.
|
|
40
|
+
Define your application dependencies in YAML configuration files and automatically
|
|
41
|
+
instantiate them with proper dependency injection.
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- Configuration-driven dependency injection
|
|
46
|
+
- YAML-based configuration files
|
|
47
|
+
- Factory pattern for creating instances
|
|
48
|
+
- Support for environment variable overrides
|
|
49
|
+
- Type safety with Pydantic validation
|
|
50
|
+
- Lazy dependency loading support
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install abcdi-config
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from pathlib import Path
|
|
62
|
+
from abcdi_config import load_configuration, configuration
|
|
63
|
+
from abcdi_config.schema import Config
|
|
64
|
+
|
|
65
|
+
# Load configuration from YAML file
|
|
66
|
+
load_configuration(Path("config.yaml"), Config)
|
|
67
|
+
|
|
68
|
+
# Access the loaded configuration
|
|
69
|
+
config = configuration()
|
|
70
|
+
|
|
71
|
+
# Get dependencies using abcdi
|
|
72
|
+
import abcdi
|
|
73
|
+
db = abcdi.get_dependency('database')
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Configuration Format
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
dependencies:
|
|
80
|
+
database:
|
|
81
|
+
class_: sqlalchemy.create_engine
|
|
82
|
+
kwargs:
|
|
83
|
+
url: postgresql://user:pass@localhost/db
|
|
84
|
+
echo: true
|
|
85
|
+
|
|
86
|
+
api_client:
|
|
87
|
+
class_: requests.Session
|
|
88
|
+
kwargs:
|
|
89
|
+
timeout: 30
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Development
|
|
93
|
+
|
|
94
|
+
This project uses `uv` for dependency management and virtual environments.
|
|
95
|
+
|
|
96
|
+
### Setup
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Install dependencies
|
|
100
|
+
uv sync
|
|
101
|
+
|
|
102
|
+
# Or install dependencies into your existing virtualenv
|
|
103
|
+
uv pip sync requirements.txt dev-requirements.txt
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Linting
|
|
107
|
+
|
|
108
|
+
#### Pylint
|
|
109
|
+
|
|
110
|
+
Pylint is configured with a focus on reducing false positives for test files while catching real issues in production code.
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
uv run pylint src/ tests/
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
#### MyPy
|
|
117
|
+
|
|
118
|
+
MyPy type checking is configured to skip third-party library stubs.
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
uv run mypy src/ tests/
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Running Tests
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
uv run pytest tests/
|
|
128
|
+
```
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# abcdi-config
|
|
2
|
+
|
|
3
|
+
A Python library for configuration-based dependency injection using the abcdi library.
|
|
4
|
+
Define your application dependencies in YAML configuration files and automatically
|
|
5
|
+
instantiate them with proper dependency injection.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Configuration-driven dependency injection
|
|
10
|
+
- YAML-based configuration files
|
|
11
|
+
- Factory pattern for creating instances
|
|
12
|
+
- Support for environment variable overrides
|
|
13
|
+
- Type safety with Pydantic validation
|
|
14
|
+
- Lazy dependency loading support
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install abcdi-config
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from abcdi_config import load_configuration, configuration
|
|
27
|
+
from abcdi_config.schema import Config
|
|
28
|
+
|
|
29
|
+
# Load configuration from YAML file
|
|
30
|
+
load_configuration(Path("config.yaml"), Config)
|
|
31
|
+
|
|
32
|
+
# Access the loaded configuration
|
|
33
|
+
config = configuration()
|
|
34
|
+
|
|
35
|
+
# Get dependencies using abcdi
|
|
36
|
+
import abcdi
|
|
37
|
+
db = abcdi.get_dependency('database')
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Configuration Format
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
dependencies:
|
|
44
|
+
database:
|
|
45
|
+
class_: sqlalchemy.create_engine
|
|
46
|
+
kwargs:
|
|
47
|
+
url: postgresql://user:pass@localhost/db
|
|
48
|
+
echo: true
|
|
49
|
+
|
|
50
|
+
api_client:
|
|
51
|
+
class_: requests.Session
|
|
52
|
+
kwargs:
|
|
53
|
+
timeout: 30
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Development
|
|
57
|
+
|
|
58
|
+
This project uses `uv` for dependency management and virtual environments.
|
|
59
|
+
|
|
60
|
+
### Setup
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Install dependencies
|
|
64
|
+
uv sync
|
|
65
|
+
|
|
66
|
+
# Or install dependencies into your existing virtualenv
|
|
67
|
+
uv pip sync requirements.txt dev-requirements.txt
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Linting
|
|
71
|
+
|
|
72
|
+
#### Pylint
|
|
73
|
+
|
|
74
|
+
Pylint is configured with a focus on reducing false positives for test files while catching real issues in production code.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
uv run pylint src/ tests/
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
#### MyPy
|
|
81
|
+
|
|
82
|
+
MyPy type checking is configured to skip third-party library stubs.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
uv run mypy src/ tests/
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Running Tests
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
uv run pytest tests/
|
|
92
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "abcdi-config"
|
|
7
|
+
description = "A Python library for configuration-based dependency injection using the abcdi library"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.13"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [{ name = "Julian Kenwood", email = "julian.kenwood@example.com" }]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Topic :: Software Development :: Libraries",
|
|
19
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
20
|
+
]
|
|
21
|
+
dynamic = ["version", "dependencies"]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/nomad010/abcdi-config"
|
|
25
|
+
Repository = "https://github.com/nomad010/abcdi-config"
|
|
26
|
+
Issues = "https://github.com/nomad010/abcdi-config/issues"
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.dynamic]
|
|
29
|
+
version = { file = "version" }
|
|
30
|
+
dependencies = { file = ["requirements.txt"] }
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.packages.find]
|
|
33
|
+
where = ["src"]
|
|
34
|
+
|
|
35
|
+
[dependency-groups]
|
|
36
|
+
dev = [
|
|
37
|
+
"build>=1.6.1",
|
|
38
|
+
"twine>=7.0.0",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.optional-dependencies]
|
|
42
|
+
dev = [
|
|
43
|
+
"build>=0.10.0",
|
|
44
|
+
"twine>=4.0.0",
|
|
45
|
+
"pytest>=7.0.0",
|
|
46
|
+
"pylint>=2.15.0",
|
|
47
|
+
"mypy>=1.0.0",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[project.scripts]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# This file was autogenerated by uv via the following command:
|
|
2
|
+
# uv pip compile requirements.in -o requirements.txt
|
|
3
|
+
abcdi==0.7.0
|
|
4
|
+
# via -r requirements.in
|
|
5
|
+
annotated-types==0.8.0
|
|
6
|
+
# via pydantic
|
|
7
|
+
pydantic==2.13.5
|
|
8
|
+
# via pydantic-settings
|
|
9
|
+
pydantic-core==2.46.5
|
|
10
|
+
# via pydantic
|
|
11
|
+
pydantic-settings==2.15.0
|
|
12
|
+
# via -r requirements.in
|
|
13
|
+
python-dotenv==1.2.4
|
|
14
|
+
# via pydantic-settings
|
|
15
|
+
pyyaml==6.0.3
|
|
16
|
+
# via -r requirements.in
|
|
17
|
+
typing-extensions==4.16.0
|
|
18
|
+
# via
|
|
19
|
+
# pydantic
|
|
20
|
+
# pydantic-core
|
|
21
|
+
# typing-inspection
|
|
22
|
+
typing-inspection==0.4.4
|
|
23
|
+
# via
|
|
24
|
+
# pydantic
|
|
25
|
+
# pydantic-settings
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import TypeVar
|
|
4
|
+
|
|
5
|
+
import abcdi
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from abcdi_config.exceptions import (
|
|
9
|
+
ConfigAlreadyLoadedError,
|
|
10
|
+
ConfigError,
|
|
11
|
+
DependencyFactoryError,
|
|
12
|
+
)
|
|
13
|
+
from abcdi_config.schema import Config
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ConfigAlreadyLoadedError",
|
|
17
|
+
"ConfigError",
|
|
18
|
+
"DependencyFactoryError",
|
|
19
|
+
"configuration",
|
|
20
|
+
"load_configuration",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
# TypeVar bound to Config for type-safe configuration classes
|
|
24
|
+
ConfigT = TypeVar("ConfigT", bound=Config)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
CONFIGURATION = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _resolve_class(cls_or_path: str) -> type[Config]:
|
|
31
|
+
"""Resolve a class from either a class object or import path string."""
|
|
32
|
+
try:
|
|
33
|
+
from importlib import import_module
|
|
34
|
+
module_path, class_name = cls_or_path.rsplit('.', 1)
|
|
35
|
+
module = import_module(module_path)
|
|
36
|
+
cls = getattr(module, class_name)
|
|
37
|
+
if not issubclass(cls, Config):
|
|
38
|
+
raise ConfigError(f"Config class {cls_or_path} is not a subclass of abcdi_config.Config")
|
|
39
|
+
return cls
|
|
40
|
+
except (ValueError, ImportError, AttributeError) as e:
|
|
41
|
+
raise ConfigError(f"Failed to resolve class {cls_or_path}: {e}") from e
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_configuration(
|
|
45
|
+
path: Path, configuration_cls: type, lazy_di: bool = False
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Load configuration from YAML and set up abcdi dependency injection context.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
path: Path to the YAML configuration file.
|
|
51
|
+
lazy_di: If True, dependencies are created lazily. Defaults to False.
|
|
52
|
+
configuration_cls: Custom configuration class (subclass of Config).
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
load_configuration(Path("config.yaml"), Config)
|
|
56
|
+
load_configuration(Path("config.yaml"), CustomConfig)
|
|
57
|
+
"""
|
|
58
|
+
global CONFIGURATION # pylint: disable=global-statement
|
|
59
|
+
if CONFIGURATION is not None:
|
|
60
|
+
raise ConfigAlreadyLoadedError()
|
|
61
|
+
|
|
62
|
+
# Determine configuration class to use
|
|
63
|
+
# Priority: CONFIGURATION_CLS env var > configuration_cls param
|
|
64
|
+
configuration_class_override = os.environ.get("CONFIGURATION_CLS")
|
|
65
|
+
if configuration_class_override is not None:
|
|
66
|
+
config_cls = _resolve_class(configuration_class_override)
|
|
67
|
+
else:
|
|
68
|
+
config_cls = configuration_cls
|
|
69
|
+
|
|
70
|
+
# Determine lazy flag - env var takes priority over function parameter
|
|
71
|
+
configuration_lazy_override = os.environ.get("CONFIGURATION_LAZY")
|
|
72
|
+
if configuration_lazy_override is not None:
|
|
73
|
+
use_lazy_di = configuration_lazy_override.lower() == "true"
|
|
74
|
+
else:
|
|
75
|
+
use_lazy_di = lazy_di
|
|
76
|
+
|
|
77
|
+
# Use context manager for proper file handling
|
|
78
|
+
with open(path, encoding='utf-8') as f:
|
|
79
|
+
yaml_data = yaml.safe_load(f)
|
|
80
|
+
# Handle empty YAML or YAML with None
|
|
81
|
+
if yaml_data is None:
|
|
82
|
+
yaml_data = {}
|
|
83
|
+
|
|
84
|
+
# Handle null dependencies
|
|
85
|
+
if yaml_data.get("dependencies") is None:
|
|
86
|
+
yaml_data["dependencies"] = {}
|
|
87
|
+
|
|
88
|
+
CONFIGURATION = config_cls.model_validate(yaml_data)
|
|
89
|
+
|
|
90
|
+
# Convert to abcdi factory format
|
|
91
|
+
# Pydantic's ImportString automatically resolves the class on model instantiation
|
|
92
|
+
# Factory[X] has class_ and kwargs, abcdi.factory expects cls, *args, **kwargs
|
|
93
|
+
dependency_config = {}
|
|
94
|
+
for name, dependency in CONFIGURATION.dependencies.items():
|
|
95
|
+
try:
|
|
96
|
+
dependency_config[name] = abcdi.factory(dependency.class_, **dependency.kwargs)
|
|
97
|
+
except (ImportError, AttributeError) as e:
|
|
98
|
+
raise DependencyFactoryError(name, e) from e
|
|
99
|
+
|
|
100
|
+
abcdi.set_context(dependencies=dependency_config, lazy=use_lazy_di)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def configuration() -> Config:
|
|
104
|
+
"""Return the loaded configuration."""
|
|
105
|
+
if CONFIGURATION is None:
|
|
106
|
+
raise ConfigError("Configuration has not been loaded. Call load_configuration() first.")
|
|
107
|
+
return CONFIGURATION
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Custom exceptions for abcdi-config."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ConfigError(Exception):
|
|
5
|
+
"""Base exception for configuration errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConfigLoadError(ConfigError):
|
|
9
|
+
"""Raised when configuration fails to load."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, message: str, original_error: Exception | None = None):
|
|
12
|
+
if original_error:
|
|
13
|
+
super().__init__(f"{message}: {original_error}")
|
|
14
|
+
self.__cause__ = original_error
|
|
15
|
+
else:
|
|
16
|
+
super().__init__(message)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ConfigAlreadyLoadedError(ConfigError):
|
|
20
|
+
"""Raised when trying to load configuration when already loaded."""
|
|
21
|
+
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
super().__init__("Configuration is already loaded. Call configuration() to access it.")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DependencyFactoryError(ConfigError):
|
|
27
|
+
"""Raised when a dependency factory cannot be created."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, name: str, original_error: Exception) -> None:
|
|
30
|
+
super().__init__(f"Failed to create factory for dependency '{name}': {original_error}")
|
|
31
|
+
self.dependency_name = name
|
|
32
|
+
self.__cause__ = original_error
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker file to indicate this package supports type checking
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ImportString
|
|
4
|
+
from pydantic_settings import BaseSettings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Factory[X](BaseModel):
|
|
8
|
+
"""Factory pattern for dynamically creating instances of classes with configuration."""
|
|
9
|
+
class_: ImportString[type[X]] # Class to instantiate (import path as string)
|
|
10
|
+
kwargs: dict[str, Any] = {} # Keyword arguments to pass to class constructor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Config(BaseSettings):
|
|
14
|
+
dependencies: dict[str, Factory[Any]] = {} # Named dependencies to create upfront and make available throughout the application
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: abcdi-config
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A Python library for configuration-based dependency injection using the abcdi library
|
|
5
|
+
Author-email: Julian Kenwood <julian.kenwood@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/nomad010/abcdi-config
|
|
8
|
+
Project-URL: Repository, https://github.com/nomad010/abcdi-config
|
|
9
|
+
Project-URL: Issues, https://github.com/nomad010/abcdi-config/issues
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.13
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: abcdi==0.7.0
|
|
21
|
+
Requires-Dist: annotated-types==0.8.0
|
|
22
|
+
Requires-Dist: pydantic==2.13.5
|
|
23
|
+
Requires-Dist: pydantic-core==2.46.5
|
|
24
|
+
Requires-Dist: pydantic-settings==2.15.0
|
|
25
|
+
Requires-Dist: python-dotenv==1.2.4
|
|
26
|
+
Requires-Dist: pyyaml==6.0.3
|
|
27
|
+
Requires-Dist: typing-extensions==4.16.0
|
|
28
|
+
Requires-Dist: typing-inspection==0.4.4
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: build>=0.10.0; extra == "dev"
|
|
31
|
+
Requires-Dist: twine>=4.0.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
33
|
+
Requires-Dist: pylint>=2.15.0; extra == "dev"
|
|
34
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# abcdi-config
|
|
38
|
+
|
|
39
|
+
A Python library for configuration-based dependency injection using the abcdi library.
|
|
40
|
+
Define your application dependencies in YAML configuration files and automatically
|
|
41
|
+
instantiate them with proper dependency injection.
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- Configuration-driven dependency injection
|
|
46
|
+
- YAML-based configuration files
|
|
47
|
+
- Factory pattern for creating instances
|
|
48
|
+
- Support for environment variable overrides
|
|
49
|
+
- Type safety with Pydantic validation
|
|
50
|
+
- Lazy dependency loading support
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install abcdi-config
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from pathlib import Path
|
|
62
|
+
from abcdi_config import load_configuration, configuration
|
|
63
|
+
from abcdi_config.schema import Config
|
|
64
|
+
|
|
65
|
+
# Load configuration from YAML file
|
|
66
|
+
load_configuration(Path("config.yaml"), Config)
|
|
67
|
+
|
|
68
|
+
# Access the loaded configuration
|
|
69
|
+
config = configuration()
|
|
70
|
+
|
|
71
|
+
# Get dependencies using abcdi
|
|
72
|
+
import abcdi
|
|
73
|
+
db = abcdi.get_dependency('database')
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Configuration Format
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
dependencies:
|
|
80
|
+
database:
|
|
81
|
+
class_: sqlalchemy.create_engine
|
|
82
|
+
kwargs:
|
|
83
|
+
url: postgresql://user:pass@localhost/db
|
|
84
|
+
echo: true
|
|
85
|
+
|
|
86
|
+
api_client:
|
|
87
|
+
class_: requests.Session
|
|
88
|
+
kwargs:
|
|
89
|
+
timeout: 30
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Development
|
|
93
|
+
|
|
94
|
+
This project uses `uv` for dependency management and virtual environments.
|
|
95
|
+
|
|
96
|
+
### Setup
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Install dependencies
|
|
100
|
+
uv sync
|
|
101
|
+
|
|
102
|
+
# Or install dependencies into your existing virtualenv
|
|
103
|
+
uv pip sync requirements.txt dev-requirements.txt
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Linting
|
|
107
|
+
|
|
108
|
+
#### Pylint
|
|
109
|
+
|
|
110
|
+
Pylint is configured with a focus on reducing false positives for test files while catching real issues in production code.
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
uv run pylint src/ tests/
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
#### MyPy
|
|
117
|
+
|
|
118
|
+
MyPy type checking is configured to skip third-party library stubs.
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
uv run mypy src/ tests/
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Running Tests
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
uv run pytest tests/
|
|
128
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
requirements.txt
|
|
5
|
+
version
|
|
6
|
+
src/abcdi_config/__init__.py
|
|
7
|
+
src/abcdi_config/exceptions.py
|
|
8
|
+
src/abcdi_config/py.typed
|
|
9
|
+
src/abcdi_config.egg-info/PKG-INFO
|
|
10
|
+
src/abcdi_config.egg-info/SOURCES.txt
|
|
11
|
+
src/abcdi_config.egg-info/dependency_links.txt
|
|
12
|
+
src/abcdi_config.egg-info/requires.txt
|
|
13
|
+
src/abcdi_config.egg-info/top_level.txt
|
|
14
|
+
src/abcdi_config/schema/__init__.py
|
|
15
|
+
tests/test_edge_cases.py
|
|
16
|
+
tests/test_factory_usage.py
|
|
17
|
+
tests/test_integration.py
|
|
18
|
+
tests/test_main.py
|
|
19
|
+
tests/test_schema.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
abcdi==0.7.0
|
|
2
|
+
annotated-types==0.8.0
|
|
3
|
+
pydantic==2.13.5
|
|
4
|
+
pydantic-core==2.46.5
|
|
5
|
+
pydantic-settings==2.15.0
|
|
6
|
+
python-dotenv==1.2.4
|
|
7
|
+
pyyaml==6.0.3
|
|
8
|
+
typing-extensions==4.16.0
|
|
9
|
+
typing-inspection==0.4.4
|
|
10
|
+
|
|
11
|
+
[dev]
|
|
12
|
+
build>=0.10.0
|
|
13
|
+
twine>=4.0.0
|
|
14
|
+
pytest>=7.0.0
|
|
15
|
+
pylint>=2.15.0
|
|
16
|
+
mypy>=1.0.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
abcdi_config
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Edge case and validation tests for abcdi-config."""
|
|
2
|
+
import tempfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from pydantic import ValidationError
|
|
7
|
+
|
|
8
|
+
from abcdi_config import load_configuration
|
|
9
|
+
from abcdi_config.schema import Config
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestValidation:
|
|
13
|
+
"""Tests for configuration validation."""
|
|
14
|
+
|
|
15
|
+
def test_missing_class_field_raises_error(self):
|
|
16
|
+
"""Test that missing 'class' field raises ValidationError."""
|
|
17
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
|
18
|
+
config_yaml = """
|
|
19
|
+
dependencies:
|
|
20
|
+
my_factory:
|
|
21
|
+
kwargs:
|
|
22
|
+
value: test
|
|
23
|
+
"""
|
|
24
|
+
f.write(config_yaml)
|
|
25
|
+
config_path = Path(f.name)
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
with pytest.raises(ValidationError):
|
|
29
|
+
load_configuration(config_path, Config)
|
|
30
|
+
finally:
|
|
31
|
+
config_path.unlink()
|
|
32
|
+
|
|
33
|
+
def test_nonexistent_class_raises_error(self):
|
|
34
|
+
"""Test that nonexistent class path raises error."""
|
|
35
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
|
36
|
+
config_yaml = """
|
|
37
|
+
dependencies:
|
|
38
|
+
my_factory:
|
|
39
|
+
class_: nonexistent.module.ThatClass
|
|
40
|
+
kwargs:
|
|
41
|
+
value: test
|
|
42
|
+
"""
|
|
43
|
+
f.write(config_yaml)
|
|
44
|
+
config_path = Path(f.name)
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
with pytest.raises(ValidationError):
|
|
48
|
+
load_configuration(config_path, Config)
|
|
49
|
+
finally:
|
|
50
|
+
config_path.unlink()
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Integration tests demonstrating Factory pattern usage."""
|
|
2
|
+
import tempfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from abcdi_config import load_configuration
|
|
8
|
+
from abcdi_config.schema import Config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CustomService:
|
|
12
|
+
"""A custom service class for testing."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, host: str, port: int, debug: bool = False):
|
|
15
|
+
self.host = host
|
|
16
|
+
self.port = port
|
|
17
|
+
self.debug = debug
|
|
18
|
+
|
|
19
|
+
def get_url(self) -> str:
|
|
20
|
+
protocol = "http" if not self.debug else "https"
|
|
21
|
+
return f"{protocol}://{self.host}:{self.port}"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestFactoryPattern:
|
|
25
|
+
"""Tests for Factory pattern usage in configurations."""
|
|
26
|
+
|
|
27
|
+
def test_factory_creates_instance(self):
|
|
28
|
+
"""Test that Factory correctly creates an instance."""
|
|
29
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
|
30
|
+
config_yaml = """
|
|
31
|
+
dependencies:
|
|
32
|
+
custom_service:
|
|
33
|
+
class_: tests.test_factory_usage.CustomService
|
|
34
|
+
kwargs:
|
|
35
|
+
host: localhost
|
|
36
|
+
port: 8080
|
|
37
|
+
debug: true
|
|
38
|
+
"""
|
|
39
|
+
f.write(config_yaml)
|
|
40
|
+
config_path = Path(f.name)
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
import abcdi
|
|
44
|
+
load_configuration(config_path, Config)
|
|
45
|
+
|
|
46
|
+
service = abcdi.get_dependency('custom_service')
|
|
47
|
+
assert isinstance(service, CustomService)
|
|
48
|
+
assert service.host == "localhost"
|
|
49
|
+
assert service.port == 8080
|
|
50
|
+
assert service.debug is True
|
|
51
|
+
assert service.get_url() == "https://localhost:8080"
|
|
52
|
+
|
|
53
|
+
finally:
|
|
54
|
+
# Clean up
|
|
55
|
+
import abcdi
|
|
56
|
+
|
|
57
|
+
import abcdi_config
|
|
58
|
+
abcdi_config.CONFIGURATION = None
|
|
59
|
+
abcdi._current_context = None
|
|
60
|
+
config_path.unlink()
|
|
61
|
+
|
|
62
|
+
def test_factory_with_multiple_instances(self):
|
|
63
|
+
"""Test creating multiple factory instances."""
|
|
64
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
|
65
|
+
config_yaml = """
|
|
66
|
+
dependencies:
|
|
67
|
+
service_a:
|
|
68
|
+
class_: tests.test_factory_usage.CustomService
|
|
69
|
+
kwargs:
|
|
70
|
+
host: host-a
|
|
71
|
+
port: 8080
|
|
72
|
+
service_b:
|
|
73
|
+
class_: tests.test_factory_usage.CustomService
|
|
74
|
+
kwargs:
|
|
75
|
+
host: host-b
|
|
76
|
+
port: 9090
|
|
77
|
+
"""
|
|
78
|
+
f.write(config_yaml)
|
|
79
|
+
config_path = Path(f.name)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
import abcdi
|
|
83
|
+
load_configuration(config_path, Config)
|
|
84
|
+
|
|
85
|
+
service_a = abcdi.get_dependency('service_a')
|
|
86
|
+
service_b = abcdi.get_dependency('service_b')
|
|
87
|
+
|
|
88
|
+
assert service_a.host == "host-a"
|
|
89
|
+
assert service_b.host == "host-b"
|
|
90
|
+
assert service_a.port == 8080
|
|
91
|
+
assert service_b.port == 9090
|
|
92
|
+
|
|
93
|
+
finally:
|
|
94
|
+
# Clean up
|
|
95
|
+
import abcdi
|
|
96
|
+
|
|
97
|
+
import abcdi_config
|
|
98
|
+
abcdi_config.CONFIGURATION = None
|
|
99
|
+
abcdi._current_context = None
|
|
100
|
+
config_path.unlink()
|
|
101
|
+
|
|
102
|
+
def test_factory_with_nested_kwargs(self):
|
|
103
|
+
"""Test Factory with nested configuration objects."""
|
|
104
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
|
105
|
+
config_yaml = """
|
|
106
|
+
dependencies:
|
|
107
|
+
database:
|
|
108
|
+
class_: tests.test_factory_usage.CustomService
|
|
109
|
+
kwargs:
|
|
110
|
+
host: db.example.com
|
|
111
|
+
port: 5432
|
|
112
|
+
debug: false
|
|
113
|
+
"""
|
|
114
|
+
f.write(config_yaml)
|
|
115
|
+
config_path = Path(f.name)
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
import abcdi
|
|
119
|
+
load_configuration(config_path, Config)
|
|
120
|
+
|
|
121
|
+
db = abcdi.get_dependency('database')
|
|
122
|
+
assert db.host == "db.example.com"
|
|
123
|
+
assert db.port == 5432
|
|
124
|
+
assert db.debug is False
|
|
125
|
+
assert db.get_url() == "http://db.example.com:5432"
|
|
126
|
+
|
|
127
|
+
finally:
|
|
128
|
+
# Clean up
|
|
129
|
+
import abcdi
|
|
130
|
+
|
|
131
|
+
import abcdi_config
|
|
132
|
+
abcdi_config.CONFIGURATION = None
|
|
133
|
+
abcdi._current_context = None
|
|
134
|
+
config_path.unlink()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class TestDependencyResolution:
|
|
138
|
+
"""Tests for dependency resolution behavior."""
|
|
139
|
+
|
|
140
|
+
def test_dependency_not_found_raises_error(self):
|
|
141
|
+
"""Test that get_dependency raises when dependency doesn't exist."""
|
|
142
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
|
143
|
+
f.write("dependencies: {}")
|
|
144
|
+
config_path = Path(f.name)
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
import abcdi
|
|
148
|
+
load_configuration(config_path, Config)
|
|
149
|
+
|
|
150
|
+
with pytest.raises(KeyError): # abcdi raises KeyError for unknown dependency
|
|
151
|
+
abcdi.get_dependency('nonexistent')
|
|
152
|
+
|
|
153
|
+
finally:
|
|
154
|
+
# Clean up
|
|
155
|
+
import abcdi
|
|
156
|
+
|
|
157
|
+
import abcdi_config
|
|
158
|
+
abcdi_config.CONFIGURATION = None
|
|
159
|
+
abcdi._current_context = None
|
|
160
|
+
config_path.unlink()
|
|
161
|
+
|
|
162
|
+
def test_multiple_dependencies_independent(self):
|
|
163
|
+
"""Test that multiple dependencies are independent instances."""
|
|
164
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
|
165
|
+
config_yaml = """
|
|
166
|
+
dependencies:
|
|
167
|
+
counter:
|
|
168
|
+
class_: tests.test_factory_usage.CustomService
|
|
169
|
+
kwargs:
|
|
170
|
+
host: localhost
|
|
171
|
+
port: 1000
|
|
172
|
+
counter2:
|
|
173
|
+
class_: tests.test_factory_usage.CustomService
|
|
174
|
+
kwargs:
|
|
175
|
+
host: localhost
|
|
176
|
+
port: 2000
|
|
177
|
+
"""
|
|
178
|
+
f.write(config_yaml)
|
|
179
|
+
config_path = Path(f.name)
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
import abcdi
|
|
183
|
+
load_configuration(config_path, Config)
|
|
184
|
+
|
|
185
|
+
counter1 = abcdi.get_dependency('counter')
|
|
186
|
+
counter2 = abcdi.get_dependency('counter2')
|
|
187
|
+
|
|
188
|
+
# Verify they are different instances
|
|
189
|
+
assert counter1 is not counter2
|
|
190
|
+
assert counter1.port == 1000
|
|
191
|
+
assert counter2.port == 2000
|
|
192
|
+
|
|
193
|
+
finally:
|
|
194
|
+
# Clean up
|
|
195
|
+
import abcdi
|
|
196
|
+
|
|
197
|
+
import abcdi_config
|
|
198
|
+
abcdi_config.CONFIGURATION = None
|
|
199
|
+
abcdi._current_context = None
|
|
200
|
+
config_path.unlink()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Integration tests for abcdi-config."""
|
|
2
|
+
import tempfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from abcdi_config import configuration, load_configuration
|
|
6
|
+
from abcdi_config.schema import Config
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CustomGreeting(Config):
|
|
10
|
+
"""A custom greeting config for testing."""
|
|
11
|
+
|
|
12
|
+
name: str = "World"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_load_configuration_with_datetime():
|
|
16
|
+
"""Test loading configuration with a stdlib class (datetime.datetime)."""
|
|
17
|
+
import abcdi
|
|
18
|
+
# Create a temporary YAML configuration file
|
|
19
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
|
20
|
+
config_yaml = """
|
|
21
|
+
dependencies:
|
|
22
|
+
my_datetime:
|
|
23
|
+
class_: datetime.datetime
|
|
24
|
+
kwargs:
|
|
25
|
+
year: 2024
|
|
26
|
+
month: 1
|
|
27
|
+
day: 15
|
|
28
|
+
"""
|
|
29
|
+
f.write(config_yaml)
|
|
30
|
+
config_path = Path(f.name)
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
# Load the configuration
|
|
34
|
+
load_configuration(config_path, Config)
|
|
35
|
+
|
|
36
|
+
# Get the loaded configuration
|
|
37
|
+
loaded_config = configuration()
|
|
38
|
+
assert 'my_datetime' in loaded_config.dependencies
|
|
39
|
+
|
|
40
|
+
# Get the dependency from abcdi context
|
|
41
|
+
dt = abcdi.get_dependency('my_datetime')
|
|
42
|
+
assert dt.year == 2024
|
|
43
|
+
assert dt.month == 1
|
|
44
|
+
assert dt.day == 15
|
|
45
|
+
|
|
46
|
+
finally:
|
|
47
|
+
# Clean up
|
|
48
|
+
import abcdi
|
|
49
|
+
|
|
50
|
+
import abcdi_config
|
|
51
|
+
abcdi_config.CONFIGURATION = None
|
|
52
|
+
abcdi._current_context = None
|
|
53
|
+
config_path.unlink()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_load_configuration_with_custom_class():
|
|
57
|
+
"""Test loading configuration with a custom class from the project."""
|
|
58
|
+
import abcdi
|
|
59
|
+
# Create a temporary YAML configuration file
|
|
60
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
|
61
|
+
config_yaml = """
|
|
62
|
+
dependencies:
|
|
63
|
+
greeting:
|
|
64
|
+
class_: tests.test_integration.CustomGreeting
|
|
65
|
+
kwargs:
|
|
66
|
+
name: World
|
|
67
|
+
"""
|
|
68
|
+
f.write(config_yaml)
|
|
69
|
+
config_path = Path(f.name)
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
# Load the configuration
|
|
73
|
+
load_configuration(config_path, Config)
|
|
74
|
+
|
|
75
|
+
# Get the dependency from abcdi context
|
|
76
|
+
greeting = abcdi.get_dependency('greeting')
|
|
77
|
+
assert greeting.name == "World"
|
|
78
|
+
|
|
79
|
+
finally:
|
|
80
|
+
# Clean up
|
|
81
|
+
import abcdi
|
|
82
|
+
|
|
83
|
+
import abcdi_config
|
|
84
|
+
abcdi_config.CONFIGURATION = None
|
|
85
|
+
abcdi._current_context = None
|
|
86
|
+
config_path.unlink()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Test that demonstrates the basic functionality of abcdi-config."""
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from abcdi_config import configuration, load_configuration
|
|
7
|
+
from abcdi_config.schema import Config
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_basic_import():
|
|
11
|
+
"""Test that the package can be imported."""
|
|
12
|
+
assert True
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_empty_config():
|
|
16
|
+
"""Test loading an empty configuration."""
|
|
17
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
|
18
|
+
f.write("dependencies: {}")
|
|
19
|
+
config_path = Path(f.name)
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
load_configuration(config_path, Config)
|
|
23
|
+
cfg = configuration()
|
|
24
|
+
assert cfg.dependencies == {}
|
|
25
|
+
finally:
|
|
26
|
+
config_path.unlink()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
# Run tests directly
|
|
31
|
+
test_basic_import()
|
|
32
|
+
test_empty_config()
|
|
33
|
+
print("All tests passed!")
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Unit tests for the schema module."""
|
|
2
|
+
import pytest
|
|
3
|
+
from pydantic import ValidationError
|
|
4
|
+
|
|
5
|
+
from abcdi_config.schema import Config, Factory
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestFactory:
|
|
9
|
+
"""Tests for the Factory class."""
|
|
10
|
+
|
|
11
|
+
def test_factory_with_no_kwargs(self):
|
|
12
|
+
"""Test Factory with minimal kwargs (empty dict)."""
|
|
13
|
+
factory = Factory(class_="datetime.datetime", kwargs={})
|
|
14
|
+
assert factory.class_.__name__ == "datetime"
|
|
15
|
+
assert factory.kwargs == {}
|
|
16
|
+
|
|
17
|
+
def test_factory_with_kwargs(self):
|
|
18
|
+
"""Test Factory with keyword arguments."""
|
|
19
|
+
factory = Factory(
|
|
20
|
+
class_="datetime.datetime",
|
|
21
|
+
kwargs={"year": 2024, "month": 1, "day": 15}
|
|
22
|
+
)
|
|
23
|
+
assert factory.class_.__name__ == "datetime"
|
|
24
|
+
assert factory.kwargs == {"year": 2024, "month": 1, "day": 15}
|
|
25
|
+
|
|
26
|
+
def test_factory_invalid_class(self):
|
|
27
|
+
"""Test Factory with invalid class path."""
|
|
28
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
29
|
+
Factory(class_="nonexistent.module.Class", kwargs={})
|
|
30
|
+
# The error message should indicate an invalid import path
|
|
31
|
+
assert "nonexistent.module.Class" in str(exc_info.value)
|
|
32
|
+
|
|
33
|
+
def test_factory_with_custom_class(self):
|
|
34
|
+
"""Test Factory with a custom class."""
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
# Use Path from pathlib as the test class - it's a known importable class
|
|
38
|
+
factory = Factory(class_="pathlib.Path", kwargs={})
|
|
39
|
+
assert factory.class_ is Path
|
|
40
|
+
assert factory.kwargs == {}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TestConfig:
|
|
44
|
+
"""Tests for the Config class."""
|
|
45
|
+
|
|
46
|
+
def test_config_with_no_dependencies(self):
|
|
47
|
+
"""Test Config with empty dependencies."""
|
|
48
|
+
config = Config(dependencies={})
|
|
49
|
+
assert config.dependencies == {}
|
|
50
|
+
|
|
51
|
+
def test_config_with_single_dependency(self):
|
|
52
|
+
"""Test Config with a single dependency."""
|
|
53
|
+
config = Config(dependencies={
|
|
54
|
+
"my_factory": Factory(class_="datetime.datetime", kwargs={})
|
|
55
|
+
})
|
|
56
|
+
assert len(config.dependencies) == 1
|
|
57
|
+
assert "my_factory" in config.dependencies
|
|
58
|
+
|
|
59
|
+
def test_config_with_multiple_dependencies(self):
|
|
60
|
+
"""Test Config with multiple dependencies."""
|
|
61
|
+
config = Config(dependencies={
|
|
62
|
+
"factory1": Factory(class_="datetime.datetime", kwargs={}),
|
|
63
|
+
"factory2": Factory(class_="os.PathLike", kwargs={}),
|
|
64
|
+
})
|
|
65
|
+
assert len(config.dependencies) == 2
|
|
66
|
+
assert "factory1" in config.dependencies
|
|
67
|
+
assert "factory2" in config.dependencies
|
|
68
|
+
|
|
69
|
+
def test_config_default_dependencies(self):
|
|
70
|
+
"""Test Config default dependencies value."""
|
|
71
|
+
config = Config()
|
|
72
|
+
assert config.dependencies == {}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.0.1
|