frequenz-core 1.3.0__tar.gz → 1.5.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.
- {frequenz_core-1.3.0/src/frequenz_core.egg-info → frequenz_core-1.5.0}/PKG-INFO +131 -34
- {frequenz_core-1.3.0 → frequenz_core-1.5.0}/README.md +101 -5
- frequenz_core-1.5.0/RELEASE_NOTES.md +11 -0
- {frequenz_core-1.3.0 → frequenz_core-1.5.0}/pyproject.toml +48 -30
- {frequenz_core-1.3.0 → frequenz_core-1.5.0}/src/frequenz/core/enum.py +4 -2
- {frequenz_core-1.3.0 → frequenz_core-1.5.0}/src/frequenz/core/id.py +18 -2
- {frequenz_core-1.3.0 → frequenz_core-1.5.0}/src/frequenz/core/math.py +34 -40
- {frequenz_core-1.3.0 → frequenz_core-1.5.0}/src/frequenz/core/typing.py +52 -2
- frequenz_core-1.5.0/src/frequenz/core/warnings/__init__.py +71 -0
- frequenz_core-1.5.0/src/frequenz/core/warnings/_asserting.py +309 -0
- frequenz_core-1.5.0/src/frequenz/core/warnings/_deprecated_aliases.py +520 -0
- frequenz_core-1.5.0/src/frequenz/core/warnings/_ignoring.py +366 -0
- {frequenz_core-1.3.0 → frequenz_core-1.5.0/src/frequenz_core.egg-info}/PKG-INFO +131 -34
- {frequenz_core-1.3.0 → frequenz_core-1.5.0}/src/frequenz_core.egg-info/SOURCES.txt +6 -0
- frequenz_core-1.5.0/src/frequenz_core.egg-info/requires.txt +50 -0
- frequenz_core-1.5.0/src/frequenz_core.egg-info/scm_file_list.json +66 -0
- frequenz_core-1.5.0/src/frequenz_core.egg-info/scm_version.json +8 -0
- frequenz_core-1.3.0/RELEASE_NOTES.md +0 -9
- frequenz_core-1.3.0/src/frequenz_core.egg-info/requires.txt +0 -48
- {frequenz_core-1.3.0 → frequenz_core-1.5.0}/LICENSE +0 -0
- {frequenz_core-1.3.0 → frequenz_core-1.5.0}/MANIFEST.in +0 -0
- {frequenz_core-1.3.0 → frequenz_core-1.5.0}/setup.cfg +0 -0
- {frequenz_core-1.3.0 → frequenz_core-1.5.0}/src/frequenz/core/__init__.py +0 -0
- {frequenz_core-1.3.0 → frequenz_core-1.5.0}/src/frequenz/core/conftest.py +0 -0
- {frequenz_core-1.3.0 → frequenz_core-1.5.0}/src/frequenz/core/datetime.py +0 -0
- {frequenz_core-1.3.0 → frequenz_core-1.5.0}/src/frequenz/core/module.py +0 -0
- {frequenz_core-1.3.0 → frequenz_core-1.5.0}/src/frequenz/core/py.typed +0 -0
- {frequenz_core-1.3.0 → frequenz_core-1.5.0}/src/frequenz_core.egg-info/dependency_links.txt +0 -0
- {frequenz_core-1.3.0 → frequenz_core-1.5.0}/src/frequenz_core.egg-info/top_level.txt +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: frequenz-core
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: Core utilities to complement Python's standard library
|
|
5
5
|
Author-email: Frequenz Energy-as-a-Service GmbH <floss@frequenz.com>
|
|
6
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: Documentation, https://frequenz-floss.github.io/frequenz-core-python/
|
|
8
8
|
Project-URL: Changelog, https://github.com/frequenz-floss/frequenz-core-python/releases
|
|
9
9
|
Project-URL: Issues, https://github.com/frequenz-floss/frequenz-core-python/issues
|
|
@@ -12,7 +12,6 @@ Project-URL: Support, https://github.com/frequenz-floss/frequenz-core-python/dis
|
|
|
12
12
|
Keywords: asyncio,collections,core,datetime,frequenz,lib,library,math,python,stdlib,typing
|
|
13
13
|
Classifier: Development Status :: 5 - Production/Stable
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
16
15
|
Classifier: Programming Language :: Python :: 3
|
|
17
16
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
17
|
Classifier: Topic :: Software Development :: Libraries
|
|
@@ -22,42 +21,44 @@ Description-Content-Type: text/markdown
|
|
|
22
21
|
License-File: LICENSE
|
|
23
22
|
Requires-Dist: typing-extensions<5,>=4.13.0
|
|
24
23
|
Provides-Extra: dev-flake8
|
|
25
|
-
Requires-Dist: flake8==7.
|
|
24
|
+
Requires-Dist: flake8==7.4.1; extra == "dev-flake8"
|
|
25
|
+
Requires-Dist: flake8-datetimez==26.8.1; extra == "dev-flake8"
|
|
26
26
|
Requires-Dist: flake8-docstrings==1.7.0; extra == "dev-flake8"
|
|
27
|
-
Requires-Dist: flake8-pyproject==1.2.
|
|
28
|
-
Requires-Dist: pydoclint==0.
|
|
27
|
+
Requires-Dist: flake8-pyproject==1.2.4; extra == "dev-flake8"
|
|
28
|
+
Requires-Dist: pydoclint==0.10.0; extra == "dev-flake8"
|
|
29
29
|
Requires-Dist: pydocstyle==6.3.0; extra == "dev-flake8"
|
|
30
30
|
Provides-Extra: dev-formatting
|
|
31
|
-
Requires-Dist: black==
|
|
32
|
-
Requires-Dist: isort==
|
|
31
|
+
Requires-Dist: black==26.5.1; extra == "dev-formatting"
|
|
32
|
+
Requires-Dist: isort==9.0.1; extra == "dev-formatting"
|
|
33
33
|
Provides-Extra: dev-mkdocs
|
|
34
|
-
Requires-Dist: Markdown==3.
|
|
35
|
-
Requires-Dist: black==
|
|
36
|
-
Requires-Dist:
|
|
37
|
-
Requires-Dist:
|
|
38
|
-
Requires-Dist: mkdocs-
|
|
39
|
-
Requires-Dist: mkdocs-
|
|
40
|
-
Requires-Dist: mkdocs-
|
|
41
|
-
Requires-Dist:
|
|
42
|
-
Requires-Dist: mkdocstrings
|
|
43
|
-
Requires-Dist:
|
|
34
|
+
Requires-Dist: Markdown==3.11; extra == "dev-mkdocs"
|
|
35
|
+
Requires-Dist: black==26.5.1; extra == "dev-mkdocs"
|
|
36
|
+
Requires-Dist: griffe-warnings-deprecated==1.1.1; extra == "dev-mkdocs"
|
|
37
|
+
Requires-Dist: mike==2.2.0; extra == "dev-mkdocs"
|
|
38
|
+
Requires-Dist: mkdocs-gen-files==0.6.1; extra == "dev-mkdocs"
|
|
39
|
+
Requires-Dist: mkdocs-literate-nav==0.6.3; extra == "dev-mkdocs"
|
|
40
|
+
Requires-Dist: mkdocs-macros-plugin==1.5.0; extra == "dev-mkdocs"
|
|
41
|
+
Requires-Dist: mkdocs-material==9.7.7; extra == "dev-mkdocs"
|
|
42
|
+
Requires-Dist: mkdocstrings[python]==1.0.6; extra == "dev-mkdocs"
|
|
43
|
+
Requires-Dist: mkdocstrings-python==2.0.7; extra == "dev-mkdocs"
|
|
44
|
+
Requires-Dist: frequenz-repo-config[lib]==0.19.0; extra == "dev-mkdocs"
|
|
44
45
|
Provides-Extra: dev-mypy
|
|
45
|
-
Requires-Dist: mypy==
|
|
46
|
-
Requires-Dist: types-Markdown==3.
|
|
46
|
+
Requires-Dist: mypy==2.3.1; extra == "dev-mypy"
|
|
47
|
+
Requires-Dist: types-Markdown==3.10.2.20260712; extra == "dev-mypy"
|
|
47
48
|
Requires-Dist: frequenz-core[dev-mkdocs,dev-noxfile,dev-pytest]; extra == "dev-mypy"
|
|
48
49
|
Provides-Extra: dev-noxfile
|
|
49
|
-
Requires-Dist: nox==
|
|
50
|
-
Requires-Dist: frequenz-repo-config[lib]==0.
|
|
50
|
+
Requires-Dist: nox==2026.8.17; extra == "dev-noxfile"
|
|
51
|
+
Requires-Dist: frequenz-repo-config[lib]==0.19.0; extra == "dev-noxfile"
|
|
51
52
|
Provides-Extra: dev-pylint
|
|
52
53
|
Requires-Dist: frequenz-core[dev-mkdocs,dev-noxfile,dev-pytest]; extra == "dev-pylint"
|
|
53
54
|
Provides-Extra: dev-pytest
|
|
54
|
-
Requires-Dist: pytest==
|
|
55
|
-
Requires-Dist: pylint==
|
|
56
|
-
Requires-Dist: frequenz-repo-config[extra-lint-examples]==0.
|
|
57
|
-
Requires-Dist: pytest-mock==3.
|
|
58
|
-
Requires-Dist: pytest-asyncio==1.
|
|
59
|
-
Requires-Dist: async-solipsism==0.
|
|
60
|
-
Requires-Dist: hypothesis==6.
|
|
55
|
+
Requires-Dist: pytest==9.1.1; extra == "dev-pytest"
|
|
56
|
+
Requires-Dist: pylint==4.0.8; extra == "dev-pytest"
|
|
57
|
+
Requires-Dist: frequenz-repo-config[extra-lint-examples]==0.19.0; extra == "dev-pytest"
|
|
58
|
+
Requires-Dist: pytest-mock==3.16.0; extra == "dev-pytest"
|
|
59
|
+
Requires-Dist: pytest-asyncio==1.4.0; extra == "dev-pytest"
|
|
60
|
+
Requires-Dist: async-solipsism==0.9; extra == "dev-pytest"
|
|
61
|
+
Requires-Dist: hypothesis==6.168.2; extra == "dev-pytest"
|
|
61
62
|
Provides-Extra: dev
|
|
62
63
|
Requires-Dist: frequenz-core[dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]; extra == "dev"
|
|
63
64
|
Dynamic: license-file
|
|
@@ -72,8 +73,8 @@ Dynamic: license-file
|
|
|
72
73
|
|
|
73
74
|
Core utilities to complement Python's standard library. This library provides
|
|
74
75
|
essential building blocks for Python applications, including mathematical
|
|
75
|
-
utilities, datetime constants, typing helpers, strongly-typed identifiers,
|
|
76
|
-
module introspection tools.
|
|
76
|
+
utilities, datetime constants, typing helpers, strongly-typed identifiers,
|
|
77
|
+
module introspection tools, and warning and deprecation helpers.
|
|
77
78
|
|
|
78
79
|
The `frequenz-core` library is designed to be lightweight, type-safe, and
|
|
79
80
|
follow modern Python best practices. It fills common gaps in the standard
|
|
@@ -83,8 +84,8 @@ library with utilities that are frequently needed across different projects.
|
|
|
83
84
|
|
|
84
85
|
The following platforms are officially supported (tested):
|
|
85
86
|
|
|
86
|
-
- **Python:** 3.11
|
|
87
|
-
- **Operating System:** Ubuntu Linux
|
|
87
|
+
- **Python:** 3.11, 3.12, 3.13, 3.14
|
|
88
|
+
- **Operating System:** Ubuntu Linux 24.04
|
|
88
89
|
- **Architectures:** amd64, arm64
|
|
89
90
|
|
|
90
91
|
## Installation
|
|
@@ -156,7 +157,7 @@ positive = Interval(0, None) # [0, ∞]
|
|
|
156
157
|
assert 1000 in positive # True
|
|
157
158
|
```
|
|
158
159
|
|
|
159
|
-
### `Enum`
|
|
160
|
+
### `Enum` Utilities
|
|
160
161
|
|
|
161
162
|
Define enums with deprecated members that raise deprecation warnings when
|
|
162
163
|
accessed:
|
|
@@ -200,6 +201,102 @@ class ApiClient:
|
|
|
200
201
|
client = ApiClient.create("my-api-key") # ✅ Works
|
|
201
202
|
```
|
|
202
203
|
|
|
204
|
+
Annotate floating point values honestly, as Python's numeric tower lets `int`
|
|
205
|
+
values through any `float` annotation:
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
from typing import assert_never
|
|
209
|
+
|
|
210
|
+
from frequenz.core.typing import FloatInt
|
|
211
|
+
|
|
212
|
+
def describe(value: FloatInt | None) -> str:
|
|
213
|
+
match value:
|
|
214
|
+
case float() | int():
|
|
215
|
+
return f"number {value}"
|
|
216
|
+
case None:
|
|
217
|
+
return "nothing"
|
|
218
|
+
case unexpected:
|
|
219
|
+
assert_never(unexpected)
|
|
220
|
+
|
|
221
|
+
assert describe(1) == "number 1" # ✅ `case float():` alone would crash here
|
|
222
|
+
assert describe(1.5) == "number 1.5"
|
|
223
|
+
assert describe(None) == "nothing"
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Warnings/Deprecations Utilities
|
|
227
|
+
|
|
228
|
+
Silence a warning around a call without the side effect
|
|
229
|
+
[`warnings.catch_warnings`](https://docs.python.org/3/library/warnings.html#warnings.catch_warnings)
|
|
230
|
+
has, which is to reset the deduplication history of the whole program, so
|
|
231
|
+
every warning already shown is shown again
|
|
232
|
+
([python/cpython#73858](https://github.com/python/cpython/issues/73858)):
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
import warnings
|
|
236
|
+
|
|
237
|
+
from frequenz.core.warnings import ignoring_deprecations
|
|
238
|
+
|
|
239
|
+
def legacy_parse(raw: str) -> int:
|
|
240
|
+
warnings.warn("legacy_parse() is deprecated", DeprecationWarning, stacklevel=2)
|
|
241
|
+
return int(raw)
|
|
242
|
+
|
|
243
|
+
def parse(raw: str) -> int:
|
|
244
|
+
# Only around the call that reaches the deprecated symbol.
|
|
245
|
+
with ignoring_deprecations():
|
|
246
|
+
return legacy_parse(raw)
|
|
247
|
+
|
|
248
|
+
with warnings.catch_warnings(record=True, action="default") as shown:
|
|
249
|
+
for _ in range(10):
|
|
250
|
+
warnings.warn("said once", UserWarning)
|
|
251
|
+
parse("1")
|
|
252
|
+
|
|
253
|
+
assert len(shown) == 1 # ✅ `catch_warnings()` in `parse()` would show it 10 times
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Keep the old import path of a symbol that moved working, serving the very same
|
|
257
|
+
object so `isinstance` keeps working through both paths:
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
from typing import TYPE_CHECKING, TypeAlias
|
|
261
|
+
|
|
262
|
+
from frequenz.core.warnings import DeprecatedAlias, deprecated_aliases
|
|
263
|
+
|
|
264
|
+
if TYPE_CHECKING:
|
|
265
|
+
# Type checkers can't see the runtime `__getattr__` in the `else` branch.
|
|
266
|
+
from decimal import Decimal as _Decimal
|
|
267
|
+
from fractions import Fraction as _Fraction
|
|
268
|
+
|
|
269
|
+
Decimal: TypeAlias = _Decimal
|
|
270
|
+
Rational: TypeAlias = _Fraction
|
|
271
|
+
else:
|
|
272
|
+
# In the `else`, never at module level: a `__getattr__` mypy can see makes
|
|
273
|
+
# every name it doesn't know in this module `Any`, typos in downstream
|
|
274
|
+
# imports included.
|
|
275
|
+
__getattr__ = deprecated_aliases(
|
|
276
|
+
__name__,
|
|
277
|
+
# Warns "<module>.Decimal is deprecated since v1.2.0. Use
|
|
278
|
+
# decimal.Decimal instead."
|
|
279
|
+
DeprecatedAlias("Decimal", new_module="decimal", since="v1.2.0"),
|
|
280
|
+
# Renamed on the way out
|
|
281
|
+
DeprecatedAlias(
|
|
282
|
+
"Rational", new_module="fractions", new_name="Fraction", since="v1.3.0"
|
|
283
|
+
),
|
|
284
|
+
)
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
And check in a test that a piece of code doesn't warn, without the `"error"`
|
|
288
|
+
filter that would make `warnings.warn()` raise inside the code under test:
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
from frequenz.core.warnings import asserting_no_deprecations
|
|
292
|
+
|
|
293
|
+
def parse(raw: str) -> int:
|
|
294
|
+
return int(raw)
|
|
295
|
+
|
|
296
|
+
with asserting_no_deprecations():
|
|
297
|
+
assert parse("1") == 1
|
|
298
|
+
```
|
|
299
|
+
|
|
203
300
|
### Strongly-Typed IDs
|
|
204
301
|
|
|
205
302
|
Create type-safe identifiers for different entities:
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
Core utilities to complement Python's standard library. This library provides
|
|
10
10
|
essential building blocks for Python applications, including mathematical
|
|
11
|
-
utilities, datetime constants, typing helpers, strongly-typed identifiers,
|
|
12
|
-
module introspection tools.
|
|
11
|
+
utilities, datetime constants, typing helpers, strongly-typed identifiers,
|
|
12
|
+
module introspection tools, and warning and deprecation helpers.
|
|
13
13
|
|
|
14
14
|
The `frequenz-core` library is designed to be lightweight, type-safe, and
|
|
15
15
|
follow modern Python best practices. It fills common gaps in the standard
|
|
@@ -19,8 +19,8 @@ library with utilities that are frequently needed across different projects.
|
|
|
19
19
|
|
|
20
20
|
The following platforms are officially supported (tested):
|
|
21
21
|
|
|
22
|
-
- **Python:** 3.11
|
|
23
|
-
- **Operating System:** Ubuntu Linux
|
|
22
|
+
- **Python:** 3.11, 3.12, 3.13, 3.14
|
|
23
|
+
- **Operating System:** Ubuntu Linux 24.04
|
|
24
24
|
- **Architectures:** amd64, arm64
|
|
25
25
|
|
|
26
26
|
## Installation
|
|
@@ -92,7 +92,7 @@ positive = Interval(0, None) # [0, ∞]
|
|
|
92
92
|
assert 1000 in positive # True
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
-
### `Enum`
|
|
95
|
+
### `Enum` Utilities
|
|
96
96
|
|
|
97
97
|
Define enums with deprecated members that raise deprecation warnings when
|
|
98
98
|
accessed:
|
|
@@ -136,6 +136,102 @@ class ApiClient:
|
|
|
136
136
|
client = ApiClient.create("my-api-key") # ✅ Works
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
+
Annotate floating point values honestly, as Python's numeric tower lets `int`
|
|
140
|
+
values through any `float` annotation:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from typing import assert_never
|
|
144
|
+
|
|
145
|
+
from frequenz.core.typing import FloatInt
|
|
146
|
+
|
|
147
|
+
def describe(value: FloatInt | None) -> str:
|
|
148
|
+
match value:
|
|
149
|
+
case float() | int():
|
|
150
|
+
return f"number {value}"
|
|
151
|
+
case None:
|
|
152
|
+
return "nothing"
|
|
153
|
+
case unexpected:
|
|
154
|
+
assert_never(unexpected)
|
|
155
|
+
|
|
156
|
+
assert describe(1) == "number 1" # ✅ `case float():` alone would crash here
|
|
157
|
+
assert describe(1.5) == "number 1.5"
|
|
158
|
+
assert describe(None) == "nothing"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Warnings/Deprecations Utilities
|
|
162
|
+
|
|
163
|
+
Silence a warning around a call without the side effect
|
|
164
|
+
[`warnings.catch_warnings`](https://docs.python.org/3/library/warnings.html#warnings.catch_warnings)
|
|
165
|
+
has, which is to reset the deduplication history of the whole program, so
|
|
166
|
+
every warning already shown is shown again
|
|
167
|
+
([python/cpython#73858](https://github.com/python/cpython/issues/73858)):
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
import warnings
|
|
171
|
+
|
|
172
|
+
from frequenz.core.warnings import ignoring_deprecations
|
|
173
|
+
|
|
174
|
+
def legacy_parse(raw: str) -> int:
|
|
175
|
+
warnings.warn("legacy_parse() is deprecated", DeprecationWarning, stacklevel=2)
|
|
176
|
+
return int(raw)
|
|
177
|
+
|
|
178
|
+
def parse(raw: str) -> int:
|
|
179
|
+
# Only around the call that reaches the deprecated symbol.
|
|
180
|
+
with ignoring_deprecations():
|
|
181
|
+
return legacy_parse(raw)
|
|
182
|
+
|
|
183
|
+
with warnings.catch_warnings(record=True, action="default") as shown:
|
|
184
|
+
for _ in range(10):
|
|
185
|
+
warnings.warn("said once", UserWarning)
|
|
186
|
+
parse("1")
|
|
187
|
+
|
|
188
|
+
assert len(shown) == 1 # ✅ `catch_warnings()` in `parse()` would show it 10 times
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Keep the old import path of a symbol that moved working, serving the very same
|
|
192
|
+
object so `isinstance` keeps working through both paths:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from typing import TYPE_CHECKING, TypeAlias
|
|
196
|
+
|
|
197
|
+
from frequenz.core.warnings import DeprecatedAlias, deprecated_aliases
|
|
198
|
+
|
|
199
|
+
if TYPE_CHECKING:
|
|
200
|
+
# Type checkers can't see the runtime `__getattr__` in the `else` branch.
|
|
201
|
+
from decimal import Decimal as _Decimal
|
|
202
|
+
from fractions import Fraction as _Fraction
|
|
203
|
+
|
|
204
|
+
Decimal: TypeAlias = _Decimal
|
|
205
|
+
Rational: TypeAlias = _Fraction
|
|
206
|
+
else:
|
|
207
|
+
# In the `else`, never at module level: a `__getattr__` mypy can see makes
|
|
208
|
+
# every name it doesn't know in this module `Any`, typos in downstream
|
|
209
|
+
# imports included.
|
|
210
|
+
__getattr__ = deprecated_aliases(
|
|
211
|
+
__name__,
|
|
212
|
+
# Warns "<module>.Decimal is deprecated since v1.2.0. Use
|
|
213
|
+
# decimal.Decimal instead."
|
|
214
|
+
DeprecatedAlias("Decimal", new_module="decimal", since="v1.2.0"),
|
|
215
|
+
# Renamed on the way out
|
|
216
|
+
DeprecatedAlias(
|
|
217
|
+
"Rational", new_module="fractions", new_name="Fraction", since="v1.3.0"
|
|
218
|
+
),
|
|
219
|
+
)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
And check in a test that a piece of code doesn't warn, without the `"error"`
|
|
223
|
+
filter that would make `warnings.warn()` raise inside the code under test:
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
from frequenz.core.warnings import asserting_no_deprecations
|
|
227
|
+
|
|
228
|
+
def parse(raw: str) -> int:
|
|
229
|
+
return int(raw)
|
|
230
|
+
|
|
231
|
+
with asserting_no_deprecations():
|
|
232
|
+
assert parse("1") == 1
|
|
233
|
+
```
|
|
234
|
+
|
|
139
235
|
### Strongly-Typed IDs
|
|
140
236
|
|
|
141
237
|
Create type-safe identifiers for different entities:
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Frequenz Core Library Release Notes
|
|
2
|
+
|
|
3
|
+
## New Features
|
|
4
|
+
|
|
5
|
+
A new `frequenz.core.warnings` module with:
|
|
6
|
+
|
|
7
|
+
- `ignoring_warnings()` and `ignoring_deprecations()` to silence warnings/deprecations around a piece of code. Unlike `warnings.catch_warnings()`, entering and leaving it doesn't reset the warnings deduplication history of the program, so warnings already shown are not shown again, working around [python/cpython#73858](https://github.com/python/cpython/issues/73858). It costs about the same as the standard library block, so it is also usable on a hot path, where repairing the damage afterwards would not be.
|
|
8
|
+
|
|
9
|
+
- `asserting_no_warnings()`, and its `asserting_no_deprecations()` shortcut, fail when the code in the block raises a matching warning/deprecation, listing each one with the place it came from. They are meant for tests, and are a better tool than an `"error"` filter, which makes `warnings.warn()` raise inside the code under test and so changes the very behaviour the test is checking.
|
|
10
|
+
|
|
11
|
+
- `deprecated_aliases()` builds a module `__getattr__` that warns when a symbol that moved to another module, or was renamed, is reached through its old import path, serving the very same object so `isinstance` keeps working through both paths.
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
[build-system]
|
|
5
5
|
requires = [
|
|
6
|
-
"setuptools ==
|
|
7
|
-
"setuptools_scm[toml] ==
|
|
8
|
-
"frequenz-repo-config[lib] == 0.
|
|
6
|
+
"setuptools == 84.0.0",
|
|
7
|
+
"setuptools_scm[toml] == 10.3.4",
|
|
8
|
+
"frequenz-repo-config[lib] == 0.19.0",
|
|
9
9
|
]
|
|
10
10
|
build-backend = "setuptools.build_meta"
|
|
11
11
|
|
|
@@ -13,7 +13,8 @@ build-backend = "setuptools.build_meta"
|
|
|
13
13
|
name = "frequenz-core"
|
|
14
14
|
description = "Core utilities to complement Python's standard library"
|
|
15
15
|
readme = "README.md"
|
|
16
|
-
license =
|
|
16
|
+
license = "MIT"
|
|
17
|
+
license-files = ["LICENSE"]
|
|
17
18
|
keywords = [
|
|
18
19
|
"asyncio",
|
|
19
20
|
"collections",
|
|
@@ -30,7 +31,6 @@ keywords = [
|
|
|
30
31
|
classifiers = [
|
|
31
32
|
"Development Status :: 5 - Production/Stable",
|
|
32
33
|
"Intended Audience :: Developers",
|
|
33
|
-
"License :: OSI Approved :: MIT License",
|
|
34
34
|
"Programming Language :: Python :: 3",
|
|
35
35
|
"Programming Language :: Python :: 3 :: Only",
|
|
36
36
|
"Topic :: Software Development :: Libraries",
|
|
@@ -46,45 +46,47 @@ email = "floss@frequenz.com"
|
|
|
46
46
|
|
|
47
47
|
[project.optional-dependencies]
|
|
48
48
|
dev-flake8 = [
|
|
49
|
-
"flake8 == 7.
|
|
49
|
+
"flake8 == 7.4.1",
|
|
50
|
+
"flake8-datetimez == 26.8.1",
|
|
50
51
|
"flake8-docstrings == 1.7.0",
|
|
51
|
-
"flake8-pyproject == 1.2.
|
|
52
|
-
"pydoclint == 0.
|
|
52
|
+
"flake8-pyproject == 1.2.4", # For reading the flake8 config from pyproject.toml
|
|
53
|
+
"pydoclint == 0.10.0",
|
|
53
54
|
"pydocstyle == 6.3.0",
|
|
54
55
|
]
|
|
55
|
-
dev-formatting = ["black ==
|
|
56
|
+
dev-formatting = ["black == 26.5.1", "isort == 9.0.1"]
|
|
56
57
|
dev-mkdocs = [
|
|
57
|
-
"Markdown == 3.
|
|
58
|
-
"black ==
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"mkdocs-
|
|
62
|
-
"mkdocs-
|
|
63
|
-
"mkdocs-
|
|
64
|
-
"
|
|
65
|
-
"mkdocstrings
|
|
66
|
-
"
|
|
58
|
+
"Markdown == 3.11",
|
|
59
|
+
"black == 26.5.1",
|
|
60
|
+
"griffe-warnings-deprecated == 1.1.1",
|
|
61
|
+
"mike == 2.2.0",
|
|
62
|
+
"mkdocs-gen-files == 0.6.1",
|
|
63
|
+
"mkdocs-literate-nav == 0.6.3",
|
|
64
|
+
"mkdocs-macros-plugin == 1.5.0",
|
|
65
|
+
"mkdocs-material == 9.7.7",
|
|
66
|
+
"mkdocstrings[python] == 1.0.6",
|
|
67
|
+
"mkdocstrings-python == 2.0.7",
|
|
68
|
+
"frequenz-repo-config[lib] == 0.19.0",
|
|
67
69
|
]
|
|
68
70
|
dev-mypy = [
|
|
69
|
-
"mypy ==
|
|
70
|
-
"types-Markdown == 3.
|
|
71
|
+
"mypy == 2.3.1",
|
|
72
|
+
"types-Markdown == 3.10.2.20260712",
|
|
71
73
|
# For checking the noxfile, docs/ script, and tests
|
|
72
74
|
"frequenz-core[dev-mkdocs,dev-noxfile,dev-pytest]",
|
|
73
75
|
]
|
|
74
|
-
dev-noxfile = ["nox ==
|
|
76
|
+
dev-noxfile = ["nox == 2026.8.17", "frequenz-repo-config[lib] == 0.19.0"]
|
|
75
77
|
dev-pylint = [
|
|
76
78
|
# dev-pytest already defines a dependency to pylint because of the examples
|
|
77
79
|
# For checking the noxfile, docs/ script, and tests
|
|
78
80
|
"frequenz-core[dev-mkdocs,dev-noxfile,dev-pytest]",
|
|
79
81
|
]
|
|
80
82
|
dev-pytest = [
|
|
81
|
-
"pytest ==
|
|
82
|
-
"pylint ==
|
|
83
|
-
"frequenz-repo-config[extra-lint-examples] == 0.
|
|
84
|
-
"pytest-mock == 3.
|
|
85
|
-
"pytest-asyncio == 1.
|
|
86
|
-
"async-solipsism == 0.
|
|
87
|
-
"hypothesis == 6.
|
|
83
|
+
"pytest == 9.1.1",
|
|
84
|
+
"pylint == 4.0.8", # We need this to check for the examples
|
|
85
|
+
"frequenz-repo-config[extra-lint-examples] == 0.19.0",
|
|
86
|
+
"pytest-mock == 3.16.0",
|
|
87
|
+
"pytest-asyncio == 1.4.0",
|
|
88
|
+
"async-solipsism == 0.9",
|
|
89
|
+
"hypothesis == 6.168.2",
|
|
88
90
|
]
|
|
89
91
|
dev = [
|
|
90
92
|
"frequenz-core[dev-mkdocs,dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]",
|
|
@@ -123,6 +125,10 @@ check-yield-types = false
|
|
|
123
125
|
arg-type-hints-in-docstring = false
|
|
124
126
|
arg-type-hints-in-signature = true
|
|
125
127
|
allow-init-docstring = true
|
|
128
|
+
check-class-attributes = true
|
|
129
|
+
check-style-mismatch = true
|
|
130
|
+
require-inline-class-var-docs = true
|
|
131
|
+
skip-checking-short-docstrings = true
|
|
126
132
|
|
|
127
133
|
[tool.pylint.similarities]
|
|
128
134
|
ignore-comments = ['yes']
|
|
@@ -155,13 +161,25 @@ disable = [
|
|
|
155
161
|
]
|
|
156
162
|
|
|
157
163
|
[tool.pytest.ini_options]
|
|
158
|
-
|
|
164
|
+
filterwarnings = [
|
|
165
|
+
"error",
|
|
166
|
+
"once::DeprecationWarning",
|
|
167
|
+
"once::PendingDeprecationWarning",
|
|
168
|
+
# We ignore warnings about protobuf gencode version being one version older
|
|
169
|
+
# than the current version, as this is supported by protobuf, and we expect to
|
|
170
|
+
# have such cases. If we go too far, we will get a proper error anyways.
|
|
171
|
+
# We use a raw string (single quotes) to avoid the need to escape special
|
|
172
|
+
# characters as this is a regex.
|
|
173
|
+
'ignore:Protobuf gencode version .*exactly one major version older.*:UserWarning',
|
|
174
|
+
]
|
|
159
175
|
testpaths = ["tests", "src"]
|
|
176
|
+
asyncio_debug = true
|
|
160
177
|
asyncio_mode = "auto"
|
|
161
178
|
asyncio_default_fixture_loop_scope = "function"
|
|
162
179
|
required_plugins = ["pytest-asyncio", "pytest-mock"]
|
|
163
180
|
|
|
164
181
|
[tool.mypy]
|
|
182
|
+
enable_error_code = ["exhaustive-match"]
|
|
165
183
|
explicit_package_bases = true
|
|
166
184
|
namespace_packages = true
|
|
167
185
|
# This option disables mypy cache, and it is sometimes useful to enable it if
|
|
@@ -76,11 +76,13 @@ class DeprecatingEnumType(enum.EnumType):
|
|
|
76
76
|
|
|
77
77
|
Tip:
|
|
78
78
|
Normally it is not necessary to use this class directly, use
|
|
79
|
-
[`Enum`][
|
|
79
|
+
[`Enum`][..Enum] instead.
|
|
80
80
|
|
|
81
81
|
Behavior:
|
|
82
82
|
|
|
83
83
|
- In the class body, members may be declared as `NAME = DeprecatedMember(value, msg)`.
|
|
84
|
+
See [`DeprecatedMember`][..DeprecatedMember] and
|
|
85
|
+
[`deprecated_member()`][..deprecated_member] for details.
|
|
84
86
|
- During class creation, these wrappers are replaced with `value` so that
|
|
85
87
|
a normal enum member or alias is created by [`EnumType`][enum.EnumType].
|
|
86
88
|
- The deprecated names are recorded so that:
|
|
@@ -98,7 +100,7 @@ class DeprecatingEnumType(enum.EnumType):
|
|
|
98
100
|
classdict: Mapping[str, Any],
|
|
99
101
|
**kw: Any,
|
|
100
102
|
) -> type[EnumT]:
|
|
101
|
-
"""Create the new enum class
|
|
103
|
+
"""Create the new enum class rewriting [`DeprecatedMember`][...DeprecatedMember] members."""
|
|
102
104
|
deprecated_names: dict[str, str] = {}
|
|
103
105
|
prepared = super().__prepare__(name, bases, **kw)
|
|
104
106
|
|
|
@@ -20,7 +20,7 @@ the ID and must be unique across all ID types.
|
|
|
20
20
|
|
|
21
21
|
Note:
|
|
22
22
|
The `str_prefix` must be unique across all ID types. If you try to use a
|
|
23
|
-
prefix that is already registered, a
|
|
23
|
+
prefix that is already registered, a warning will be logged when defining
|
|
24
24
|
the class.
|
|
25
25
|
|
|
26
26
|
To encourage consistency, the class name must end with the suffix "Id" (e.g.,
|
|
@@ -61,7 +61,6 @@ Example: Creating an ID type with a non-standard name
|
|
|
61
61
|
```
|
|
62
62
|
'''
|
|
63
63
|
|
|
64
|
-
|
|
65
64
|
import logging
|
|
66
65
|
from typing import Any, ClassVar, Self, cast
|
|
67
66
|
|
|
@@ -157,6 +156,13 @@ class BaseId:
|
|
|
157
156
|
|
|
158
157
|
Equality is defined as being of the exact same type and having the same
|
|
159
158
|
underlying ID.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
other: The object to compare against.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
True if `other` is of the same type and has the same underlying ID,
|
|
165
|
+
`NotImplemented` if `other` is of a different type.
|
|
160
166
|
"""
|
|
161
167
|
# pylint thinks this is not an unidiomatic typecheck, but in this case
|
|
162
168
|
# it is not. isinstance() returns True for subclasses, which is not
|
|
@@ -173,6 +179,13 @@ class BaseId:
|
|
|
173
179
|
"""Check if this instance is less than another object.
|
|
174
180
|
|
|
175
181
|
Comparison is only defined between instances of the exact same type.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
other: The object to compare against.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
True if this instance is less than `other`, `NotImplemented` if
|
|
188
|
+
`other` is of a different type.
|
|
176
189
|
"""
|
|
177
190
|
# pylint: disable-next=unidiomatic-typecheck
|
|
178
191
|
if type(other) is not type(self):
|
|
@@ -185,6 +198,9 @@ class BaseId:
|
|
|
185
198
|
|
|
186
199
|
The hash is based on the exact type and the underlying ID to ensure
|
|
187
200
|
that IDs of different types but with the same numeric value have different hashes.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
The hash of this instance.
|
|
188
204
|
"""
|
|
189
205
|
return hash((type(self), self._id))
|
|
190
206
|
|