frequenz-core 1.2.0__tar.gz → 1.4.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.2.0/src/frequenz_core.egg-info → frequenz_core-1.4.0}/PKG-INFO +55 -31
- {frequenz_core-1.2.0 → frequenz_core-1.4.0}/README.md +27 -3
- frequenz_core-1.4.0/RELEASE_NOTES.md +23 -0
- {frequenz_core-1.2.0 → frequenz_core-1.4.0}/pyproject.toml +33 -28
- {frequenz_core-1.2.0 → frequenz_core-1.4.0}/src/frequenz/core/enum.py +36 -11
- {frequenz_core-1.2.0 → frequenz_core-1.4.0}/src/frequenz/core/id.py +18 -2
- {frequenz_core-1.2.0 → frequenz_core-1.4.0}/src/frequenz/core/math.py +34 -40
- {frequenz_core-1.2.0 → frequenz_core-1.4.0}/src/frequenz/core/typing.py +52 -2
- {frequenz_core-1.2.0 → frequenz_core-1.4.0/src/frequenz_core.egg-info}/PKG-INFO +55 -31
- {frequenz_core-1.2.0 → frequenz_core-1.4.0}/src/frequenz_core.egg-info/SOURCES.txt +2 -0
- frequenz_core-1.4.0/src/frequenz_core.egg-info/requires.txt +49 -0
- frequenz_core-1.4.0/src/frequenz_core.egg-info/scm_file_list.json +55 -0
- frequenz_core-1.4.0/src/frequenz_core.egg-info/scm_version.json +8 -0
- frequenz_core-1.2.0/RELEASE_NOTES.md +0 -41
- frequenz_core-1.2.0/src/frequenz_core.egg-info/requires.txt +0 -48
- {frequenz_core-1.2.0 → frequenz_core-1.4.0}/LICENSE +0 -0
- {frequenz_core-1.2.0 → frequenz_core-1.4.0}/MANIFEST.in +0 -0
- {frequenz_core-1.2.0 → frequenz_core-1.4.0}/setup.cfg +0 -0
- {frequenz_core-1.2.0 → frequenz_core-1.4.0}/src/frequenz/core/__init__.py +0 -0
- {frequenz_core-1.2.0 → frequenz_core-1.4.0}/src/frequenz/core/conftest.py +0 -0
- {frequenz_core-1.2.0 → frequenz_core-1.4.0}/src/frequenz/core/datetime.py +0 -0
- {frequenz_core-1.2.0 → frequenz_core-1.4.0}/src/frequenz/core/module.py +0 -0
- {frequenz_core-1.2.0 → frequenz_core-1.4.0}/src/frequenz/core/py.typed +0 -0
- {frequenz_core-1.2.0 → frequenz_core-1.4.0}/src/frequenz_core.egg-info/dependency_links.txt +0 -0
- {frequenz_core-1.2.0 → frequenz_core-1.4.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.4.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
|
|
@@ -23,41 +22,42 @@ License-File: LICENSE
|
|
|
23
22
|
Requires-Dist: typing-extensions<5,>=4.13.0
|
|
24
23
|
Provides-Extra: dev-flake8
|
|
25
24
|
Requires-Dist: flake8==7.3.0; extra == "dev-flake8"
|
|
25
|
+
Requires-Dist: flake8-datetimez==20.10.0; 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.9.1; 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==8.0.1; extra == "dev-formatting"
|
|
33
33
|
Provides-Extra: dev-mkdocs
|
|
34
|
-
Requires-Dist: Markdown==3.
|
|
35
|
-
Requires-Dist: black==
|
|
36
|
-
Requires-Dist: mike==2.
|
|
37
|
-
Requires-Dist: mkdocs-gen-files==0.
|
|
38
|
-
Requires-Dist: mkdocs-literate-nav==0.6.
|
|
39
|
-
Requires-Dist: mkdocs-macros-plugin==1.
|
|
40
|
-
Requires-Dist: mkdocs-material==9.
|
|
41
|
-
Requires-Dist: mkdocstrings[python]==0.
|
|
42
|
-
Requires-Dist: mkdocstrings-python==
|
|
43
|
-
Requires-Dist: frequenz-repo-config[lib]==0.
|
|
34
|
+
Requires-Dist: Markdown==3.10.2; extra == "dev-mkdocs"
|
|
35
|
+
Requires-Dist: black==26.5.1; extra == "dev-mkdocs"
|
|
36
|
+
Requires-Dist: mike==2.2.0; extra == "dev-mkdocs"
|
|
37
|
+
Requires-Dist: mkdocs-gen-files==0.6.1; extra == "dev-mkdocs"
|
|
38
|
+
Requires-Dist: mkdocs-literate-nav==0.6.3; extra == "dev-mkdocs"
|
|
39
|
+
Requires-Dist: mkdocs-macros-plugin==1.5.0; extra == "dev-mkdocs"
|
|
40
|
+
Requires-Dist: mkdocs-material==9.7.7; extra == "dev-mkdocs"
|
|
41
|
+
Requires-Dist: mkdocstrings[python]==1.0.6; extra == "dev-mkdocs"
|
|
42
|
+
Requires-Dist: mkdocstrings-python==2.0.5; extra == "dev-mkdocs"
|
|
43
|
+
Requires-Dist: frequenz-repo-config[lib]==0.18.0; extra == "dev-mkdocs"
|
|
44
44
|
Provides-Extra: dev-mypy
|
|
45
|
-
Requires-Dist: mypy==
|
|
46
|
-
Requires-Dist: types-Markdown==3.
|
|
45
|
+
Requires-Dist: mypy==2.3.0; extra == "dev-mypy"
|
|
46
|
+
Requires-Dist: types-Markdown==3.10.2.20260712; extra == "dev-mypy"
|
|
47
47
|
Requires-Dist: frequenz-core[dev-mkdocs,dev-noxfile,dev-pytest]; extra == "dev-mypy"
|
|
48
48
|
Provides-Extra: dev-noxfile
|
|
49
|
-
Requires-Dist: nox==
|
|
50
|
-
Requires-Dist: frequenz-repo-config[lib]==0.
|
|
49
|
+
Requires-Dist: nox==2026.7.11; extra == "dev-noxfile"
|
|
50
|
+
Requires-Dist: frequenz-repo-config[lib]==0.18.0; extra == "dev-noxfile"
|
|
51
51
|
Provides-Extra: dev-pylint
|
|
52
52
|
Requires-Dist: frequenz-core[dev-mkdocs,dev-noxfile,dev-pytest]; extra == "dev-pylint"
|
|
53
53
|
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.
|
|
54
|
+
Requires-Dist: pytest==9.1.1; extra == "dev-pytest"
|
|
55
|
+
Requires-Dist: pylint==4.0.6; extra == "dev-pytest"
|
|
56
|
+
Requires-Dist: frequenz-repo-config[extra-lint-examples]==0.18.0; extra == "dev-pytest"
|
|
57
|
+
Requires-Dist: pytest-mock==3.15.1; extra == "dev-pytest"
|
|
58
|
+
Requires-Dist: pytest-asyncio==1.4.0; extra == "dev-pytest"
|
|
59
|
+
Requires-Dist: async-solipsism==0.9; extra == "dev-pytest"
|
|
60
|
+
Requires-Dist: hypothesis==6.163.0; extra == "dev-pytest"
|
|
61
61
|
Provides-Extra: dev
|
|
62
62
|
Requires-Dist: frequenz-core[dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]; extra == "dev"
|
|
63
63
|
Dynamic: license-file
|
|
@@ -162,13 +162,15 @@ Define enums with deprecated members that raise deprecation warnings when
|
|
|
162
162
|
accessed:
|
|
163
163
|
|
|
164
164
|
```python
|
|
165
|
-
from frequenz.core.enum import Enum,
|
|
165
|
+
from frequenz.core.enum import Enum, deprecated_member, unique
|
|
166
166
|
|
|
167
|
+
@unique
|
|
167
168
|
class TaskStatus(Enum):
|
|
168
169
|
OPEN = 1
|
|
169
170
|
IN_PROGRESS = 2
|
|
170
|
-
|
|
171
|
-
|
|
171
|
+
# Duplicate values are fine with `@unique` as long as they are deprecated
|
|
172
|
+
PENDING = deprecated_member(1, "PENDING is deprecated, use OPEN instead")
|
|
173
|
+
DONE = deprecated_member(3, "DONE is deprecated, use FINISHED instead")
|
|
172
174
|
FINISHED = 4
|
|
173
175
|
|
|
174
176
|
status1 = TaskStatus.PENDING # Warns: "PENDING is deprecated, use OPEN instead"
|
|
@@ -198,6 +200,28 @@ class ApiClient:
|
|
|
198
200
|
client = ApiClient.create("my-api-key") # ✅ Works
|
|
199
201
|
```
|
|
200
202
|
|
|
203
|
+
Annotate floating point values honestly, as Python's numeric tower lets `int`
|
|
204
|
+
values through any `float` annotation:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
from typing import assert_never
|
|
208
|
+
|
|
209
|
+
from frequenz.core.typing import FloatInt
|
|
210
|
+
|
|
211
|
+
def describe(value: FloatInt | None) -> str:
|
|
212
|
+
match value:
|
|
213
|
+
case float() | int():
|
|
214
|
+
return f"number {value}"
|
|
215
|
+
case None:
|
|
216
|
+
return "nothing"
|
|
217
|
+
case unexpected:
|
|
218
|
+
assert_never(unexpected)
|
|
219
|
+
|
|
220
|
+
assert describe(1) == "number 1" # ✅ `case float():` alone would crash here
|
|
221
|
+
assert describe(1.5) == "number 1.5"
|
|
222
|
+
assert describe(None) == "nothing"
|
|
223
|
+
```
|
|
224
|
+
|
|
201
225
|
### Strongly-Typed IDs
|
|
202
226
|
|
|
203
227
|
Create type-safe identifiers for different entities:
|
|
@@ -98,13 +98,15 @@ Define enums with deprecated members that raise deprecation warnings when
|
|
|
98
98
|
accessed:
|
|
99
99
|
|
|
100
100
|
```python
|
|
101
|
-
from frequenz.core.enum import Enum,
|
|
101
|
+
from frequenz.core.enum import Enum, deprecated_member, unique
|
|
102
102
|
|
|
103
|
+
@unique
|
|
103
104
|
class TaskStatus(Enum):
|
|
104
105
|
OPEN = 1
|
|
105
106
|
IN_PROGRESS = 2
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
# Duplicate values are fine with `@unique` as long as they are deprecated
|
|
108
|
+
PENDING = deprecated_member(1, "PENDING is deprecated, use OPEN instead")
|
|
109
|
+
DONE = deprecated_member(3, "DONE is deprecated, use FINISHED instead")
|
|
108
110
|
FINISHED = 4
|
|
109
111
|
|
|
110
112
|
status1 = TaskStatus.PENDING # Warns: "PENDING is deprecated, use OPEN instead"
|
|
@@ -134,6 +136,28 @@ class ApiClient:
|
|
|
134
136
|
client = ApiClient.create("my-api-key") # ✅ Works
|
|
135
137
|
```
|
|
136
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
|
+
|
|
137
161
|
### Strongly-Typed IDs
|
|
138
162
|
|
|
139
163
|
Create type-safe identifiers for different entities:
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Frequenz Core Library Release Notes
|
|
2
|
+
|
|
3
|
+
## Upgrading
|
|
4
|
+
|
|
5
|
+
- [`Interval`][frequenz.core.math.Interval]'s type parameter no longer includes `None`. The exported type variable `LessThanComparableOrNoneT` (bound to `LessThanComparable | None`) was deprecated and replaced with `LessThanComparableT` (bound to `LessThanComparable`). `None` is still accepted as a value for `start` / `end` to indicate an unbounded side, but it is treated purely as bound metadata rather than a value in the interval's comparable space.
|
|
6
|
+
|
|
7
|
+
Migration:
|
|
8
|
+
|
|
9
|
+
- `Interval[int | None]` → `Interval[int]`
|
|
10
|
+
- `Interval[LessThanComparable | None]` → `Interval[LessThanComparable]`
|
|
11
|
+
- `LessThanComparableOrNoneT` (importable name) → `LessThanComparableT`
|
|
12
|
+
|
|
13
|
+
Note that explicitly using `Interval[int | None]` still works, as `(int | None) | None` is equivalent to `int | None`, even when it is no longer needed nor recommended. Old code will mostly work, but there is one soft breaking change: `None in some_interval` and `None in some_interval_set` is now a type-check error at call sites, matching the design intent that `None` is a bound marker and never a member value.
|
|
14
|
+
|
|
15
|
+
## New Features
|
|
16
|
+
|
|
17
|
+
- Added [`FloatInt`][frequenz.core.typing.FloatInt], a type alias for `float | int`.
|
|
18
|
+
|
|
19
|
+
[PEP 484](https://peps.python.org/pep-0484/)'s [numeric tower](https://peps.python.org/pep-0484/#the-numeric-tower) makes `int` assignable wherever `float` is annotated, while at runtime `isinstance(1, float)` is `False`. A plain `float` annotation therefore silently admits values that break `match … case float():` arms and `float`-only methods like `hex()`.
|
|
20
|
+
|
|
21
|
+
Annotate with `FloatInt` instead of a plain `float`: the alias docstring documents the trap in detail, including the inherent `bool ⊂ int` leak.
|
|
22
|
+
|
|
23
|
+
- [`is_close_to_zero()`][frequenz.core.math.is_close_to_zero] now annotates its `value` and `abs_tol` parameters as [`FloatInt`][frequenz.core.typing.FloatInt]. This is a pure widening, `int` arguments were always accepted by type checkers, the annotation just didn't admit it.
|
|
@@ -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 == 83.0.0",
|
|
7
|
+
"setuptools_scm[toml] == 10.2.1",
|
|
8
|
+
"frequenz-repo-config[lib] == 0.18.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",
|
|
@@ -47,44 +47,45 @@ email = "floss@frequenz.com"
|
|
|
47
47
|
[project.optional-dependencies]
|
|
48
48
|
dev-flake8 = [
|
|
49
49
|
"flake8 == 7.3.0",
|
|
50
|
+
"flake8-datetimez == 20.10.0",
|
|
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.9.1",
|
|
53
54
|
"pydocstyle == 6.3.0",
|
|
54
55
|
]
|
|
55
|
-
dev-formatting = ["black ==
|
|
56
|
+
dev-formatting = ["black == 26.5.1", "isort == 8.0.1"]
|
|
56
57
|
dev-mkdocs = [
|
|
57
|
-
"Markdown == 3.
|
|
58
|
-
"black ==
|
|
59
|
-
"mike == 2.
|
|
60
|
-
"mkdocs-gen-files == 0.
|
|
61
|
-
"mkdocs-literate-nav == 0.6.
|
|
62
|
-
"mkdocs-macros-plugin == 1.
|
|
63
|
-
"mkdocs-material == 9.
|
|
64
|
-
"mkdocstrings[python] == 0.
|
|
65
|
-
"mkdocstrings-python ==
|
|
66
|
-
"frequenz-repo-config[lib] == 0.
|
|
58
|
+
"Markdown == 3.10.2",
|
|
59
|
+
"black == 26.5.1",
|
|
60
|
+
"mike == 2.2.0",
|
|
61
|
+
"mkdocs-gen-files == 0.6.1",
|
|
62
|
+
"mkdocs-literate-nav == 0.6.3",
|
|
63
|
+
"mkdocs-macros-plugin == 1.5.0",
|
|
64
|
+
"mkdocs-material == 9.7.7",
|
|
65
|
+
"mkdocstrings[python] == 1.0.6",
|
|
66
|
+
"mkdocstrings-python == 2.0.5",
|
|
67
|
+
"frequenz-repo-config[lib] == 0.18.0",
|
|
67
68
|
]
|
|
68
69
|
dev-mypy = [
|
|
69
|
-
"mypy ==
|
|
70
|
-
"types-Markdown == 3.
|
|
70
|
+
"mypy == 2.3.0",
|
|
71
|
+
"types-Markdown == 3.10.2.20260712",
|
|
71
72
|
# For checking the noxfile, docs/ script, and tests
|
|
72
73
|
"frequenz-core[dev-mkdocs,dev-noxfile,dev-pytest]",
|
|
73
74
|
]
|
|
74
|
-
dev-noxfile = ["nox ==
|
|
75
|
+
dev-noxfile = ["nox == 2026.7.11", "frequenz-repo-config[lib] == 0.18.0"]
|
|
75
76
|
dev-pylint = [
|
|
76
77
|
# dev-pytest already defines a dependency to pylint because of the examples
|
|
77
78
|
# For checking the noxfile, docs/ script, and tests
|
|
78
79
|
"frequenz-core[dev-mkdocs,dev-noxfile,dev-pytest]",
|
|
79
80
|
]
|
|
80
81
|
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.
|
|
82
|
+
"pytest == 9.1.1",
|
|
83
|
+
"pylint == 4.0.6", # We need this to check for the examples
|
|
84
|
+
"frequenz-repo-config[extra-lint-examples] == 0.18.0",
|
|
85
|
+
"pytest-mock == 3.15.1",
|
|
86
|
+
"pytest-asyncio == 1.4.0",
|
|
87
|
+
"async-solipsism == 0.9",
|
|
88
|
+
"hypothesis == 6.163.0",
|
|
88
89
|
]
|
|
89
90
|
dev = [
|
|
90
91
|
"frequenz-core[dev-mkdocs,dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]",
|
|
@@ -123,6 +124,10 @@ check-yield-types = false
|
|
|
123
124
|
arg-type-hints-in-docstring = false
|
|
124
125
|
arg-type-hints-in-signature = true
|
|
125
126
|
allow-init-docstring = true
|
|
127
|
+
check-class-attributes = true
|
|
128
|
+
check-style-mismatch = true
|
|
129
|
+
require-inline-class-var-docs = true
|
|
130
|
+
skip-checking-short-docstrings = true
|
|
126
131
|
|
|
127
132
|
[tool.pylint.similarities]
|
|
128
133
|
ignore-comments = ['yes']
|
|
@@ -27,12 +27,19 @@ EnumT = TypeVar("EnumT", bound=enum.Enum)
|
|
|
27
27
|
"""Type variable for enum types."""
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
ValueT = TypeVar("ValueT")
|
|
31
|
+
"""Type variable for enum member values."""
|
|
32
|
+
|
|
33
|
+
|
|
30
34
|
class DeprecatedMemberWarning(DeprecationWarning):
|
|
31
35
|
"""Warning category for deprecated enum members."""
|
|
32
36
|
|
|
33
37
|
|
|
34
38
|
class DeprecatedMember:
|
|
35
|
-
"""
|
|
39
|
+
"""Class to mark members as deprecated.
|
|
40
|
+
|
|
41
|
+
This class should not be used directly, use
|
|
42
|
+
[`deprecated_member`][frequenz.core.enum.deprecated_member] instead.
|
|
36
43
|
|
|
37
44
|
Please read the [`Enum`][frequenz.core.enum.Enum] documentation for details and
|
|
38
45
|
examples.
|
|
@@ -48,16 +55,34 @@ class DeprecatedMember:
|
|
|
48
55
|
self.message = message
|
|
49
56
|
|
|
50
57
|
|
|
58
|
+
def deprecated_member(value: ValueT, message: str) -> ValueT:
|
|
59
|
+
"""Mark an enum member as deprecated.
|
|
60
|
+
|
|
61
|
+
Please read the [`Enum`][frequenz.core.enum.Enum] documentation for details and
|
|
62
|
+
examples.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
value: The value of the enum member to mark as deprecated.
|
|
66
|
+
message: The deprecation message to be shown when the member is accessed.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
The wrapped value, to mark the enum member as deprecated.
|
|
70
|
+
"""
|
|
71
|
+
return cast(ValueT, DeprecatedMember(value, message))
|
|
72
|
+
|
|
73
|
+
|
|
51
74
|
class DeprecatingEnumType(enum.EnumType):
|
|
52
|
-
"""Enum metaclass that supports
|
|
75
|
+
"""Enum metaclass that supports deprecated members.
|
|
53
76
|
|
|
54
77
|
Tip:
|
|
55
78
|
Normally it is not necessary to use this class directly, use
|
|
56
|
-
[`Enum`][
|
|
79
|
+
[`Enum`][..Enum] instead.
|
|
57
80
|
|
|
58
81
|
Behavior:
|
|
59
82
|
|
|
60
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.
|
|
61
86
|
- During class creation, these wrappers are replaced with `value` so that
|
|
62
87
|
a normal enum member or alias is created by [`EnumType`][enum.EnumType].
|
|
63
88
|
- The deprecated names are recorded so that:
|
|
@@ -75,7 +100,7 @@ class DeprecatingEnumType(enum.EnumType):
|
|
|
75
100
|
classdict: Mapping[str, Any],
|
|
76
101
|
**kw: Any,
|
|
77
102
|
) -> type[EnumT]:
|
|
78
|
-
"""Create the new enum class
|
|
103
|
+
"""Create the new enum class rewriting [`DeprecatedMember`][...DeprecatedMember] members."""
|
|
79
104
|
deprecated_names: dict[str, str] = {}
|
|
80
105
|
prepared = super().__prepare__(name, bases, **kw)
|
|
81
106
|
|
|
@@ -163,7 +188,7 @@ if TYPE_CHECKING:
|
|
|
163
188
|
else:
|
|
164
189
|
|
|
165
190
|
class Enum(enum.Enum, metaclass=DeprecatingEnumType):
|
|
166
|
-
"""Base class for enums that support
|
|
191
|
+
"""Base class for enums that support deprecated members.
|
|
167
192
|
|
|
168
193
|
This class extends the standard library's [`enum.Enum`][] to support marking
|
|
169
194
|
certain members as deprecated. Deprecated members can be accessed, but doing so
|
|
@@ -171,7 +196,7 @@ else:
|
|
|
171
196
|
a [`DeprecatedMemberWarning`][frequenz.core.enum.DeprecatedMemberWarning].
|
|
172
197
|
|
|
173
198
|
To declare a deprecated member, use the
|
|
174
|
-
[`
|
|
199
|
+
[`deprecated_member()`][frequenz.core.enum.deprecated_member] function.
|
|
175
200
|
|
|
176
201
|
When using the enum constructor (i.e. `MyEnum(value)`), a warning is only emitted if
|
|
177
202
|
the resolved member has no non-deprecated aliases. If there is at least one
|
|
@@ -179,13 +204,13 @@ else:
|
|
|
179
204
|
|
|
180
205
|
Example:
|
|
181
206
|
```python
|
|
182
|
-
from frequenz.core.enum import Enum,
|
|
207
|
+
from frequenz.core.enum import Enum, deprecated_member
|
|
183
208
|
|
|
184
209
|
class TaskStatus(Enum):
|
|
185
210
|
OPEN = 1
|
|
186
211
|
IN_PROGRESS = 2
|
|
187
|
-
PENDING =
|
|
188
|
-
DONE =
|
|
212
|
+
PENDING = deprecated_member(1, "PENDING is deprecated, use OPEN instead")
|
|
213
|
+
DONE = deprecated_member(3, "DONE is deprecated, use FINISHED instead")
|
|
189
214
|
FINISHED = 4
|
|
190
215
|
|
|
191
216
|
# Accessing deprecated members:
|
|
@@ -219,14 +244,14 @@ def unique(enumeration: type[EnumT]) -> type[EnumT]:
|
|
|
219
244
|
|
|
220
245
|
Example:
|
|
221
246
|
```python
|
|
222
|
-
from frequenz.core.enum import Enum,
|
|
247
|
+
from frequenz.core.enum import Enum, deprecated_member, unique
|
|
223
248
|
|
|
224
249
|
@unique
|
|
225
250
|
class TaskStatus(Enum):
|
|
226
251
|
OPEN = 1
|
|
227
252
|
IN_PROGRESS = 2
|
|
228
253
|
# This is okay, as PENDING is a deprecated alias.
|
|
229
|
-
PENDING =
|
|
254
|
+
PENDING = deprecated_member(1, "Use OPEN instead")
|
|
230
255
|
```
|
|
231
256
|
|
|
232
257
|
Args:
|
|
@@ -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
|
|
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
import math
|
|
7
7
|
from dataclasses import dataclass
|
|
8
|
-
from typing import Generic, Protocol, Self, TypeVar
|
|
8
|
+
from typing import Generic, Protocol, Self, TypeVar
|
|
9
9
|
|
|
10
|
+
from .typing import FloatInt
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
|
|
13
|
+
def is_close_to_zero(value: FloatInt, abs_tol: FloatInt = 1e-9) -> bool:
|
|
12
14
|
"""Check if a floating point value is close to zero.
|
|
13
15
|
|
|
14
16
|
A value of 1e-9 is a commonly used absolute tolerance to balance precision
|
|
@@ -34,76 +36,68 @@ class LessThanComparable(Protocol):
|
|
|
34
36
|
"""Return whether self is less than other."""
|
|
35
37
|
|
|
36
38
|
|
|
39
|
+
LessThanComparableT = TypeVar("LessThanComparableT", bound=LessThanComparable)
|
|
40
|
+
"""Type variable for a value that is [`LessThanComparable`][..LessThanComparable]."""
|
|
41
|
+
|
|
42
|
+
|
|
37
43
|
LessThanComparableOrNoneT = TypeVar(
|
|
38
44
|
"LessThanComparableOrNoneT", bound=LessThanComparable | None
|
|
39
45
|
)
|
|
40
|
-
"""Type variable for a value that
|
|
46
|
+
"""Type variable for a value that is [`LessThanComparable`][..LessThanComparable] or `None`.
|
|
47
|
+
|
|
48
|
+
Warning: Deprecated
|
|
49
|
+
This type variable is deprecated and it will be removed in a future version. Use
|
|
50
|
+
[`LessThanComparableT`][..LessThanComparableT] instead.
|
|
51
|
+
"""
|
|
41
52
|
|
|
42
53
|
|
|
43
54
|
@dataclass(frozen=True, repr=False)
|
|
44
|
-
class Interval(Generic[
|
|
55
|
+
class Interval(Generic[LessThanComparableT]):
|
|
45
56
|
"""An interval to test if a value is within its limits.
|
|
46
57
|
|
|
47
|
-
The `start
|
|
48
|
-
included in the range when
|
|
58
|
+
The [`.start`][.start] and [`.end`][.end] are inclusive, meaning that the
|
|
59
|
+
[`.start`][.start] and [`.end`][.end] limits are included in the range when
|
|
60
|
+
checking if a value is contained by the interval.
|
|
49
61
|
|
|
50
|
-
If the `start
|
|
51
|
-
direction.
|
|
62
|
+
If the [`.start`][.start] or [`.end`][.end] is `None`, it means that the interval
|
|
63
|
+
is unbounded in that direction. `None` is used purely as a bound marker; it is
|
|
64
|
+
never a value in the interval.
|
|
52
65
|
|
|
53
|
-
If `start
|
|
66
|
+
If [`.start`][.start] is bigger than [`.end`][.end], a `ValueError` is raised.
|
|
54
67
|
|
|
55
68
|
The type stored in the interval must be comparable, meaning that it must implement
|
|
56
69
|
the `__lt__` method to be able to compare values.
|
|
57
70
|
"""
|
|
58
71
|
|
|
59
|
-
start:
|
|
60
|
-
"""The start of the interval."""
|
|
72
|
+
start: LessThanComparableT | None
|
|
73
|
+
"""The start of the interval, or `None` to indicate no lower bound (-∞)."""
|
|
61
74
|
|
|
62
|
-
end:
|
|
63
|
-
"""The end of the interval."""
|
|
75
|
+
end: LessThanComparableT | None
|
|
76
|
+
"""The end of the interval, or `None` to indicate no upper bound (+∞)."""
|
|
64
77
|
|
|
65
78
|
def __post_init__(self) -> None:
|
|
66
79
|
"""Check if the start is less than or equal to the end."""
|
|
67
80
|
if self.start is None or self.end is None:
|
|
68
81
|
return
|
|
69
|
-
start
|
|
70
|
-
end = cast(LessThanComparable, self.end)
|
|
71
|
-
if start > end:
|
|
82
|
+
if self.start > self.end:
|
|
72
83
|
raise ValueError(
|
|
73
84
|
f"The start ({self.start}) can't be bigger than end ({self.end})"
|
|
74
85
|
)
|
|
75
86
|
|
|
76
|
-
def __contains__(self, item:
|
|
77
|
-
"""
|
|
78
|
-
Check if the value is within the range of the container.
|
|
87
|
+
def __contains__(self, item: LessThanComparableT) -> bool:
|
|
88
|
+
"""Check if the value is within the range of the interval.
|
|
79
89
|
|
|
80
90
|
Args:
|
|
81
91
|
item: The value to check.
|
|
82
92
|
|
|
83
93
|
Returns:
|
|
84
|
-
|
|
94
|
+
True if value is within the range, otherwise False.
|
|
85
95
|
"""
|
|
86
|
-
if
|
|
96
|
+
if self.start is not None and item < self.start:
|
|
97
|
+
return False
|
|
98
|
+
if self.end is not None and item > self.end:
|
|
87
99
|
return False
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if self.start is None and self.end is None:
|
|
91
|
-
return True
|
|
92
|
-
if self.start is None:
|
|
93
|
-
start = cast(LessThanComparable, self.end)
|
|
94
|
-
return not casted_item > start
|
|
95
|
-
if self.end is None:
|
|
96
|
-
return not self.start > item
|
|
97
|
-
# mypy seems to get confused here, not being able to narrow start and end to
|
|
98
|
-
# just LessThanComparable, complaining with:
|
|
99
|
-
# error: Unsupported left operand type for <= (some union)
|
|
100
|
-
# But we know if they are not None, they should be LessThanComparable, and
|
|
101
|
-
# actually mypy is being able to figure it out in the lines above, just not in
|
|
102
|
-
# this one, so it should be safe to cast.
|
|
103
|
-
return not (
|
|
104
|
-
casted_item < cast(LessThanComparable, self.start)
|
|
105
|
-
or casted_item > cast(LessThanComparable, self.end)
|
|
106
|
-
)
|
|
100
|
+
return True
|
|
107
101
|
|
|
108
102
|
def __repr__(self) -> str:
|
|
109
103
|
"""Return a string representation of this instance."""
|
|
@@ -11,10 +11,60 @@ It also provides a metaclass used by the decorator to disable the `__init__`:
|
|
|
11
11
|
[`NoInitConstructibleMeta`][frequenz.core.typing.NoInitConstructibleMeta]. This is
|
|
12
12
|
useful mostly for disabling `__init__` while having to use another metaclass too (like
|
|
13
13
|
[`abc.ABCMeta`][abc.ABCMeta]).
|
|
14
|
+
|
|
15
|
+
Finally, it provides [`FloatInt`][frequenz.core.typing.FloatInt], an honest type alias
|
|
16
|
+
for `float | int`, to annotate floating point values that can also be an `int` at
|
|
17
|
+
runtime.
|
|
14
18
|
"""
|
|
15
19
|
|
|
16
20
|
from collections.abc import Callable
|
|
17
|
-
from typing import Any, NoReturn, TypeVar, cast, overload
|
|
21
|
+
from typing import Any, NoReturn, TypeAlias, TypeVar, cast, overload
|
|
22
|
+
|
|
23
|
+
FloatInt: TypeAlias = float | int
|
|
24
|
+
"""A floating point value that can also be an `int` at runtime.
|
|
25
|
+
|
|
26
|
+
[PEP 484's numeric tower](https://peps.python.org/pep-0484/#the-numeric-tower) makes
|
|
27
|
+
`int` assignable to any `float`-annotated parameter, attribute or variable, so a plain
|
|
28
|
+
`float` annotation is a lie: type checkers (even `mypy --strict`) happily accept `int`
|
|
29
|
+
values, but `isinstance(1, float)` is `False` at runtime. This breaks `match … case
|
|
30
|
+
float():` arms (an `int` value falls through to
|
|
31
|
+
[`assert_never()`][typing.assert_never]), calls to `float`-only methods like
|
|
32
|
+
[`hex()`][float.hex], and any other code dispatching on the concrete runtime type.
|
|
33
|
+
|
|
34
|
+
Annotating with this alias instead makes the heterogeneity explicit, so type checkers
|
|
35
|
+
push the code reading these values to handle both branches, typically by matching with
|
|
36
|
+
`case float() | int():`.
|
|
37
|
+
|
|
38
|
+
The full analysis, including the alternatives that were rejected, is recorded in
|
|
39
|
+
[issue #250](https://github.com/frequenz-floss/frequenz-client-common-python/issues/250).
|
|
40
|
+
|
|
41
|
+
Danger:
|
|
42
|
+
`bool` is a subclass of `int`, so `True` and `False` also satisfy this alias. This
|
|
43
|
+
is inherent to Python's type system and is not guarded against.
|
|
44
|
+
|
|
45
|
+
Example:
|
|
46
|
+
```python
|
|
47
|
+
from typing import assert_never
|
|
48
|
+
|
|
49
|
+
from frequenz.core.typing import FloatInt
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def describe(value: FloatInt | None) -> str:
|
|
53
|
+
match value:
|
|
54
|
+
case float() | int():
|
|
55
|
+
return f"number {value}"
|
|
56
|
+
case None:
|
|
57
|
+
return "nothing"
|
|
58
|
+
case unexpected:
|
|
59
|
+
assert_never(unexpected)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
assert describe(1) == "number 1"
|
|
63
|
+
assert describe(1.5) == "number 1.5"
|
|
64
|
+
assert describe(None) == "nothing"
|
|
65
|
+
```
|
|
66
|
+
"""
|
|
67
|
+
|
|
18
68
|
|
|
19
69
|
TypeT = TypeVar("TypeT", bound=type)
|
|
20
70
|
"""A type variable that is bound to a type."""
|
|
@@ -207,7 +257,7 @@ class NoInitConstructibleMeta(type):
|
|
|
207
257
|
class NoInitConstructibleABCMeta(ABCMeta, NoInitConstructibleMeta):
|
|
208
258
|
pass
|
|
209
259
|
|
|
210
|
-
class MyAbstractClass(
|
|
260
|
+
class MyAbstractClass(metaclass=NoInitConstructibleABCMeta):
|
|
211
261
|
@abstractmethod
|
|
212
262
|
def do_something(self) -> None:
|
|
213
263
|
...
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: frequenz-core
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.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
|
|
@@ -23,41 +22,42 @@ License-File: LICENSE
|
|
|
23
22
|
Requires-Dist: typing-extensions<5,>=4.13.0
|
|
24
23
|
Provides-Extra: dev-flake8
|
|
25
24
|
Requires-Dist: flake8==7.3.0; extra == "dev-flake8"
|
|
25
|
+
Requires-Dist: flake8-datetimez==20.10.0; 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.9.1; 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==8.0.1; extra == "dev-formatting"
|
|
33
33
|
Provides-Extra: dev-mkdocs
|
|
34
|
-
Requires-Dist: Markdown==3.
|
|
35
|
-
Requires-Dist: black==
|
|
36
|
-
Requires-Dist: mike==2.
|
|
37
|
-
Requires-Dist: mkdocs-gen-files==0.
|
|
38
|
-
Requires-Dist: mkdocs-literate-nav==0.6.
|
|
39
|
-
Requires-Dist: mkdocs-macros-plugin==1.
|
|
40
|
-
Requires-Dist: mkdocs-material==9.
|
|
41
|
-
Requires-Dist: mkdocstrings[python]==0.
|
|
42
|
-
Requires-Dist: mkdocstrings-python==
|
|
43
|
-
Requires-Dist: frequenz-repo-config[lib]==0.
|
|
34
|
+
Requires-Dist: Markdown==3.10.2; extra == "dev-mkdocs"
|
|
35
|
+
Requires-Dist: black==26.5.1; extra == "dev-mkdocs"
|
|
36
|
+
Requires-Dist: mike==2.2.0; extra == "dev-mkdocs"
|
|
37
|
+
Requires-Dist: mkdocs-gen-files==0.6.1; extra == "dev-mkdocs"
|
|
38
|
+
Requires-Dist: mkdocs-literate-nav==0.6.3; extra == "dev-mkdocs"
|
|
39
|
+
Requires-Dist: mkdocs-macros-plugin==1.5.0; extra == "dev-mkdocs"
|
|
40
|
+
Requires-Dist: mkdocs-material==9.7.7; extra == "dev-mkdocs"
|
|
41
|
+
Requires-Dist: mkdocstrings[python]==1.0.6; extra == "dev-mkdocs"
|
|
42
|
+
Requires-Dist: mkdocstrings-python==2.0.5; extra == "dev-mkdocs"
|
|
43
|
+
Requires-Dist: frequenz-repo-config[lib]==0.18.0; extra == "dev-mkdocs"
|
|
44
44
|
Provides-Extra: dev-mypy
|
|
45
|
-
Requires-Dist: mypy==
|
|
46
|
-
Requires-Dist: types-Markdown==3.
|
|
45
|
+
Requires-Dist: mypy==2.3.0; extra == "dev-mypy"
|
|
46
|
+
Requires-Dist: types-Markdown==3.10.2.20260712; extra == "dev-mypy"
|
|
47
47
|
Requires-Dist: frequenz-core[dev-mkdocs,dev-noxfile,dev-pytest]; extra == "dev-mypy"
|
|
48
48
|
Provides-Extra: dev-noxfile
|
|
49
|
-
Requires-Dist: nox==
|
|
50
|
-
Requires-Dist: frequenz-repo-config[lib]==0.
|
|
49
|
+
Requires-Dist: nox==2026.7.11; extra == "dev-noxfile"
|
|
50
|
+
Requires-Dist: frequenz-repo-config[lib]==0.18.0; extra == "dev-noxfile"
|
|
51
51
|
Provides-Extra: dev-pylint
|
|
52
52
|
Requires-Dist: frequenz-core[dev-mkdocs,dev-noxfile,dev-pytest]; extra == "dev-pylint"
|
|
53
53
|
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.
|
|
54
|
+
Requires-Dist: pytest==9.1.1; extra == "dev-pytest"
|
|
55
|
+
Requires-Dist: pylint==4.0.6; extra == "dev-pytest"
|
|
56
|
+
Requires-Dist: frequenz-repo-config[extra-lint-examples]==0.18.0; extra == "dev-pytest"
|
|
57
|
+
Requires-Dist: pytest-mock==3.15.1; extra == "dev-pytest"
|
|
58
|
+
Requires-Dist: pytest-asyncio==1.4.0; extra == "dev-pytest"
|
|
59
|
+
Requires-Dist: async-solipsism==0.9; extra == "dev-pytest"
|
|
60
|
+
Requires-Dist: hypothesis==6.163.0; extra == "dev-pytest"
|
|
61
61
|
Provides-Extra: dev
|
|
62
62
|
Requires-Dist: frequenz-core[dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]; extra == "dev"
|
|
63
63
|
Dynamic: license-file
|
|
@@ -162,13 +162,15 @@ Define enums with deprecated members that raise deprecation warnings when
|
|
|
162
162
|
accessed:
|
|
163
163
|
|
|
164
164
|
```python
|
|
165
|
-
from frequenz.core.enum import Enum,
|
|
165
|
+
from frequenz.core.enum import Enum, deprecated_member, unique
|
|
166
166
|
|
|
167
|
+
@unique
|
|
167
168
|
class TaskStatus(Enum):
|
|
168
169
|
OPEN = 1
|
|
169
170
|
IN_PROGRESS = 2
|
|
170
|
-
|
|
171
|
-
|
|
171
|
+
# Duplicate values are fine with `@unique` as long as they are deprecated
|
|
172
|
+
PENDING = deprecated_member(1, "PENDING is deprecated, use OPEN instead")
|
|
173
|
+
DONE = deprecated_member(3, "DONE is deprecated, use FINISHED instead")
|
|
172
174
|
FINISHED = 4
|
|
173
175
|
|
|
174
176
|
status1 = TaskStatus.PENDING # Warns: "PENDING is deprecated, use OPEN instead"
|
|
@@ -198,6 +200,28 @@ class ApiClient:
|
|
|
198
200
|
client = ApiClient.create("my-api-key") # ✅ Works
|
|
199
201
|
```
|
|
200
202
|
|
|
203
|
+
Annotate floating point values honestly, as Python's numeric tower lets `int`
|
|
204
|
+
values through any `float` annotation:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
from typing import assert_never
|
|
208
|
+
|
|
209
|
+
from frequenz.core.typing import FloatInt
|
|
210
|
+
|
|
211
|
+
def describe(value: FloatInt | None) -> str:
|
|
212
|
+
match value:
|
|
213
|
+
case float() | int():
|
|
214
|
+
return f"number {value}"
|
|
215
|
+
case None:
|
|
216
|
+
return "nothing"
|
|
217
|
+
case unexpected:
|
|
218
|
+
assert_never(unexpected)
|
|
219
|
+
|
|
220
|
+
assert describe(1) == "number 1" # ✅ `case float():` alone would crash here
|
|
221
|
+
assert describe(1.5) == "number 1.5"
|
|
222
|
+
assert describe(None) == "nothing"
|
|
223
|
+
```
|
|
224
|
+
|
|
201
225
|
### Strongly-Typed IDs
|
|
202
226
|
|
|
203
227
|
Create type-safe identifiers for different entities:
|
|
@@ -16,4 +16,6 @@ src/frequenz_core.egg-info/PKG-INFO
|
|
|
16
16
|
src/frequenz_core.egg-info/SOURCES.txt
|
|
17
17
|
src/frequenz_core.egg-info/dependency_links.txt
|
|
18
18
|
src/frequenz_core.egg-info/requires.txt
|
|
19
|
+
src/frequenz_core.egg-info/scm_file_list.json
|
|
20
|
+
src/frequenz_core.egg-info/scm_version.json
|
|
19
21
|
src/frequenz_core.egg-info/top_level.txt
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
typing-extensions<5,>=4.13.0
|
|
2
|
+
|
|
3
|
+
[dev]
|
|
4
|
+
frequenz-core[dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]
|
|
5
|
+
|
|
6
|
+
[dev-flake8]
|
|
7
|
+
flake8==7.3.0
|
|
8
|
+
flake8-datetimez==20.10.0
|
|
9
|
+
flake8-docstrings==1.7.0
|
|
10
|
+
flake8-pyproject==1.2.4
|
|
11
|
+
pydoclint==0.9.1
|
|
12
|
+
pydocstyle==6.3.0
|
|
13
|
+
|
|
14
|
+
[dev-formatting]
|
|
15
|
+
black==26.5.1
|
|
16
|
+
isort==8.0.1
|
|
17
|
+
|
|
18
|
+
[dev-mkdocs]
|
|
19
|
+
Markdown==3.10.2
|
|
20
|
+
black==26.5.1
|
|
21
|
+
mike==2.2.0
|
|
22
|
+
mkdocs-gen-files==0.6.1
|
|
23
|
+
mkdocs-literate-nav==0.6.3
|
|
24
|
+
mkdocs-macros-plugin==1.5.0
|
|
25
|
+
mkdocs-material==9.7.7
|
|
26
|
+
mkdocstrings[python]==1.0.6
|
|
27
|
+
mkdocstrings-python==2.0.5
|
|
28
|
+
frequenz-repo-config[lib]==0.18.0
|
|
29
|
+
|
|
30
|
+
[dev-mypy]
|
|
31
|
+
mypy==2.3.0
|
|
32
|
+
types-Markdown==3.10.2.20260712
|
|
33
|
+
frequenz-core[dev-mkdocs,dev-noxfile,dev-pytest]
|
|
34
|
+
|
|
35
|
+
[dev-noxfile]
|
|
36
|
+
nox==2026.7.11
|
|
37
|
+
frequenz-repo-config[lib]==0.18.0
|
|
38
|
+
|
|
39
|
+
[dev-pylint]
|
|
40
|
+
frequenz-core[dev-mkdocs,dev-noxfile,dev-pytest]
|
|
41
|
+
|
|
42
|
+
[dev-pytest]
|
|
43
|
+
pytest==9.1.1
|
|
44
|
+
pylint==4.0.6
|
|
45
|
+
frequenz-repo-config[extra-lint-examples]==0.18.0
|
|
46
|
+
pytest-mock==3.15.1
|
|
47
|
+
pytest-asyncio==1.4.0
|
|
48
|
+
async-solipsism==0.9
|
|
49
|
+
hypothesis==6.163.0
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files": [
|
|
3
|
+
".cookiecutter-replay.json",
|
|
4
|
+
".editorconfig",
|
|
5
|
+
".github/ISSUE_TEMPLATE/bug.yml",
|
|
6
|
+
".github/ISSUE_TEMPLATE/config.yml",
|
|
7
|
+
".github/ISSUE_TEMPLATE/feature.yml",
|
|
8
|
+
".github/RELEASE_NOTES.template.md",
|
|
9
|
+
".github/dependabot.yml",
|
|
10
|
+
".github/keylabeler.yml",
|
|
11
|
+
".github/labeler.yml",
|
|
12
|
+
".github/workflows/auto-dependabot.yaml",
|
|
13
|
+
".github/workflows/black-migration.yaml",
|
|
14
|
+
".github/workflows/ci-pr.yaml",
|
|
15
|
+
".github/workflows/ci.yaml",
|
|
16
|
+
".github/workflows/dco-merge-queue.yml",
|
|
17
|
+
".github/workflows/isort-migration.yaml",
|
|
18
|
+
".github/workflows/labeler.yml",
|
|
19
|
+
".github/workflows/release-notes-check.yml",
|
|
20
|
+
".github/workflows/repo-config-migration.yaml",
|
|
21
|
+
".gitignore",
|
|
22
|
+
"CODEOWNERS",
|
|
23
|
+
"CONTRIBUTING.md",
|
|
24
|
+
"LICENSE",
|
|
25
|
+
"MANIFEST.in",
|
|
26
|
+
"README.md",
|
|
27
|
+
"RELEASE_NOTES.md",
|
|
28
|
+
"docs/CONTRIBUTING.md",
|
|
29
|
+
"docs/SUMMARY.md",
|
|
30
|
+
"docs/_css/mkdocstrings.css",
|
|
31
|
+
"docs/_css/style.css",
|
|
32
|
+
"docs/_img/logo.png",
|
|
33
|
+
"docs/_overrides/main.html",
|
|
34
|
+
"docs/_scripts/mkdocstrings_autoapi.py",
|
|
35
|
+
"docs/index.md",
|
|
36
|
+
"mkdocs.yml",
|
|
37
|
+
"noxfile.py",
|
|
38
|
+
"pyproject.toml",
|
|
39
|
+
"src/frequenz/core/__init__.py",
|
|
40
|
+
"src/frequenz/core/conftest.py",
|
|
41
|
+
"src/frequenz/core/datetime.py",
|
|
42
|
+
"src/frequenz/core/enum.py",
|
|
43
|
+
"src/frequenz/core/id.py",
|
|
44
|
+
"src/frequenz/core/math.py",
|
|
45
|
+
"src/frequenz/core/module.py",
|
|
46
|
+
"src/frequenz/core/py.typed",
|
|
47
|
+
"src/frequenz/core/typing.py",
|
|
48
|
+
"tests/math/test_interval.py",
|
|
49
|
+
"tests/math/test_is_close_to_zero.py",
|
|
50
|
+
"tests/test_enum.py",
|
|
51
|
+
"tests/test_id.py",
|
|
52
|
+
"tests/test_module.py",
|
|
53
|
+
"tests/test_typing.py"
|
|
54
|
+
]
|
|
55
|
+
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# Frequenz Core Library Release Notes
|
|
2
|
-
|
|
3
|
-
## Summary
|
|
4
|
-
|
|
5
|
-
## New Features
|
|
6
|
-
|
|
7
|
-
* `frequenz.core.enum` now provides a `@unique` decorator that is aware of deprecations, and will only check for uniqueness among non-deprecated enum members.
|
|
8
|
-
|
|
9
|
-
For example this works:
|
|
10
|
-
|
|
11
|
-
```py
|
|
12
|
-
>>> from frequenz.core.enum import DeprecatedMember, Enum, unique
|
|
13
|
-
>>>
|
|
14
|
-
>>> @unique
|
|
15
|
-
... class Status(Enum):
|
|
16
|
-
... ACTIVE = 1
|
|
17
|
-
... INACTIVE = 2
|
|
18
|
-
... PENDING = DeprecatedMember(1, "PENDING is deprecated, use ACTIVE instead")
|
|
19
|
-
...
|
|
20
|
-
>>>
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
While using the standard library's `enum.unique` decorator raises a `ValueError`:
|
|
24
|
-
|
|
25
|
-
```py
|
|
26
|
-
>>> from enum import unique
|
|
27
|
-
>>> from frequenz.core.enum import DeprecatedMember, Enum
|
|
28
|
-
>>>
|
|
29
|
-
>>> @unique
|
|
30
|
-
... class Status(Enum):
|
|
31
|
-
... ACTIVE = 1
|
|
32
|
-
... INACTIVE = 2
|
|
33
|
-
... PENDING = DeprecatedMember(1, "PENDING is deprecated, use ACTIVE instead")
|
|
34
|
-
...
|
|
35
|
-
Traceback (most recent call last):
|
|
36
|
-
File "<stdin>", line 1, in <module>
|
|
37
|
-
File "/usr/lib/python3.12/enum.py", line 1617, in unique
|
|
38
|
-
raise ValueError('duplicate values found in %r: %s' %
|
|
39
|
-
ValueError: duplicate values found in <enum 'Status'>: PENDING -> ACTIVE
|
|
40
|
-
>>>
|
|
41
|
-
```
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
typing-extensions<5,>=4.13.0
|
|
2
|
-
|
|
3
|
-
[dev]
|
|
4
|
-
frequenz-core[dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]
|
|
5
|
-
|
|
6
|
-
[dev-flake8]
|
|
7
|
-
flake8==7.3.0
|
|
8
|
-
flake8-docstrings==1.7.0
|
|
9
|
-
flake8-pyproject==1.2.3
|
|
10
|
-
pydoclint==0.6.11
|
|
11
|
-
pydocstyle==6.3.0
|
|
12
|
-
|
|
13
|
-
[dev-formatting]
|
|
14
|
-
black==25.1.0
|
|
15
|
-
isort==6.0.1
|
|
16
|
-
|
|
17
|
-
[dev-mkdocs]
|
|
18
|
-
Markdown==3.8.2
|
|
19
|
-
black==25.1.0
|
|
20
|
-
mike==2.1.3
|
|
21
|
-
mkdocs-gen-files==0.5.0
|
|
22
|
-
mkdocs-literate-nav==0.6.2
|
|
23
|
-
mkdocs-macros-plugin==1.3.9
|
|
24
|
-
mkdocs-material==9.6.18
|
|
25
|
-
mkdocstrings[python]==0.30.0
|
|
26
|
-
mkdocstrings-python==1.18.2
|
|
27
|
-
frequenz-repo-config[lib]==0.13.5
|
|
28
|
-
|
|
29
|
-
[dev-mypy]
|
|
30
|
-
mypy==1.17.1
|
|
31
|
-
types-Markdown==3.8.0.20250809
|
|
32
|
-
frequenz-core[dev-mkdocs,dev-noxfile,dev-pytest]
|
|
33
|
-
|
|
34
|
-
[dev-noxfile]
|
|
35
|
-
nox==2025.5.1
|
|
36
|
-
frequenz-repo-config[lib]==0.13.5
|
|
37
|
-
|
|
38
|
-
[dev-pylint]
|
|
39
|
-
frequenz-core[dev-mkdocs,dev-noxfile,dev-pytest]
|
|
40
|
-
|
|
41
|
-
[dev-pytest]
|
|
42
|
-
pytest==8.4.1
|
|
43
|
-
pylint==3.3.8
|
|
44
|
-
frequenz-repo-config[extra-lint-examples]==0.13.5
|
|
45
|
-
pytest-mock==3.14.1
|
|
46
|
-
pytest-asyncio==1.1.0
|
|
47
|
-
async-solipsism==0.8
|
|
48
|
-
hypothesis==6.138.11
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|