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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,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