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.
Files changed (25) hide show
  1. {frequenz_core-1.2.0/src/frequenz_core.egg-info → frequenz_core-1.4.0}/PKG-INFO +55 -31
  2. {frequenz_core-1.2.0 → frequenz_core-1.4.0}/README.md +27 -3
  3. frequenz_core-1.4.0/RELEASE_NOTES.md +23 -0
  4. {frequenz_core-1.2.0 → frequenz_core-1.4.0}/pyproject.toml +33 -28
  5. {frequenz_core-1.2.0 → frequenz_core-1.4.0}/src/frequenz/core/enum.py +36 -11
  6. {frequenz_core-1.2.0 → frequenz_core-1.4.0}/src/frequenz/core/id.py +18 -2
  7. {frequenz_core-1.2.0 → frequenz_core-1.4.0}/src/frequenz/core/math.py +34 -40
  8. {frequenz_core-1.2.0 → frequenz_core-1.4.0}/src/frequenz/core/typing.py +52 -2
  9. {frequenz_core-1.2.0 → frequenz_core-1.4.0/src/frequenz_core.egg-info}/PKG-INFO +55 -31
  10. {frequenz_core-1.2.0 → frequenz_core-1.4.0}/src/frequenz_core.egg-info/SOURCES.txt +2 -0
  11. frequenz_core-1.4.0/src/frequenz_core.egg-info/requires.txt +49 -0
  12. frequenz_core-1.4.0/src/frequenz_core.egg-info/scm_file_list.json +55 -0
  13. frequenz_core-1.4.0/src/frequenz_core.egg-info/scm_version.json +8 -0
  14. frequenz_core-1.2.0/RELEASE_NOTES.md +0 -41
  15. frequenz_core-1.2.0/src/frequenz_core.egg-info/requires.txt +0 -48
  16. {frequenz_core-1.2.0 → frequenz_core-1.4.0}/LICENSE +0 -0
  17. {frequenz_core-1.2.0 → frequenz_core-1.4.0}/MANIFEST.in +0 -0
  18. {frequenz_core-1.2.0 → frequenz_core-1.4.0}/setup.cfg +0 -0
  19. {frequenz_core-1.2.0 → frequenz_core-1.4.0}/src/frequenz/core/__init__.py +0 -0
  20. {frequenz_core-1.2.0 → frequenz_core-1.4.0}/src/frequenz/core/conftest.py +0 -0
  21. {frequenz_core-1.2.0 → frequenz_core-1.4.0}/src/frequenz/core/datetime.py +0 -0
  22. {frequenz_core-1.2.0 → frequenz_core-1.4.0}/src/frequenz/core/module.py +0 -0
  23. {frequenz_core-1.2.0 → frequenz_core-1.4.0}/src/frequenz/core/py.typed +0 -0
  24. {frequenz_core-1.2.0 → frequenz_core-1.4.0}/src/frequenz_core.egg-info/dependency_links.txt +0 -0
  25. {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.2.0
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.3; extra == "dev-flake8"
28
- Requires-Dist: pydoclint==0.6.11; extra == "dev-flake8"
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==25.1.0; extra == "dev-formatting"
32
- Requires-Dist: isort==6.0.1; extra == "dev-formatting"
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.8.2; extra == "dev-mkdocs"
35
- Requires-Dist: black==25.1.0; extra == "dev-mkdocs"
36
- Requires-Dist: mike==2.1.3; extra == "dev-mkdocs"
37
- Requires-Dist: mkdocs-gen-files==0.5.0; extra == "dev-mkdocs"
38
- Requires-Dist: mkdocs-literate-nav==0.6.2; extra == "dev-mkdocs"
39
- Requires-Dist: mkdocs-macros-plugin==1.3.9; extra == "dev-mkdocs"
40
- Requires-Dist: mkdocs-material==9.6.18; extra == "dev-mkdocs"
41
- Requires-Dist: mkdocstrings[python]==0.30.0; extra == "dev-mkdocs"
42
- Requires-Dist: mkdocstrings-python==1.18.2; extra == "dev-mkdocs"
43
- Requires-Dist: frequenz-repo-config[lib]==0.13.5; extra == "dev-mkdocs"
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==1.17.1; extra == "dev-mypy"
46
- Requires-Dist: types-Markdown==3.8.0.20250809; extra == "dev-mypy"
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==2025.5.1; extra == "dev-noxfile"
50
- Requires-Dist: frequenz-repo-config[lib]==0.13.5; extra == "dev-noxfile"
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==8.4.1; extra == "dev-pytest"
55
- Requires-Dist: pylint==3.3.8; extra == "dev-pytest"
56
- Requires-Dist: frequenz-repo-config[extra-lint-examples]==0.13.5; extra == "dev-pytest"
57
- Requires-Dist: pytest-mock==3.14.1; extra == "dev-pytest"
58
- Requires-Dist: pytest-asyncio==1.1.0; extra == "dev-pytest"
59
- Requires-Dist: async-solipsism==0.8; extra == "dev-pytest"
60
- Requires-Dist: hypothesis==6.138.11; extra == "dev-pytest"
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, DeprecatedMember
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
- PENDING = DeprecatedMember(1, "PENDING is deprecated, use OPEN instead")
171
- DONE = DeprecatedMember(3, "DONE is deprecated, use FINISHED instead")
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, DeprecatedMember
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
- PENDING = DeprecatedMember(1, "PENDING is deprecated, use OPEN instead")
107
- DONE = DeprecatedMember(3, "DONE is deprecated, use FINISHED instead")
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 == 80.9.0",
7
- "setuptools_scm[toml] == 9.2.0",
8
- "frequenz-repo-config[lib] == 0.13.5",
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 = { text = "MIT" }
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.3", # For reading the flake8 config from pyproject.toml
52
- "pydoclint == 0.6.11",
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 == 25.1.0", "isort == 6.0.1"]
56
+ dev-formatting = ["black == 26.5.1", "isort == 8.0.1"]
56
57
  dev-mkdocs = [
57
- "Markdown == 3.8.2",
58
- "black == 25.1.0",
59
- "mike == 2.1.3",
60
- "mkdocs-gen-files == 0.5.0",
61
- "mkdocs-literate-nav == 0.6.2",
62
- "mkdocs-macros-plugin == 1.3.9",
63
- "mkdocs-material == 9.6.18",
64
- "mkdocstrings[python] == 0.30.0",
65
- "mkdocstrings-python == 1.18.2",
66
- "frequenz-repo-config[lib] == 0.13.5",
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 == 1.17.1",
70
- "types-Markdown == 3.8.0.20250809",
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 == 2025.5.1", "frequenz-repo-config[lib] == 0.13.5"]
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 == 8.4.1",
82
- "pylint == 3.3.8", # We need this to check for the examples
83
- "frequenz-repo-config[extra-lint-examples] == 0.13.5",
84
- "pytest-mock == 3.14.1",
85
- "pytest-asyncio == 1.1.0",
86
- "async-solipsism == 0.8",
87
- "hypothesis == 6.138.11",
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
- """Marker used in enum class bodies to declare deprecated members.
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 `DeprecatedMember` wrappers.
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`][frequenz.core.enum.Enum] instead.
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, rewriting `DeprecatedMember` instances."""
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 DeprecatedMember.
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
- [`DeprecatedMember`][frequenz.core.enum.DeprecatedMember] wrapper in the class body.
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, DeprecatedMember
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 = DeprecatedMember(1, "PENDING is deprecated, use OPEN instead")
188
- DONE = DeprecatedMember(3, "DONE is deprecated, use FINISHED instead")
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, DeprecatedMember, unique
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 = DeprecatedMember(1, "Use OPEN instead")
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 `ValueError` will be raised when defining
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, cast
8
+ from typing import Generic, Protocol, Self, TypeVar
9
9
 
10
+ from .typing import FloatInt
10
11
 
11
- def is_close_to_zero(value: float, abs_tol: float = 1e-9) -> bool:
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 a `LessThanComparable` or `None`."""
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[LessThanComparableOrNoneT]):
55
+ class Interval(Generic[LessThanComparableT]):
45
56
  """An interval to test if a value is within its limits.
46
57
 
47
- The `start` and `end` are inclusive, meaning that the `start` and `end` limites are
48
- included in the range when checking if a value is contained by the interval.
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` or `end` is `None`, it means that the interval is unbounded in that
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` is bigger than `end`, a `ValueError` is raised.
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: LessThanComparableOrNoneT
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: LessThanComparableOrNoneT
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 = cast(LessThanComparable, self.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: LessThanComparableOrNoneT) -> bool:
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
- bool: True if value is within the range, otherwise False.
94
+ True if value is within the range, otherwise False.
85
95
  """
86
- if item is None:
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
- casted_item = cast(LessThanComparable, item)
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(metaclas=NoInitConstructibleABCMeta):
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.2.0
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.3; extra == "dev-flake8"
28
- Requires-Dist: pydoclint==0.6.11; extra == "dev-flake8"
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==25.1.0; extra == "dev-formatting"
32
- Requires-Dist: isort==6.0.1; extra == "dev-formatting"
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.8.2; extra == "dev-mkdocs"
35
- Requires-Dist: black==25.1.0; extra == "dev-mkdocs"
36
- Requires-Dist: mike==2.1.3; extra == "dev-mkdocs"
37
- Requires-Dist: mkdocs-gen-files==0.5.0; extra == "dev-mkdocs"
38
- Requires-Dist: mkdocs-literate-nav==0.6.2; extra == "dev-mkdocs"
39
- Requires-Dist: mkdocs-macros-plugin==1.3.9; extra == "dev-mkdocs"
40
- Requires-Dist: mkdocs-material==9.6.18; extra == "dev-mkdocs"
41
- Requires-Dist: mkdocstrings[python]==0.30.0; extra == "dev-mkdocs"
42
- Requires-Dist: mkdocstrings-python==1.18.2; extra == "dev-mkdocs"
43
- Requires-Dist: frequenz-repo-config[lib]==0.13.5; extra == "dev-mkdocs"
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==1.17.1; extra == "dev-mypy"
46
- Requires-Dist: types-Markdown==3.8.0.20250809; extra == "dev-mypy"
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==2025.5.1; extra == "dev-noxfile"
50
- Requires-Dist: frequenz-repo-config[lib]==0.13.5; extra == "dev-noxfile"
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==8.4.1; extra == "dev-pytest"
55
- Requires-Dist: pylint==3.3.8; extra == "dev-pytest"
56
- Requires-Dist: frequenz-repo-config[extra-lint-examples]==0.13.5; extra == "dev-pytest"
57
- Requires-Dist: pytest-mock==3.14.1; extra == "dev-pytest"
58
- Requires-Dist: pytest-asyncio==1.1.0; extra == "dev-pytest"
59
- Requires-Dist: async-solipsism==0.8; extra == "dev-pytest"
60
- Requires-Dist: hypothesis==6.138.11; extra == "dev-pytest"
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, DeprecatedMember
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
- PENDING = DeprecatedMember(1, "PENDING is deprecated, use OPEN instead")
171
- DONE = DeprecatedMember(3, "DONE is deprecated, use FINISHED instead")
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
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "1.4.0",
3
+ "distance": 0,
4
+ "node": "g96e40b27995efe79ab7899dea61139096d2983f6",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-08-17"
8
+ }
@@ -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