kapro 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.
Files changed (34) hide show
  1. kapro-0.0.1/PKG-INFO +167 -0
  2. kapro-0.0.1/README.md +150 -0
  3. kapro-0.0.1/pyproject.toml +193 -0
  4. kapro-0.0.1/setup.cfg +4 -0
  5. kapro-0.0.1/src/kapro/__init__.py +72 -0
  6. kapro-0.0.1/src/kapro/cached/__init__.py +29 -0
  7. kapro-0.0.1/src/kapro/cached/instance.py +108 -0
  8. kapro-0.0.1/src/kapro/cached/instance.pyi +53 -0
  9. kapro-0.0.1/src/kapro/cached/klass.py +240 -0
  10. kapro-0.0.1/src/kapro/cached/klass.pyi +72 -0
  11. kapro-0.0.1/src/kapro/cached/mixed.py +146 -0
  12. kapro-0.0.1/src/kapro/cached/mixed.pyi +59 -0
  13. kapro-0.0.1/src/kapro/cached/post.py +70 -0
  14. kapro-0.0.1/src/kapro/cached/post.pyi +29 -0
  15. kapro-0.0.1/src/kapro/cached/pre.py +70 -0
  16. kapro-0.0.1/src/kapro/cached/pre.pyi +29 -0
  17. kapro-0.0.1/src/kapro/class_property.py +204 -0
  18. kapro-0.0.1/src/kapro/class_property.pyi +96 -0
  19. kapro-0.0.1/src/kapro/primitives.py +331 -0
  20. kapro-0.0.1/src/kapro/primitives.pyi +79 -0
  21. kapro-0.0.1/src/kapro/py.typed +0 -0
  22. kapro-0.0.1/src/kapro.egg-info/PKG-INFO +167 -0
  23. kapro-0.0.1/src/kapro.egg-info/SOURCES.txt +32 -0
  24. kapro-0.0.1/src/kapro.egg-info/dependency_links.txt +1 -0
  25. kapro-0.0.1/src/kapro.egg-info/requires.txt +1 -0
  26. kapro-0.0.1/src/kapro.egg-info/top_level.txt +1 -0
  27. kapro-0.0.1/tests/test_cached.py +836 -0
  28. kapro-0.0.1/tests/test_cached_types.py +160 -0
  29. kapro-0.0.1/tests/test_class_property.py +402 -0
  30. kapro-0.0.1/tests/test_class_property_types.py +77 -0
  31. kapro-0.0.1/tests/test_pin_here.py +425 -0
  32. kapro-0.0.1/tests/test_pin_types.py +81 -0
  33. kapro-0.0.1/tests/test_primitives.py +588 -0
  34. kapro-0.0.1/tests/test_properties.py +1193 -0
