Skip to content

Commit 560ff8e

Browse files
Prepare for 2.0.7 (#63)
1 parent f6d6fbc commit 560ff8e

File tree

9 files changed

+163
-43
lines changed

9 files changed

+163
-43
lines changed

.flake8

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[flake8]
22
exclude = __pycache__,built,build,venv
3-
ignore = E203, E266, W503
3+
ignore = E203, E266, W503, E701, E704
44
max-line-length = 88
55
max-complexity = 18
66
select = B,C,E,F,W,T4,B9

.github/workflows/build.yml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ jobs:
2121
build:
2222
runs-on: ubuntu-latest
2323
strategy:
24+
fail-fast: false
2425
matrix:
25-
python-version: [3.8, 3.9, "3.10", "3.11", "3.12"]
26+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
2627

2728
steps:
2829
- uses: actions/checkout@v1
@@ -69,18 +70,18 @@ jobs:
6970
7071
- name: Install distribution dependencies
7172
run: pip install --upgrade build
72-
if: matrix.python-version == 3.11
73+
if: matrix.python-version == 3.12
7374

7475
- name: Create distribution package
7576
run: python -m build
76-
if: matrix.python-version == 3.11
77+
if: matrix.python-version == 3.12
7778

7879
- name: Upload distribution package
7980
uses: actions/upload-artifact@master
8081
with:
8182
name: dist
8283
path: dist
83-
if: matrix.python-version == 3.11
84+
if: matrix.python-version == 3.12
8485

8586
publish:
8687
runs-on: ubuntu-latest
@@ -93,10 +94,10 @@ jobs:
9394
name: dist
9495
path: dist
9596

96-
- name: Use Python 3.11
97+
- name: Use Python 3.12
9798
uses: actions/setup-python@v1
9899
with:
99-
python-version: '3.11'
100+
python-version: '3.12'
100101

101102
- name: Install dependencies
102103
run: |

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.0.7] - 2025-03-28
9+
10+
- Add the possibility to specify the `ActivationScope` class when instantiating
11+
the `Container` or the `Services` object. This class will be used when
12+
creating scopes. For the issue #55.
13+
- Add an **experimental** class, `TrackingActivationScope` to support nested
14+
scopes transparently, using `contextvars.ContextVar`. For more context, see
15+
the tests `test_nested_scope_1`, `test_nested_scope_2`,
16+
`test_nested_scope_async_1`. For the issue #55.
17+
- Raise a `TypeError` if trying to obtain a service from a disposed scope.
18+
- Remove Python 3.8 from the build matrix, add Python 3.13.
19+
- Handle setuptools warning: _SetuptoolsDeprecationWarning: License classifiers are deprecated_.
20+
821
## [2.0.6] - 2023-12-09 :hammer:
922
- Fixes import for Protocols support regardless of Python version (partially
1023
broken for Python 3.9), by @fennel-akunesh

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,19 @@ name = "rodi"
77
dynamic = ["version"]
88
authors = [{ name = "Roberto Prevato", email = "roberto.prevato@gmail.com" }]
99
description = "Implementation of dependency injection for Python 3"
10+
license = { file = "LICENSE" }
1011
readme = "README.md"
1112
requires-python = ">=3.7"
1213
classifiers = [
1314
"Development Status :: 5 - Production/Stable",
14-
"License :: OSI Approved :: MIT License",
1515
"Programming Language :: Python :: 3",
1616
"Programming Language :: Python :: 3.7",
1717
"Programming Language :: Python :: 3.8",
1818
"Programming Language :: Python :: 3.9",
1919
"Programming Language :: Python :: 3.10",
2020
"Programming Language :: Python :: 3.11",
2121
"Programming Language :: Python :: 3.12",
22+
"Programming Language :: Python :: 3.13",
2223
"Operating System :: OS Independent",
2324
]
2425
keywords = ["dependency", "injection", "type", "hints", "typing"]

rodi/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "2.0.6"
1+
__version__ = "2.0.7"

rodi/__init__.py

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import contextvars
12
import inspect
23
import re
34
import sys
@@ -259,6 +260,8 @@ def get(
259260
*,
260261
default: Optional[Any] = ...,
261262
) -> T:
263+
if self.provider is None:
264+
raise TypeError("This scope is disposed.")
262265
return self.provider.get(desired_type, scope or self, default=default)
263266

264267
def dispose(self):
@@ -270,6 +273,48 @@ def dispose(self):
270273
self.scoped_services = None
271274

272275

