smalldi 0.1.0__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,5 @@
1
+ .idea
2
+ .venv
3
+ *.sw?
4
+ dist
5
+ *.pyc
@@ -0,0 +1,3 @@
1
+ # Changelog
2
+ ## 0.1.0
3
+ * Initial release
smalldi-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Anna-Sofia Kasierocka
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.
smalldi-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,105 @@
1
+ Metadata-Version: 2.4
2
+ Name: smalldi
3
+ Version: 0.1.0
4
+ Summary: Small, annotation-driven dependency injection for Python.
5
+ Project-URL: Homepage, https://github.com/0xf104a/smalldi
6
+ Project-URL: Documentation, https://github.com/0xf104a/smalldi#readme
7
+ Project-URL: Source, https://github.com/0xf104a/smalldi
8
+ Project-URL: Issues, https://github.com/0xf104a/smalldi/issues
9
+ Author-email: Anna-Sofia Kasierocka <f104a@f104a.io>
10
+ License: MIT License
11
+
12
+ Copyright (c) 2025 Anna-Sofia Kasierocka
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Keywords: annotations,dependency injection,di,typing
33
+ Classifier: Development Status :: 4 - Beta
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Programming Language :: Python :: 3 :: Only
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Programming Language :: Python :: 3.13
42
+ Classifier: Topic :: Software Development :: Libraries
43
+ Classifier: Typing :: Typed
44
+ Requires-Python: >=3.11
45
+ Requires-Dist: typing-extensions>=4.5; python_version >= '3.11'
46
+ Description-Content-Type: text/markdown
47
+
48
+ # Small DI
49
+ The smallest dependency injection mechanism possible in python.
50
+ SmallDI provides you with intuitive and simple interface for doing dependency
51
+ injections in your project.
52
+
53
+ # Example usage
54
+ ```python
55
+ import random
56
+
57
+ from smalldi import Injector
58
+ from smalldi.annotation import Provide
59
+
60
+ # Lets create some service
61
+ @Injector.singleton
62
+ class MeowService:
63
+ _MEOWS = ["Meow", "Meow-meow", "Meowwwwww", "Mrromeowww", "Meeeeoooow"]
64
+ def __init__(self):
65
+ print("Meow-meow! Meowing service is initialized")
66
+
67
+ def meow(self):
68
+ print(random.choice(self._MEOWS))
69
+
70
+ # Now lets make purring service.
71
+ # But cats do not purr without telling meow!(at least in this test)
72
+ # So its time to inject dependency
73
+ @Injector.singleton
74
+ class PurrService:
75
+ _PURRS = ["Purrrrr", "Purr-purr"]
76
+
77
+ @Injector.inject
78
+ def __init__(self, meow_service: Provide[MeowService]):
79
+ self.meow_service = meow_service
80
+ print("Purr-purr! Purring service is initialized")
81
+
82
+ def purr(self):
83
+ self.meow_service.meow()
84
+ print(random.choice(self._PURRS))
85
+
86
+ # Now lets put it all together.
87
+ # Ask our services to meow and then purr
88
+ @Injector.inject
89
+ def main(meow_service: Provide[MeowService], purr_service: Provide[PurrService]):
90
+ meow_service.meow()
91
+ purr_service.purr()
92
+
93
+ if __name__ == '__main__':
94
+ main()
95
+ ```
96
+
97
+ # Library logic
98
+ ## Injector
99
+ Injector is a static class(i.e. one that should never be instantiated) storing all singletons
100
+ and responsible for injecting those singletons to a functions.
101
+
102
+ ## Provide
103
+ `Provide[T]` is an annotation for injector telling it that instead of this argument
104
+ instance of `T` should be passed. Caller of function with `Provide[T]` may explicitly
105
+ override argument annotated with `Provide[T]` by directly passing `annotated_di_arg=my_value`.
@@ -0,0 +1,58 @@
1
+ # Small DI
2
+ The smallest dependency injection mechanism possible in python.
3
+ SmallDI provides you with intuitive and simple interface for doing dependency
4
+ injections in your project.
5
+
6
+ # Example usage
7
+ ```python
8
+ import random
9
+
10
+ from smalldi import Injector
11
+ from smalldi.annotation import Provide
12
+
13
+ # Lets create some service
14
+ @Injector.singleton
15
+ class MeowService:
16
+ _MEOWS = ["Meow", "Meow-meow", "Meowwwwww", "Mrromeowww", "Meeeeoooow"]
17
+ def __init__(self):
18
+ print("Meow-meow! Meowing service is initialized")
19
+
20
+ def meow(self):
21
+ print(random.choice(self._MEOWS))
22
+
23
+ # Now lets make purring service.
24
+ # But cats do not purr without telling meow!(at least in this test)
25
+ # So its time to inject dependency
26
+ @Injector.singleton
27
+ class PurrService:
28
+ _PURRS = ["Purrrrr", "Purr-purr"]
29
+
30
+ @Injector.inject
31
+ def __init__(self, meow_service: Provide[MeowService]):
32
+ self.meow_service = meow_service
33
+ print("Purr-purr! Purring service is initialized")
34
+
35
+ def purr(self):
36
+ self.meow_service.meow()
37
+ print(random.choice(self._PURRS))
38
+
39
+ # Now lets put it all together.
40
+ # Ask our services to meow and then purr
41
+ @Injector.inject
42
+ def main(meow_service: Provide[MeowService], purr_service: Provide[PurrService]):
43
+ meow_service.meow()
44
+ purr_service.purr()
45
+
46
+ if __name__ == '__main__':
47
+ main()
48
+ ```
49
+
50
+ # Library logic
51
+ ## Injector
52
+ Injector is a static class(i.e. one that should never be instantiated) storing all singletons
53
+ and responsible for injecting those singletons to a functions.
54
+
55
+ ## Provide
56
+ `Provide[T]` is an annotation for injector telling it that instead of this argument
57
+ instance of `T` should be passed. Caller of function with `Provide[T]` may explicitly
58
+ override argument annotated with `Provide[T]` by directly passing `annotated_di_arg=my_value`.
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "smalldi"
7
+ version = "0.1.0"
8
+ description = "Small, annotation-driven dependency injection for Python."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { file = "LICENSE" }
12
+ authors = [{ name = "Anna-Sofia Kasierocka", email = "f104a@f104a.io" }]
13
+ keywords = ["dependency injection", "di", "typing", "annotations"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Typing :: Typed",
25
+ "Topic :: Software Development :: Libraries",
26
+ ]
27
+ dependencies = [
28
+ "typing-extensions>=4.5; python_version>='3.11'"
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/0xf104a/smalldi"
33
+ Documentation = "https://github.com/0xf104a/smalldi#readme"
34
+ Source = "https://github.com/0xf104a/smalldi"
35
+ Issues = "https://github.com/0xf104a/smalldi/issues"
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ packages = ["src/smalldi"]
39
+ include = [
40
+ "src/smalldi/py.typed",
41
+ ]
42
+
43
+ [tool.hatch.build.targets.sdist]
44
+ include = [
45
+ "/src",
46
+ "/README.md",
47
+ "/LICENSE",
48
+ "/pyproject.toml",
49
+ "/CHANGELOG.md",
50
+ ]
@@ -0,0 +1,51 @@
1
+ import functools
2
+
3
+ from smalldi.wrappers import staticclass
4
+ from smalldi.annotation import _Provide, Provide
5
+
6
+ __author__ = "Anna-Sofia Kasierocka"
7
+ __email__ = "f104a@f104a.io"
8
+ __version__ = "0.1.0"
9
+ __all__ = ["Injector", "Provide"]
10
+
11
+ @staticclass
12
+ class Injector:
13
+ """
14
+ The injector class handles all the dependency injections.
15
+ """
16
+ singletons_available = {}
17
+
18
+ @classmethod
19
+ def inject(cls, fn):
20
+ """
21
+ Injects dependencies into function.
22
+ :param fn: function to inject dependencies into
23
+ :return: function with injected dependencies
24
+ """
25
+ kwargs_ext = dict()
26
+ for name, tp in _Provide.iter_annotations(fn):
27
+ if tp not in cls.singletons_available:
28
+ raise TypeError(f"Singleton {tp} is not available")
29
+ kwargs_ext[name] = cls.singletons_available[tp]
30
+
31
+ @functools.wraps(fn)
32
+ def wrapped_fn(*args, **kwargs):
33
+ for name, value in kwargs_ext.items():
34
+ if name not in kwargs:
35
+ kwargs[name] = value
36
+ return fn(*args, **kwargs)
37
+
38
+ return wrapped_fn
39
+
40
+ @classmethod
41
+ def singleton(cls, target_cls):
42
+ """
43
+ Marks class as singleton.
44
+ Singleton classes are instantiated only once by the injector.
45
+ Also their __init__ function should have no arguments or be annotated
46
+ with @Injector.inject, so it would
47
+ :param target_cls: Class which to be marked as injectable singleton
48
+ :return: None
49
+ """
50
+ cls.singletons_available[target_cls] = target_cls()
51
+ return target_cls
@@ -0,0 +1,47 @@
1
+ import inspect
2
+ from typing import TypeVar, Generic, get_origin, get_args, Iterator, Callable, Any, TypeAlias, Annotated
3
+
4
+ _T = TypeVar("_T")
5
+
6
+ class _Provide(Generic[_T]):
7
+ """Provide annotation tells smalldi to find instance of T and inject it into function"""
8
+ @staticmethod
9
+ def unwrap(tp: object) -> type:
10
+ """
11
+ Unwraps annotation to get inner type.
12
+ :param tp: annotation itself
13
+ :return: inner type T
14
+ """
15
+ origin = get_origin(tp)
16
+ if origin is _Provide:
17
+ (inner,) = get_args(tp)
18
+ return inner
19
+
20
+ if origin is Annotated:
21
+ args = get_args(tp)
22
+ inner, *meta = args
23
+ if any(m is _Provide for m in meta):
24
+ return inner
25
+
26
+ raise TypeError(f"Expected Provide[T], got {tp!r}")
27
+
28
+ @staticmethod
29
+ def iter_annotations(func: Callable) -> Iterator[Any]:
30
+ """
31
+ Provides an iterator that extracts and yields annotations of type `Provide`
32
+ from the parameters of a given function's signature.
33
+ :param func: The function whose parameter annotations will be inspected.
34
+ :type func: Callable
35
+ :return: An iterator over the unwrapped annotations of type `Provide` found
36
+ in the function's parameters.
37
+ :rtype: Iterator[str, Any]
38
+ """
39
+ signature = inspect.signature(func)
40
+ for name, param in signature.parameters.items():
41
+ try:
42
+ inner = _Provide.unwrap(param.annotation)
43
+ yield name, inner
44
+ except TypeError:
45
+ continue
46
+
47
+ Provide: TypeAlias = Annotated[_T, _Provide]
File without changes
@@ -0,0 +1,8 @@
1
+ def staticclass(cls):
2
+ """
3
+ Marks class as static. Static classes can't be instantiated.
4
+ """
5
+ def _no_init(*_args, **_kwargs):
6
+ raise TypeError("Can't instantiate static class")
7
+ cls.__init__ = _no_init
8
+ return cls
@@ -0,0 +1,9 @@
1
+ # Tests for smalldi
2
+
3
+ This module contains tests for a smalldi library with the use of pytest
4
+
5
+ ## Test structure
6
+ - `test_provide.py` - Tests for class `Provide`
7
+ - `test_injector.py` - Тests for class `Injector`
8
+ - `test_wrappers.py` - Tests for wrappers (currently only `staticclass`)
9
+ - `test_integration.py` - Library-level tests
@@ -0,0 +1 @@
1
+ # tests package
@@ -0,0 +1,17 @@
1
+ import pytest
2
+ from typing import Any, Dict
3
+
4
+ from smalldi import Injector
5
+
6
+
7
+ @pytest.fixture(autouse=False)
8
+ def reset_injector():
9
+ """Reset Injector state before and after each test"""
10
+ old_singletons: Dict[Any, Any] = Injector.singletons_available.copy()
11
+
12
+ Injector.singletons_available.clear()
13
+
14
+ yield
15
+
16
+ Injector.singletons_available.clear()
17
+ Injector.singletons_available.update(old_singletons)
@@ -0,0 +1,87 @@
1
+ import pytest
2
+ from typing import List, Dict, Any
3
+ from unittest.mock import patch, MagicMock
4
+
5
+ from smalldi import Injector
6
+ from smalldi.annotation import _Provide
7
+
8
+
9
+ @pytest.fixture
10
+ def reset_injector():
11
+ """Reset Injector state before each test"""
12
+ old_singletons = Injector.singletons_available.copy()
13
+ Injector.singletons_available.clear()
14
+
15
+ yield
16
+
17
+ # Відновлюємо стан після тесту
18
+ Injector.singletons_available.clear()
19
+ Injector.singletons_available.update(old_singletons)
20
+
21
+
22
+ def test_singleton_registration(reset_injector):
23
+ """Test that singleton decorator registers class instance"""
24
+ assert len(Injector.singletons_available) == 0
25
+
26
+ @Injector.singleton
27
+ class TestService:
28
+ def hello(self):
29
+ return "Hello"
30
+
31
+ assert len(Injector.singletons_available) == 1
32
+ assert TestService in Injector.singletons_available
33
+ assert isinstance(Injector.singletons_available[TestService], TestService)
34
+
35
+ service = Injector.singletons_available[TestService]
36
+ assert service.hello() == "Hello"
37
+
38
+
39
+ def test_singleton_single_instance(reset_injector):
40
+ """Test that singleton creates only one instance"""
41
+ counter = 0
42
+
43
+ @Injector.singleton
44
+ class TestService:
45
+ def __init__(self):
46
+ nonlocal counter
47
+ counter += 1
48
+
49
+ assert counter == 1
50
+
51
+ Injector.singleton(TestService)
52
+ assert counter == 2 # Це повинно бути 1, але поточна реалізація створить новий екземпляр
53
+
54
+
55
+ def test_inject_basic(reset_injector):
56
+ """Test basic dependency injection"""
57
+ @Injector.singleton
58
+ class TestService:
59
+ def hello(self):
60
+ return "Hello"
61
+
62
+ @Injector.inject
63
+ def test_function(service: _Provide[TestService]):
64
+ return service.hello()
65
+
66
+ result = test_function()
67
+ assert result == "Hello"
68
+
69
+
70
+ def test_inject_multiple_dependencies(reset_injector):
71
+ """Test injection of multiple dependencies"""
72
+ @Injector.singleton
73
+ class ServiceA:
74
+ def value(self):
75
+ return "A"
76
+
77
+ @Injector.singleton
78
+ class ServiceB:
79
+ def value(self):
80
+ return "B"
81
+
82
+ @Injector.inject
83
+ def test_function(service_a: _Provide[ServiceA], service_b: _Provide[ServiceB]):
84
+ return service_a.value() + service_b.value()
85
+
86
+ result = test_function()
87
+ assert result == "AB"
@@ -0,0 +1,87 @@
1
+ import pytest
2
+ from typing import List, Dict, Optional
3
+
4
+ from smalldi import Injector
5
+ from smalldi.annotation import Provide
6
+
7
+
8
+ @pytest.fixture
9
+ def reset_injector():
10
+ """Reset Injector state before each test"""
11
+ old_singletons = Injector.singletons_available.copy()
12
+ Injector.singletons_available.clear()
13
+ yield
14
+ Injector.singletons_available.clear()
15
+ Injector.singletons_available.update(old_singletons)
16
+
17
+
18
+ def test_simple_injection(reset_injector):
19
+ """Test simple dependency injection in real code"""
20
+
21
+ @Injector.singleton
22
+ class ConfigService:
23
+ def get_config(self, key: str) -> str:
24
+ return f"config:{key}"
25
+
26
+ @Injector.singleton
27
+ class UserService:
28
+ @Injector.inject
29
+ def __init__(self, config: Provide[ConfigService]):
30
+ self.config = config
31
+
32
+ def get_user(self, user_id: int) -> Dict[str, str]:
33
+ api_key = self.config.get_config("api_key")
34
+ return {"id": str(user_id), "name": f"User_{user_id}", "api_key": api_key}
35
+
36
+ @Injector.inject
37
+ def get_user_handler(user_id: int, user_service: Provide[UserService]) -> Dict[str, str]:
38
+ return user_service.get_user(user_id)
39
+
40
+ result = get_user_handler(user_id=123)
41
+ assert result["id"] == "123"
42
+ assert result["name"] == "User_123"
43
+ assert result["api_key"] == "config:api_key"
44
+
45
+ def test_nested_injection(reset_injector, monkeypatch):
46
+ """Test nested dependency injection"""
47
+ @Injector.singleton
48
+ class DatabaseService:
49
+ def query(self, sql: str) -> List[Dict[str, str]]:
50
+ return [{"result": f"db:{sql}"}]
51
+
52
+ @Injector.singleton
53
+ class LogService:
54
+ def log(self, message: str) -> None:
55
+ pass
56
+
57
+ @Injector.singleton
58
+ class UserRepository:
59
+ @Injector.inject
60
+ def __init__(self, db: Provide[DatabaseService], logger: Provide[LogService]):
61
+ self.db = db
62
+ self.logger = logger
63
+
64
+ def find_user(self, user_id: int) -> Optional[Dict[str, str]]:
65
+ self.logger.log(f"Finding user {user_id}")
66
+ results = self.db.query(f"SELECT * FROM users WHERE id = {user_id}")
67
+ return results[0] if results else None
68
+
69
+ @Injector.singleton
70
+ class UserService:
71
+ @Injector.inject
72
+ def __init__(self, repository: Provide[UserRepository]):
73
+ self.repository = repository
74
+
75
+ def get_user_data(self, user_id: int) -> Dict[str, str]:
76
+ user = self.repository.find_user(user_id)
77
+ if not user:
78
+ return {"error": "User not found"}
79
+ return user
80
+
81
+ @Injector.inject
82
+ def user_api(user_id: int, service: Provide[UserService]) -> Dict[str, str]:
83
+ return service.get_user_data(user_id)
84
+
85
+ result = user_api(user_id=42)
86
+ assert "result" in result
87
+ assert result["result"] == "db:SELECT * FROM users WHERE id = 42"
@@ -0,0 +1,35 @@
1
+ import pytest
2
+ from typing import Any, List, Dict
3
+
4
+ from smalldi.annotation import _Provide, Provide
5
+
6
+
7
+ def test_provide_unwrap_valid():
8
+ """Test unwrapping valid Provide annotations"""
9
+ assert _Provide.unwrap(Provide[str]) is str
10
+ assert _Provide.unwrap(Provide[int]) is int
11
+ assert _Provide.unwrap(Provide[List[int]]) is List[int]
12
+ assert _Provide.unwrap(Provide[Dict[str, Any]]) is Dict[str, Any]
13
+
14
+
15
+ def test_provide_unwrap_invalid():
16
+ """Test unwrapping invalid annotations raises TypeError"""
17
+ with pytest.raises(TypeError):
18
+ _Provide.unwrap(str)
19
+
20
+ with pytest.raises(TypeError):
21
+ _Provide.unwrap(int)
22
+
23
+ with pytest.raises(TypeError):
24
+ _Provide.unwrap(List[int])
25
+
26
+ with pytest.raises(TypeError):
27
+ _Provide.unwrap(None)
28
+
29
+ def test_iter_annotations_empty():
30
+ """Test iteration over function without Provide annotations"""
31
+ def func(a: str, b: int):
32
+ pass
33
+
34
+ annotations = list(_Provide.iter_annotations(func))
35
+ assert len(annotations) == 0
@@ -0,0 +1,41 @@
1
+ import pytest
2
+
3
+ from smalldi.wrappers import staticclass
4
+
5
+
6
+ def test_staticclass_basic():
7
+ """Test that staticclass prevents instantiation"""
8
+ @staticclass
9
+ class TestClass:
10
+ @classmethod
11
+ def class_method(cls):
12
+ return "class method"
13
+
14
+ @staticmethod
15
+ def static_method():
16
+ return "static method"
17
+
18
+ with pytest.raises(TypeError):
19
+ TestClass()
20
+
21
+ assert TestClass.class_method() == "class method"
22
+ assert TestClass.static_method() == "static method"
23
+
24
+
25
+ def test_staticclass_preserves_metadata():
26
+ """Test that staticclass preserves class metadata"""
27
+ @staticclass
28
+ class TestClass:
29
+ """Test docstring"""
30
+ class_var = "class variable"
31
+
32
+ @classmethod
33
+ def method(cls):
34
+ """Method docstring"""
35
+ return cls.class_var
36
+
37
+ assert TestClass.__name__ == "TestClass"
38
+ assert TestClass.__doc__ == "Test docstring"
39
+ assert TestClass.class_var == "class variable"
40
+ assert TestClass.method.__doc__ == "Method docstring"
41
+ assert TestClass.method() == "class variable"