From 65de932f8d12b0e5d9acc6a03389a7d2df9642b4 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Wed, 18 Sep 2024 23:42:38 +0200 Subject: [PATCH 01/20] Add `lalib.fields` sub-package --- src/lalib/fields/__init__.py | 1 + tests/fields/__init__.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 src/lalib/fields/__init__.py create mode 100644 tests/fields/__init__.py diff --git a/src/lalib/fields/__init__.py b/src/lalib/fields/__init__.py new file mode 100644 index 0000000..1db1a3b --- /dev/null +++ b/src/lalib/fields/__init__.py @@ -0,0 +1 @@ +"""A collection of common fields used in linear algebra.""" diff --git a/tests/fields/__init__.py b/tests/fields/__init__.py new file mode 100644 index 0000000..66e8480 --- /dev/null +++ b/tests/fields/__init__.py @@ -0,0 +1 @@ +"""Tests for the `lalib.fields` sub-package.""" From 917c217ca02b60b037279763881310aa18ede99a Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Thu, 19 Sep 2024 12:14:20 +0200 Subject: [PATCH 02/20] Rename `lalib.elements.gf2.GF2` & friends - the future (concrete) Galois `Field` implementation shall receive the name `GF2` (as per common math notation) => name conflict with the current `GF2` class implementing the elements of the future Galois `Field` => rename the current `GF2` class into `GF2Element` - because `GF2Element` is a bit tedius to type for the end user, we introduce a `gf2` alias in line with the naming convention for the built-in data types (e.g., `int` or `float`) that are also used as elements of (other) `Field`s => name conflict with the current `lalib.elements.gf2` module => rename the module into `lalib.elements.galois` - adjust the docstrings to refer to "the `gf2` type" - adjust the top-level imports and tests --- src/lalib/__init__.py | 8 +- src/lalib/elements/__init__.py | 14 +-- src/lalib/elements/{gf2.py => galois.py} | 55 +++++++----- .../elements/{test_gf2.py => test_galois.py} | 88 +++++++++---------- tests/test_docstrings.py | 2 +- 5 files changed, 89 insertions(+), 78 deletions(-) rename src/lalib/elements/{gf2.py => galois.py} (91%) rename tests/elements/{test_gf2.py => test_galois.py} (92%) diff --git a/src/lalib/__init__.py b/src/lalib/__init__.py index 8a1a66f..165418e 100644 --- a/src/lalib/__init__.py +++ b/src/lalib/__init__.py @@ -22,7 +22,7 @@ one from importlib import metadata -from lalib.elements import gf2 +from lalib import elements try: @@ -40,15 +40,15 @@ else: del pkg_info -GF2, one, zero = gf2.GF2, gf2.one, gf2.zero +gf2, one, zero = elements.gf2, elements.one, elements.zero -del gf2 +del elements del metadata __all__ = ( - "GF2", + "gf2", "one", "zero", ) diff --git a/src/lalib/elements/__init__.py b/src/lalib/elements/__init__.py index fe5dc37..1e3366a 100644 --- a/src/lalib/elements/__init__.py +++ b/src/lalib/elements/__init__.py @@ -9,26 +9,26 @@ Then, use them: >>> one + zero one ->>> GF2(0) +>>> gf2(0) zero ->>> GF2(42) +>>> gf2(42) Traceback (most recent call last): ... ValueError: ... ->>> GF2(42, strict=False) +>>> gf2(42, strict=False) one """ -from lalib.elements import gf2 +from lalib.elements import galois -GF2, one, zero = gf2.GF2, gf2.one, gf2.zero +gf2, one, zero = galois.gf2, galois.one, galois.zero -del gf2 +del galois __all__ = ( - "GF2", + "gf2", "one", "zero", ) diff --git a/src/lalib/elements/gf2.py b/src/lalib/elements/galois.py similarity index 91% rename from src/lalib/elements/gf2.py rename to src/lalib/elements/galois.py index a69ec66..7a60c37 100644 --- a/src/lalib/elements/gf2.py +++ b/src/lalib/elements/galois.py @@ -2,7 +2,7 @@ This module defines two singleton objects, `one` and `zero`, that follow the rules of a Galois field of two elements, -or `GF2` for short: +also called `GF2` in `lalib`: >>> one + one zero @@ -22,7 +22,10 @@ zero zero Further usage explanations of `one` and `zero` -can be found in the various docstrings of the `GF2` class. +can be found in the various docstrings of the `GF2Element` class. + +This class is also referred to as just "the `gf2` type" outside +this module. """ import abc @@ -102,15 +105,16 @@ def to_gf2( return 1 -class _GF2Meta(abc.ABCMeta): - """Make data type of `one` and `zero` appear to be `GF2`.""" +class GF2Meta(abc.ABCMeta): + """Make data type of `one` and `zero` appear to be `gf2`.""" def __repr__(cls) -> str: - return "GF2" + """Text representation for `GF2Element` and sub-classes.""" + return "gf2" @functools.total_ordering -class GF2(metaclass=_GF2Meta): +class GF2Element(metaclass=GF2Meta): """A Galois field value: either `one` or `zero`. Implements the singleton design pattern such that @@ -276,8 +280,8 @@ class GF2(metaclass=_GF2Meta): Either `1` or `0`. Reasoning: - - `int(one) == 1` => `GF2(1 / 1) == one` - - `int(zero) == 0` => `GF2(0 / 1) == zero` + - `int(one) == 1` => `gf2(1 / 1) == one` + - `int(zero) == 0` => `gf2(0 / 1) == zero` See also docstring for `.denominator`. """ @@ -287,7 +291,7 @@ class GF2(metaclass=_GF2Meta): def denominator(self) -> Literal[1]: """Smallest denominator when expressed as a `Rational` number. - Always `1` for `GF2` values. + Always `1` for `gf2` values. See also docstring for `.numerator`. """ @@ -308,7 +312,7 @@ class GF2(metaclass=_GF2Meta): True """ try: - other = GF2(other) + other = GF2Element(other) except (TypeError, ValueError): return False else: @@ -327,7 +331,7 @@ class GF2(metaclass=_GF2Meta): True """ try: - other = GF2(other) + other = GF2Element(other) except TypeError: return NotImplemented except ValueError: @@ -358,16 +362,16 @@ class GF2(metaclass=_GF2Meta): def _compute(self, other: object, func: Callable) -> Self: """Run arithmetic operations using `int`s. - The `GF2` atithmetic operations can transparently be conducted + The `gf2` atithmetic operations can transparently be conducted by converting `self` and `other` into `int`s first, and then "do the math". Besides the generic arithmetic, this method also handles the - casting of non-`GF2` values and various errors occuring + casting of non-`gf2` values and various errors occuring along the way. """ try: - other = GF2(other) + other = GF2Element(other) except TypeError: return NotImplemented except ValueError: @@ -391,7 +395,7 @@ class GF2(metaclass=_GF2Meta): def __add__(self, other: object) -> Self: """Addition / Subtraction: `self + other` / `self - other`. - For `GF2`, addition and subtraction are identical. Besides + For `gf2`, addition and subtraction are identical. Besides `one + one` which cannot result in a "two", all operations behave as one would expect from `int`s. @@ -417,7 +421,7 @@ class GF2(metaclass=_GF2Meta): def __mul__(self, other: object) -> Self: """Multiplication: `self * other`. - Multiplying `GF2` values is like multiplying `int`s. + Multiplying `gf2` values is like multiplying `int`s. Example usage: @@ -435,7 +439,7 @@ class GF2(metaclass=_GF2Meta): def __truediv__(self, other: object) -> Self: """Division: `self / other` and `self // other`. - Dividing `GF2` values is like dividing `int`s. + Dividing `gf2` values is like dividing `int`s. Example usage: @@ -466,7 +470,7 @@ class GF2(metaclass=_GF2Meta): def __mod__(self, other: object) -> Self: """Modulo Division: `self % other`. - Modulo dividing `GF2` values is like modulo dividing `int`s. + Modulo dividing `gf2` values is like modulo dividing `int`s. Example usage: @@ -493,7 +497,7 @@ class GF2(metaclass=_GF2Meta): def __pow__(self, other: object, _modulo: Optional[object] = None) -> Self: """Exponentiation: `self ** other`. - Powers of `GF2` values are like powers of `int`s. + Powers of `gf2` values are like powers of `int`s. Example usage: @@ -516,16 +520,16 @@ class GF2(metaclass=_GF2Meta): return self._compute(other, lambda s, o: o**s) -numbers.Rational.register(GF2) +numbers.Rational.register(GF2Element) -class GF2One(GF2): +class GF2One(GF2Element): """The Galois field value `one`.""" _value = 1 -class GF2Zero(GF2): +class GF2Zero(GF2Element): """The Galois field value `zero`.""" _value = 0 @@ -533,3 +537,10 @@ class GF2Zero(GF2): one = GF2One() zero = GF2Zero() + + +# Outside this module the `GF2Element` is just "the `gf2` type" +gf2 = GF2Element + + +del GF2Meta diff --git a/tests/elements/test_gf2.py b/tests/elements/test_galois.py similarity index 92% rename from tests/elements/test_gf2.py rename to tests/elements/test_galois.py index 89ba236..5e82714 100644 --- a/tests/elements/test_gf2.py +++ b/tests/elements/test_galois.py @@ -1,4 +1,4 @@ -"""Test the `GF2` singeltons `one` and `zero`.""" +"""Test the `gf2` singeltons `one` and `zero`.""" import decimal import fractions @@ -11,25 +11,25 @@ import sys import pytest -from lalib.elements import gf2 +from lalib.elements import galois one, zero = ( - gf2.one, - gf2.zero, + galois.one, + galois.zero, ) -to_gf2 = gf2.to_gf2 +to_gf2 = galois.to_gf2 -GF2, GF2One, GF2Zero = ( - gf2.GF2, - gf2.GF2One, - gf2.GF2Zero, +gf2, GF2One, GF2Zero = ( + galois.gf2, + galois.GF2One, + galois.GF2Zero, ) -_THRESHOLD = gf2.THRESHOLD +_THRESHOLD = galois.THRESHOLD -del gf2 +del galois CROSS_REFERENCE = not os.environ.get("NO_CROSS_REFERENCE") @@ -158,16 +158,16 @@ class TestGF2Casting: to_gf2(float("NaN"), strict=strict) -@pytest.mark.parametrize("cls", [GF2, GF2One, GF2Zero]) +@pytest.mark.parametrize("cls", [gf2, GF2One, GF2Zero]) class TestGF2ConstructorWithCastedValue: - """Test the `GF2` class's constructor. + """Test the `gf2` class's constructor. - `GF2(value, ...)` returns either `one` or `zero`. + `gf2(value, ...)` returns either `one` or `zero`. """ @pytest.mark.parametrize("value", strict_one_like_values) def test_cast_ones_strictly(self, cls, value): - """`GF2(value, strict=True)` returns `one`.""" + """`gf2(value, strict=True)` returns `one`.""" result1 = cls(value) # `strict=True` by default assert result1 is one @@ -176,13 +176,13 @@ class TestGF2ConstructorWithCastedValue: @pytest.mark.parametrize("value", one_like_values) def test_cast_ones_not_strictly(self, cls, value): - """`GF2(value, strict=False)` returns `one`.""" + """`gf2(value, strict=False)` returns `one`.""" result = cls(value, strict=False) assert result is one @pytest.mark.parametrize("value", non_strict_one_like_values) def test_cannot_cast_ones_strictly(self, cls, value): - """`GF2(value, strict=False)` returns `1`.""" + """`gf2(value, strict=False)` returns `1`.""" with pytest.raises(ValueError, match="`1`-like or `0`-like"): cls(value) @@ -191,7 +191,7 @@ class TestGF2ConstructorWithCastedValue: @pytest.mark.parametrize("value", zero_like_values) def test_cast_zeros(self, cls, value): - """`GF2(value, strict=...)` returns `zero`.""" + """`gf2(value, strict=...)` returns `zero`.""" result1 = cls(value) # `strict=True` by default assert result1 is zero @@ -229,7 +229,7 @@ class TestGF2ConstructorWithCastedValue: @pytest.mark.parametrize("scaler", [1, 10, 100, 1000]) def test_get_one_if_within_threshold(self, cls, scaler): - """`GF2()` returns `one` if `value` is larger than `threshold`.""" + """`gf2()` returns `one` if `value` is larger than `threshold`.""" # `not_within_threshold` is larger than the `default_threshold` # but still different from `1` => `strict=False` value = scaler * not_within_threshold @@ -241,7 +241,7 @@ class TestGF2ConstructorWithCastedValue: @pytest.mark.parametrize("scaler", [1, 10, 100, 1000]) @pytest.mark.parametrize("strict", [True, False]) def test_get_zero_if_within_threshold(self, cls, scaler, strict): - """`GF2()` returns `zero` if `value` is smaller than `threshold`.""" + """`gf2()` returns `zero` if `value` is smaller than `threshold`.""" # `within_threshold` is smaller than the `default_threshold` value = scaler * within_threshold threshold = scaler * default_threshold @@ -252,9 +252,9 @@ class TestGF2ConstructorWithCastedValue: @pytest.mark.parametrize("strict", [True, False]) class TestGF2ConstructorWithoutCastedValue: - """Test the `GF2` class's constructor. + """Test the `gf2` class's constructor. - `GF2()` returns either `one` or `zero`. + `gf2()` returns either `one` or `zero`. """ def test_get_one_from_sub_class_with_no_input_value(self, strict): @@ -262,9 +262,9 @@ class TestGF2ConstructorWithoutCastedValue: result = GF2One(strict=strict) assert result is one - @pytest.mark.parametrize("cls", [GF2, GF2Zero]) + @pytest.mark.parametrize("cls", [gf2, GF2Zero]) def test_get_zero_with_no_input_value(self, cls, strict): - """`GF2()` and `GF2Zero()` return `zero`.""" + """`gf2()` and `GF2Zero()` return `zero`.""" result = cls(strict=strict) assert result is zero @@ -273,12 +273,12 @@ class TestGenericBehavior: """Test the classes behind `one` and `zero`.""" def test_cannot_instantiate_base_class_alone(self, monkeypatch): - """`GF2One` and `GF2Zero` must be instantiated before `GF2`.""" - monkeypatch.setattr(GF2, "_instances", {}) + """`GF2One` and `GF2Zero` must be instantiated before `gf2`.""" + monkeypatch.setattr(gf2, "_instances", {}) with pytest.raises(RuntimeError, match="internal error"): - GF2() + gf2() - @pytest.mark.parametrize("cls", [GF2, GF2One, GF2Zero]) + @pytest.mark.parametrize("cls", [gf2, GF2One, GF2Zero]) def test_create_singletons(self, cls): """Singleton pattern: The classes always return the same instance.""" first = cls() @@ -292,7 +292,7 @@ class TestGenericBehavior: the sub-classes that create `one` and `zero`. """ sub_cls = type(obj) - assert sub_cls is not GF2 + assert sub_cls is not gf2 new_obj = sub_cls() assert new_obj is obj @@ -311,7 +311,7 @@ class TestGenericBehavior: """`one` and `zero` are officially `Numbers`s.""" assert isinstance(obj, type_) - @pytest.mark.parametrize("cls", [GF2, GF2One, GF2Zero]) + @pytest.mark.parametrize("cls", [gf2, GF2One, GF2Zero]) @pytest.mark.parametrize( "method", [ @@ -339,8 +339,8 @@ class TestGenericBehavior: value, ): """Ensure all of `numbers.Rational`'s abstact methods are implemented.""" - monkeypatch.setattr(GF2, "_instances", {}) - monkeypatch.delattr(GF2, method) + monkeypatch.setattr(gf2, "_instances", {}) + monkeypatch.delattr(gf2, method) sub_cls = type("GF2Baby", (cls, numbers.Rational), {}) @@ -362,14 +362,14 @@ class TestGenericBehavior: @pytest.mark.parametrize("func", [repr, str]) @pytest.mark.parametrize("obj", [one, zero]) def test_text_repr_for_classes(self, func, obj): - """'GF2' is the text representation for all sub-classes ... + """'gf2' is the text representation for all sub-classes ... - ... which is valid code referring to the base class `GF2`. + ... which is valid code referring to the base class `gf2`. - `GF2()` returns `zero` if called without arguments. + `gf2()` returns `zero` if called without arguments. """ base_cls = eval(func(type(obj))) # noqa: S307 - assert base_cls is GF2 + assert base_cls is gf2 new_obj = base_cls() assert new_obj is zero @@ -595,10 +595,10 @@ class TestArithmetic: if CROSS_REFERENCE: # cast `one` and `zero` as `integer`s before doing the math - result3 = GF2((operator(int(abs(first)), int(abs(second))) + 2) % 2) + result3 = gf2((operator(int(abs(first)), int(abs(second))) + 2) % 2) assert result3 is expected - result4 = GF2((operator(int(abs(second)), int(abs(first))) + 2) % 2) + result4 = gf2((operator(int(abs(second)), int(abs(first))) + 2) % 2) assert result4 is expected @pytest.mark.parametrize( @@ -631,10 +631,10 @@ class TestArithmetic: if CROSS_REFERENCE: # cast `one` and `zero` as `integer`s before doing the math - result3 = GF2(int(abs(first)) * int(abs(second))) + result3 = gf2(int(abs(first)) * int(abs(second))) assert result3 is expected - result4 = GF2(int(abs(second)) * int(abs(first))) + result4 = gf2(int(abs(second)) * int(abs(first))) assert result4 is expected @pytest.mark.parametrize( @@ -668,7 +668,7 @@ class TestArithmetic: if CROSS_REFERENCE: # cast `one` and `zero` as `integer`s before doing the math - result2 = GF2(operator(int(abs(first)), int(abs(second)))) + result2 = gf2(operator(int(abs(first)), int(abs(second)))) assert result2 is expected @pytest.mark.parametrize( @@ -698,7 +698,7 @@ class TestArithmetic: if CROSS_REFERENCE: # cast `one` and `zero` as `integer`s before doing the math - result2 = GF2(int(abs(first)) % int(abs(second))) + result2 = gf2(int(abs(first)) % int(abs(second))) assert result2 is expected @pytest.mark.parametrize( @@ -775,7 +775,7 @@ class TestArithmetic: if CROSS_REFERENCE: # cast `one` and `zero` as `integer`s before doing the math - result2 = GF2(int(abs(first)) ** int(abs(second))) + result2 = gf2(int(abs(first)) ** int(abs(second))) assert result2 is expected @pytest.mark.parametrize("obj", [one, zero]) @@ -826,7 +826,7 @@ class TestArithmetic: ) def test_can_import_typing_extensions(): """For Python versions 3.11+ we do not need the "typing-extensions".""" - package = importlib.import_module("lalib.elements.gf2") + package = importlib.import_module("lalib.elements.galois") importlib.reload(package) assert package.Self is not None diff --git a/tests/test_docstrings.py b/tests/test_docstrings.py index dd4ac9f..f5a3f2d 100644 --- a/tests/test_docstrings.py +++ b/tests/test_docstrings.py @@ -15,7 +15,7 @@ import xdoctest [ "lalib", "lalib.elements", - "lalib.elements.gf2", + "lalib.elements.galois", ], ) def test_docstrings(module): From 348cd5376748391c39d14394d520d97e1d9ee6c9 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Thu, 19 Sep 2024 14:22:10 +0200 Subject: [PATCH 03/20] Make `lalib.elements.galois.to_gf2()` a detail - `to_gf2()` is only to be used within `GF2Element.__new__()` => rename it into `_to_gf2()` - the test cases in `TestGF2ConstructorWithCastedValue` cover everything in `TestGF2Casting` => retire the redundant test cases and make the test suite run faster --- src/lalib/elements/galois.py | 4 +- tests/elements/test_galois.py | 73 +---------------------------------- 2 files changed, 3 insertions(+), 74 deletions(-) diff --git a/src/lalib/elements/galois.py b/src/lalib/elements/galois.py index 7a60c37..979c9e0 100644 --- a/src/lalib/elements/galois.py +++ b/src/lalib/elements/galois.py @@ -46,7 +46,7 @@ except ImportError: # pragma: no cover to support Python 3.9 & 3.10 THRESHOLD = 1e-12 -def to_gf2( +def _to_gf2( value: complex, # `mypy` reads `complex | float | int` *, strict: bool = True, @@ -148,7 +148,7 @@ class GF2Element(metaclass=GF2Meta): msg = "Must create `one` and `zero` first (internal error)" raise RuntimeError(msg) from None else: - value = to_gf2(value, strict=strict, threshold=threshold) # type: ignore[arg-type] + value = _to_gf2(value, strict=strict, threshold=threshold) # type: ignore[arg-type] try: return cls._instances[value] diff --git a/tests/elements/test_galois.py b/tests/elements/test_galois.py index 5e82714..b3efa7a 100644 --- a/tests/elements/test_galois.py +++ b/tests/elements/test_galois.py @@ -19,8 +19,6 @@ one, zero = ( galois.zero, ) -to_gf2 = galois.to_gf2 - gf2, GF2One, GF2Zero = ( galois.gf2, galois.GF2One, @@ -89,77 +87,8 @@ def test_thresholds(): assert within_threshold < default_threshold < not_within_threshold -class TestGF2Casting: - """Test the `to_gf2()` function. - - `to_gf2(...)` casts numbers into either `1` or `0`. - """ - - @pytest.mark.parametrize("value", strict_one_like_values) - def test_cast_ones_strictly(self, value): - """`to_gf2(value, strict=True)` returns `1`.""" - result1 = to_gf2(value) # `strict=True` by default - assert result1 == 1 - - result2 = to_gf2(value, strict=True) - assert result2 == 1 - - @pytest.mark.parametrize("value", one_like_values) - def test_cast_ones_not_strictly(self, value): - """`to_gf2(value, strict=False)` returns `1`.""" - result = to_gf2(value, strict=False) - assert result == 1 - - @pytest.mark.parametrize("value", non_strict_one_like_values) - def test_cannot_cast_ones_strictly(self, value): - """`to_gf2(value, strict=False)` returns `1`.""" - with pytest.raises(ValueError, match="`1`-like or `0`-like"): - to_gf2(value) - - with pytest.raises(ValueError, match="`1`-like or `0`-like"): - to_gf2(value, strict=True) - - @pytest.mark.parametrize("value", zero_like_values) - def test_cast_zeros(self, value): - """`to_gf2(value, strict=...)` returns `0`.""" - result1 = to_gf2(value) # `strict=True` by default - assert result1 == 0 - - result2 = to_gf2(value, strict=True) - assert result2 == 0 - - result3 = to_gf2(value, strict=False) - assert result3 == 0 - - @pytest.mark.parametrize( - "value", - [ - complex(1, not_within_threshold), - complex(0, not_within_threshold), - ], - ) - @pytest.mark.parametrize("strict", [True, False]) - def test_cannot_cast_with_non_zero_imag_part(self, value, strict): - """Cannot create `1` or `0` if `.imag != 0`.""" - with pytest.raises(ValueError, match="`1`-like or `0`-like"): - to_gf2(value, strict=strict) - - @pytest.mark.parametrize("value", ["abc", (1,), [1]]) - @pytest.mark.parametrize("strict", [True, False]) - def test_cannot_cast_from_wrong_type(self, value, strict): - """Cannot create `1` or `0` from a non-numeric value.""" - with pytest.raises(TypeError): - to_gf2(value, strict=strict) - - @pytest.mark.parametrize("strict", [True, False]) - def test_cannot_cast_from_nan_value(self, strict): - """Cannot create `1` or `0` from undefined value.""" - with pytest.raises(ValueError, match="`1`-like or `0`-like"): - to_gf2(float("NaN"), strict=strict) - - @pytest.mark.parametrize("cls", [gf2, GF2One, GF2Zero]) -class TestGF2ConstructorWithCastedValue: +class TestGF2Casting: """Test the `gf2` class's constructor. `gf2(value, ...)` returns either `one` or `zero`. From 06d003b61589d0bac75dac5923fae75f269cb45e Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Thu, 19 Sep 2024 15:36:10 +0200 Subject: [PATCH 04/20] Hide `gf2` sub-classes even more - the `gf2` sub-classes `GF2One` and `GF2Zero` have no purpose other than to provide unique `help(one)` and `help(zero)` messages - delete them at the bottom of `lalib.elements.galois` to prevent them from being imported at all (`type(one)` and `type(zero)` still appear to be the `gf2` class, which is a little white lie, provided by some meta class) - differentiate between official and inofficial API in the test suite --- src/lalib/elements/galois.py | 6 ++++++ tests/elements/test_galois.py | 28 +++++++++++++++++++++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/lalib/elements/galois.py b/src/lalib/elements/galois.py index 979c9e0..329714c 100644 --- a/src/lalib/elements/galois.py +++ b/src/lalib/elements/galois.py @@ -523,6 +523,10 @@ class GF2Element(metaclass=GF2Meta): numbers.Rational.register(GF2Element) +# The `GF2One` and `GF2Zero` sub-classes' primary purpose +# is to give `one` and `zero` their very own `help()` message + + class GF2One(GF2Element): """The Galois field value `one`.""" @@ -544,3 +548,5 @@ gf2 = GF2Element del GF2Meta +del GF2One +del GF2Zero diff --git a/tests/elements/test_galois.py b/tests/elements/test_galois.py index b3efa7a..4920c82 100644 --- a/tests/elements/test_galois.py +++ b/tests/elements/test_galois.py @@ -14,15 +14,16 @@ import pytest from lalib.elements import galois -one, zero = ( +gf2, one, zero = ( # official API outside of `lalib.elements.galois` + galois.gf2, galois.one, galois.zero, ) -gf2, GF2One, GF2Zero = ( - galois.gf2, - galois.GF2One, - galois.GF2Zero, +GF2Element, GF2One, GF2Zero = ( # not part of the official API + galois.GF2Element, + type(galois.one), # The `GF2One` and `GF2Zero` sub-classes + type(galois.zero), # are deleted in `lalib.elements.galois` ) _THRESHOLD = galois.THRESHOLD @@ -87,11 +88,28 @@ def test_thresholds(): assert within_threshold < default_threshold < not_within_threshold +class TestGF2SubClasses: + """Test the sub-classes behind `one` and `zero`.""" + + def test_gf2_is_an_alias(self): + """The "`gf2` type" is really just `GF2Element`.""" + assert gf2 is GF2Element + + @pytest.mark.parametrize("cls", [GF2One, GF2Zero]) + def test_sub_classes_for_gf2(self, cls): + """`GF2One` and `GF2Zero` are sub-classes of `gf2`.""" + assert issubclass(cls, gf2) + + @pytest.mark.parametrize("cls", [gf2, GF2One, GF2Zero]) class TestGF2Casting: """Test the `gf2` class's constructor. `gf2(value, ...)` returns either `one` or `zero`. + + The sub-classes behind `one` and `zero` provide the + same functionality as `gf2` and have the sole purpose + of providing a unique `help()` message for `one` and `zero`. """ @pytest.mark.parametrize("value", strict_one_like_values) From 4c0c7887e5ffd7023e8de4b5d4f2c9b62d9e5ee7 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Fri, 27 Sep 2024 15:29:33 +0200 Subject: [PATCH 05/20] Allow and unify the usage of TODOs --- noxfile.py | 1 + poetry.lock | 20 +++++++++++++++++++- pyproject.toml | 3 +++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index bf01f0c..45830ce 100644 --- a/noxfile.py +++ b/noxfile.py @@ -205,6 +205,7 @@ def lint(session: nox.Session) -> None: "flake8-isort", "flake8-quotes", "flake8-string-format", + "flake8-todos", "flake8-pyproject", "flake8-pytest-style", "mypy", diff --git a/poetry.lock b/poetry.lock index 87fc215..f30fd5e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -704,6 +704,24 @@ files = [ [package.dependencies] flake8 = "*" +[[package]] +name = "flake8-todos" +version = "0.3.1" +description = "Python linter to check TODO comments for consistency and best practice." +optional = false +python-versions = ">=3.8" +files = [ + {file = "flake8_todos-0.3.1-py3-none-any.whl", hash = "sha256:0b4faca80bec49be7bf8e5dd46eec12f619e09cecac8a84a87775272300222b2"}, + {file = "flake8_todos-0.3.1.tar.gz", hash = "sha256:942101a08f831d7d9f7d864d86169f3bcfaf8b6d56a004c0280eb51725266b16"}, +] + +[package.dependencies] +flake8 = "*" +pycodestyle = "*" + +[package.extras] +dev = ["isort[pyproject]", "pytest"] + [[package]] name = "identify" version = "2.6.1" @@ -1681,4 +1699,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "e9e490a864511852844926112978e57c1421328f6231437f8f280ddfc88cecde" +content-hash = "d5e420036c32cb6043efd303ae45663287a7bda00036c94fd9497f9346fde701" diff --git a/pyproject.toml b/pyproject.toml index 95e4da8..2e4f244 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,6 +57,7 @@ flake8-eradicate = "^1.5" flake8-isort = "^6.1" flake8-quotes = "^3.4" flake8-string-format = "^0.3" +flake8-todos = "^0.3" flake8-pyproject = "^1.2" flake8-pytest-style = "^2.0" mypy = "^1.11" @@ -171,6 +172,7 @@ select = [ "PT", # flake8-pytest-style => enforce a consistent style with pytest "Q", # flake8-quotes => use double quotes everywhere (complying with black) "S", # flake8-bandit => common security issues + "T00", # flake8-todos => unify TODOs "T10", # flake8-debugger => no debugger usage # violations not covered by `ruff` below @@ -355,6 +357,7 @@ select = [ "PT", # flake8-pytest-style => enforce a consistent style with pytest "Q", # flake8-quotes => use double quotes everywhere "S", # flake8-bandit => common security issues + "TD", # flake8-todos => unify TODOs "T10", # flake8-debugger => no debugger usage # violations not covered by `flake8` above From 3d9f990c68b0b395240997e12da558b45ef82ff2 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Fri, 27 Sep 2024 16:04:37 +0200 Subject: [PATCH 06/20] Reset `random.seed()` before every test case --- noxfile.py | 5 +++++ poetry.lock | 17 ++++++++++++++++- pyproject.toml | 1 + 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 45830ce..24be972 100644 --- a/noxfile.py +++ b/noxfile.py @@ -230,11 +230,14 @@ TEST_DEPENDENCIES = ( "packaging", "pytest", "pytest-cov", + "pytest-randomly", "semver", 'typing-extensions; python_version < "3.11"', # to support Python 3.9 & 3.10 "xdoctest", ) +TEST_RANDOM_SEED = "--randomly-seed=42" + @nox.session(python=SUPPORTED_PYTHONS) def test(session: nox.Session) -> None: @@ -250,6 +253,7 @@ def test(session: nox.Session) -> None: args = posargs or ( "--cov", "--no-cov-on-fail", + TEST_RANDOM_SEED, TESTS_LOCATION, ) @@ -294,6 +298,7 @@ def test_coverage_run(session: nox.Session) -> None: "run", "-m", "pytest", + TEST_RANDOM_SEED, TESTS_LOCATION, ) diff --git a/poetry.lock b/poetry.lock index f30fd5e..c402d6e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1219,6 +1219,21 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] +[[package]] +name = "pytest-randomly" +version = "3.15.0" +description = "Pytest plugin to randomly order tests and control random.seed." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_randomly-3.15.0-py3-none-any.whl", hash = "sha256:0516f4344b29f4e9cdae8bce31c4aeebf59d0b9ef05927c33354ff3859eeeca6"}, + {file = "pytest_randomly-3.15.0.tar.gz", hash = "sha256:b908529648667ba5e54723088edd6f82252f540cc340d748d1fa985539687047"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} +pytest = "*" + [[package]] name = "pyyaml" version = "6.0.2" @@ -1699,4 +1714,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "d5e420036c32cb6043efd303ae45663287a7bda00036c94fd9497f9346fde701" +content-hash = "31a7b1ad8dd8949814d1f397022e400cd37643c6f5e9e438054de472091bcd69" diff --git a/pyproject.toml b/pyproject.toml index 2e4f244..086646f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,6 +77,7 @@ coverage = "^7.6" packaging = "^24.1" # to test the version identifier pytest = "^8.3" pytest-cov = "^5.0" +pytest-randomly = "^3.15" semver = "^3.0" # to test the version identifier tomli = [ { python = "<3.11", version = "^2.0" } ] xdoctest = { extras = ["colors"], version = "^1.2" } From 08067b3d6eeb623f5174abc2d2932670e11c27d3 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Fri, 27 Sep 2024 16:16:01 +0200 Subject: [PATCH 07/20] Fix pytest not respecting the singleton pattern This is only a temporary fix! See issue: https://github.com/webartifex/lalib/issues/1 --- src/lalib/elements/galois.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/lalib/elements/galois.py b/src/lalib/elements/galois.py index 329714c..03a9ea2 100644 --- a/src/lalib/elements/galois.py +++ b/src/lalib/elements/galois.py @@ -316,7 +316,13 @@ class GF2Element(metaclass=GF2Meta): except (TypeError, ValueError): return False else: - return self is other # `one` and `zero` are singletons + # TODO(webartifex): investigate the below issue + # https://github.com/webartifex/lalib/issues/1 + # `one` and `zero` are singletons + # => yet, the following does not work with `pytest` + # in Python 3.9 & 3.10: `return self is other` + # => for now, use the following fix: + return int(self) == int(other) def __lt__(self, other: object) -> bool: """Comparison: `self < other`. From febed693b8ede464a98b7ce3ba01b9b57d83b20c Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Mon, 14 Oct 2024 14:38:09 +0200 Subject: [PATCH 08/20] Make `assert`s more Pythonic --- tests/test_version.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/test_version.py b/tests/test_version.py index be02fae..acc3d6c 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -292,9 +292,9 @@ class VersionClassification: ) if is_so_by_parts: - assert parsed_version.is_devrelease is True - assert parsed_version.is_prerelease is True - assert parsed_version.is_postrelease is False + assert parsed_version.is_devrelease + assert parsed_version.is_prerelease + assert not parsed_version.is_postrelease return is_so_by_parts @@ -307,9 +307,9 @@ class VersionClassification: ) if is_so_by_parts: - assert parsed_version.is_devrelease is False - assert parsed_version.is_prerelease is True - assert parsed_version.is_postrelease is False + assert not parsed_version.is_devrelease + assert parsed_version.is_prerelease + assert not parsed_version.is_postrelease return is_so_by_parts @@ -322,9 +322,9 @@ class VersionClassification: ) if is_so_by_parts: - assert parsed_version.is_devrelease is False - assert parsed_version.is_prerelease is False - assert parsed_version.is_postrelease is False + assert not parsed_version.is_devrelease + assert not parsed_version.is_prerelease + assert not parsed_version.is_postrelease return is_so_by_parts @@ -337,9 +337,9 @@ class VersionClassification: ) if is_so_by_parts: - assert parsed_version.is_devrelease is False - assert parsed_version.is_prerelease is False - assert parsed_version.is_postrelease is True + assert not parsed_version.is_devrelease + assert not parsed_version.is_prerelease + assert parsed_version.is_postrelease return is_so_by_parts From cbc1f8fd3a9a19164d6f467e1ecb99e6c9c93789 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Mon, 14 Oct 2024 14:50:02 +0200 Subject: [PATCH 09/20] Add `lalib.config` for library-wide settings Also, refactor `lalib.elements.galois` (incl. tests) to use the new settings --- src/lalib/config.py | 5 +++++ src/lalib/elements/galois.py | 12 +++++++----- tests/elements/test_galois.py | 9 ++++----- 3 files changed, 16 insertions(+), 10 deletions(-) create mode 100644 src/lalib/config.py diff --git a/src/lalib/config.py b/src/lalib/config.py new file mode 100644 index 0000000..5a20788 --- /dev/null +++ b/src/lalib/config.py @@ -0,0 +1,5 @@ +"""Library-wide default settings.""" + +NDIGITS = 12 + +THRESHOLD = 1 / (10**NDIGITS) diff --git a/src/lalib/elements/galois.py b/src/lalib/elements/galois.py index 03a9ea2..dd2bfad 100644 --- a/src/lalib/elements/galois.py +++ b/src/lalib/elements/galois.py @@ -43,14 +43,14 @@ except ImportError: # pragma: no cover to support Python 3.9 & 3.10 from typing_extensions import Self -THRESHOLD = 1e-12 +from lalib import config def _to_gf2( value: complex, # `mypy` reads `complex | float | int` *, strict: bool = True, - threshold: float = THRESHOLD, + threshold: float = config.THRESHOLD, ) -> int: """Cast a number as a possible Galois field value: `1` or `0`. @@ -132,7 +132,7 @@ class GF2Element(metaclass=GF2Meta): value: object = None, *, strict: bool = True, - threshold: float = THRESHOLD, + threshold: float = config.THRESHOLD, ) -> Self: """See docstring for `.__init__()`.""" if isinstance(value, cls): @@ -162,7 +162,7 @@ class GF2Element(metaclass=GF2Meta): value: object = None, *, strict: bool = True, - threshold: float = THRESHOLD, + threshold: float = config.THRESHOLD, ) -> None: """Obtain one of two objects: `one` or `zero`. @@ -242,7 +242,7 @@ class GF2Element(metaclass=GF2Meta): """Round `self` up to the next `int`: `math.ceil(self)`.""" return int(self) - def __round__(self, ndigits: Optional[int] = 0) -> int: + def __round__(self, _ndigits: int = config.NDIGITS) -> int: """Round `self` to the next `int`: `round(self)`.""" return int(self) @@ -556,3 +556,5 @@ gf2 = GF2Element del GF2Meta del GF2One del GF2Zero + +del config diff --git a/tests/elements/test_galois.py b/tests/elements/test_galois.py index 4920c82..1af29a9 100644 --- a/tests/elements/test_galois.py +++ b/tests/elements/test_galois.py @@ -11,6 +11,7 @@ import sys import pytest +from lalib import config from lalib.elements import galois @@ -26,17 +27,15 @@ GF2Element, GF2One, GF2Zero = ( # not part of the official API type(galois.zero), # are deleted in `lalib.elements.galois` ) -_THRESHOLD = galois.THRESHOLD - del galois CROSS_REFERENCE = not os.environ.get("NO_CROSS_REFERENCE") -default_threshold = _THRESHOLD -within_threshold = _THRESHOLD / 10 -not_within_threshold = _THRESHOLD * 10 +default_threshold = config.THRESHOLD +within_threshold = config.THRESHOLD / 10 +not_within_threshold = config.THRESHOLD * 10 strict_one_like_values = ( 1, From 153094eef52d6c1604a4493d2505f523983e9198 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Mon, 14 Oct 2024 15:17:42 +0200 Subject: [PATCH 10/20] Add `Q`, `R`, `C`, and `GF2` fields - add `lalib.fields.base.Field`, a blueprint for all concrete fields, providing a unified interface to be used outside of the `lalib.fields` sub-package - implement `lalib.fields.complex_.ComplexField`, or `C` for short, the field over the complex numbers (modeled as `complex` numbers) - implement `lalib.fields.galois.GaloisField2`, or `GF2` for short, the (finite) field over the two elements `one` and `zero` + adapt `lalib.elements.galois.GF2Element.__eq__()` to return `NotImplemented` instead of `False` for non-castable `other`s => this fixes a minor issue with `pytest.approx()` - implement `lalib.fields.rational.RationalField`, or `Q` for short, the field over the rational numbers (modeled as `fractions.Fraction`s) - implement `lalib.fields.real.RealField`, or `R` for short, the field over the real numbers (modeled as `float`s) - organize top-level imports for `lalib.fields`, making `Q`, `R`, `C`, and `GF2` importable with `from lalib.fields import *` - provide extensive unit and integration tests for the new objects: + test generic and common behavior in `tests.fields.test_base` + test specific behavior is other modules + test the well-known math axioms for all fields (integration tests) + test the new objects' docstrings + add "pytest-repeat" to run randomized tests many times --- noxfile.py | 2 + poetry.lock | 16 ++- pyproject.toml | 3 + src/lalib/config.py | 8 ++ src/lalib/elements/galois.py | 2 +- src/lalib/fields/__init__.py | 31 +++++ src/lalib/fields/base.py | 227 ++++++++++++++++++++++++++++++ src/lalib/fields/complex_.py | 110 +++++++++++++++ src/lalib/fields/galois.py | 45 ++++++ src/lalib/fields/rational.py | 90 ++++++++++++ src/lalib/fields/real.py | 97 +++++++++++++ src/lalib/fields/utils.py | 22 +++ tests/fields/test_axioms.py | 92 +++++++++++++ tests/fields/test_base.py | 253 ++++++++++++++++++++++++++++++++++ tests/fields/test_complex.py | 96 +++++++++++++ tests/fields/test_galois.py | 100 ++++++++++++++ tests/fields/test_rational.py | 29 ++++ tests/fields/utils.py | 75 ++++++++++ tests/test_docstrings.py | 6 + 19 files changed, 1302 insertions(+), 2 deletions(-) create mode 100644 src/lalib/fields/base.py create mode 100644 src/lalib/fields/complex_.py create mode 100644 src/lalib/fields/galois.py create mode 100644 src/lalib/fields/rational.py create mode 100644 src/lalib/fields/real.py create mode 100644 src/lalib/fields/utils.py create mode 100644 tests/fields/test_axioms.py create mode 100644 tests/fields/test_base.py create mode 100644 tests/fields/test_complex.py create mode 100644 tests/fields/test_galois.py create mode 100644 tests/fields/test_rational.py create mode 100644 tests/fields/utils.py diff --git a/noxfile.py b/noxfile.py index 24be972..fc0fee2 100644 --- a/noxfile.py +++ b/noxfile.py @@ -231,6 +231,7 @@ TEST_DEPENDENCIES = ( "pytest", "pytest-cov", "pytest-randomly", + "pytest-repeat", "semver", 'typing-extensions; python_version < "3.11"', # to support Python 3.9 & 3.10 "xdoctest", @@ -290,6 +291,7 @@ def test_coverage_run(session: nox.Session) -> None: session.install(".") install_pinned(session, "coverage", *TEST_DEPENDENCIES) + session.env["N_RANDOM_DRAWS"] = "10" session.env["NO_CROSS_REFERENCE"] = "true" session.run( "python", diff --git a/poetry.lock b/poetry.lock index c402d6e..0c5eed0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1234,6 +1234,20 @@ files = [ importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} pytest = "*" +[[package]] +name = "pytest-repeat" +version = "0.9.3" +description = "pytest plugin for repeating tests" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest_repeat-0.9.3-py3-none-any.whl", hash = "sha256:26ab2df18226af9d5ce441c858f273121e92ff55f5bb311d25755b8d7abdd8ed"}, + {file = "pytest_repeat-0.9.3.tar.gz", hash = "sha256:ffd3836dfcd67bb270bec648b330e20be37d2966448c4148c4092d1e8aba8185"}, +] + +[package.dependencies] +pytest = "*" + [[package]] name = "pyyaml" version = "6.0.2" @@ -1714,4 +1728,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "31a7b1ad8dd8949814d1f397022e400cd37643c6f5e9e438054de472091bcd69" +content-hash = "a10f3c13ed0c67e6ea65500335281cd269e3efdd0fae544ad667d07637ef118f" diff --git a/pyproject.toml b/pyproject.toml index 086646f..3114aae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,6 +78,7 @@ packaging = "^24.1" # to test the version identifier pytest = "^8.3" pytest-cov = "^5.0" pytest-randomly = "^3.15" +pytest-repeat = "^0.9" semver = "^3.0" # to test the version identifier tomli = [ { python = "<3.11", version = "^2.0" } ] xdoctest = { extras = ["colors"], version = "^1.2" } @@ -381,6 +382,8 @@ extend-ignore = [ # never check the following codes ] +allowed-confusables = ["ℂ", "ℝ", "ℚ"] + [tool.ruff.lint.flake8-pytest-style] # aligned with [tool.flake8] above diff --git a/src/lalib/config.py b/src/lalib/config.py index 5a20788..e27ced9 100644 --- a/src/lalib/config.py +++ b/src/lalib/config.py @@ -1,5 +1,13 @@ """Library-wide default settings.""" +import math + + NDIGITS = 12 THRESHOLD = 1 / (10**NDIGITS) + +MAX_DENOMINATOR = math.trunc(1 / THRESHOLD) + + +del math diff --git a/src/lalib/elements/galois.py b/src/lalib/elements/galois.py index dd2bfad..e2651b2 100644 --- a/src/lalib/elements/galois.py +++ b/src/lalib/elements/galois.py @@ -314,7 +314,7 @@ class GF2Element(metaclass=GF2Meta): try: other = GF2Element(other) except (TypeError, ValueError): - return False + return NotImplemented else: # TODO(webartifex): investigate the below issue # https://github.com/webartifex/lalib/issues/1 diff --git a/src/lalib/fields/__init__.py b/src/lalib/fields/__init__.py index 1db1a3b..c03b9b8 100644 --- a/src/lalib/fields/__init__.py +++ b/src/lalib/fields/__init__.py @@ -1 +1,32 @@ """A collection of common fields used in linear algebra.""" + +from lalib.fields import base +from lalib.fields import complex_ +from lalib.fields import galois +from lalib.fields import rational +from lalib.fields import real +from lalib.fields import utils + + +Field = base.Field + +Q = rational.Q +R = real.R +C = complex_.C +GF2 = galois.GF2 + + +del base +del complex_ +del galois +del rational +del real +del utils # `import`ed and `del`eted to not be in the final namespace + + +__all__ = ( + "Q", + "R", + "C", + "GF2", +) diff --git a/src/lalib/fields/base.py b/src/lalib/fields/base.py new file mode 100644 index 0000000..0e94569 --- /dev/null +++ b/src/lalib/fields/base.py @@ -0,0 +1,227 @@ +"""The abstract blueprint of a `Field`.""" + +import abc +import numbers +from typing import Any, Callable, Generic, TypeVar + + +T = TypeVar("T", bound=numbers.Real) + + +class Field(abc.ABC, Generic[T]): + """The abstract blueprint of a mathematical field.""" + + @property + @abc.abstractmethod + def _math_name(self) -> str: + """The common abbreviation used in math notation.""" + + @staticmethod + @abc.abstractmethod + def _dtype(value: Any, /) -> T: + """Data type to store the `Field` elements.""" + + def _cast_func(self, value: Any, /, **kwargs: Any) -> T: + """Function to cast `value`s as field elements.""" + return self._dtype(value, **kwargs) + + @staticmethod + def _post_cast_filter(possible_element: T, /) -> bool: + """Function to filter out castable `value`s. + + Called after a successfull call of the `._cast_func()`. + + For example, if one wants to avoid non-finite `Field` elements, + an overwriting `._post_cast_filter()` could return `False` for + non-finite `value`s like `float("NaN")`. + + By default, all castable `value`s may become `Field` elements. + """ + return True + + @property + @abc.abstractmethod + def _additive_identity(self) -> T: + """The field's additive identity.""" + + @property + @abc.abstractmethod + def _multiplicative_identity(self) -> T: + """The field's multiplicative identity.""" + + def cast( + self, + value: object, + /, + **cast_kwargs: Any, + ) -> T: + """Cast a (numeric) `value` as an element of the field. + + Args: + value: to be cast as the "right" data type + as defined in the concrete `Field` sub-class + **cast_kwargs: extra `kwargs` to the `._cast_func()` + + Returns: + element: of the concrete `Field` + + Raises: + ValueError: `value` is not an element of the field + """ + try: + element = self._cast_func(value, **cast_kwargs) # type: ignore[arg-type] + except (ArithmeticError, TypeError, ValueError): + msg = "`value` is not an element of the field" + raise ValueError(msg) from None + + if not self._post_cast_filter(element): + msg = "`value` is not an element of the field" + raise ValueError(msg) + + return element + + def validate( + self, + value: object, + /, + *, + silent: bool = True, + **cast_kwargs: Any, + ) -> bool: + """Check if a (numeric) `value` is an element of the `Field`. + + Wraps `.cast()`, catches the documented `ValueError`, + and returns a `bool`ean indicating field membership. + + Args: + value: see docstring for `.cast()` + silent: suppress the `ValueError` + **cast_kwargs: extra `kwargs` to `.cast()` the `value` + + Returns: + is_element + + Raises: + ValueError: `value` is not `.cast()`able + (suppressed by default) + """ + try: + self.cast(value, **cast_kwargs) + except ValueError: + if not silent: + raise + return False + + return True + + @property + def dtype(self) -> Callable[[Any], T]: + """Data type to store the `Field` elements.""" + return self._dtype + + @property + def zero(self) -> T: + """The field's additive identity.""" + return self._additive_identity + + def is_zero(self, value: T, /, **cast_kwargs: Any) -> bool: + """Check if `value` equals the `.zero`-like field element. + + This method, together with `.is_one()` below, provides a unified + way across the different `Field`s to check if a given `value` + equals the field's additive or multiplicative identity. + + Concrete `Field`s may use a different logic. For example, some + compare the absolute difference between the `value` and the + `.zero`-or-`.one`-like field element to a small `threshold`. + + Overwriting methods should + - check the `value` for field membership first, and + - accept arbitrary keyword-only arguments + that they may simply ignore + + Args: + value: see docstring for `.cast()` + **cast_kwargs: extra `kwargs` to `.cast()` the `value` + + Returns: + is_zero + + Raises: + ValueError: `value` is not `.cast()`able + """ + return self.cast(value, **cast_kwargs) == self._additive_identity + + @property + def one(self) -> T: + """The field's multiplicative identity.""" + return self._multiplicative_identity + + def is_one(self, value: T, /, **cast_kwargs: Any) -> bool: + """Check if `value` equals the `.one`-like field element. + + See docstring for `.is_zero()` above for more details. + + Args: + value: see docstring for `.cast()` + **cast_kwargs: extra `kwargs` to `.cast()` the `value` + + Returns: + is_one + + Raises: + ValueError: `value` is not `.cast()`able + """ + return self.cast(value, **cast_kwargs) == self._multiplicative_identity + + @abc.abstractmethod + def random( + self, + *, + lower: T, + upper: T, + **cast_kwargs: Any, + ) -> T: + """Draw a uniformly distributed random element from the field. + + `lower` and `upper` may come in reversed order. + Overwriting methods should sort them if necessary. + + Extra keyword arguments should be passed through to `.cast()`. + """ + + def _get_bounds( + self, + lower: T, + upper: T, + /, + **cast_kwargs: Any, + ) -> tuple[T, T]: + """Get the `lower` & `upper` bounds for `Field.random()`. + + Utility method to either + - resolve the given `lower` and `upper` bounds into a + `.cast()`ed element of the `Field`, or + - obtain their default `value`s, which are + + the `.additive_identity` for `lower`, and + + the `.multiplicative_identity` for `upper` + + Extra keyword arguments are passed through to `.cast()`. + """ + lower = lower if lower is not None else self._additive_identity + upper = upper if upper is not None else self._multiplicative_identity + + return (self.cast(lower, **cast_kwargs), self.cast(upper, **cast_kwargs)) + + def __repr__(self) -> str: + """Text representations: `repr(...)` and `str(...)`. + + The `.math_name` should be a valid name within `lalib`. + If not, use the "" convention; in other words, + the text representation is no valid code on its own. + + See: https://docs.python.org/3/reference/datamodel.html#object.__repr__ + """ + return self._math_name + + __str__ = __repr__ diff --git a/src/lalib/fields/complex_.py b/src/lalib/fields/complex_.py new file mode 100644 index 0000000..533f5bf --- /dev/null +++ b/src/lalib/fields/complex_.py @@ -0,0 +1,110 @@ +"""The concrete `ComplexField`.""" + +import cmath +import random +from typing import Any + +from lalib import config +from lalib.fields import base +from lalib.fields import utils + + +class ComplexField(utils.SingletonMixin, base.Field): + """The `Field` over ℂ, the complex numbers.""" + + _math_name = "ℂ" + _dtype = complex + _post_cast_filter = cmath.isfinite + _additive_identity = 0 + 0j + _multiplicative_identity = 1 + 0j + + def random( + self, + *, + lower: complex = _additive_identity, + upper: complex = _multiplicative_identity, + ndigits: int = config.NDIGITS, + **_cast_kwargs: Any, + ) -> complex: + """Draw a uniformly distributed random element from the field. + + The `.real` and `.imag`inary parts of the `lower` and `upper` + bounds evaluated separately; i.e., the `random_element` is drawn + from a rectangle with opposing corners `lower` and `upper`. + + `lower` and `upper` may come in reversed order. + + Args: + lower: bound of the random interval + upper: bound of the random interval + ndigits: no. of significant digits to the right of the "."; + both the `.real` and the `.imag`inary parts are rounded + + Returns: + random_element + + Raises: + ValueError: `lower` and `upper` are not `.cast()`able + """ + lower, upper = self._get_bounds(lower, upper) + + random_real, random_imag = ( + # `random.uniform()` can handle `upper < lower` + round(random.uniform(lower.real, upper.real), ndigits), # noqa: S311 + round(random.uniform(lower.imag, upper.imag), ndigits), # noqa: S311 + ) + + return complex(random_real, random_imag) + + def is_zero( + self, + value: complex, + /, + *, + threshold: float = config.THRESHOLD, + **_cast_kwargs: Any, + ) -> bool: + """Check if `value` equals `0.0 + 0.0j`. + + To be precise: Check if `value` deviates by less than the + `threshold` from `0.0 + 0.0j` in absolute terms. + + Args: + value: to be compared to `0.0 + 0.0j` + threshold: for the equality check + + Returns: + is_zero + + Raises: + ValueError: `value` is not `.cast()`able + """ + return abs(self.cast(value)) < threshold + + def is_one( + self, + value: complex, + /, + *, + threshold: float = config.THRESHOLD, + **_cast_kwargs: Any, + ) -> bool: + """Check if `value` equals `1.0 + 0.0j`. + + To be precise: Check if `value` deviates by less than the + `threshold` from `1.0 + 0.0j` in absolute terms. + + Args: + value: to be compared to `1.0 + 0.0j` + threshold: for the equality check + + Returns: + is_one + + Raises: + ValueError: `value` is not `.cast()`able + """ + return abs(self.cast(value) - (1.0 + 0j)) < threshold + + +C = ComplexField() diff --git a/src/lalib/fields/galois.py b/src/lalib/fields/galois.py new file mode 100644 index 0000000..cbfd3b3 --- /dev/null +++ b/src/lalib/fields/galois.py @@ -0,0 +1,45 @@ +"""The concrete `GaloisField2`.""" + +import random +from typing import Any + +from lalib.elements import galois as gf2_elements +from lalib.fields import base +from lalib.fields import utils + + +class GaloisField2(utils.SingletonMixin, base.Field): + """The Galois `Field` of 2 elements.""" + + _math_name = "GF2" + _dtype = gf2_elements.GF2Element + _additive_identity = gf2_elements.zero + _multiplicative_identity = gf2_elements.one + + def random( + self, + lower: gf2_elements.GF2Element = gf2_elements.zero, + upper: gf2_elements.GF2Element = gf2_elements.one, + **cast_kwargs: Any, + ) -> gf2_elements.GF2Element: + """Draw a uniformly distributed random element from the field. + + Args: + lower: bound of the random interval + upper: bound of the random interval + **cast_kwargs: extra `kwargs` to `.cast()` + the `lower` and `upper` bounds + + Returns: + random_element: either `one` or `zero` + + Raises: + ValueError: `lower` and `upper` are not `.cast()`able + """ + lower, upper = self._get_bounds(lower, upper, **cast_kwargs) + + # `random.choice()` can handle `upper < lower` + return random.choice((lower, upper)) # noqa: S311 + + +GF2 = GaloisField2() diff --git a/src/lalib/fields/rational.py b/src/lalib/fields/rational.py new file mode 100644 index 0000000..9894c6a --- /dev/null +++ b/src/lalib/fields/rational.py @@ -0,0 +1,90 @@ +"""The concrete `RationalField`.""" + +import fractions +import random +from typing import Any + +from lalib import config +from lalib.fields import base +from lalib.fields import utils + + +class RationalField(utils.SingletonMixin, base.Field): + """The `Field` over ℚ, the rational numbers. + + Although `Q.cast()` accepts `float`s as possible field elements, + do so only with care as `float`s are inherently imprecise numbers: + + >>> 0.1 + 0.2 + 0.30000000000000004 + + To mitigate this, `Q.cast()` cuts off the decimals, as configured + with the `MAX_DENOMINATOR` setting. So, `float`s with just a couple + of digits return the possibly desired field element. For example: + + >>> Q.cast(0.1) + Fraction(1, 10) + + Yet, with the hidden `max_denominator` argument, we can easily + see how `float`s may result in "weird" `Fraction`s. + + >>> Q.cast(0.1, max_denominator=1_000_000_000_000) + Fraction(1, 10) + >>> Q.cast(0.1, max_denominator=1_000_000_000_000_000_000) + Fraction(3602879701896397, 36028797018963968) + + It is recommended to use `str`ings instead: + + >>> Q.cast("0.1") + Fraction(1, 10) + >>> Q.cast("1/10") + Fraction(1, 10) + """ + + _math_name = "ℚ" + _dtype = fractions.Fraction + + def _cast_func( + self, + value: Any, + /, + max_denominator: int = config.MAX_DENOMINATOR, + **_kwargs: Any, + ) -> fractions.Fraction: + return fractions.Fraction(value).limit_denominator(max_denominator) + + _additive_identity = fractions.Fraction(0, 1) + _multiplicative_identity = fractions.Fraction(1, 1) + + def random( + self, + *, + lower: fractions.Fraction = _additive_identity, + upper: fractions.Fraction = _multiplicative_identity, + max_denominator: int = config.MAX_DENOMINATOR, + **_cast_kwargs: Any, + ) -> fractions.Fraction: + """Draw a uniformly distributed random element from the field. + + `lower` and `upper` may come in reversed order. + + Args: + lower: bound of the random interval + upper: bound of the random interval + max_denominator: maximum for `random_element.denominator` + + Returns: + random_element + + Raises: + ValueError: `lower` and `upper` are not `.cast()`able + """ + lower, upper = self._get_bounds(lower, upper) + + # `random.uniform()` can handle `upper < lower` + random_value = random.uniform(float(lower), float(upper)) # noqa: S311 + + return self._cast_func(random_value).limit_denominator(max_denominator) + + +Q = RationalField() diff --git a/src/lalib/fields/real.py b/src/lalib/fields/real.py new file mode 100644 index 0000000..aaa7c26 --- /dev/null +++ b/src/lalib/fields/real.py @@ -0,0 +1,97 @@ +"""The concrete `RealField`.""" + +import math +import random +from typing import Any + +from lalib import config +from lalib.fields import base +from lalib.fields import utils + + +class RealField(utils.SingletonMixin, base.Field): + """The `Field` over ℝ, the real numbers.""" + + _math_name = "ℝ" + _dtype = float + _post_cast_filter = math.isfinite + _additive_identity = 0.0 + _multiplicative_identity = 1.0 + + def random( + self, + *, + lower: float = _additive_identity, + upper: float = _multiplicative_identity, + ndigits: int = config.NDIGITS, + **_cast_kwargs: Any, + ) -> float: + """Draw a uniformly distributed random element from the field. + + `lower` and `upper` may come in reversed order. + + Args: + lower: bound of the random interval + upper: bound of the random interval + ndigits: no. of significant digits to the right of the "." + + Returns: + random_element + + Raises: + ValueError: `lower` and `upper` are not `.cast()`able + """ + lower, upper = self._get_bounds(lower, upper) + + # `random.uniform()` can handle `upper < lower` + lower, upper = float(lower), float(upper) + rand_value = random.uniform(lower, upper) # noqa: S311 + + return round(rand_value, ndigits) + + def is_zero( + self, + value: float, + /, + *, + threshold: float = config.THRESHOLD, + **_cast_kwargs: Any, + ) -> bool: + """Check if `value` equals `0.0`. + + Args: + value: to be compared to `0.0` + threshold: for the equality check + + Returns: + is_zero (boolean) + + Raises: + ValueError: `value` is not `.cast()`able + """ + return abs(self.cast(value)) < threshold + + def is_one( + self, + value: float, + /, + *, + threshold: float = config.THRESHOLD, + **_cast_kwargs: Any, + ) -> bool: + """Check if `value` equals `1.0`. + + Args: + value: to be compared to `1.0` + threshold: for the equality check + + Returns: + is_one + + Raises: + ValueError: `value` is not `.cast()`able + """ + return abs(self.cast(value) - 1.0) < threshold + + +R = RealField() diff --git a/src/lalib/fields/utils.py b/src/lalib/fields/utils.py new file mode 100644 index 0000000..5bf697a --- /dev/null +++ b/src/lalib/fields/utils.py @@ -0,0 +1,22 @@ +"""Generic utilities for the library.""" + +from typing import Any + + +try: + from typing import Self +except ImportError: # pragma: no cover to support Python 3.9 & 3.10 + from typing_extensions import Self + + +class SingletonMixin: + """Utility class to provide singleton pattern implementation.""" + + _instance: Self + + @staticmethod + def __new__(cls: type[Self], *args: Any, **kwargs: Any) -> Self: + """Check if the `_instance` already exists.""" + if getattr(cls, "_instance", None) is None: + cls._instance = super().__new__(cls, *args, **kwargs) + return cls._instance diff --git a/tests/fields/test_axioms.py b/tests/fields/test_axioms.py new file mode 100644 index 0000000..67ddf70 --- /dev/null +++ b/tests/fields/test_axioms.py @@ -0,0 +1,92 @@ +"""Ensure all `Field`s fulfill the axioms from math. + +Source: https://en.wikipedia.org/wiki/Field_(mathematics)#Classic_definition +""" + +import contextlib +import operator + +import pytest + +from tests.fields import utils + + +@pytest.mark.repeat(utils.N_RANDOM_DRAWS) +@pytest.mark.parametrize("field", utils.ALL_FIELDS) +class TestAllFieldsManyTimes: + """Run the tests many times for all `field`s.""" + + @pytest.mark.parametrize("opr", [operator.add, operator.mul]) + def test_associativity(self, field, opr): + """`a + (b + c) == (a + b) + c` ... + + ... and `a * (b * c) == (a * b) * c`. + """ + a, b, c = field.random(), field.random(), field.random() + + left = opr(a, opr(b, c)) + right = opr(opr(a, b), c) + + assert left == pytest.approx(right, abs=utils.DEFAULT_THRESHOLD) + + @pytest.mark.parametrize("opr", [operator.add, operator.mul]) + def test_commutativity(self, field, opr): + """`a + b == b + a` ... + + ... and `a * b == b * a`. + """ + a, b = field.random(), field.random() + + left = opr(a, b) + right = opr(b, a) + + assert left == pytest.approx(right, abs=utils.DEFAULT_THRESHOLD) + + def test_additive_identity(self, field): + """`a + 0 == a`.""" + a = field.random() + + left = a + field.zero + right = a + + assert left == pytest.approx(right, abs=utils.DEFAULT_THRESHOLD) + + def test_multiplicative_identity(self, field): + """`a * 1 == a`.""" + a = field.random() + + left = a * field.one + right = a + + assert left == pytest.approx(right, abs=utils.DEFAULT_THRESHOLD) + + def test_additive_inverse(self, field): + """`a + (-a) == 0`.""" + a = field.random() + + left = a + (-a) + right = field.zero + + assert left == pytest.approx(right, abs=utils.DEFAULT_THRESHOLD) + + def test_multiplicative_inverse(self, field): + """`a * (1 / a) == 1`.""" + a = field.random() + + # Realistically, `ZeroDivisionError` only occurs for `GF2` + # => With a high enough `utils.N_RANDOM_DRAWS` + # this test case is also `assert`ed for `GF2` + with contextlib.suppress(ZeroDivisionError): + left = a * (field.one / a) + right = field.one + + assert left == pytest.approx(right, abs=utils.DEFAULT_THRESHOLD) + + def test_distributivity(self, field): + """`a * (b + c) == (a * b) + (a * c)`.""" + a, b, c = field.random(), field.random(), field.random() + + left = a * (b + c) + right = (a * b) + (a * c) + + assert left == pytest.approx(right, abs=utils.DEFAULT_THRESHOLD) diff --git a/tests/fields/test_base.py b/tests/fields/test_base.py new file mode 100644 index 0000000..31ac80f --- /dev/null +++ b/tests/fields/test_base.py @@ -0,0 +1,253 @@ +"""Generic tests for all `lalib.fields.*.Field`s. + +The abstract base class `lalib.fields.base.Field` +defines generic behavior that all concrete `Field`s +in the `lalib.fields` sub-package must implement. +""" + +import random + +import pytest + +from lalib import fields +from tests.fields import utils + + +@pytest.mark.parametrize("field", utils.ALL_FIELDS) +class TestGenericClassBehavior: + """Generic `Field` behavior.""" + + def test_create_singletons(self, field): + """All `field`s so far are singletons.""" + cls = type(field) + new_field = cls() + + assert new_field is field + + @pytest.mark.parametrize("func", [repr, str]) + def test_text_repr(self, field, func): + """The text representations behave like Python literals.""" + new_field = eval(func(field), fields.__dict__) # noqa: S307 + + assert new_field is field + + +class TestCastAndValidateFieldElements: + """Test `Field.cast()` and `Field.validate()`. + + Every `field` must be able to tell if a given `value` is + an element of the `field`, and, if so, `.cast()` it as such. + """ + + @pytest.mark.parametrize("field", utils.NON_10_FIELDS) + @pytest.mark.parametrize("value", utils.NUMBERS) + def test_number_is_field_element(self, field, value): + """Common numbers are typically `field` elements. + + This is not true for `GF2`, which, by default, + only accepts `1`-like and `0`-like numbers. + """ + utils.is_field_element(field, value) + + @pytest.mark.parametrize("field", utils.ALL_FIELDS) + @pytest.mark.parametrize("value", utils.ONES_N_ZEROS) + def test_one_and_zero_number_is_field_element(self, field, value): + """`1`-like and `0`-like numbers are always `field` elements.""" + utils.is_field_element(field, value) + + @pytest.mark.parametrize("field", utils.ALL_FIELDS) + @pytest.mark.parametrize("value", ["abc", (1, 2, 3)]) + def test_non_numeric_value_is_not_field_element(self, field, value): + """Values of non-numeric data types are typically not `field` elements.""" + utils.is_not_field_element(field, value) + + @pytest.mark.parametrize("field", utils.ALL_FIELDS) + @pytest.mark.parametrize("pre_value", ["NaN", "+inf", "-inf"]) + def test_non_finite_number_is_not_field_element(self, field, pre_value): + """For now, we only allow finite numbers as `field` elements. + + Notes: + - `Q._cast_func()` cannot handle non-finite `value`s + and raises an `OverflowError` or `ValueError` + => `Field.cast()` catches these errors + and (re-)raises a `ValueError` instead + => no need to define a specific `._post_cast_filter()` + - `R._cast_func()` and `C._cast_func()` + handle non-finite `value`s without any complaints + => using a `._post_cast_filter()`, we don't allow + non-finite but castable `value`s to be `field` elements + - `GF2._cast_func()` handles non-finite `value`s + by raising a `ValueError` already + => `Field.cast()` re-raises it with an adapted message + => no need to define a specific `._post_cast_filter()` + """ + value = float(pre_value) + utils.is_not_field_element(field, value) + + +class TestDTypes: + """Test the `Field.dtype` property.""" + + @pytest.mark.parametrize("field", utils.ALL_FIELDS) + def test_field_dtype(self, field): + """`field.dtype` must be a `type`.""" + assert isinstance(field.dtype, type) + + @pytest.mark.parametrize("field", utils.ALL_FIELDS) + def test_element_is_instance_of_field_dtype(self, field): + """Elements are an instance of `field.dtype`.""" + element = field.random() + + assert isinstance(element, field.dtype) + + @pytest.mark.parametrize("field", utils.ALL_FIELDS) + def test_element_dtype_is_subclass_of_field_dtype(self, field): + """Elements may have a more specific `.dtype` than their `field.dtype`.""" + element = field.random() + dtype = type(element) + + assert issubclass(dtype, field.dtype) + + +class TestIsZero: + """Test `Field.zero` & `Field.is_zero()`.""" + + @pytest.mark.parametrize("field", utils.ALL_FIELDS) + @pytest.mark.parametrize("value", utils.ZEROS) + def test_is_exactly_zero(self, field, value): + """`value` is equal to `field.zero`.""" + assert field.zero == value + assert field.is_zero(value) + + @pytest.mark.parametrize("field", utils.ALL_FIELDS) + def test_is_almost_zero(self, field): + """`value` is within an acceptable threshold of `field.zero`.""" + value = 0.0 + utils.WITHIN_THRESHOLD + + assert pytest.approx(field.zero, abs=utils.DEFAULT_THRESHOLD) == value + assert field.is_zero(value) + + @pytest.mark.parametrize("field", utils.NON_10_FIELDS) + def test_is_slightly_not_zero(self, field): + """`value` is not within an acceptable threshold of `field.zero`.""" + value = 0.0 + utils.NOT_WITHIN_THRESHOLD + + assert pytest.approx(field.zero, abs=utils.DEFAULT_THRESHOLD) != value + assert not field.is_zero(value) + + @pytest.mark.parametrize("field", utils.ALL_FIELDS) + @pytest.mark.parametrize("value", utils.ONES) + def test_is_not_zero(self, field, value): + """`value` is not equal to `field.zero`.""" + assert field.zero != value + assert not field.is_zero(value) + + +class TestIsOne: + """Test `Field.one` & `Field.is_one()`.""" + + @pytest.mark.parametrize("field", utils.ALL_FIELDS) + @pytest.mark.parametrize("value", utils.ONES) + def test_is_exactly_one(self, field, value): + """`value` is equal to `field.one`.""" + assert field.one == value + assert field.is_one(value) + + @pytest.mark.parametrize("field", utils.ALL_FIELDS) + def test_is_almost_one(self, field): + """`value` is within an acceptable threshold of `field.one`.""" + value = 1.0 + utils.WITHIN_THRESHOLD + + assert pytest.approx(field.one, abs=utils.DEFAULT_THRESHOLD) == value + assert field.is_one(value) + + @pytest.mark.parametrize("field", utils.NON_10_FIELDS) + def test_is_slightly_not_one(self, field): + """`value` is not within an acceptable threshold of `field.one`.""" + value = 1.0 + utils.NOT_WITHIN_THRESHOLD + + assert pytest.approx(field.one, abs=utils.DEFAULT_THRESHOLD) != value + assert not field.is_one(value) + + @pytest.mark.parametrize("field", utils.ALL_FIELDS) + @pytest.mark.parametrize("value", utils.ZEROS) + def test_is_not_one(self, field, value): + """`value` is not equal to `field.one`.""" + assert field.one != value + assert not field.is_one(value) + + +@pytest.mark.repeat(utils.N_RANDOM_DRAWS) +class TestDrawRandomFieldElement: + """Test `Field.random()`.""" + + @pytest.mark.parametrize("field", utils.ALL_FIELDS) + def test_draw_element_with_default_bounds(self, field): + """Draw a random element from the `field`, ... + + ... within the `field`'s default bounds. + + Here, the default bounds come from the default arguments. + """ + element = field.random() + + assert field.validate(element) + + @pytest.mark.parametrize("field", utils.ALL_FIELDS) + def test_draw_element_with_default_bounds_set_to_none(self, field): + """Draw a random element from the `field`, ... + + ... within the `field`'s default bounds. + + If no default arguments are defined in `field.random()`, + the internal `Field._get_bounds()` method provides them. + """ + element = field.random(lower=None, upper=None) + + assert field.validate(element) + + @pytest.mark.parametrize("field", utils.NON_10_FIELDS) + def test_draw_element_with_custom_bounds(self, field): + """Draw a random element from the `field` ... + + ... within the bounds passed in as arguments. + + For `GF2`, this only works in non-`strict` mode. + """ + lower = 200 * random.random() - 100 # noqa: S311 + upper = 200 * random.random() - 100 # noqa: S311 + + # `field.random()` sorts the bounds internally + # => test both directions + element1 = field.random(lower=lower, upper=upper) + element2 = field.random(lower=upper, upper=lower) + + assert field.validate(element1) + assert field.validate(element2) + + # Done implicitly in `field.random()` above + lower, upper = field.cast(lower), field.cast(upper) + + # Not all data types behind the `Field._cast_func()` + # support sorting the numbers (e.g., `complex`) + try: + swap = upper < lower + except TypeError: + pass + else: + if swap: + lower, upper = upper, lower + + assert lower <= element1 <= upper + assert lower <= element2 <= upper + + +def test_numbers(): + """We use `0`, `1`, `+42`, and `-42` in different data types.""" + unique_one_and_zero = {int(n) for n in utils.ONES_N_ZEROS} + unique_non_one_and_zero = {int(n) for n in utils.NON_ONES_N_ZEROS} + unique_numbers = {int(n) for n in utils.NUMBERS} + + assert unique_one_and_zero == {0, 1} + assert unique_non_one_and_zero == {+42, -42} + assert unique_numbers == {0, 1, +42, -42} diff --git a/tests/fields/test_complex.py b/tests/fields/test_complex.py new file mode 100644 index 0000000..dc5b1f3 --- /dev/null +++ b/tests/fields/test_complex.py @@ -0,0 +1,96 @@ +"""Tests for the `lalib.fields.complex_.ComplexField` only.""" + +import random + +import pytest + +from lalib import fields +from tests.fields import utils + + +C = fields.C + + +class TestCastAndValidateFieldElements: + """Test specifics for `C.cast()` and `C.validate()`.""" + + @pytest.mark.parametrize("pre_value", [1, 0, +42, -42]) + def test_complex_number_is_field_element(self, pre_value): + """`C` must be able to process `complex` numbers.""" + value = complex(pre_value, 0) + utils.is_field_element(C, value) + + @pytest.mark.parametrize("pre_value", ["NaN", "+inf", "-inf"]) + def test_non_finite_complex_number_is_not_field_element(self, pre_value): + """For now, we only allow finite numbers as field elements. + + This also holds true for `complex` numbers + with a non-finite `.real` part. + """ + value = complex(pre_value) + utils.is_not_field_element(C, value) + + +class TestIsZero: + """Test specifics for `C.zero` and `C.is_zero()`.""" + + def test_is_almost_zero(self): + """`value` is within an acceptable threshold of `C.zero`.""" + value = 0.0 + utils.WITHIN_THRESHOLD + + assert pytest.approx(C.zero, abs=utils.DEFAULT_THRESHOLD) == value + assert C.is_zero(value) + + def test_is_slightly_not_zero(self): + """`value` is not within an acceptable threshold of `C.zero`.""" + value = 0.0 + utils.NOT_WITHIN_THRESHOLD + + assert pytest.approx(C.zero, abs=utils.DEFAULT_THRESHOLD) != value + assert not C.is_zero(value) + + +class TestIsOne: + """Test specifics for `C.one` and `C.is_one()`.""" + + def test_is_almost_one(self): + """`value` is within an acceptable threshold of `C.one`.""" + value = 1.0 + utils.WITHIN_THRESHOLD + + assert pytest.approx(C.one, abs=utils.DEFAULT_THRESHOLD) == value + assert C.is_one(value) + + def test_is_slightly_not_one(self): + """`value` is not within an acceptable threshold of `C.one`.""" + value = 1.0 + utils.NOT_WITHIN_THRESHOLD + + assert pytest.approx(C.one, abs=utils.DEFAULT_THRESHOLD) != value + assert not C.is_one(value) + + +@pytest.mark.repeat(utils.N_RANDOM_DRAWS) +class TestDrawRandomFieldElement: + """Test specifics for `C.random()`.""" + + def test_draw_elements_with_custom_bounds(self): + """Draw a random element from `C` ... + + ... within the bounds passed in as arguments. + + For `C`, the bounds are interpreted in a 2D fashion. + """ + lower = complex( + 200 * random.random() - 100, # noqa: S311 + 200 * random.random() - 100, # noqa: S311 + ) + upper = complex( + 200 * random.random() - 100, # noqa: S311 + 200 * random.random() - 100, # noqa: S311 + ) + + element = C.random(lower=lower, upper=upper) + + l_r, u_r = min(lower.real, upper.real), max(lower.real, upper.real) + l_i, u_i = min(lower.imag, upper.imag), max(lower.imag, upper.imag) + + assert l_r <= element.real <= u_r + assert l_i <= element.imag <= u_i diff --git a/tests/fields/test_galois.py b/tests/fields/test_galois.py new file mode 100644 index 0000000..5b73c73 --- /dev/null +++ b/tests/fields/test_galois.py @@ -0,0 +1,100 @@ +"""Tests for the `lalib.fields.galois.GaloisField2` only.""" + +import itertools + +import pytest + +from lalib import fields +from tests.fields import utils + + +GF2 = fields.GF2 + + +class TestCastAndValidateFieldElements: + """Test specifics for `GF2.cast()` and `GF2.validate()`.""" + + @pytest.mark.parametrize("value", utils.NUMBERS) + def test_number_is_field_element(self, value): + """Common numbers are always `GF2` elements in non-`strict` mode.""" + left = GF2.cast(value, strict=False) + right = bool(value) + + assert left == right + assert GF2.validate(value, strict=False) + + @pytest.mark.parametrize("value", utils.ONES_N_ZEROS) + def test_one_and_zero_number_is_field_element(self, value): + """`1`-like and `0`-like `value`s are `GF2` elements.""" + utils.is_field_element(GF2, value) + + @pytest.mark.parametrize("pre_value", [1, 0]) + def test_complex_number_is_field_element(self, pre_value): + """By design, `GF2` can process `complex` numbers.""" + value = complex(pre_value, 0) + utils.is_field_element(GF2, value) + + @pytest.mark.parametrize("pre_value", [+42, -42]) + def test_complex_number_is_not_field_element(self, pre_value): + """By design, `GF2` can process `complex` numbers ... + + ... but they must be `one`-like or `zero`-like + to become a `GF2` element. + """ + value = complex(pre_value, 0) + utils.is_not_field_element(GF2, value) + + @pytest.mark.parametrize("pre_value", ["NaN", "+inf", "-inf"]) + def test_non_finite_complex_number_is_not_field_element(self, pre_value): + """For now, we only allow finite numbers as field elements. + + This also holds true for `complex` numbers + with a non-finite `.real` part. + """ + value = complex(pre_value) + utils.is_not_field_element(GF2, value) + + +class TestIsZero: + """Test specifics for `GF2.zero` and `GF2.is_zero()`.""" + + def test_is_slightly_not_zero(self): + """`value` is not within an acceptable threshold of `GF2.zero`.""" + value = 0.0 + utils.NOT_WITHIN_THRESHOLD + + assert GF2.zero != value + + with pytest.raises(ValueError, match="not an element of the field"): + GF2.is_zero(value) + + +class TestIsOne: + """Test specifics for `GF2.one` and `GF2.is_one()`.""" + + def test_is_slightly_not_one(self): + """`value` is not within an acceptable threshold of `GF2.one`.""" + value = 1.0 + utils.NOT_WITHIN_THRESHOLD + + assert GF2.one != value + + with pytest.raises(ValueError, match="not an element of the field"): + GF2.is_one(value) + + +@pytest.mark.repeat(utils.N_RANDOM_DRAWS) +class TestDrawRandomFieldElement: + """Test specifics for `GF2.random()`.""" + + @pytest.mark.parametrize("bounds", itertools.product([0, 1], repeat=2)) + def test_draw_element_with_custom_bounds(self, bounds): + """Draw a random element from `GF2` in non-`strict` mode ... + + ... within the bounds passed in as arguments. + """ + lower, upper = bounds + element = GF2.random(lower=lower, upper=upper) + + if upper < lower: + lower, upper = upper, lower + + assert lower <= element <= upper diff --git a/tests/fields/test_rational.py b/tests/fields/test_rational.py new file mode 100644 index 0000000..6d3c850 --- /dev/null +++ b/tests/fields/test_rational.py @@ -0,0 +1,29 @@ +"""Tests for the `lalib.fields.rational.RationalField` only.""" + +import fractions + +import pytest + +from lalib import fields + + +Q = fields.Q + + +class TestCastAndValidateFieldElements: + """Test specifics for `Q.cast()` and `Q.validate()`.""" + + @pytest.mark.parametrize( + "value", + ["1", "0", "1/1", "0/1", "+42", "-42", "+42/1", "-42/1"], + ) + def test_str_is_field_element(self, value): + """`fractions.Fraction()` also accepts `str`ings. + + Source: https://docs.python.org/3/library/fractions.html#fractions.Fraction + """ + left = Q.cast(value) + right = fractions.Fraction(value) + + assert left == right + assert Q.validate(value) diff --git a/tests/fields/utils.py b/tests/fields/utils.py new file mode 100644 index 0000000..912a1cf --- /dev/null +++ b/tests/fields/utils.py @@ -0,0 +1,75 @@ +"""Utilities to test the `lalib.fields` sub-package.""" + +import decimal +import fractions +import os + +import pytest + +from lalib import config +from lalib import elements +from lalib import fields + + +ALL_FIELDS = (fields.Q, fields.R, fields.C, fields.GF2) +NON_10_FIELDS = (fields.Q, fields.R, fields.C) + +ONES = ( + 1, + 1.0, + fractions.Fraction(1, 1), + decimal.Decimal("1.0"), + elements.one, + True, +) + +ZEROS = ( + 0, + 0.0, + fractions.Fraction(0, 1), + decimal.Decimal("+0.0"), + decimal.Decimal("-0.0"), + elements.zero, + False, +) + +ONES_N_ZEROS = ONES + ZEROS + +NON_ONES_N_ZEROS = ( + +42, + +42.0, + fractions.Fraction(+42, 1), + decimal.Decimal("+42.0"), + -42, + -42.0, + fractions.Fraction(-42, 1), + decimal.Decimal("-42.0"), +) + +NUMBERS = ONES_N_ZEROS + NON_ONES_N_ZEROS + +DEFAULT_THRESHOLD = config.THRESHOLD +WITHIN_THRESHOLD = config.THRESHOLD / 10 +NOT_WITHIN_THRESHOLD = config.THRESHOLD * 10 + +N_RANDOM_DRAWS = os.environ.get("N_RANDOM_DRAWS") or 1 + + +def is_field_element(field, value): + """Utility method to avoid redundant logic in tests.""" + element = field.cast(value) + + assert element == value + assert field.validate(value) + + +def is_not_field_element(field, value): + """Utility method to avoid redundant logic in tests.""" + with pytest.raises(ValueError, match="not an element of the field"): + field.cast(value) + + assert not field.validate(value) + assert not field.validate(value, silent=True) + + with pytest.raises(ValueError, match="not an element of the field"): + field.validate(value, silent=False) diff --git a/tests/test_docstrings.py b/tests/test_docstrings.py index f5a3f2d..5eaf571 100644 --- a/tests/test_docstrings.py +++ b/tests/test_docstrings.py @@ -16,6 +16,12 @@ import xdoctest "lalib", "lalib.elements", "lalib.elements.galois", + "lalib.fields", + "lalib.fields.base", + "lalib.fields.complex_", + "lalib.fields.galois", + "lalib.fields.rational", + "lalib.fields.real", ], ) def test_docstrings(module): From 04addacb099ef00c73a1218774533a55c42f3693 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Mon, 14 Oct 2024 16:34:17 +0200 Subject: [PATCH 11/20] Add `tests.utils` module --- tests/utils.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 tests/utils.py diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..34f8784 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1 @@ +"""Utilities to test the `lalib` package.""" From de740ebb5f0b7b05ff19fe11e5a636be407dc8d1 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Mon, 14 Oct 2024 16:36:02 +0200 Subject: [PATCH 12/20] Unify the various `*_THRESHOLD`s --- tests/elements/test_galois.py | 42 ++++++++++++++++------------------- tests/fields/utils.py | 8 +++---- tests/utils.py | 7 ++++++ 3 files changed, 30 insertions(+), 27 deletions(-) diff --git a/tests/elements/test_galois.py b/tests/elements/test_galois.py index 1af29a9..06a4800 100644 --- a/tests/elements/test_galois.py +++ b/tests/elements/test_galois.py @@ -11,8 +11,8 @@ import sys import pytest -from lalib import config from lalib.elements import galois +from tests import utils gf2, one, zero = ( # official API outside of `lalib.elements.galois` @@ -33,17 +33,13 @@ del galois CROSS_REFERENCE = not os.environ.get("NO_CROSS_REFERENCE") -default_threshold = config.THRESHOLD -within_threshold = config.THRESHOLD / 10 -not_within_threshold = config.THRESHOLD * 10 - strict_one_like_values = ( 1, 1.0, - 1.0 + within_threshold, + 1.0 + utils.WITHIN_THRESHOLD, (1 + 0j), - (1 + 0j) + complex(0, within_threshold), - (1 + 0j) + complex(within_threshold, 0), + (1 + 0j) + complex(0, utils.WITHIN_THRESHOLD), + (1 + 0j) + complex(utils.WITHIN_THRESHOLD, 0), decimal.Decimal("1"), fractions.Fraction(1, 1), "1", @@ -52,9 +48,9 @@ strict_one_like_values = ( ) non_strict_one_like_values = ( - 0.0 + not_within_threshold, - 1.0 + not_within_threshold, - (1 + 0j) + complex(not_within_threshold, 0), + 0.0 + utils.NOT_WITHIN_THRESHOLD, + 1.0 + utils.NOT_WITHIN_THRESHOLD, + (1 + 0j) + complex(utils.NOT_WITHIN_THRESHOLD, 0), 42, decimal.Decimal("42"), fractions.Fraction(42, 1), @@ -70,10 +66,10 @@ one_like_values = strict_one_like_values + non_strict_one_like_values zero_like_values = ( 0, 0.0, - 0.0 + within_threshold, + 0.0 + utils.WITHIN_THRESHOLD, (0 + 0j), - (0 + 0j) + complex(0, within_threshold), - (0 + 0j) + complex(within_threshold, 0), + (0 + 0j) + complex(0, utils.WITHIN_THRESHOLD), + (0 + 0j) + complex(utils.WITHIN_THRESHOLD, 0), decimal.Decimal("0"), fractions.Fraction(0, 1), "0", @@ -84,7 +80,7 @@ zero_like_values = ( def test_thresholds(): """Sanity check for the thresholds used in the tests below.""" - assert within_threshold < default_threshold < not_within_threshold + assert utils.WITHIN_THRESHOLD < utils.DEFAULT_THRESHOLD < utils.NOT_WITHIN_THRESHOLD class TestGF2SubClasses: @@ -150,8 +146,8 @@ class TestGF2Casting: @pytest.mark.parametrize( "value", [ - complex(1, not_within_threshold), - complex(0, not_within_threshold), + complex(1, utils.NOT_WITHIN_THRESHOLD), + complex(0, utils.NOT_WITHIN_THRESHOLD), ], ) @pytest.mark.parametrize("strict", [True, False]) @@ -176,10 +172,10 @@ class TestGF2Casting: @pytest.mark.parametrize("scaler", [1, 10, 100, 1000]) def test_get_one_if_within_threshold(self, cls, scaler): """`gf2()` returns `one` if `value` is larger than `threshold`.""" - # `not_within_threshold` is larger than the `default_threshold` + # `NOT_WITHIN_THRESHOLD` is larger than the `DEFAULT_THRESHOLD` # but still different from `1` => `strict=False` - value = scaler * not_within_threshold - threshold = scaler * default_threshold + value = scaler * utils.NOT_WITHIN_THRESHOLD + threshold = scaler * utils.DEFAULT_THRESHOLD result = cls(value, strict=False, threshold=threshold) assert result is one @@ -188,9 +184,9 @@ class TestGF2Casting: @pytest.mark.parametrize("strict", [True, False]) def test_get_zero_if_within_threshold(self, cls, scaler, strict): """`gf2()` returns `zero` if `value` is smaller than `threshold`.""" - # `within_threshold` is smaller than the `default_threshold` - value = scaler * within_threshold - threshold = scaler * default_threshold + # `WITHIN_THRESHOLD` is smaller than the `DEFAULT_THRESHOLD` + value = scaler * utils.WITHIN_THRESHOLD + threshold = scaler * utils.DEFAULT_THRESHOLD result = cls(value, strict=strict, threshold=threshold) assert result is zero diff --git a/tests/fields/utils.py b/tests/fields/utils.py index 912a1cf..3bb9303 100644 --- a/tests/fields/utils.py +++ b/tests/fields/utils.py @@ -6,9 +6,9 @@ import os import pytest -from lalib import config from lalib import elements from lalib import fields +from tests import utils as root_utils ALL_FIELDS = (fields.Q, fields.R, fields.C, fields.GF2) @@ -48,9 +48,9 @@ NON_ONES_N_ZEROS = ( NUMBERS = ONES_N_ZEROS + NON_ONES_N_ZEROS -DEFAULT_THRESHOLD = config.THRESHOLD -WITHIN_THRESHOLD = config.THRESHOLD / 10 -NOT_WITHIN_THRESHOLD = config.THRESHOLD * 10 +DEFAULT_THRESHOLD = root_utils.DEFAULT_THRESHOLD +WITHIN_THRESHOLD = root_utils.WITHIN_THRESHOLD +NOT_WITHIN_THRESHOLD = root_utils.NOT_WITHIN_THRESHOLD N_RANDOM_DRAWS = os.environ.get("N_RANDOM_DRAWS") or 1 diff --git a/tests/utils.py b/tests/utils.py index 34f8784..f12f96f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1 +1,8 @@ """Utilities to test the `lalib` package.""" + +from lalib import config + + +DEFAULT_THRESHOLD = config.THRESHOLD +WITHIN_THRESHOLD = config.THRESHOLD / 10 +NOT_WITHIN_THRESHOLD = config.THRESHOLD * 10 From 7e3e67c300648b7d936bc6dd2ea675062a34e5da Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 15 Oct 2024 01:49:32 +0200 Subject: [PATCH 13/20] Add smoke tests - extend `pytest` with an option to run only the minimum number of (unit) test cases to just keep the coverage at 100% - rationale: + many of the unit test cases partly overlap with respect to the lines of source code executed + also, integration tests, by definition, do not contribute to a higher test coverage - implementation: mark "redundant" test cases as one of: + `pytest.mark.integration_test` => code usage from the perspective of the end user + `pytest.mark.overlapping_test` => tests not contributing to the 100% coverage + `pytest.mark.sanity_test` => tests providing confidence in the test data - add `tests.conftest` module => programatically convert the above markers into `@pytest.mark.no_cover` and collect the non-"redundant" tests - add nox session "test-fast" to run only the minimum number of (unit) test while holding coverage at 100% - refactor some test modules + wrap some test cases in a class + move sanity tests to the end of the files --- noxfile.py | 20 ++++++++++ tests/conftest.py | 59 +++++++++++++++++++++++++++++ tests/elements/test_galois.py | 40 +++++++++++++++++--- tests/fields/test_axioms.py | 4 ++ tests/fields/test_base.py | 14 +++++++ tests/fields/test_complex.py | 4 ++ tests/fields/test_galois.py | 4 ++ tests/fields/test_rational.py | 4 ++ tests/test_docstrings.py | 1 + tests/test_top_level_imports.py | 1 + tests/test_version.py | 66 ++++++++++++++++++--------------- 11 files changed, 182 insertions(+), 35 deletions(-) create mode 100644 tests/conftest.py diff --git a/noxfile.py b/noxfile.py index fc0fee2..e03e1bb 100644 --- a/noxfile.py +++ b/noxfile.py @@ -254,6 +254,14 @@ def test(session: nox.Session) -> None: args = posargs or ( "--cov", "--no-cov-on-fail", + *( # If this function is run via the "test-fast" session, + ( # the following arguments are added: + "--cov-fail-under=100", + "--smoke-tests-only", + ) + if session.env.get("_smoke_tests_only") + else () + ), TEST_RANDOM_SEED, TESTS_LOCATION, ) @@ -339,6 +347,18 @@ def test_docstrings(session: nox.Session) -> None: session.run("xdoctest", "src/lalib") +@nox.session(name="test-fast", python=MAIN_PYTHON) +def test_fast(session: nox.Session) -> None: + """Test code with `pytest`, selected (smoke) tests only. + + The (unit) test cases are selected such that their number + is minimal but the achieved coverage remains at 100%. + """ + # See implementation notes in `pre_commit_test_hook()` below + session.env["_smoke_tests_only"] = "true" + test(session) + + @nox.session(name="clean-cwd", python=MAIN_PYTHON, venv_backend="none") def clean_cwd(session: nox.Session) -> None: """Remove (almost) all glob patterns listed in git's ignore file. diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..7917b02 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,59 @@ +"""Configurations and utilities for all tests.""" + +import pytest + + +def pytest_addoption(parser): + """Define custom CLI options for `pytest`.""" + parser.addoption( + "--smoke-tests-only", + action="store_true", + default=False, + help="Run the minimum number of (unit) tests to achieve full coverage", + ) + + +def pytest_configure(config): + """Define custom markers explicitly.""" + config.addinivalue_line( + "markers", + "integration_test: non-unit test case; skipped during coverage reporting", + ) + config.addinivalue_line( + "markers", + "overlapping_test: test case not contributing towards higher coverage", + ) + config.addinivalue_line( + "markers", + "sanity_test: test case providing confidence in the test data", + ) + + +def pytest_collection_modifyitems(config, items): + """Pre-process the test cases programatically. + + - Add `no_cover` marker to test cases with any of these markers: + + "integration_test" + + "overlapping_test" + + "sanity_test" + - Select test cases with none of the above markers as smoke tests, + i.e., the minimum number of test cases to achieve 100% coverage + """ + smoke_tests = [] + + for item in items: + if ( + "integration_test" in item.keywords + or "overlapping_test" in item.keywords + or "sanity_test" in item.keywords + ): + item.add_marker(pytest.mark.no_cover) + + elif config.getoption("--smoke-tests-only"): + smoke_tests.append(item) + + if config.getoption("--smoke-tests-only"): + if not smoke_tests: + pytest.exit("No smoke tests found") + + items[:] = smoke_tests diff --git a/tests/elements/test_galois.py b/tests/elements/test_galois.py index 06a4800..4dff652 100644 --- a/tests/elements/test_galois.py +++ b/tests/elements/test_galois.py @@ -78,11 +78,7 @@ zero_like_values = ( ) -def test_thresholds(): - """Sanity check for the thresholds used in the tests below.""" - assert utils.WITHIN_THRESHOLD < utils.DEFAULT_THRESHOLD < utils.NOT_WITHIN_THRESHOLD - - +@pytest.mark.overlapping_test class TestGF2SubClasses: """Test the sub-classes behind `one` and `zero`.""" @@ -122,6 +118,7 @@ class TestGF2Casting: result = cls(value, strict=False) assert result is one + @pytest.mark.overlapping_test @pytest.mark.parametrize("value", non_strict_one_like_values) def test_cannot_cast_ones_strictly(self, cls, value): """`gf2(value, strict=False)` returns `1`.""" @@ -156,6 +153,7 @@ class TestGF2Casting: with pytest.raises(ValueError, match="`1`-like or `0`-like"): cls(value, strict=strict) + @pytest.mark.overlapping_test @pytest.mark.parametrize("value", ["abc", (1,), [1]]) @pytest.mark.parametrize("strict", [True, False]) def test_cannot_cast_from_wrong_type(self, cls, value, strict): @@ -163,12 +161,14 @@ class TestGF2Casting: with pytest.raises(TypeError): cls(value, strict=strict) + @pytest.mark.overlapping_test @pytest.mark.parametrize("strict", [True, False]) def test_cannot_cast_from_nan_value(self, cls, strict): """Cannot create `one` or `zero` from undefined value.""" with pytest.raises(ValueError, match="`1`-like or `0`-like"): cls(float("NaN"), strict=strict) + @pytest.mark.overlapping_test @pytest.mark.parametrize("scaler", [1, 10, 100, 1000]) def test_get_one_if_within_threshold(self, cls, scaler): """`gf2()` returns `one` if `value` is larger than `threshold`.""" @@ -180,6 +180,7 @@ class TestGF2Casting: result = cls(value, strict=False, threshold=threshold) assert result is one + @pytest.mark.overlapping_test @pytest.mark.parametrize("scaler", [1, 10, 100, 1000]) @pytest.mark.parametrize("strict", [True, False]) def test_get_zero_if_within_threshold(self, cls, scaler, strict): @@ -192,6 +193,7 @@ class TestGF2Casting: assert result is zero +@pytest.mark.overlapping_test @pytest.mark.parametrize("strict", [True, False]) class TestGF2ConstructorWithoutCastedValue: """Test the `gf2` class's constructor. @@ -220,6 +222,7 @@ class TestGenericBehavior: with pytest.raises(RuntimeError, match="internal error"): gf2() + @pytest.mark.overlapping_test @pytest.mark.parametrize("cls", [gf2, GF2One, GF2Zero]) def test_create_singletons(self, cls): """Singleton pattern: The classes always return the same instance.""" @@ -227,6 +230,7 @@ class TestGenericBehavior: second = cls() assert first is second + @pytest.mark.overlapping_test @pytest.mark.parametrize("obj", [one, zero]) def test_sub_classes_return_objs(self, obj): """`type(one)` and `type(zero)` return ... @@ -239,6 +243,7 @@ class TestGenericBehavior: new_obj = sub_cls() assert new_obj is obj + @pytest.mark.overlapping_test @pytest.mark.parametrize("obj", [one, zero]) @pytest.mark.parametrize( "type_", @@ -253,6 +258,7 @@ class TestGenericBehavior: """`one` and `zero` are officially `Numbers`s.""" assert isinstance(obj, type_) + @pytest.mark.overlapping_test @pytest.mark.parametrize("cls", [gf2, GF2One, GF2Zero]) @pytest.mark.parametrize( "method", @@ -320,14 +326,17 @@ class TestGenericBehavior: class TestNumericBehavior: """Test how `one` and `zero` behave like numbers.""" + @pytest.mark.overlapping_test def test_make_complex(self): """`one` and `zero` behave like `1 + 0j` and `0 + 0j`.""" assert (complex(one), complex(zero)) == (1 + 0j, 0 + 0j) + @pytest.mark.overlapping_test def test_make_float(self): """`one` and `zero` behave like `1.0` and `0.0`.""" assert (float(one), float(zero)) == (1.0, 0.0) + @pytest.mark.overlapping_test @pytest.mark.parametrize("func", [int, hash]) def test_make_int(self, func): """`one` and `zero` behave like `1` and `0`. @@ -340,6 +349,7 @@ class TestNumericBehavior: """`one` and `zero` behave like `True` and `False`.""" assert (bool(one), bool(zero)) == (True, False) + @pytest.mark.overlapping_test @pytest.mark.parametrize("obj", [one, zero]) def test_get_abs_value(self, obj): """`abs(one)` and `abs(zero)` are `one` and `zero`.""" @@ -366,10 +376,12 @@ class TestNumericBehavior: """`one.conjugate()` and `zero.conjugate()` are `1 + 0j` and `0 + 0j`.""" assert (one.conjugate(), zero.conjugate()) == (1 + 0j, 0 + 0j) + @pytest.mark.overlapping_test def test_one_as_fraction(self): """`one.numerator / one.denominator` equals `1`.""" assert (one.numerator, one.denominator) == (1, 1) + @pytest.mark.overlapping_test def test_zero_as_fraction(self): """`one.numerator / one.denominator` equals `0`.""" assert (zero.numerator, zero.denominator) == (0, 1) @@ -378,11 +390,13 @@ class TestNumericBehavior: class TestComparison: """Test `one` and `zero` interact with relational operators.""" + @pytest.mark.overlapping_test @pytest.mark.parametrize("obj", [one, zero]) def test_equal_to_itself(self, obj): """`one` and `zero` are equal to themselves.""" assert obj == obj # noqa: PLR0124 + @pytest.mark.overlapping_test @pytest.mark.parametrize( ["first", "second"], [ @@ -401,6 +415,7 @@ class TestComparison: assert first == second assert second == first + @pytest.mark.overlapping_test @pytest.mark.parametrize( ["first", "second"], [ @@ -418,6 +433,7 @@ class TestComparison: assert first != second assert second != first + @pytest.mark.overlapping_test @pytest.mark.parametrize( ["first", "second"], [ @@ -441,27 +457,32 @@ class TestComparison: """`one > zero` and `one >= zero`.""" assert operator(one, zero) + @pytest.mark.overlapping_test @pytest.mark.parametrize("operator", [operator.lt, operator.le]) def test_one_not_smaller_than_or_equal_to_zero(self, operator): """`not one < zero` and `not one <= zero`.""" assert not operator(one, zero) + @pytest.mark.overlapping_test @pytest.mark.parametrize("operator", [operator.lt, operator.le]) def test_zero_smaller_than_or_equal_to_one(self, operator): """`zero < one` and `zero <= one`.""" assert operator(zero, one) + @pytest.mark.overlapping_test @pytest.mark.parametrize("operator", [operator.gt, operator.ge]) def test_zero_not_greater_than_or_equalt_to_one(self, operator): """`not zero > one` and `not zero >= one`.""" assert not operator(zero, one) + @pytest.mark.overlapping_test @pytest.mark.parametrize("obj", [one, zero]) def test_obj_not_strictly_greater_than_itself(self, obj): """`obj >= obj` but not `obj > obj`.""" assert obj >= obj # noqa: PLR0124 assert not obj > obj # noqa: PLR0124 + @pytest.mark.overlapping_test @pytest.mark.parametrize("obj", [one, zero]) def test_obj_not_strictly_smaller_than_itself(self, obj): """`obj <= obj` but not `obj < obj`.""" @@ -579,6 +600,7 @@ class TestArithmetic: result4 = gf2(int(abs(second)) * int(abs(first))) assert result4 is expected + @pytest.mark.overlapping_test @pytest.mark.parametrize( "objs", [ @@ -613,6 +635,7 @@ class TestArithmetic: result2 = gf2(operator(int(abs(first)), int(abs(second)))) assert result2 is expected + @pytest.mark.overlapping_test @pytest.mark.parametrize( "objs", [ @@ -762,6 +785,7 @@ class TestArithmetic: operator(42, obj) +@pytest.mark.overlapping_test @pytest.mark.skipif( not sys.version_info < (3, 11), reason='"typing-extensions" are installed to support Python 3.9 & 3.10', @@ -772,3 +796,9 @@ def test_can_import_typing_extensions(): importlib.reload(package) assert package.Self is not None + + +@pytest.mark.sanity_test +def test_thresholds(): + """Sanity check for the thresholds used in the tests below.""" + assert utils.WITHIN_THRESHOLD < utils.DEFAULT_THRESHOLD < utils.NOT_WITHIN_THRESHOLD diff --git a/tests/fields/test_axioms.py b/tests/fields/test_axioms.py index 67ddf70..680ebb6 100644 --- a/tests/fields/test_axioms.py +++ b/tests/fields/test_axioms.py @@ -11,6 +11,10 @@ import pytest from tests.fields import utils +# None of the test cases below contributes towards higher coverage +pytestmark = pytest.mark.integration_test + + @pytest.mark.repeat(utils.N_RANDOM_DRAWS) @pytest.mark.parametrize("field", utils.ALL_FIELDS) class TestAllFieldsManyTimes: diff --git a/tests/fields/test_base.py b/tests/fields/test_base.py index 31ac80f..80f00dc 100644 --- a/tests/fields/test_base.py +++ b/tests/fields/test_base.py @@ -39,6 +39,7 @@ class TestCastAndValidateFieldElements: an element of the `field`, and, if so, `.cast()` it as such. """ + @pytest.mark.overlapping_test @pytest.mark.parametrize("field", utils.NON_10_FIELDS) @pytest.mark.parametrize("value", utils.NUMBERS) def test_number_is_field_element(self, field, value): @@ -49,12 +50,14 @@ class TestCastAndValidateFieldElements: """ utils.is_field_element(field, value) + @pytest.mark.overlapping_test @pytest.mark.parametrize("field", utils.ALL_FIELDS) @pytest.mark.parametrize("value", utils.ONES_N_ZEROS) def test_one_and_zero_number_is_field_element(self, field, value): """`1`-like and `0`-like numbers are always `field` elements.""" utils.is_field_element(field, value) + @pytest.mark.overlapping_test @pytest.mark.parametrize("field", utils.ALL_FIELDS) @pytest.mark.parametrize("value", ["abc", (1, 2, 3)]) def test_non_numeric_value_is_not_field_element(self, field, value): @@ -93,6 +96,7 @@ class TestDTypes: """`field.dtype` must be a `type`.""" assert isinstance(field.dtype, type) + @pytest.mark.overlapping_test @pytest.mark.parametrize("field", utils.ALL_FIELDS) def test_element_is_instance_of_field_dtype(self, field): """Elements are an instance of `field.dtype`.""" @@ -100,6 +104,7 @@ class TestDTypes: assert isinstance(element, field.dtype) + @pytest.mark.overlapping_test @pytest.mark.parametrize("field", utils.ALL_FIELDS) def test_element_dtype_is_subclass_of_field_dtype(self, field): """Elements may have a more specific `.dtype` than their `field.dtype`.""" @@ -119,6 +124,7 @@ class TestIsZero: assert field.zero == value assert field.is_zero(value) + @pytest.mark.overlapping_test @pytest.mark.parametrize("field", utils.ALL_FIELDS) def test_is_almost_zero(self, field): """`value` is within an acceptable threshold of `field.zero`.""" @@ -127,6 +133,7 @@ class TestIsZero: assert pytest.approx(field.zero, abs=utils.DEFAULT_THRESHOLD) == value assert field.is_zero(value) + @pytest.mark.overlapping_test @pytest.mark.parametrize("field", utils.NON_10_FIELDS) def test_is_slightly_not_zero(self, field): """`value` is not within an acceptable threshold of `field.zero`.""" @@ -135,6 +142,7 @@ class TestIsZero: assert pytest.approx(field.zero, abs=utils.DEFAULT_THRESHOLD) != value assert not field.is_zero(value) + @pytest.mark.overlapping_test @pytest.mark.parametrize("field", utils.ALL_FIELDS) @pytest.mark.parametrize("value", utils.ONES) def test_is_not_zero(self, field, value): @@ -153,6 +161,7 @@ class TestIsOne: assert field.one == value assert field.is_one(value) + @pytest.mark.overlapping_test @pytest.mark.parametrize("field", utils.ALL_FIELDS) def test_is_almost_one(self, field): """`value` is within an acceptable threshold of `field.one`.""" @@ -161,6 +170,7 @@ class TestIsOne: assert pytest.approx(field.one, abs=utils.DEFAULT_THRESHOLD) == value assert field.is_one(value) + @pytest.mark.overlapping_test @pytest.mark.parametrize("field", utils.NON_10_FIELDS) def test_is_slightly_not_one(self, field): """`value` is not within an acceptable threshold of `field.one`.""" @@ -169,6 +179,7 @@ class TestIsOne: assert pytest.approx(field.one, abs=utils.DEFAULT_THRESHOLD) != value assert not field.is_one(value) + @pytest.mark.overlapping_test @pytest.mark.parametrize("field", utils.ALL_FIELDS) @pytest.mark.parametrize("value", utils.ZEROS) def test_is_not_one(self, field, value): @@ -193,6 +204,7 @@ class TestDrawRandomFieldElement: assert field.validate(element) + @pytest.mark.overlapping_test @pytest.mark.parametrize("field", utils.ALL_FIELDS) def test_draw_element_with_default_bounds_set_to_none(self, field): """Draw a random element from the `field`, ... @@ -206,6 +218,7 @@ class TestDrawRandomFieldElement: assert field.validate(element) + @pytest.mark.overlapping_test @pytest.mark.parametrize("field", utils.NON_10_FIELDS) def test_draw_element_with_custom_bounds(self, field): """Draw a random element from the `field` ... @@ -242,6 +255,7 @@ class TestDrawRandomFieldElement: assert lower <= element2 <= upper +@pytest.mark.sanity_test def test_numbers(): """We use `0`, `1`, `+42`, and `-42` in different data types.""" unique_one_and_zero = {int(n) for n in utils.ONES_N_ZEROS} diff --git a/tests/fields/test_complex.py b/tests/fields/test_complex.py index dc5b1f3..be9bc85 100644 --- a/tests/fields/test_complex.py +++ b/tests/fields/test_complex.py @@ -8,6 +8,10 @@ from lalib import fields from tests.fields import utils +# None of the test cases below contributes towards higher coverage +pytestmark = pytest.mark.overlapping_test + + C = fields.C diff --git a/tests/fields/test_galois.py b/tests/fields/test_galois.py index 5b73c73..be69dfc 100644 --- a/tests/fields/test_galois.py +++ b/tests/fields/test_galois.py @@ -8,6 +8,10 @@ from lalib import fields from tests.fields import utils +# None of the test cases below contributes towards higher coverage +pytestmark = pytest.mark.overlapping_test + + GF2 = fields.GF2 diff --git a/tests/fields/test_rational.py b/tests/fields/test_rational.py index 6d3c850..62b909b 100644 --- a/tests/fields/test_rational.py +++ b/tests/fields/test_rational.py @@ -7,6 +7,10 @@ import pytest from lalib import fields +# None of the test cases below contributes towards higher coverage +pytestmark = pytest.mark.overlapping_test + + Q = fields.Q diff --git a/tests/test_docstrings.py b/tests/test_docstrings.py index 5eaf571..ce1ba38 100644 --- a/tests/test_docstrings.py +++ b/tests/test_docstrings.py @@ -10,6 +10,7 @@ import pytest import xdoctest +@pytest.mark.integration_test @pytest.mark.parametrize( "module", [ diff --git a/tests/test_top_level_imports.py b/tests/test_top_level_imports.py index 909e46d..fb5be32 100644 --- a/tests/test_top_level_imports.py +++ b/tests/test_top_level_imports.py @@ -6,6 +6,7 @@ from typing import Any import pytest +@pytest.mark.integration_test @pytest.mark.parametrize( "path_to_package", [ diff --git a/tests/test_version.py b/tests/test_version.py index acc3d6c..c6c6429 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -236,36 +236,7 @@ INVALID_NOT_SEMANTIC = ( INVALID_VERSIONS = INVALID_NOT_READABLE + INVALID_NOT_SEMANTIC -@pytest.mark.parametrize( - ["version1", "version2"], - zip( # loop over pairs of neighboring elements - VALID_AND_NORMALIZED_VERSIONS, - VALID_AND_NORMALIZED_VERSIONS[1:], - ), -) -def test_versions_are_strictly_ordered(version1, version2): - """`VALID_AND_NORMALIZED_VERSIONS` are ordered.""" - version1_parsed = pkg_version.Version(version1) - version2_parsed = pkg_version.Version(version2) - - assert version1_parsed < version2_parsed - - -@pytest.mark.parametrize( - ["version1", "version2"], - zip( # loop over pairs of neighboring elements - VALID_AND_NOT_NORMALIZED_VERSIONS, - VALID_AND_NOT_NORMALIZED_VERSIONS[1:], - ), -) -def test_versions_are_weakly_ordered(version1, version2): - """`VALID_AND_NOT_NORMALIZED_VERSIONS` are ordered.""" - version1_parsed = pkg_version.Version(version1) - version2_parsed = pkg_version.Version(version2) - - assert version1_parsed <= version2_parsed - - +@pytest.mark.overlapping_test class VersionClassification: """Classifying version identifiers. @@ -344,6 +315,7 @@ class VersionClassification: return is_so_by_parts +@pytest.mark.overlapping_test class TestVersionIdentifier(VersionClassification): """The versions must comply with PEP440 ... @@ -504,6 +476,7 @@ class TestVersionIdentifier(VersionClassification): assert parsed_version.public != unparsed_version +@pytest.mark.overlapping_test class TestVersionIdentifierWithPattern: """Test the versioning with a custom `regex` pattern.""" @@ -585,3 +558,36 @@ class TestUnavailablePackageMetadata: with self.hide_metadata_from_package("lalib") as lalib_pkg: assert lalib_pkg.__pkg_name__ == "unknown" assert lalib_pkg.__version__ == "unknown" + + +@pytest.mark.sanity_test +class TestSampleVersionData: + """Ensure the `VALID_*_VERSIONS` are in order.""" + + @pytest.mark.parametrize( + ["version1", "version2"], + zip( # loop over pairs of neighboring elements + VALID_AND_NORMALIZED_VERSIONS, + VALID_AND_NORMALIZED_VERSIONS[1:], + ), + ) + def test_versions_are_strictly_ordered(self, version1, version2): + """`VALID_AND_NORMALIZED_VERSIONS` are ordered.""" + version1_parsed = pkg_version.Version(version1) + version2_parsed = pkg_version.Version(version2) + + assert version1_parsed < version2_parsed + + @pytest.mark.parametrize( + ["version1", "version2"], + zip( # loop over pairs of neighboring elements + VALID_AND_NOT_NORMALIZED_VERSIONS, + VALID_AND_NOT_NORMALIZED_VERSIONS[1:], + ), + ) + def test_versions_are_weakly_ordered(self, version1, version2): + """`VALID_AND_NOT_NORMALIZED_VERSIONS` are ordered.""" + version1_parsed = pkg_version.Version(version1) + version2_parsed = pkg_version.Version(version2) + + assert version1_parsed <= version2_parsed From 6bd21ce1341eb2a1182121adad13923d4f641a10 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 15 Oct 2024 02:04:34 +0200 Subject: [PATCH 14/20] Fix missing module Forgotten in commit 153094eef52d6c1604a4493d2505f523983e9198 --- tests/test_top_level_imports.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_top_level_imports.py b/tests/test_top_level_imports.py index fb5be32..c12214a 100644 --- a/tests/test_top_level_imports.py +++ b/tests/test_top_level_imports.py @@ -12,6 +12,7 @@ import pytest [ "lalib", "lalib.elements", + "lalib.fields", ], ) def test_top_level_imports(path_to_package: str): From 849b786e13e8958f565a97bbc6a7916210109101 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 15 Oct 2024 02:15:10 +0200 Subject: [PATCH 15/20] Add fields to top-level imports for `lalib` --- src/lalib/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/lalib/__init__.py b/src/lalib/__init__.py index 165418e..53a7a35 100644 --- a/src/lalib/__init__.py +++ b/src/lalib/__init__.py @@ -23,6 +23,7 @@ one from importlib import metadata from lalib import elements +from lalib import fields try: @@ -42,8 +43,11 @@ else: gf2, one, zero = elements.gf2, elements.one, elements.zero +Q, R, C, GF2 = fields.Q, fields.R, fields.C, fields.GF2 + del elements +del fields del metadata @@ -51,4 +55,8 @@ __all__ = ( "gf2", "one", "zero", + "Q", + "R", + "C", + "GF2", ) From f952d959514d705676d084e1ccfbfe038e146ece Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 15 Oct 2024 02:17:23 +0200 Subject: [PATCH 16/20] Make `value` a positional-only argument in `gf2()` Most of Python's built-in data types behave like this as well --- src/lalib/elements/galois.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lalib/elements/galois.py b/src/lalib/elements/galois.py index e2651b2..11e44c6 100644 --- a/src/lalib/elements/galois.py +++ b/src/lalib/elements/galois.py @@ -48,6 +48,7 @@ from lalib import config def _to_gf2( value: complex, # `mypy` reads `complex | float | int` + /, *, strict: bool = True, threshold: float = config.THRESHOLD, @@ -160,6 +161,7 @@ class GF2Element(metaclass=GF2Meta): def __init__( self, value: object = None, + /, *, strict: bool = True, threshold: float = config.THRESHOLD, From 25c718fe6a9d1c3c14a9585d63064349949d9e05 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Wed, 16 Oct 2024 01:32:14 +0200 Subject: [PATCH 17/20] Make Python 3.13 the new default --- .github/workflows/audit.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/release.yml | 1 + .github/workflows/test_coverage.yml | 1 + .github/workflows/test_docstrings.yml | 2 +- .github/workflows/tests.yml | 2 +- .readthedocs.yml | 2 +- README.md | 2 +- noxfile.py | 4 ++-- pyproject.toml | 1 + 11 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index dda4fb9..17f5ca9 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -8,7 +8,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: 3.12 + python-version: 3.13 architecture: x64 - run: python --version diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index da7d9d2..4ac03bc 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -8,7 +8,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: 3.12 + python-version: 3.13 architecture: x64 - run: python --version diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 318be47..ad6e324 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,7 +8,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: 3.12 + python-version: 3.13 architecture: x64 - run: python --version diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 37db62a..0c4a35e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,6 +16,7 @@ jobs: 3.10 3.11 3.12 + 3.13 architecture: x64 - run: python --version diff --git a/.github/workflows/test_coverage.yml b/.github/workflows/test_coverage.yml index 3a9b498..7c8a7a9 100644 --- a/.github/workflows/test_coverage.yml +++ b/.github/workflows/test_coverage.yml @@ -14,6 +14,7 @@ jobs: 3.10 3.11 3.12 + 3.13 architecture: x64 - run: python --version diff --git a/.github/workflows/test_docstrings.yml b/.github/workflows/test_docstrings.yml index cc55589..7c32390 100644 --- a/.github/workflows/test_docstrings.yml +++ b/.github/workflows/test_docstrings.yml @@ -8,7 +8,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: 3.12 + python-version: 3.13 architecture: x64 - run: python --version diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8d703e6..f29c6fa 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,7 +5,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] name: test-${{ matrix.python-version }} steps: - uses: actions/checkout@v4 diff --git a/.readthedocs.yml b/.readthedocs.yml index 78c25f1..e427896 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -3,7 +3,7 @@ version: 2 build: os: ubuntu-24.04 tools: - python: "3.12" + python: "3.13" sphinx: configuration: docs/conf.py diff --git a/README.md b/README.md index 890a074..382d774 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ To execute all default tasks, simply invoke: `nox` This includes running the test suite for the project's main Python version - (i.e., [3.12](https://devguide.python.org/versions/)). + (i.e., [3.13](https://devguide.python.org/versions/)). #### Code Formatting & Linting diff --git a/noxfile.py b/noxfile.py index e03e1bb..4db6f24 100644 --- a/noxfile.py +++ b/noxfile.py @@ -57,7 +57,7 @@ def load_supported_python_versions(*, reverse: bool = False) -> list[str]: SUPPORTED_PYTHONS = load_supported_python_versions(reverse=True) -MAIN_PYTHON = "3.12" +MAIN_PYTHON = "3.13" DOCS_SRC, DOCS_BUILD = ("docs/", ".cache/docs/") TESTS_LOCATION = "tests/" @@ -460,7 +460,7 @@ def start(session: nox.Session) -> None: session.env["PIP_CACHE_DIR"] = ".cache/pip" session.env["PIP_DISABLE_PIP_VERSION_CHECK"] = "true" - if session.python in ("3.12", "3.11"): + if session.python in ("3.13", "3.12", "3.11"): session.env["PRAGMA_SUPPORT_39_N_310"] = "to support Python 3.9 & 3.10" else: session.env["PRAGMA_SUPPORT_39_N_310"] = f"{_magic_number =}" diff --git a/pyproject.toml b/pyproject.toml index 3114aae..72c975f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] description = "A Python library to study linear algebra" license = "MIT" From 81912f1a815097fbda9de2335a9ce16681777360 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Wed, 16 Oct 2024 11:23:54 +0200 Subject: [PATCH 18/20] Make linters check for unused function arguments --- noxfile.py | 1 + poetry.lock | 16 +++++++++++++++- pyproject.toml | 9 +++++++++ src/lalib/elements/galois.py | 2 +- src/lalib/fields/base.py | 2 +- 5 files changed, 27 insertions(+), 3 deletions(-) diff --git a/noxfile.py b/noxfile.py index 4db6f24..efb7135 100644 --- a/noxfile.py +++ b/noxfile.py @@ -208,6 +208,7 @@ def lint(session: nox.Session) -> None: "flake8-todos", "flake8-pyproject", "flake8-pytest-style", + "flake8-unused-arguments", "mypy", "pep8-naming", # flake8 plug-in "pydoclint[flake8]", diff --git a/poetry.lock b/poetry.lock index 0c5eed0..d0ee9fd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -722,6 +722,20 @@ pycodestyle = "*" [package.extras] dev = ["isort[pyproject]", "pytest"] +[[package]] +name = "flake8-unused-arguments" +version = "0.0.13" +description = "flake8 extension to warn on unused function arguments" +optional = false +python-versions = "*" +files = [ + {file = "flake8-unused-arguments-0.0.13.tar.gz", hash = "sha256:c5894d294424bf7915e44dff0917222c454151016f96edc55b9f847bf307f3f7"}, + {file = "flake8_unused_arguments-0.0.13-py3-none-any.whl", hash = "sha256:df6a76b73a6ce67720332182a80f2f0a80783cab1ccae8175f39787cd3a74b31"}, +] + +[package.dependencies] +flake8 = ">3.0.0" + [[package]] name = "identify" version = "2.6.1" @@ -1728,4 +1742,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "a10f3c13ed0c67e6ea65500335281cd269e3efdd0fae544ad667d07637ef118f" +content-hash = "e0962e284e415b91fad954a9ca20d3dd45be434d23c63ce1e38da75f69e9557c" diff --git a/pyproject.toml b/pyproject.toml index 72c975f..51f94c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,7 @@ flake8-string-format = "^0.3" flake8-todos = "^0.3" flake8-pyproject = "^1.2" flake8-pytest-style = "^2.0" +flake8-unused-arguments = "^0.0" mypy = "^1.11" pep8-naming = "^0.14" # flake8 plug-in pydoclint = { extras = ["flake8"], version = "^0.5" } @@ -177,6 +178,7 @@ select = [ "S", # flake8-bandit => common security issues "T00", # flake8-todos => unify TODOs "T10", # flake8-debugger => no debugger usage + "U100", # flake8-unused-arguments => declared function arguments must be used # violations not covered by `ruff` below @@ -255,6 +257,12 @@ docstring-quotes = "double" inline-quotes = "double" multiline-quotes = "double" +# Plug-in: flake8-unused-arguments +# Source: https://github.com/nhoad/flake8-unused-arguments +# Make flake8-unused-arguments behave like ruff's "ARG" error code +unused-arguments-ignore-abstract-functions = true +unused-arguments-ignore-stub-functions = true + [tool.isort] # aligned with [tool.ruff.lint.isort] below @@ -347,6 +355,7 @@ select = [ # violations also covered by `flake8` above "ANN", # flake8-annotations => enforce type checking for functions + "ARG", # flake8-unused-arguments => declared function arguments must be used "B", # flake8-bugbear => bugs and design flaws "C4", # flake8-comprehensions => better comprehensions "C90", # mccabe => cyclomatic complexity diff --git a/src/lalib/elements/galois.py b/src/lalib/elements/galois.py index 11e44c6..7ea450d 100644 --- a/src/lalib/elements/galois.py +++ b/src/lalib/elements/galois.py @@ -109,7 +109,7 @@ def _to_gf2( class GF2Meta(abc.ABCMeta): """Make data type of `one` and `zero` appear to be `gf2`.""" - def __repr__(cls) -> str: + def __repr__(cls) -> str: # noqa: RUF100,U100 """Text representation for `GF2Element` and sub-classes.""" return "gf2" diff --git a/src/lalib/fields/base.py b/src/lalib/fields/base.py index 0e94569..b24ccf9 100644 --- a/src/lalib/fields/base.py +++ b/src/lalib/fields/base.py @@ -26,7 +26,7 @@ class Field(abc.ABC, Generic[T]): return self._dtype(value, **kwargs) @staticmethod - def _post_cast_filter(possible_element: T, /) -> bool: + def _post_cast_filter(possible_element: T, /) -> bool: # noqa: ARG004 """Function to filter out castable `value`s. Called after a successfull call of the `._cast_func()`. From 6ddb545491d33303e9f83cde1ffd50b39735b45c Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Wed, 16 Oct 2024 11:39:52 +0200 Subject: [PATCH 19/20] Remove unnecessary `Optional` The type hints `object` and `Optional[object]` both allow the default value `None`. Also, this commit gets rid of ruff's "FA100" error for the two changed lines. --- src/lalib/elements/galois.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/lalib/elements/galois.py b/src/lalib/elements/galois.py index 7ea450d..42aebab 100644 --- a/src/lalib/elements/galois.py +++ b/src/lalib/elements/galois.py @@ -32,9 +32,7 @@ import abc import functools import math import numbers - -# When giving up support for Python 3.9, we can get rid of `Optional` -from typing import Callable, ClassVar, Literal, Optional +from typing import Callable, ClassVar, Literal try: @@ -502,7 +500,7 @@ class GF2Element(metaclass=GF2Meta): """ return self._compute(other, lambda s, o: o % s) - def __pow__(self, other: object, _modulo: Optional[object] = None) -> Self: + def __pow__(self, other: object, _modulo: object = None) -> Self: """Exponentiation: `self ** other`. Powers of `gf2` values are like powers of `int`s. @@ -520,7 +518,7 @@ class GF2Element(metaclass=GF2Meta): """ return self._compute(other, lambda s, o: s**o) - def __rpow__(self, other: object, _modulo: Optional[object] = None) -> Self: + def __rpow__(self, other: object, _modulo: object = None) -> Self: """(Reflected) Exponentiation: `other ** self`. See docstring for `.__pow__()`. From c7f076c35c0e96f32c109976baac689afd632c93 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Wed, 16 Oct 2024 11:55:52 +0200 Subject: [PATCH 20/20] Update dependencies - bandit (1.7.9 -> 1.7.10) - black (24.8.0 -> 24.10.0) - charset-normalizer (3.3.2 -> 3.4.0) - coverage (7.6.1 -> 7.6.3) - distlib (0.3.8 -> 0.3.9) - markupsafe (2.1.5 -> 3.0.1) - mypy (1.11.2 -> 1.12.0) - pydoclint (0.5.7 -> 0.5.9) - rich (13.8.1 -> 13.9.2) - ruff (0.6.5 -> 0.6.9) - sphinx (8.0.2 -> 8.1.3) - virtualenv (20.26.5 -> 20.26.6) --- poetry.lock | 668 ++++++++++++++++++++++++++-------------------------- 1 file changed, 340 insertions(+), 328 deletions(-) diff --git a/poetry.lock b/poetry.lock index d0ee9fd..cfec135 100644 --- a/poetry.lock +++ b/poetry.lock @@ -70,13 +70,13 @@ dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] [[package]] name = "bandit" -version = "1.7.9" +version = "1.7.10" description = "Security oriented static analyser for python code." optional = false python-versions = ">=3.8" files = [ - {file = "bandit-1.7.9-py3-none-any.whl", hash = "sha256:52077cb339000f337fb25f7e045995c4ad01511e716e5daac37014b9752de8ec"}, - {file = "bandit-1.7.9.tar.gz", hash = "sha256:7c395a436743018f7be0a4cbb0a4ea9b902b6d87264ddecf8cfdc73b4f78ff61"}, + {file = "bandit-1.7.10-py3-none-any.whl", hash = "sha256:665721d7bebbb4485a339c55161ac0eedde27d51e638000d91c8c2d68343ad02"}, + {file = "bandit-1.7.10.tar.gz", hash = "sha256:59ed5caf5d92b6ada4bf65bc6437feea4a9da1093384445fed4d472acc6cff7b"}, ] [package.dependencies] @@ -94,33 +94,33 @@ yaml = ["PyYAML"] [[package]] name = "black" -version = "24.8.0" +version = "24.10.0" description = "The uncompromising code formatter." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, - {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, - {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, - {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, - {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, - {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, - {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, - {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, - {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, - {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, - {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, - {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, - {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, - {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, - {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, - {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, - {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, - {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, - {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, - {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, - {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, - {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, + {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, + {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, + {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, + {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, + {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, + {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, + {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, + {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, + {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, + {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, + {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, + {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, + {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, + {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, + {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, + {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, + {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, + {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, + {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, + {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, + {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, + {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, ] [package.dependencies] @@ -134,7 +134,7 @@ typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +d = ["aiohttp (>=3.10)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] @@ -162,101 +162,116 @@ files = [ [[package]] name = "charset-normalizer" -version = "3.3.2" +version = "3.4.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, + {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, + {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, ] [[package]] @@ -286,83 +301,73 @@ files = [ [[package]] name = "coverage" -version = "7.6.1" +version = "7.6.3" description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, - {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, - {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, - {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, - {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, - {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, - {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, - {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, - {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, - {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, - {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, - {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, - {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, - {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, - {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, - {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, - {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, - {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, + {file = "coverage-7.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6da42bbcec130b188169107ecb6ee7bd7b4c849d24c9370a0c884cf728d8e976"}, + {file = "coverage-7.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c222958f59b0ae091f4535851cbb24eb57fc0baea07ba675af718fb5302dddb2"}, + {file = "coverage-7.6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab84a8b698ad5a6c365b08061920138e7a7dd9a04b6feb09ba1bfae68346ce6d"}, + {file = "coverage-7.6.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70a6756ce66cd6fe8486c775b30889f0dc4cb20c157aa8c35b45fd7868255c5c"}, + {file = "coverage-7.6.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c2e6fa98032fec8282f6b27e3f3986c6e05702828380618776ad794e938f53a"}, + {file = "coverage-7.6.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:921fbe13492caf6a69528f09d5d7c7d518c8d0e7b9f6701b7719715f29a71e6e"}, + {file = "coverage-7.6.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6d99198203f0b9cb0b5d1c0393859555bc26b548223a769baf7e321a627ed4fc"}, + {file = "coverage-7.6.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:87cd2e29067ea397a47e352efb13f976eb1b03e18c999270bb50589323294c6e"}, + {file = "coverage-7.6.3-cp310-cp310-win32.whl", hash = "sha256:a3328c3e64ea4ab12b85999eb0779e6139295bbf5485f69d42cf794309e3d007"}, + {file = "coverage-7.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:bca4c8abc50d38f9773c1ec80d43f3768df2e8576807d1656016b9d3eeaa96fd"}, + {file = "coverage-7.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c51ef82302386d686feea1c44dbeef744585da16fcf97deea2a8d6c1556f519b"}, + {file = "coverage-7.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0ca37993206402c6c35dc717f90d4c8f53568a8b80f0bf1a1b2b334f4d488fba"}, + {file = "coverage-7.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c77326300b839c44c3e5a8fe26c15b7e87b2f32dfd2fc9fee1d13604347c9b38"}, + {file = "coverage-7.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e484e479860e00da1f005cd19d1c5d4a813324e5951319ac3f3eefb497cc549"}, + {file = "coverage-7.6.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c6c0f4d53ef603397fc894a895b960ecd7d44c727df42a8d500031716d4e8d2"}, + {file = "coverage-7.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:37be7b5ea3ff5b7c4a9db16074dc94523b5f10dd1f3b362a827af66a55198175"}, + {file = "coverage-7.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:43b32a06c47539fe275106b376658638b418c7cfdfff0e0259fbf877e845f14b"}, + {file = "coverage-7.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee77c7bef0724165e795b6b7bf9c4c22a9b8468a6bdb9c6b4281293c6b22a90f"}, + {file = "coverage-7.6.3-cp311-cp311-win32.whl", hash = "sha256:43517e1f6b19f610a93d8227e47790722c8bf7422e46b365e0469fc3d3563d97"}, + {file = "coverage-7.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:04f2189716e85ec9192df307f7c255f90e78b6e9863a03223c3b998d24a3c6c6"}, + {file = "coverage-7.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27bd5f18d8f2879e45724b0ce74f61811639a846ff0e5c0395b7818fae87aec6"}, + {file = "coverage-7.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d546cfa78844b8b9c1c0533de1851569a13f87449897bbc95d698d1d3cb2a30f"}, + {file = "coverage-7.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9975442f2e7a5cfcf87299c26b5a45266ab0696348420049b9b94b2ad3d40234"}, + {file = "coverage-7.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:583049c63106c0555e3ae3931edab5669668bbef84c15861421b94e121878d3f"}, + {file = "coverage-7.6.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2341a78ae3a5ed454d524206a3fcb3cec408c2a0c7c2752cd78b606a2ff15af4"}, + {file = "coverage-7.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a4fb91d5f72b7e06a14ff4ae5be625a81cd7e5f869d7a54578fc271d08d58ae3"}, + {file = "coverage-7.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e279f3db904e3b55f520f11f983cc8dc8a4ce9b65f11692d4718ed021ec58b83"}, + {file = "coverage-7.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa23ce39661a3e90eea5f99ec59b763b7d655c2cada10729ed920a38bfc2b167"}, + {file = "coverage-7.6.3-cp312-cp312-win32.whl", hash = "sha256:52ac29cc72ee7e25ace7807249638f94c9b6a862c56b1df015d2b2e388e51dbd"}, + {file = "coverage-7.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:40e8b1983080439d4802d80b951f4a93d991ef3261f69e81095a66f86cf3c3c6"}, + {file = "coverage-7.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9134032f5aa445ae591c2ba6991d10136a1f533b1d2fa8f8c21126468c5025c6"}, + {file = "coverage-7.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:99670790f21a96665a35849990b1df447993880bb6463a0a1d757897f30da929"}, + {file = "coverage-7.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc7d6b380ca76f5e817ac9eef0c3686e7834c8346bef30b041a4ad286449990"}, + {file = "coverage-7.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f7b26757b22faf88fcf232f5f0e62f6e0fd9e22a8a5d0d5016888cdfe1f6c1c4"}, + {file = "coverage-7.6.3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c59d6a4a4633fad297f943c03d0d2569867bd5372eb5684befdff8df8522e39"}, + {file = "coverage-7.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f263b18692f8ed52c8de7f40a0751e79015983dbd77b16906e5b310a39d3ca21"}, + {file = "coverage-7.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:79644f68a6ff23b251cae1c82b01a0b51bc40c8468ca9585c6c4b1aeee570e0b"}, + {file = "coverage-7.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:71967c35828c9ff94e8c7d405469a1fb68257f686bca7c1ed85ed34e7c2529c4"}, + {file = "coverage-7.6.3-cp313-cp313-win32.whl", hash = "sha256:e266af4da2c1a4cbc6135a570c64577fd3e6eb204607eaff99d8e9b710003c6f"}, + {file = "coverage-7.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:ea52bd218d4ba260399a8ae4bb6b577d82adfc4518b93566ce1fddd4a49d1dce"}, + {file = "coverage-7.6.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8d4c6ea0f498c7c79111033a290d060c517853a7bcb2f46516f591dab628ddd3"}, + {file = "coverage-7.6.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:331b200ad03dbaa44151d74daeb7da2cf382db424ab923574f6ecca7d3b30de3"}, + {file = "coverage-7.6.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54356a76b67cf8a3085818026bb556545ebb8353951923b88292556dfa9f812d"}, + {file = "coverage-7.6.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ebec65f5068e7df2d49466aab9128510c4867e532e07cb6960075b27658dca38"}, + {file = "coverage-7.6.3-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d33a785ea8354c480515e781554d3be582a86297e41ccbea627a5c632647f2cd"}, + {file = "coverage-7.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f7ddb920106bbbbcaf2a274d56f46956bf56ecbde210d88061824a95bdd94e92"}, + {file = "coverage-7.6.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:70d24936ca6c15a3bbc91ee9c7fc661132c6f4c9d42a23b31b6686c05073bde5"}, + {file = "coverage-7.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c30e42ea11badb147f0d2e387115b15e2bd8205a5ad70d6ad79cf37f6ac08c91"}, + {file = "coverage-7.6.3-cp313-cp313t-win32.whl", hash = "sha256:365defc257c687ce3e7d275f39738dcd230777424117a6c76043459db131dd43"}, + {file = "coverage-7.6.3-cp313-cp313t-win_amd64.whl", hash = "sha256:23bb63ae3f4c645d2d82fa22697364b0046fbafb6261b258a58587441c5f7bd0"}, + {file = "coverage-7.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:da29ceabe3025a1e5a5aeeb331c5b1af686daab4ff0fb4f83df18b1180ea83e2"}, + {file = "coverage-7.6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:df8c05a0f574d480947cba11b947dc41b1265d721c3777881da2fb8d3a1ddfba"}, + {file = "coverage-7.6.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec1e3b40b82236d100d259854840555469fad4db64f669ab817279eb95cd535c"}, + {file = "coverage-7.6.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4adeb878a374126f1e5cf03b87f66279f479e01af0e9a654cf6d1509af46c40"}, + {file = "coverage-7.6.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43d6a66e33b1455b98fc7312b124296dad97a2e191c80320587234a77b1b736e"}, + {file = "coverage-7.6.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1990b1f4e2c402beb317840030bb9f1b6a363f86e14e21b4212e618acdfce7f6"}, + {file = "coverage-7.6.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:12f9515d875859faedb4144fd38694a761cd2a61ef9603bf887b13956d0bbfbb"}, + {file = "coverage-7.6.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99ded130555c021d99729fabd4ddb91a6f4cc0707df4b1daf912c7850c373b13"}, + {file = "coverage-7.6.3-cp39-cp39-win32.whl", hash = "sha256:c3a79f56dee9136084cf84a6c7c4341427ef36e05ae6415bf7d787c96ff5eaa3"}, + {file = "coverage-7.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:aac7501ae73d4a02f4b7ac8fcb9dc55342ca98ffb9ed9f2dfb8a25d53eda0e4d"}, + {file = "coverage-7.6.3-pp39.pp310-none-any.whl", hash = "sha256:b9853509b4bf57ba7b1f99b9d866c422c9c5248799ab20e652bbb8a184a38181"}, + {file = "coverage-7.6.3.tar.gz", hash = "sha256:bb7d5fe92bd0dc235f63ebe9f8c6e0884f7360f88f3411bfed1350c872ef2054"}, ] [package.dependencies] @@ -373,13 +378,13 @@ toml = ["tomli"] [[package]] name = "distlib" -version = "0.3.8" +version = "0.3.9" description = "Distribution utilities" optional = false python-versions = "*" files = [ - {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, - {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, ] [[package]] @@ -866,71 +871,72 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "markupsafe" -version = "2.1.5" +version = "3.0.1" description = "Safely add untrusted strings to HTML/XML markup." optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, - {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:db842712984e91707437461930e6011e60b39136c7331e971952bb30465bc1a1"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ffb4a8e7d46ed96ae48805746755fadd0909fea2306f93d5d8233ba23dda12a"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67c519635a4f64e495c50e3107d9b4075aec33634272b5db1cde839e07367589"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48488d999ed50ba8d38c581d67e496f955821dc183883550a6fbc7f1aefdc170"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f31ae06f1328595d762c9a2bf29dafd8621c7d3adc130cbb46278079758779ca"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80fcbf3add8790caddfab6764bde258b5d09aefbe9169c183f88a7410f0f6dea"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3341c043c37d78cc5ae6e3e305e988532b072329639007fd408a476642a89fd6"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cb53e2a99df28eee3b5f4fea166020d3ef9116fdc5764bc5117486e6d1211b25"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-win32.whl", hash = "sha256:db15ce28e1e127a0013dfb8ac243a8e392db8c61eae113337536edb28bdc1f97"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:4ffaaac913c3f7345579db4f33b0020db693f302ca5137f106060316761beea9"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:26627785a54a947f6d7336ce5963569b5d75614619e75193bdb4e06e21d447ad"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b954093679d5750495725ea6f88409946d69cfb25ea7b4c846eef5044194f583"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:973a371a55ce9ed333a3a0f8e0bcfae9e0d637711534bcb11e130af2ab9334e7"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:244dbe463d5fb6d7ce161301a03a6fe744dac9072328ba9fc82289238582697b"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d98e66a24497637dd31ccab090b34392dddb1f2f811c4b4cd80c230205c074a3"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ad91738f14eb8da0ff82f2acd0098b6257621410dcbd4df20aaa5b4233d75a50"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7044312a928a66a4c2a22644147bc61a199c1709712069a344a3fb5cfcf16915"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a4792d3b3a6dfafefdf8e937f14906a51bd27025a36f4b188728a73382231d91"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-win32.whl", hash = "sha256:fa7d686ed9883f3d664d39d5a8e74d3c5f63e603c2e3ff0abcba23eac6542635"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ba25a71ebf05b9bb0e2ae99f8bc08a07ee8e98c612175087112656ca0f5c8bf"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8ae369e84466aa70f3154ee23c1451fda10a8ee1b63923ce76667e3077f2b0c4"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40f1e10d51c92859765522cbd79c5c8989f40f0419614bcdc5015e7b6bf97fc5"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a4cb365cb49b750bdb60b846b0c0bc49ed62e59a76635095a179d440540c346"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee3941769bd2522fe39222206f6dd97ae83c442a94c90f2b7a25d847d40f4729"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62fada2c942702ef8952754abfc1a9f7658a4d5460fabe95ac7ec2cbe0d02abc"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c2d64fdba74ad16138300815cfdc6ab2f4647e23ced81f59e940d7d4a1469d9"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fb532dd9900381d2e8f48172ddc5a59db4c445a11b9fab40b3b786da40d3b56b"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0f84af7e813784feb4d5e4ff7db633aba6c8ca64a833f61d8e4eade234ef0c38"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-win32.whl", hash = "sha256:cbf445eb5628981a80f54087f9acdbf84f9b7d862756110d172993b9a5ae81aa"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:a10860e00ded1dd0a65b83e717af28845bb7bd16d8ace40fe5531491de76b79f"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e81c52638315ff4ac1b533d427f50bc0afc746deb949210bc85f05d4f15fd772"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:312387403cd40699ab91d50735ea7a507b788091c416dd007eac54434aee51da"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ae99f31f47d849758a687102afdd05bd3d3ff7dbab0a8f1587981b58a76152a"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c97ff7fedf56d86bae92fa0a646ce1a0ec7509a7578e1ed238731ba13aabcd1c"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7420ceda262dbb4b8d839a4ec63d61c261e4e77677ed7c66c99f4e7cb5030dd"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45d42d132cff577c92bfba536aefcfea7e26efb975bd455db4e6602f5c9f45e7"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c8817557d0de9349109acb38b9dd570b03cc5014e8aabf1cbddc6e81005becd"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a54c43d3ec4cf2a39f4387ad044221c66a376e58c0d0e971d47c475ba79c6b5"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-win32.whl", hash = "sha256:c91b394f7601438ff79a4b93d16be92f216adb57d813a78be4446fe0f6bc2d8c"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:fe32482b37b4b00c7a52a07211b479653b7fe4f22b2e481b9a9b099d8a430f2f"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:17b2aea42a7280db02ac644db1d634ad47dcc96faf38ab304fe26ba2680d359a"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:852dc840f6d7c985603e60b5deaae1d89c56cb038b577f6b5b8c808c97580f1d"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0778de17cff1acaeccc3ff30cd99a3fd5c50fc58ad3d6c0e0c4c58092b859396"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:800100d45176652ded796134277ecb13640c1a537cad3b8b53da45aa96330453"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d06b24c686a34c86c8c1fba923181eae6b10565e4d80bdd7bc1c8e2f11247aa4"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:33d1c36b90e570ba7785dacd1faaf091203d9942bc036118fab8110a401eb1a8"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:beeebf760a9c1f4c07ef6a53465e8cfa776ea6a2021eda0d0417ec41043fe984"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bbde71a705f8e9e4c3e9e33db69341d040c827c7afa6789b14c6e16776074f5a"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-win32.whl", hash = "sha256:82b5dba6eb1bcc29cc305a18a3c5365d2af06ee71b123216416f7e20d2a84e5b"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:730d86af59e0e43ce277bb83970530dd223bf7f2a838e086b50affa6ec5f9295"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4935dd7883f1d50e2ffecca0aa33dc1946a94c8f3fdafb8df5c330e48f71b132"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e9393357f19954248b00bed7c56f29a25c930593a77630c719653d51e7669c2a"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40621d60d0e58aa573b68ac5e2d6b20d44392878e0bfc159012a5787c4e35bc8"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f94190df587738280d544971500b9cafc9b950d32efcb1fba9ac10d84e6aa4e6"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6a387d61fe41cdf7ea95b38e9af11cfb1a63499af2759444b99185c4ab33f5b"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8ad4ad1429cd4f315f32ef263c1342166695fad76c100c5d979c45d5570ed58b"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e24bfe89c6ac4c31792793ad9f861b8f6dc4546ac6dc8f1c9083c7c4f2b335cd"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2a4b34a8d14649315c4bc26bbfa352663eb51d146e35eef231dd739d54a5430a"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-win32.whl", hash = "sha256:242d6860f1fd9191aef5fae22b51c5c19767f93fb9ead4d21924e0bcb17619d8"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:93e8248d650e7e9d49e8251f883eed60ecbc0e8ffd6349e18550925e31bd029b"}, + {file = "markupsafe-3.0.1.tar.gz", hash = "sha256:3e683ee4f5d0fa2dde4db77ed8dd8a876686e3fc417655c2ece9a90576905344"}, ] [[package]] @@ -957,38 +963,43 @@ files = [ [[package]] name = "mypy" -version = "1.11.2" +version = "1.12.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"}, - {file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"}, - {file = "mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383"}, - {file = "mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"}, - {file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"}, - {file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"}, - {file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"}, - {file = "mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104"}, - {file = "mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"}, - {file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"}, - {file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"}, - {file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"}, - {file = "mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987"}, - {file = "mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"}, - {file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"}, - {file = "mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b"}, - {file = "mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86"}, - {file = "mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce"}, - {file = "mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1"}, - {file = "mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b"}, - {file = "mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6"}, - {file = "mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70"}, - {file = "mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d"}, - {file = "mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d"}, - {file = "mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24"}, - {file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"}, - {file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"}, + {file = "mypy-1.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4397081e620dc4dc18e2f124d5e1d2c288194c2c08df6bdb1db31c38cd1fe1ed"}, + {file = "mypy-1.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:684a9c508a283f324804fea3f0effeb7858eb03f85c4402a967d187f64562469"}, + {file = "mypy-1.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cabe4cda2fa5eca7ac94854c6c37039324baaa428ecbf4de4567279e9810f9e"}, + {file = "mypy-1.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:060a07b10e999ac9e7fa249ce2bdcfa9183ca2b70756f3bce9df7a92f78a3c0a"}, + {file = "mypy-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:0eff042d7257f39ba4ca06641d110ca7d2ad98c9c1fb52200fe6b1c865d360ff"}, + {file = "mypy-1.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b86de37a0da945f6d48cf110d5206c5ed514b1ca2614d7ad652d4bf099c7de7"}, + {file = "mypy-1.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:20c7c5ce0c1be0b0aea628374e6cf68b420bcc772d85c3c974f675b88e3e6e57"}, + {file = "mypy-1.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a64ee25f05fc2d3d8474985c58042b6759100a475f8237da1f4faf7fcd7e6309"}, + {file = "mypy-1.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:faca7ab947c9f457a08dcb8d9a8664fd438080e002b0fa3e41b0535335edcf7f"}, + {file = "mypy-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:5bc81701d52cc8767005fdd2a08c19980de9ec61a25dbd2a937dfb1338a826f9"}, + {file = "mypy-1.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8462655b6694feb1c99e433ea905d46c478041a8b8f0c33f1dab00ae881b2164"}, + {file = "mypy-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:923ea66d282d8af9e0f9c21ffc6653643abb95b658c3a8a32dca1eff09c06475"}, + {file = "mypy-1.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ebf9e796521f99d61864ed89d1fb2926d9ab6a5fab421e457cd9c7e4dd65aa9"}, + {file = "mypy-1.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e478601cc3e3fa9d6734d255a59c7a2e5c2934da4378f3dd1e3411ea8a248642"}, + {file = "mypy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:c72861b7139a4f738344faa0e150834467521a3fba42dc98264e5aa9507dd601"}, + {file = "mypy-1.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52b9e1492e47e1790360a43755fa04101a7ac72287b1a53ce817f35899ba0521"}, + {file = "mypy-1.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:48d3e37dd7d9403e38fa86c46191de72705166d40b8c9f91a3de77350daa0893"}, + {file = "mypy-1.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f106db5ccb60681b622ac768455743ee0e6a857724d648c9629a9bd2ac3f721"}, + {file = "mypy-1.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:233e11b3f73ee1f10efada2e6da0f555b2f3a5316e9d8a4a1224acc10e7181d3"}, + {file = "mypy-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:4ae8959c21abcf9d73aa6c74a313c45c0b5a188752bf37dace564e29f06e9c1b"}, + {file = "mypy-1.12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eafc1b7319b40ddabdc3db8d7d48e76cfc65bbeeafaa525a4e0fa6b76175467f"}, + {file = "mypy-1.12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9b9ce1ad8daeb049c0b55fdb753d7414260bad8952645367e70ac91aec90e07e"}, + {file = "mypy-1.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfe012b50e1491d439172c43ccb50db66d23fab714d500b57ed52526a1020bb7"}, + {file = "mypy-1.12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2c40658d4fa1ab27cb53d9e2f1066345596af2f8fe4827defc398a09c7c9519b"}, + {file = "mypy-1.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:dee78a8b9746c30c1e617ccb1307b351ded57f0de0d287ca6276378d770006c0"}, + {file = "mypy-1.12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b5df6c8a8224f6b86746bda716bbe4dbe0ce89fd67b1fa4661e11bfe38e8ec8"}, + {file = "mypy-1.12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5feee5c74eb9749e91b77f60b30771563327329e29218d95bedbe1257e2fe4b0"}, + {file = "mypy-1.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:77278e8c6ffe2abfba6db4125de55f1024de9a323be13d20e4f73b8ed3402bd1"}, + {file = "mypy-1.12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:dcfb754dea911039ac12434d1950d69a2f05acd4d56f7935ed402be09fad145e"}, + {file = "mypy-1.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:06de0498798527451ffb60f68db0d368bd2bae2bbfb5237eae616d4330cc87aa"}, + {file = "mypy-1.12.0-py3-none-any.whl", hash = "sha256:fd313226af375d52e1e36c383f39bf3836e1f192801116b31b090dfcd3ec5266"}, + {file = "mypy-1.12.0.tar.gz", hash = "sha256:65a22d87e757ccd95cbbf6f7e181e6caa87128255eb2b6be901bb71b26d8a99d"}, ] [package.dependencies] @@ -1133,13 +1144,13 @@ files = [ [[package]] name = "pydoclint" -version = "0.5.7" +version = "0.5.9" description = "A Python docstring linter that checks arguments, returns, yields, and raises sections" optional = false python-versions = ">=3.8" files = [ - {file = "pydoclint-0.5.7-py2.py3-none-any.whl", hash = "sha256:22561b8f876b7bd3d4966a3b7fe27bfdbfa41fa50d0719f13b08844a39e2717c"}, - {file = "pydoclint-0.5.7.tar.gz", hash = "sha256:d2938efc95233205822bebe5da5c8e1c4796da09988a2a73c2d6b63cef4577a6"}, + {file = "pydoclint-0.5.9-py2.py3-none-any.whl", hash = "sha256:089327003cef6fe5605cbaa9887859ea5229ce0c9abb52775ffd57513094c1ae"}, + {file = "pydoclint-0.5.9.tar.gz", hash = "sha256:e200f964a5d9fbbb2ff1078bd7cb5433a0564d2482b6a1ba1be848f66bc4924f"}, ] [package.dependencies] @@ -1347,47 +1358,48 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rich" -version = "13.8.1" +version = "13.9.2" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.8.0" files = [ - {file = "rich-13.8.1-py3-none-any.whl", hash = "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06"}, - {file = "rich-13.8.1.tar.gz", hash = "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a"}, + {file = "rich-13.9.2-py3-none-any.whl", hash = "sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1"}, + {file = "rich-13.9.2.tar.gz", hash = "sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c"}, ] [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.6.5" +version = "0.6.9" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.6.5-py3-none-linux_armv6l.whl", hash = "sha256:7e4e308f16e07c95fc7753fc1aaac690a323b2bb9f4ec5e844a97bb7fbebd748"}, - {file = "ruff-0.6.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:932cd69eefe4daf8c7d92bd6689f7e8182571cb934ea720af218929da7bd7d69"}, - {file = "ruff-0.6.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3a8d42d11fff8d3143ff4da41742a98f8f233bf8890e9fe23077826818f8d680"}, - {file = "ruff-0.6.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a50af6e828ee692fb10ff2dfe53f05caecf077f4210fae9677e06a808275754f"}, - {file = "ruff-0.6.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:794ada3400a0d0b89e3015f1a7e01f4c97320ac665b7bc3ade24b50b54cb2972"}, - {file = "ruff-0.6.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:381413ec47f71ce1d1c614f7779d88886f406f1fd53d289c77e4e533dc6ea200"}, - {file = "ruff-0.6.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:52e75a82bbc9b42e63c08d22ad0ac525117e72aee9729a069d7c4f235fc4d276"}, - {file = "ruff-0.6.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09c72a833fd3551135ceddcba5ebdb68ff89225d30758027280968c9acdc7810"}, - {file = "ruff-0.6.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:800c50371bdcb99b3c1551d5691e14d16d6f07063a518770254227f7f6e8c178"}, - {file = "ruff-0.6.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e25ddd9cd63ba1f3bd51c1f09903904a6adf8429df34f17d728a8fa11174253"}, - {file = "ruff-0.6.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7291e64d7129f24d1b0c947ec3ec4c0076e958d1475c61202497c6aced35dd19"}, - {file = "ruff-0.6.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9ad7dfbd138d09d9a7e6931e6a7e797651ce29becd688be8a0d4d5f8177b4b0c"}, - {file = "ruff-0.6.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:005256d977021790cc52aa23d78f06bb5090dc0bfbd42de46d49c201533982ae"}, - {file = "ruff-0.6.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:482c1e6bfeb615eafc5899127b805d28e387bd87db38b2c0c41d271f5e58d8cc"}, - {file = "ruff-0.6.5-py3-none-win32.whl", hash = "sha256:cf4d3fa53644137f6a4a27a2b397381d16454a1566ae5335855c187fbf67e4f5"}, - {file = "ruff-0.6.5-py3-none-win_amd64.whl", hash = "sha256:3e42a57b58e3612051a636bc1ac4e6b838679530235520e8f095f7c44f706ff9"}, - {file = "ruff-0.6.5-py3-none-win_arm64.whl", hash = "sha256:51935067740773afdf97493ba9b8231279e9beef0f2a8079188c4776c25688e0"}, - {file = "ruff-0.6.5.tar.gz", hash = "sha256:4d32d87fab433c0cf285c3683dd4dae63be05fd7a1d65b3f5bf7cdd05a6b96fb"}, + {file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"}, + {file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"}, + {file = "ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039"}, + {file = "ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d"}, + {file = "ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117"}, + {file = "ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93"}, + {file = "ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2"}, ] [[package]] @@ -1470,13 +1482,13 @@ test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools [[package]] name = "sphinx" -version = "8.0.2" +version = "8.1.3" description = "Python documentation generator" optional = false python-versions = ">=3.10" files = [ - {file = "sphinx-8.0.2-py3-none-any.whl", hash = "sha256:56173572ae6c1b9a38911786e206a110c9749116745873feae4f9ce88e59391d"}, - {file = "sphinx-8.0.2.tar.gz", hash = "sha256:0cce1ddcc4fd3532cf1dd283bc7d886758362c5c1de6598696579ce96d8ffa5b"}, + {file = "sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2"}, + {file = "sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927"}, ] [package.dependencies] @@ -1490,17 +1502,17 @@ packaging = ">=23.0" Pygments = ">=2.17" requests = ">=2.30.0" snowballstemmer = ">=2.2" -sphinxcontrib-applehelp = "*" -sphinxcontrib-devhelp = "*" -sphinxcontrib-htmlhelp = ">=2.0.0" -sphinxcontrib-jsmath = "*" -sphinxcontrib-qthelp = "*" +sphinxcontrib-applehelp = ">=1.0.7" +sphinxcontrib-devhelp = ">=1.0.6" +sphinxcontrib-htmlhelp = ">=2.0.6" +sphinxcontrib-jsmath = ">=1.0.1" +sphinxcontrib-qthelp = ">=1.0.6" sphinxcontrib-serializinghtml = ">=1.1.9" tomli = {version = ">=2", markers = "python_version < \"3.11\""} [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=6.0)", "mypy (==1.11.0)", "pytest (>=6.0)", "ruff (==0.5.5)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-Pillow (==10.2.0.20240520)", "types-Pygments (==2.18.0.20240506)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20240724)", "types-requests (>=2.30.0)"] +lint = ["flake8 (>=6.0)", "mypy (==1.11.1)", "pyright (==1.1.384)", "pytest (>=6.0)", "ruff (==0.6.9)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-Pillow (==10.2.0.20240822)", "types-Pygments (==2.18.0.20240506)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20241005)", "types-requests (==2.32.0.20240914)", "types-urllib3 (==1.26.25.14)"] test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] [[package]] @@ -1632,13 +1644,13 @@ pbr = ">=2.0.0" [[package]] name = "tomli" -version = "2.0.1" +version = "2.0.2" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, + {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, ] [[package]] @@ -1671,13 +1683,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.26.5" +version = "20.26.6" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.26.5-py3-none-any.whl", hash = "sha256:4f3ac17b81fba3ce3bd6f4ead2749a72da5929c01774948e243db9ba41df4ff6"}, - {file = "virtualenv-20.26.5.tar.gz", hash = "sha256:ce489cac131aa58f4b25e321d6d186171f78e6cb13fafbf32a840cee67733ff4"}, + {file = "virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2"}, + {file = "virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48"}, ] [package.dependencies]