kapro-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,167 @@
1
+ Metadata-Version: 2.4
2
+ Name: kapro
3
+ Version: 0.0.1
4
+ Summary: kapro
5
+ Author-email: Alex Kalaverin <alex@kalaver.in>
6
+ Project-URL: Homepage, https://kalaver.in
7
+ Classifier: Intended Audience :: Developers
8
+ Classifier: Programming Language :: Python
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: License :: OSI Approved :: BSD License
13
+ Classifier: Operating System :: OS Independent
14
+ Requires-Python: <3.14,>=3.12
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: kain>=1.1.11
17
+
18
+ ---
19
+ title: kapro
20
+ description: Class-level, mixed, and pinned descriptors for Python
21
+ ---
22
+
23
+ [ref: #kapro]
24
+
25
+ # kapro
26
+
27
+ `kapro` is a small Python library of property descriptors.
28
+
29
+ It gives you three public entry points:
30
+
31
+ - `class_property` — a descriptor that lives on the class and is shared by every instance.
32
+ - `mixed_property` — a descriptor that can be defined on the class but resolved per instance.
33
+ - `pin` — a cached instance property that stores its value in the instance dictionary.
34
+
35
+ [ref: #installation]
36
+
37
+ ## Installation
38
+
39
+ Install with `uv`:
40
+
41
+ ```bash
42
+ uv add kapro
43
+ ```
44
+
45
+ Or with any PEP 517-compatible tool:
46
+
47
+ ```bash
48
+ pip install kapro
49
+ ```
50
+
51
+ [ref: #class-property]
52
+
53
+ ## Class-level properties with `class_property`
54
+
55
+ `class_property` behaves like a class-level attribute, but it is computed lazily and cached on the class.
56
+
57
+ ```python
58
+ from kapro import class_property
59
+
60
+ class Config:
61
+ @class_property
62
+ def name(cls) -> str:
63
+ return "default"
64
+
65
+ assert Config.name == "default"
66
+ assert Config().name == "default"
67
+ ```
68
+
69
+ The value is stored on the class, so every instance sees the same value.
70
+
71
+ [ref: #mixed-property]
72
+
73
+ ## Mixed properties with `mixed_property`
74
+
75
+ `mixed_property` calls the same function with the class when accessed on the class and with the instance when accessed on an instance.
76
+
77
+ ```python
78
+ from kapro import mixed_property
79
+
80
+ class User:
81
+ def __init__(self, id: int) -> None:
82
+ self.id = id
83
+
84
+ @mixed_property
85
+ def label(node) -> str:
86
+ if isinstance(node, type):
87
+ return "User model"
88
+ return f"user: {node.id}"
89
+
90
+ assert User.label == "User model"
91
+ assert User(id=7).label == "user: 7"
92
+ ```
93
+
94
+ [ref: #pin]
95
+
96
+ ## Cached instance properties with `pin`
97
+
98
+ `pin` is the cached equivalent of a plain method-based property.
99
+ The first access computes the value and stores it in the instance dictionary.
100
+
101
+ ```python
102
+ from kapro import pin
103
+
104
+ class Expensive:
105
+ @pin
106
+ def payload(self) -> dict:
107
+ return {"loaded": True}
108
+
109
+ obj = Expensive()
110
+ assert obj.payload is obj.payload
111
+ ```
112
+
113
+ [ref: #pin-flavors]
114
+
115
+ ### `pin` flavors
116
+
117
+ `pin` exposes several variants through class attributes.
118
+
119
+ | Flavor | Decorator | Caches on |
120
+ | --- | --- | --- |
121
+ | `native` | `@pin` | instance (default) |
122
+ | `cls` | `@pin.cls` | class |
123
+ | `any` | `@pin.any` | class or instance |
124
+ | `pre` | `@pin.pre` | instance, runs before the underlying call |
125
+ | `post` | `@pin.post` | instance, runs after the underlying call |
126
+
127
+ ```python
128
+ from kapro import pin
129
+
130
+ class Service:
131
+ @pin.cls
132
+ def version(cls) -> str:
133
+ return "1.0"
134
+
135
+ assert Service.version == "1.0"
136
+ assert Service().version == "1.0"
137
+ ```
138
+
139
+ [ref: #full-example]
140
+
141
+ ## Full example
142
+
143
+ ```python
144
+ from kapro import class_property, mixed_property, pin
145
+
146
+ class App:
147
+ @class_property
148
+ def name(cls) -> str:
149
+ return "MyApp"
150
+
151
+ @mixed_property
152
+ def greeting(node) -> str:
153
+ return f"Hello from {node.name}"
154
+
155
+ @pin
156
+ def config(self) -> dict:
157
+ return {"debug": True}
158
+
159
+
160
+ assert App.name == "MyApp"
161
+ assert App.greeting == "Hello from MyApp"
162
+ assert App().greeting == "Hello from MyApp"
163
+
164
+ app = App()
165
+ assert app.config == {"debug": True}
166
+ assert app.config is app.config
167
+ ```
kapro-0.0.1/README.md ADDED
@@ -0,0 +1,150 @@
1
+ ---
2
+ title: kapro
3
+ description: Class-level, mixed, and pinned descriptors for Python
4
+ ---
5
+
6
+ [ref: #kapro]
7
+
8
+ # kapro
9
+
10
+ `kapro` is a small Python library of property descriptors.
11
+
12
+ It gives you three public entry points:
13
+
14
+ - `class_property` — a descriptor that lives on the class and is shared by every instance.
15
+ - `mixed_property` — a descriptor that can be defined on the class but resolved per instance.
16
+ - `pin` — a cached instance property that stores its value in the instance dictionary.
17
+
18
+ [ref: #installation]
19
+
20
+ ## Installation
21
+
22
+ Install with `uv`:
23
+
24
+ ```bash
25
+ uv add kapro
26
+ ```
27
+
28
+ Or with any PEP 517-compatible tool:
29
+
30
+ ```bash
31
+ pip install kapro
32
+ ```
33
+
34
+ [ref: #class-property]
35
+
36
+ ## Class-level properties with `class_property`
37
+
38
+ `class_property` behaves like a class-level attribute, but it is computed lazily and cached on the class.
39
+
40
+ ```python
41
+ from kapro import class_property
42
+
43
+ class Config:
44
+ @class_property
45
+ def name(cls) -> str:
46
+ return "default"
47
+
48
+ assert Config.name == "default"
49
+ assert Config().name == "default"
50
+ ```
51
+
52
+ The value is stored on the class, so every instance sees the same value.
53
+
54
+ [ref: #mixed-property]
55
+
56
+ ## Mixed properties with `mixed_property`
57
+
58
+ `mixed_property` calls the same function with the class when accessed on the class and with the instance when accessed on an instance.
59
+
60
+ ```python
61
+ from kapro import mixed_property
62
+
63
+ class User:
64
+ def __init__(self, id: int) -> None:
65
+ self.id = id
66
+
67
+ @mixed_property
68
+ def label(node) -> str:
69
+ if isinstance(node, type):
70
+ return "User model"
71
+ return f"user: {node.id}"
72
+
73
+ assert User.label == "User model"
74
+ assert User(id=7).label == "user: 7"
75
+ ```
76
+
77
+ [ref: #pin]
78
+
79
+ ## Cached instance properties with `pin`
80
+
81
+ `pin` is the cached equivalent of a plain method-based property.
82
+ The first access computes the value and stores it in the instance dictionary.
83
+
84
+ ```python
85
+ from kapro import pin
86
+
87
+ class Expensive:
88
+ @pin
89
+ def payload(self) -> dict:
90
+ return {"loaded": True}
91
+
92
+ obj = Expensive()
93
+ assert obj.payload is obj.payload
94
+ ```
95
+
96
+ [ref: #pin-flavors]
97
+
98
+ ### `pin` flavors
99
+
100
+ `pin` exposes several variants through class attributes.
101
+
102
+ | Flavor | Decorator | Caches on |
103
+ | --- | --- | --- |
104
+ | `native` | `@pin` | instance (default) |
105
+ | `cls` | `@pin.cls` | class |
106
+ | `any` | `@pin.any` | class or instance |
107
+ | `pre` | `@pin.pre` | instance, runs before the underlying call |
108
+ | `post` | `@pin.post` | instance, runs after the underlying call |
109
+
110
+ ```python
111
+ from kapro import pin
112
+
113
+ class Service:
114
+ @pin.cls
115
+ def version(cls) -> str:
116
+ return "1.0"
117
+
118
+ assert Service.version == "1.0"
119
+ assert Service().version == "1.0"
120
+ ```
121
+
122
+ [ref: #full-example]
123
+
124
+ ## Full example
125
+
126
+ ```python
127
+ from kapro import class_property, mixed_property, pin
128
+
129
+ class App:
130
+ @class_property
131
+ def name(cls) -> str:
132
+ return "MyApp"
133
+
134
+ @mixed_property
135
+ def greeting(node) -> str:
136
+ return f"Hello from {node.name}"
137
+
138
+ @pin
139
+ def config(self) -> dict:
140
+ return {"debug": True}
141
+
142
+
143
+ assert App.name == "MyApp"
144
+ assert App.greeting == "Hello from MyApp"
145
+ assert App().greeting == "Hello from MyApp"
146
+
147
+ app = App()
148
+ assert app.config == {"debug": True}
149
+ assert app.config is app.config
150
+ ```
@@ -0,0 +1,193 @@
1
+ [build-system]
2
+ build-backend = "setuptools.build_meta"
3
+ requires = [
4
+ "setuptools"
5
+ ]
6
+
7
+ [dependency-groups]
8
+ build = [
9
+ "setuptools"
10
+ ]
11
+ dev = [
12
+ "async-asgi-testclient>=1.4.11",
13
+ "bandit[toml]>=1.9.4",
14
+ "basedmypy>=2.10.0",
15
+ "basedpyright>=1.39.4",
16
+ "better-exceptions>=0.3.3",
17
+ "black>=26.3.1",
18
+ "colorama>=0.4.6",
19
+ "faker>=40.18.0",
20
+ "hexora>=0.2.4",
21
+ "pre-commit>=4.6.0",
22
+ "pyrefly>=1.0.0",
23
+ "pytest-asyncio>=1.3.0",
24
+ "pytest-check>=2.8.0",
25
+ "pytest-clarity>=1.0.1",
26
+ "pytest-cov>=7.1.0",
27
+ "pytest-dotenv>=0.5.2",
28
+ "pytest-icdiff>=0.9",
29
+ "pytest-mock>=3.15.1",
30
+ "pytest-randomly>=4.1.0",
31
+ "pytest-timeout>=2.4.0",
32
+ "pytest-xdist>=3.8.0",
33
+ "pytest>=9.0.3",
34
+ "python-lsp-server[all]>=1.14.0",
35
+ "refurb>=2.3.1",
36
+ "righttyper>=0.1.0",
37
+ "ruff>=0.15.13",
38
+ "structlog>=25.5.0",
39
+ "time-machine>=3.2.0",
40
+ "ty>=0.0.36",
41
+ "typing-extensions>=4.15.0",
42
+ "typing-inspection>=0.4.2",
43
+ "uv-publish>=1.4",
44
+ "vulture>=2.16",
45
+ "yamlfix>=1.19.1",
46
+ "yamllint>=1.38.0"
47
+ ]
48
+
49
+ [project]
50
+ authors = [
51
+ {name = "Alex Kalaverin", email = "alex@kalaver.in"}
52
+ ]
53
+ classifiers = [
54
+ "Intended Audience :: Developers",
55
+ "Programming Language :: Python",
56
+ "Programming Language :: Python :: 3",
57
+ "Programming Language :: Python :: 3.12",
58
+ "Programming Language :: Python :: 3.13",
59
+ "License :: OSI Approved :: BSD License",
60
+ "Operating System :: OS Independent"
61
+ ]
62
+ dependencies = [
63
+ "kain>=1.1.11"
64
+ ]
65
+ description = "kapro"
66
+ name = "kapro"
67
+ readme = "README.md"
68
+ requires-python = ">=3.12,<3.14"
69
+ version = "0.0.1"
70
+
71
+ [project.urls]
72
+ Homepage = "https://kalaver.in"
73
+
74
+ [tool.bandit]
75
+ exclude_dirs = [".venv", ".git"]
76
+
77
+ [tool.black]
78
+ line-length = 79
79
+ target-version = ["py312"]
80
+
81
+ [tool.coverage.report]
82
+ exclude_lines = [
83
+ "pragma: no cover",
84
+ "if __name__ == .__main__.:",
85
+ "^\\s+\\.{3}",
86
+ "if TYPE_CHECKING:",
87
+ "raise NotImplementedError",
88
+ "pass"
89
+ ]
90
+ fail_under = 90
91
+ show_missing = true
92
+
93
+ [tool.coverage.run]
94
+ branch = true
95
+ omit = [
96
+ "*/migrations/*",
97
+ "*/__init__.py",
98
+ "*/if TYPE_CHECKING:*",
99
+ "tests/*"
100
+ ]
101
+ source = ["src"]
102
+
103
+ [tool.pylint.messages_control]
104
+ enable = "all"
105
+
106
+ [tool.pytest.ini_options]
107
+ addopts = [
108
+ "-ra",
109
+ "--strict-config",
110
+ "--strict-markers",
111
+ "--tb=short",
112
+ "--timeout=60"
113
+ ]
114
+ faulthandler_timeout = 60
115
+ filterwarnings = [
116
+ "default"
117
+ ]
118
+ markers = [
119
+ ]
120
+ minversion = "8.0"
121
+ testpaths = ["tests"]
122
+
123
+ [tool.refurb]
124
+ ignore = [
125
+ 123,
126
+ 149,
127
+ 176,
128
+ 191
129
+ ]
130
+
131
+ [tool.ruff]
132
+ exclude = []
133
+ extend = "etc/lint/ruff.toml"
134
+
135
+ [tool.ruff.lint.isort]
136
+ section-order = [
137
+ "future",
138
+ "standard-library",
139
+ "third-party",
140
+ "first-party",
141
+ "local-project",
142
+ "local-folder"
143
+ ]
144
+
145
+ [tool.ruff.lint.isort.sections]
146
+ local-project = ["kapro"]
147
+
148
+ [tool.ruff.lint.per-file-ignores]
149
+ "*.pyi" = [
150
+ "ANN",
151
+ "N",
152
+ "PYI"
153
+ ]
154
+ "contrib/*" = []
155
+ "docs/*" = [
156
+ "INP001", # File `docs/conf.py` is part of an implicit namespace package. Add an `__init__.py`.
157
+ "I001", # [*] Import block is un-sorted or un-formatted
158
+ "PTH100", # `os.path.abspath()` should be replaced by `Path.resolve()`
159
+ "A001" # Variable `copyright` is shadowing a Python builtin
160
+ ]
161
+ "src/*" = []
162
+ "tests/*" = [
163
+ "ANN001", # Missing type annotation for function argument `mock_workflow_is_replaying`
164
+ "ANN401", # Dynamically typed expressions (typing.Any) are disallowed in `key`
165
+ "ARG001", # Unused function argument: `node`
166
+ "ARG002", # Unused method argument: `mock_workflow_info`
167
+ "ARG005", # Unused lambda argument: `cls`
168
+ "B018", # Found useless expression. Either assign it to a variable or remove it.
169
+ "FBT003", # Boolean positional value in function call
170
+ "INP001", # File `tests/workflows/test_turnover_workflows.py` is part of an implicit namespace package. Add an `__init__.py`.
171
+ "N805", # First argument of a method should be named `self`
172
+ "N805", # First argument of a method should be named `self`
173
+ "PLC0415", # `import` should be at the top-level of a file
174
+ "PLR0124", # Name compared with itself, consider replacing `Nothing == Nothing`
175
+ "PLR2004", # Magic value used in comparison, consider replacing `3` with a constant variable
176
+ "PYI024", # Use `typing.NamedTuple` instead of `collections.namedtuple`
177
+ "S101", # Use of `assert` detected
178
+ "SLF001", # Private member accessed: `_proxy`
179
+ "TC002", # Move third-party import `faker.Faker` into a type-checking block
180
+ "TC003", # Move standard library import `collections.abc.Generator` into a type-checking block
181
+ "TRY301" # Abstract `raise` to an inner function
182
+ ]
183
+
184
+ [tool.setuptools.packages.find]
185
+ where = ["src"]
186
+
187
+ [tool.ty.rules]
188
+ possibly-missing-attribute = "ignore"
189
+
190
+ [tool.uv]
191
+ default-groups = [
192
+ "dev"
193
+ ]
kapro-0.0.1/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, TypeVar, final, overload, override
4
+
5
+ from kapro.cached import (
6
+ cached_property,
7
+ class_cached_property,
8
+ mixed_cached_property,
9
+ post_cached_property,
10
+ pre_cached_property,
11
+ )
12
+ from kapro.class_property import class_property, mixed_property
13
+ from kapro.primitives import (
14
+ ContextFaultError,
15
+ Nothing,
16
+ bound_property,
17
+ )
18
+
19
+ T_co = TypeVar("T_co", covariant=True)
20
+
21
+
22
+ @final
23
+ class pin[T_co](bound_property[T_co]): # noqa: N801
24
+ native = cached_property
25
+ cls = class_cached_property
26
+ any = mixed_cached_property
27
+ pre = pre_cached_property
28
+ post = post_cached_property
29
+
30
+ @overload
31
+ def __get__(
32
+ self,
33
+ node: None,
34
+ klass: Any = ..., # noqa: ANN401
35
+ ) -> pin[T_co]: ...
36
+
37
+ @overload
38
+ def __get__(
39
+ self,
40
+ node: object,
41
+ klass: Any = ..., # noqa: ANN401
42
+ ) -> T_co: ...
43
+
44
+ @override
45
+ def __get__(
46
+ self,
47
+ node: object | None,
48
+ klass: Any = Nothing,
49
+ ) -> pin[T_co] | T_co:
50
+ if node is None:
51
+ raise ContextFaultError(self.header_with_context(klass))
52
+
53
+ cache = getattr(node, "__dict__", None)
54
+ if cache is None:
55
+ raise TypeError(
56
+ f"{self.header_with_context(node)} has no __dict__",
57
+ )
58
+
59
+ try:
60
+ return cache[self.name] # type: ignore[no-any-return, index]
61
+
62
+ except KeyError:
63
+ value = self.function(node)
64
+ cache[self.name] = value # type: ignore[index]
65
+ return value # type: ignore[no-any-return]
66
+
67
+
68
+ __all__ = (
69
+ "class_property",
70
+ "mixed_property",
71
+ "pin",
72
+ )
@@ -0,0 +1,29 @@
1
+ from kapro.cached.instance import cached_property
2
+ from kapro.cached.klass import (
3
+ class_cached_property,
4
+ class_parent_cached_property,
5
+ )
6
+ from kapro.cached.mixed import (
7
+ mixed_cached_property,
8
+ mixed_parent_cached_property,
9
+ )
10
+ from kapro.cached.post import (
11
+ post_cached_property,
12
+ post_parent_cached_property,
13
+ )
14
+ from kapro.cached.pre import (
15
+ pre_cached_property,
16
+ pre_parent_cached_property,
17
+ )
18
+
19
+ __all__ = (
20
+ "cached_property",
21
+ "class_cached_property",
22
+ "class_parent_cached_property",
23
+ "mixed_cached_property",
24
+ "mixed_parent_cached_property",
25
+ "post_cached_property",
26
+ "post_parent_cached_property",
27
+ "pre_cached_property",
28
+ "pre_parent_cached_property",
29
+ )