276+
class TrackingActivationScope(ActivationScope):
277+
"""
278+
This is an experimental class to support nested scopes transparently.
279+
To use it, create a container including the `scope_cls` parameter:
280+
`Container(scope_cls=TrackingActivationScope)`.
281+
"""
282+
283+
_active_scopes = contextvars.ContextVar("active_scopes", default=[])
284+
285+
__slots__ = ("scoped_services", "provider", "parent_scope")
286+
287+
def __init__(self, provider=None, scoped_services=None):
288+
# Get the current stack of active scopes
289+
stack = self._active_scopes.get()
290+
291+
# Detect the parent scope if it exists
292+
self.parent_scope = stack[-1] if stack else None
293+
294+
# Initialize scoped services
295+
scoped_services = scoped_services or {}
296+
if self.parent_scope:
297+
scoped_services.update(self.parent_scope.scoped_services)
298+
299+
super().__init__(provider, scoped_services)
300+
301+
def __enter__(self):
302+
# Push this scope onto the stack
303+
stack = self._active_scopes.get()
304+
self._active_scopes.set(stack + [self])
305+
return self
306+
307+
def __exit__(self, exc_type, exc_val, exc_tb):
308+
# Pop this scope from the stack
309+
stack = self._active_scopes.get()
310+
self._active_scopes.set(stack[:-1])
311+
self.dispose()
312+
313+
def dispose(self):
314+
if self.provider:
315+
self.provider = None
316+
317+
273318
class ResolutionContext:
274319
__slots__ = ("resolved", "dynamic_chain")
275320
__deletable__ = ("resolved",)
@@ -679,13 +724,18 @@ class Services:
679724
Provides methods to activate instances of classes, by cached activator functions.
680725
"""
681726

682-
__slots__ = ("_map", "_executors")
727+
__slots__ = ("_map", "_executors", "_scope_cls")
683728

684-
def __init__(self, services_map=None):
729+
def __init__(
730+
self,
731+
services_map=None,
732+
scope_cls: Optional[Type[ActivationScope]] = None,
733+
):
685734
if services_map is None:
686735
services_map = {}
687736
self._map = services_map
688737
self._executors = {}
738+
self._scope_cls = scope_cls or ActivationScope
689739

690740
def __contains__(self, item):
691741
return item in self._map
@@ -696,6 +746,11 @@ def __getitem__(self, item):
696746
def __setitem__(self, key, value):
697747
self.set(key, value)
698748

749+
def create_scope(
750+
self, scoped: Optional[Dict[Union[Type, str], Any]] = None
751+
) -> ActivationScope:
752+
return self._scope_cls(self, scoped)
753+
699754
def set(self, new_type: Union[Type, str], value: Any):
700755
"""
701756
Sets a new service of desired type, as singleton.
@@ -733,7 +788,7 @@ def get(
733788
:return: an instance of the desired type
734789
"""
735790
if scope is None:
736-
scope = ActivationScope(self)
791+
scope = self.create_scope()
737792

738793
resolver = self._map.get(desired_type)
739794
scoped_service = scope.scoped_services.get(desired_type) if scope else None
@@ -781,15 +836,15 @@ def get_executor(self, method: Callable) -> Callable:
781836
if iscoroutinefunction(method):
782837

783838
async def async_executor(
784-
scoped: Optional[Dict[Union[Type, str], Any]] = None
839+
scoped: Optional[Dict[Union[Type, str], Any]] = None,
785840
):
786-
with ActivationScope(self, scoped) as context:
841+
with self.create_scope(scoped) as context:
787842
return await method(*[fn(context) for fn in fns])
788843

789844
return async_executor
790845

791846
def executor(scoped: Optional[Dict[Union[Type, str], Any]] = None):
792-
with ActivationScope(self, scoped) as context:
847+
with self.create_scope(scoped) as context:
793848
return method(*[fn(context) for fn in fns])
794849

795850
return executor
@@ -842,13 +897,19 @@ class Container(ContainerProtocol):
842897
Configuration class for a collection of services.
843898
"""
844899

845-
__slots__ = ("_map", "_aliases", "_exact_aliases", "strict")
900+
__slots__ = ("_map", "_aliases", "_exact_aliases", "_scope_cls", "strict")
846901

847-
def __init__(self, *, strict: bool = False):
902+
def __init__(
903+
self,
904+
*,
905+
strict: bool = False,
906+
scope_cls: Optional[Type[ActivationScope]] = None,
907+
):
848908
self._map: Dict[Type, Callable] = {}
849909
self._aliases: DefaultDict[str, Set[Type]] = defaultdict(set)
850910
self._exact_aliases: Dict[str, Type] = {}
851911
self._provider: Optional[Services] = None
912+
self._scope_cls = scope_cls
852913
self.strict = strict
853914

854915
@property
@@ -1205,7 +1266,7 @@ def build_provider(self) -> Services:
12051266
for name, _type in self._exact_aliases.items():
12061267
_map[name] = self._get_alias_target_type(name, _map, _type)
12071268

1208-
return Services(_map)
1269+
return Services(_map, scope_cls=self._scope_cls)
12091270

12101271
@staticmethod
12111272
def _get_alias_target_type(name, _map, _type):

tests/examples.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from typing import Optional
44

55

6-
# domain object:
76
class Cat:
87
def __init__(self, name: str):
98
self.name = name

tests/test_fn_exec.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Functions exec tests.
33
exec functions are designed to enable executing any function injecting parameters.
44
"""
5+
56
import pytest
67

78
from rodi import Container, inject

0 commit comments

Comments
 (0)