From d507e1f56db3860532ba87136bcafb69958b3465 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 11:53:17 +0200 Subject: [PATCH 1/9] Add `lalib.elements` sub-package --- src/lalib/elements/__init__.py | 1 + tests/elements/__init__.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 src/lalib/elements/__init__.py create mode 100644 tests/elements/__init__.py diff --git a/src/lalib/elements/__init__.py b/src/lalib/elements/__init__.py new file mode 100644 index 0000000..ae48619 --- /dev/null +++ b/src/lalib/elements/__init__.py @@ -0,0 +1 @@ +"""Various elements of various fields.""" diff --git a/tests/elements/__init__.py b/tests/elements/__init__.py new file mode 100644 index 0000000..e3f2918 --- /dev/null +++ b/tests/elements/__init__.py @@ -0,0 +1 @@ +"""Tests for the `lalib.elements` sub-package.""" From 9083cebe189232506abe385f9f5a109923286157 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Wed, 18 Sep 2024 15:08:19 +0200 Subject: [PATCH 2/9] Fix missing empty line ... ... to make overview on sections clearer --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 4540443..dad8d29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,6 +78,7 @@ semver = "^3.0" # to test the version identifier tomli = [ { python = "<3.11", version = "^2.0" } ] xdoctest = { extras = ["colors"], version = "^1.2" } + [tool.poetry.urls] "Issues Tracker" = "https://github.com/webartifex/lalib/issues" From d9dcea83798d0516613d734a445facc51f398644 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Wed, 18 Sep 2024 15:15:24 +0200 Subject: [PATCH 3/9] Do not allow mere "pragma: no cover"s --- pyproject.toml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index dad8d29..8f3672d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -122,6 +122,13 @@ show_missing = true skip_covered = true skip_empty = true +exclude_lines = [ + + # "pragma: no cover" + # => Intentionally commented out as we thrive for 100% test coverage + +] + [tool.coverage.run] From d405c22c90d22c12c4ad904c1ebf9a6a0043d2c3 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Wed, 18 Sep 2024 15:17:53 +0200 Subject: [PATCH 4/9] Enforce PEP257 strictly ... ... and put docstrings for class constructors into `.__init__()` methods Source: https://peps.python.org/pep-0257/#multi-line-docstrings --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 8f3672d..698ccd0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -185,6 +185,8 @@ extend-ignore = [ # never check the following codes "ANN401", # allow dynamically typed expressions with `typing.Any` + "DOC301", # PEP257 => class constructor's docstring go in `.__init__()` + # Comply with black's style # Sources: https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#pycodestyle "E203", "E701", "E704", "W503", From 3cecf0d989956299477fb0ea272b0efc9a7ecee6 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Wed, 18 Sep 2024 18:04:35 +0200 Subject: [PATCH 5/9] Add `GF2` type for Galois field elements - add `GF2` class in the `lalib.elements` sub-package implementing a typical Galois field with two elements - the singleton objects `one` and `zero` are the concrete instances of the `GF2` type for the end users - besides the typical Galois arithmetic, `one` and `zero` behave like the built-in numbers `1` and `0` and implement the `numbers.Rational` interface - add exhaustive docstrings with usage examples - add (unit) test cases with 100% coverage --- noxfile.py | 7 + poetry.lock | 2 +- pyproject.toml | 5 + src/lalib/elements/gf2.py | 535 ++++++++++++++++++++++++ tests/elements/test_gf2.py | 832 +++++++++++++++++++++++++++++++++++++ tests/test_docstrings.py | 1 + 6 files changed, 1381 insertions(+), 1 deletion(-) create mode 100644 src/lalib/elements/gf2.py create mode 100644 tests/elements/test_gf2.py diff --git a/noxfile.py b/noxfile.py index 0ce38b7..bf01f0c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -230,6 +230,7 @@ TEST_DEPENDENCIES = ( "pytest", "pytest-cov", "semver", + 'typing-extensions; python_version < "3.11"', # to support Python 3.9 & 3.10 "xdoctest", ) @@ -284,6 +285,7 @@ def test_coverage_run(session: nox.Session) -> None: session.install(".") install_pinned(session, "coverage", *TEST_DEPENDENCIES) + session.env["NO_CROSS_REFERENCE"] = "true" session.run( "python", "-m", @@ -430,6 +432,11 @@ 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"): + 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 =}" + def suppress_poetry_export_warning(session: nox.Session) -> None: """Temporary fix to avoid poetry's warning ... diff --git a/poetry.lock b/poetry.lock index e172dc3..0e30eaa 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1674,4 +1674,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "3a34bd29eb4226a6054fe5ddba556605fcd621ceff7661334edf429d715c320f" +content-hash = "e9e490a864511852844926112978e57c1421328f6231437f8f280ddfc88cecde" diff --git a/pyproject.toml b/pyproject.toml index 698ccd0..95e4da8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,8 @@ repository = "https://github.com/webartifex/lalib" python = "^3.9" +typing-extensions = [ { python = "<3.11", version = "^4.12" } ] # to support Python 3.9 & 3.10 + [tool.poetry.group.dev.dependencies] @@ -127,6 +129,9 @@ exclude_lines = [ # "pragma: no cover" # => Intentionally commented out as we thrive for 100% test coverage + # PyPI's "typing-extensions" are needed to make `mypy` work + "pragma: no cover ${PRAGMA_SUPPORT_39_N_310}", + ] diff --git a/src/lalib/elements/gf2.py b/src/lalib/elements/gf2.py new file mode 100644 index 0000000..a69ec66 --- /dev/null +++ b/src/lalib/elements/gf2.py @@ -0,0 +1,535 @@ +"""A Galois field implementation with two elements. + +This module defines two singleton objects, `one` and `zero`, +that follow the rules of a Galois field of two elements, +or `GF2` for short: + +>>> one + one +zero +>>> zero + one +one +>>> one * one +one +>>> one * zero +zero + +They mix with numbers that compare equal to either `1` or `0`, +for example: + +>>> one + 1 +zero +>>> 0 * zero +zero + +Further usage explanations of `one` and `zero` +can be found in the various docstrings of the `GF2` class. +""" + +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 + + +try: + from typing import Self +except ImportError: # pragma: no cover to support Python 3.9 & 3.10 + from typing_extensions import Self + + +THRESHOLD = 1e-12 + + +def to_gf2( + value: complex, # `mypy` reads `complex | float | int` + *, + strict: bool = True, + threshold: float = THRESHOLD, +) -> int: + """Cast a number as a possible Galois field value: `1` or `0`. + + By default, the `value` is parsed in a `strict` mode where + `value`s equal to `1` or `0` within the specified `threshold` + return either `1` or `0` exactly. + + Args: + value: to be cast; must behave like a number; + for `complex` numbers their `.real` part is used + strict: if `True`, only accept `value`s equal to + `1` or `0` within the `threshold` as `1` or `0`; + otherwise, cast any number different from `0` as `1` + threshold: used for the equality checks to find + `1`-like and `0`-like `value`s + + Returns: + either `1` or `0` + + Raises: + TypeError: `value` does not behave like a number + ValueError: `value != 1` or `value != 0` in `strict` mode + """ + try: + value = complex(value) + except (TypeError, ValueError): + msg = "`value` must be a number" + raise TypeError(msg) from None + + if not (abs(value.imag) < threshold): + msg = "`value` must be either `1`-like or `0`-like" + raise ValueError(msg) + + value = value.real + + if math.isnan(value): + msg = "`value` must be either `1`-like or `0`-like" + raise ValueError(msg) + + if strict: + if abs(value - 1) < threshold: + return 1 + if abs(value) < threshold: + return 0 + + msg = "`value` must be either `1`-like or `0`-like" + raise ValueError(msg) + + if abs(value) < threshold: + return 0 + + return 1 + + +class _GF2Meta(abc.ABCMeta): + """Make data type of `one` and `zero` appear to be `GF2`.""" + + def __repr__(cls) -> str: + return "GF2" + + +@functools.total_ordering +class GF2(metaclass=_GF2Meta): + """A Galois field value: either `one` or `zero`. + + Implements the singleton design pattern such that + only one instance per field value exists in the + computer's memory, i.e., there is only one `one` + and one `zero` object at all times. + """ + + _instances: ClassVar = {} + _value: int + + @staticmethod + def __new__( + cls: type[Self], + value: object = None, + *, + strict: bool = True, + threshold: float = THRESHOLD, + ) -> Self: + """See docstring for `.__init__()`.""" + if isinstance(value, cls): + return value + + if value is None: + try: + value = cls._value + except AttributeError: + try: + return cls._instances[0] + except KeyError: + 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] + + try: + return cls._instances[value] + except KeyError: + obj = super().__new__(cls) + cls._instances[int(obj)] = obj + return obj + + def __init__( + self, + value: object = None, + *, + strict: bool = True, + threshold: float = THRESHOLD, + ) -> None: + """Obtain one of two objects: `one` or `zero`. + + Args: + value: to be cast; must behave like a number; + for `complex` numbers their `.real` part is used + strict: if `True`, only accept `value`s equal to + `1` or `0` within the `threshold` as `one` or `zero`; + otherwise, cast any number different from `0` as `one` + threshold: used for the equality checks to find + `1`-like and `0`-like `value`s + + Returns: + either `one` or `zero` + + Raises: + TypeError: `value` does not behave like a number + ValueError: `value != 1` or `value != 0` in `strict` mode + """ + + def __repr__(self) -> str: + """Text representation: `repr(one)` and `repr(zero)`. + + `eval(repr(self)) == self` must be `True`; in other words, + the text representation of an object must be valid code on its + own and evaluate into a (new) object with the same value. + + See: https://docs.python.org/3/reference/datamodel.html#object.__repr__ + """ + return "one" if self._value else "zero" + + __str__ = __repr__ + + def __complex__(self) -> complex: + """Cast `self` as a `complex` number: `complex(self)`.""" + return complex(self._value, 0) + + def __float__(self) -> float: + """Cast `self` as a `float`ing-point number: `float(self)`.""" + return float(self._value) + + def __int__(self) -> int: + """Cast `self` as a `int`: `int(self)`.""" + return int(self._value) + + __hash__ = __int__ + + def __bool__(self) -> bool: + """Cast `self` as a `bool`ean: `bool(self)`. + + Example usage: + + >>> bool(zero) + False + >>> if zero + one: + ... result = one + ... else: + ... result = zero + >>> result + one + """ + return bool(self._value) + + def __abs__(self) -> Self: + """Take the absolute value of `self`: `abs(self)`.""" + return self + + def __trunc__(self) -> int: + """Truncate `self` to the next `int`: `math.trunc(self)`.""" + return int(self) + + def __floor__(self) -> int: + """Round `self` down to the next `int`: `math.floor(self)`.""" + return int(self) + + def __ceil__(self) -> int: + """Round `self` up to the next `int`: `math.ceil(self)`.""" + return int(self) + + def __round__(self, ndigits: Optional[int] = 0) -> int: + """Round `self` to the next `int`: `round(self)`.""" + return int(self) + + @property + def real(self) -> int: + """The `.real` part of a `complex` number. + + For a non-`complex` number this is the number itself. + """ + # Return an `int` to align with `.imag` above + return int(self._value) + + @property + def imag(self) -> Literal[0]: + """The `.imag`inary part of a `complex` number. + + For a non-`complex` number this is always `0`. + """ + # `numbers.Real` returns an `int` here + # whereas `numbers.Complex` returns a `float` + # => must return an `int` to make `mypy` happy + return 0 + + def conjugate(self) -> Self: + """The conjugate of a `complex` number. + + For a non-`complex` number this is the number itself. + """ + return self + + @property + def numerator(self) -> int: + """Smallest numerator when expressed as a `Rational` number. + + Either `1` or `0`. + + Reasoning: + - `int(one) == 1` => `GF2(1 / 1) == one` + - `int(zero) == 0` => `GF2(0 / 1) == zero` + + See also docstring for `.denominator`. + """ + return int(self) + + @property + def denominator(self) -> Literal[1]: + """Smallest denominator when expressed as a `Rational` number. + + Always `1` for `GF2` values. + + See also docstring for `.numerator`. + """ + return 1 + + def __eq__(self, other: object) -> bool: + """Comparison: `self == other`. + + Example usage: + + >>> zero == one + False + >>> one == one + True + >>> one != zero + True + >>> one == 1 + True + """ + try: + other = GF2(other) + except (TypeError, ValueError): + return False + else: + return self is other # `one` and `zero` are singletons + + def __lt__(self, other: object) -> bool: + """Comparison: `self < other`. + + Example usage: + + >>> zero < one + True + >>> one < one + False + >>> 0 < one + True + """ + try: + other = GF2(other) + except TypeError: + return NotImplemented + except ValueError: + msg = "`other` must be either `1`-like or `0`-like" + raise ValueError(msg) from None + else: + return int(self) < int(other) + + def __le__(self, other: object) -> bool: + """Comparison: `self <= other`. + + Example usage: + + >>> zero <= one + True + >>> zero <= zero + True + >>> one <= zero + False + >>> zero <= 1 + True + """ + # The `numbers.Rational` abstract base class requires both + # `.__lt__()` and `.__le__()` to be present alongside + # `.__eq__()` => `@functools.total_ordering` is not enough + return self == other or self < other + + def _compute(self, other: object, func: Callable) -> Self: + """Run arithmetic operations using `int`s. + + 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 + along the way. + """ + try: + other = GF2(other) + except TypeError: + return NotImplemented + except ValueError: + msg = "`other` must be a `1`-like or `0`-like value" + raise ValueError(msg) from None + else: + try: + return self.__class__(func(int(self), int(other))) + except ZeroDivisionError: + msg = "division by `0`-like value" + raise ZeroDivisionError(msg) from None + + def __pos__(self) -> Self: + """Make `self` positive: `+self`.""" + return self + + def __neg__(self) -> Self: + """Make `self` negative: `-self`.""" + return self + + def __add__(self, other: object) -> Self: + """Addition / Subtraction: `self + other` / `self - other`. + + 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. + + Example usage: + + >>> one + one + zero + >>> one + zero + one + >>> zero - one + one + >>> zero + 0 + zero + >>> 1 + one + zero + """ + return self._compute(other, lambda s, o: (s + o) % 2) + + __radd__ = __add__ + __sub__ = __add__ + __rsub__ = __add__ + + def __mul__(self, other: object) -> Self: + """Multiplication: `self * other`. + + Multiplying `GF2` values is like multiplying `int`s. + + Example usage: + + >>> one * one + one + >>> zero * one + zero + >>> 0 * one + zero + """ + return self._compute(other, lambda s, o: s * o) + + __rmul__ = __mul__ + + def __truediv__(self, other: object) -> Self: + """Division: `self / other` and `self // other`. + + Dividing `GF2` values is like dividing `int`s. + + Example usage: + + >>> one / one + one + >>> zero // one + zero + >>> one / zero + Traceback (most recent call last): + ... + ZeroDivisionError: ... + >>> 1 // one + one + """ + return self._compute(other, lambda s, o: s / o) + + __floordiv__ = __truediv__ + + def __rtruediv__(self, other: object) -> Self: + """(Reflected) Division: `other / self` and `other // self`. + + See docstring for `.__truediv__()`. + """ + return self._compute(other, lambda s, o: o / s) + + __rfloordiv__ = __rtruediv__ + + def __mod__(self, other: object) -> Self: + """Modulo Division: `self % other`. + + Modulo dividing `GF2` values is like modulo dividing `int`s. + + Example usage: + + >>> one % one + zero + >>> zero % one + zero + >>> one % zero + Traceback (most recent call last): + ... + ZeroDivisionError: ... + >>> 1 % one + zero + """ + return self._compute(other, lambda s, o: s % o) + + def __rmod__(self, other: object) -> Self: + """(Reflected) Modulo Division: `other % self`. + + See docstring for `.__mod__()`. + """ + return self._compute(other, lambda s, o: o % s) + + def __pow__(self, other: object, _modulo: Optional[object] = None) -> Self: + """Exponentiation: `self ** other`. + + Powers of `GF2` values are like powers of `int`s. + + Example usage: + + >>> one ** one + one + >>> zero ** one + zero + >>> one ** zero + one + >>> 1 ** one + one + """ + return self._compute(other, lambda s, o: s**o) + + def __rpow__(self, other: object, _modulo: Optional[object] = None) -> Self: + """(Reflected) Exponentiation: `other ** self`. + + See docstring for `.__pow__()`. + """ + return self._compute(other, lambda s, o: o**s) + + +numbers.Rational.register(GF2) + + +class GF2One(GF2): + """The Galois field value `one`.""" + + _value = 1 + + +class GF2Zero(GF2): + """The Galois field value `zero`.""" + + _value = 0 + + +one = GF2One() +zero = GF2Zero() diff --git a/tests/elements/test_gf2.py b/tests/elements/test_gf2.py new file mode 100644 index 0000000..89ba236 --- /dev/null +++ b/tests/elements/test_gf2.py @@ -0,0 +1,832 @@ +"""Test the `GF2` singeltons `one` and `zero`.""" + +import decimal +import fractions +import importlib +import math +import numbers +import operator +import os +import sys + +import pytest + +from lalib.elements import gf2 + + +one, zero = ( + gf2.one, + gf2.zero, +) + +to_gf2 = gf2.to_gf2 + +GF2, GF2One, GF2Zero = ( + gf2.GF2, + gf2.GF2One, + gf2.GF2Zero, +) + +_THRESHOLD = gf2.THRESHOLD + +del gf2 + + +CROSS_REFERENCE = not os.environ.get("NO_CROSS_REFERENCE") + + +default_threshold = _THRESHOLD +within_threshold = _THRESHOLD / 10 +not_within_threshold = _THRESHOLD * 10 + +strict_one_like_values = ( + 1, + 1.0, + 1.0 + within_threshold, + (1 + 0j), + (1 + 0j) + complex(0, within_threshold), + (1 + 0j) + complex(within_threshold, 0), + decimal.Decimal("1"), + fractions.Fraction(1, 1), + "1", + "1.0", + "1+0j", +) + +non_strict_one_like_values = ( + 0.0 + not_within_threshold, + 1.0 + not_within_threshold, + (1 + 0j) + complex(not_within_threshold, 0), + 42, + decimal.Decimal("42"), + fractions.Fraction(42, 1), + "42", + "42.0", + "42+0j", + "+inf", + "-inf", +) + +one_like_values = strict_one_like_values + non_strict_one_like_values + +zero_like_values = ( + 0, + 0.0, + 0.0 + within_threshold, + (0 + 0j), + (0 + 0j) + complex(0, within_threshold), + (0 + 0j) + complex(within_threshold, 0), + decimal.Decimal("0"), + fractions.Fraction(0, 1), + "0", + "0.0", + "0+0j", +) + + +def test_thresholds(): + """Sanity check for the thresholds used in the tests below.""" + 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: + """Test the `GF2` class's constructor. + + `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`.""" + result1 = cls(value) # `strict=True` by default + assert result1 is one + + result2 = cls(value, strict=True) + assert result2 is one + + @pytest.mark.parametrize("value", one_like_values) + def test_cast_ones_not_strictly(self, cls, value): + """`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`.""" + with pytest.raises(ValueError, match="`1`-like or `0`-like"): + cls(value) + + with pytest.raises(ValueError, match="`1`-like or `0`-like"): + cls(value, strict=True) + + @pytest.mark.parametrize("value", zero_like_values) + def test_cast_zeros(self, cls, value): + """`GF2(value, strict=...)` returns `zero`.""" + result1 = cls(value) # `strict=True` by default + assert result1 is zero + + result2 = cls(value, strict=True) + assert result2 is zero + + result3 = cls(value, strict=False) + assert result3 is zero + + @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, cls, value, strict): + """Cannot create `one` or `zero` if `.imag != 0`.""" + with pytest.raises(ValueError, match="`1`-like or `0`-like"): + cls(value, strict=strict) + + @pytest.mark.parametrize("value", ["abc", (1,), [1]]) + @pytest.mark.parametrize("strict", [True, False]) + def test_cannot_cast_from_wrong_type(self, cls, value, strict): + """Cannot create `one` or `zero` from a non-numeric value.""" + with pytest.raises(TypeError): + cls(value, strict=strict) + + @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.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` + # but still different from `1` => `strict=False` + value = scaler * not_within_threshold + threshold = scaler * default_threshold + + result = cls(value, strict=False, threshold=threshold) + assert result is one + + @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`.""" + # `within_threshold` is smaller than the `default_threshold` + value = scaler * within_threshold + threshold = scaler * default_threshold + + result = cls(value, strict=strict, threshold=threshold) + assert result is zero + + +@pytest.mark.parametrize("strict", [True, False]) +class TestGF2ConstructorWithoutCastedValue: + """Test the `GF2` class's constructor. + + `GF2()` returns either `one` or `zero`. + """ + + def test_get_one_from_sub_class_with_no_input_value(self, strict): + """`GF2One()` returns `one`.""" + result = GF2One(strict=strict) + assert result is one + + @pytest.mark.parametrize("cls", [GF2, GF2Zero]) + def test_get_zero_with_no_input_value(self, cls, strict): + """`GF2()` and `GF2Zero()` return `zero`.""" + result = cls(strict=strict) + assert result is zero + + +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", {}) + with pytest.raises(RuntimeError, match="internal error"): + GF2() + + @pytest.mark.parametrize("cls", [GF2, GF2One, GF2Zero]) + def test_create_singletons(self, cls): + """Singleton pattern: The classes always return the same instance.""" + first = cls() + second = cls() + assert first is second + + @pytest.mark.parametrize("obj", [one, zero]) + def test_sub_classes_return_objs(self, obj): + """`type(one)` and `type(zero)` return ... + + the sub-classes that create `one` and `zero`. + """ + sub_cls = type(obj) + assert sub_cls is not GF2 + + new_obj = sub_cls() + assert new_obj is obj + + @pytest.mark.parametrize("obj", [one, zero]) + @pytest.mark.parametrize( + "type_", + [ + numbers.Number, + numbers.Complex, + numbers.Real, + numbers.Rational, + ], + ) + def test_objs_are_numbers(self, obj, type_): + """`one` and `zero` are officially `Numbers`s.""" + assert isinstance(obj, type_) + + @pytest.mark.parametrize("cls", [GF2, GF2One, GF2Zero]) + @pytest.mark.parametrize( + "method", + [ + "__abs__", + "__trunc__", + "__floor__", + "__ceil__", + "__round__", + "__floordiv__", + "__rfloordiv__", + "__mod__", + "__rmod__", + "__lt__", + "__le__", + "numerator", + "denominator", + ], + ) + @pytest.mark.parametrize("value", [1, 0]) + def test_classes_fulfill_rational_numbers_abc( + self, + cls, + method, + monkeypatch, + value, + ): + """Ensure all of `numbers.Rational`'s abstact methods are implemented.""" + monkeypatch.setattr(GF2, "_instances", {}) + monkeypatch.delattr(GF2, method) + + sub_cls = type("GF2Baby", (cls, numbers.Rational), {}) + + with pytest.raises(TypeError, match="instantiate abstract class"): + sub_cls(value) + + @pytest.mark.parametrize("func", [repr, str]) + @pytest.mark.parametrize("obj", [one, zero]) + def test_text_repr_for_objs(self, func, obj): + """`repr(one)` and `repr(zero)` return "one" and "zero" ... + + ... which is valid code evaluating into the objects themselves. + + `str()` does the same as `repr()`. + """ + new_obj = eval(func(obj)) # noqa: S307 + assert new_obj is obj + + @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 ... + + ... which is valid code referring to the base class `GF2`. + + `GF2()` returns `zero` if called without arguments. + """ + base_cls = eval(func(type(obj))) # noqa: S307 + assert base_cls is GF2 + + new_obj = base_cls() + assert new_obj is zero + + +class TestNumericBehavior: + """Test how `one` and `zero` behave like numbers.""" + + def test_make_complex(self): + """`one` and `zero` behave like `1 + 0j` and `0 + 0j`.""" + assert (complex(one), complex(zero)) == (1 + 0j, 0 + 0j) + + 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.parametrize("func", [int, hash]) + def test_make_int(self, func): + """`one` and `zero` behave like `1` and `0`. + + That also holds true for their hash values. + """ + assert (func(one), func(zero)) == (1, 0) + + def test_make_bool(self): + """`one` and `zero` behave like `True` and `False`.""" + assert (bool(one), bool(zero)) == (True, False) + + @pytest.mark.parametrize("obj", [one, zero]) + def test_get_abs_value(self, obj): + """`abs(one)` and `abs(zero)` are `one` and `zero`.""" + assert abs(obj) is obj + + @pytest.mark.parametrize("obj", [one, zero]) + @pytest.mark.parametrize("func", [math.trunc, math.floor, math.ceil, round]) + def test_round_obj(self, obj, func): + """`func(one)` and `func(zero)` equal `1` and `0`.""" + assert func(obj) in (1, 0) + + if CROSS_REFERENCE: + assert func(obj) == obj + + def test_real_part(self): + """`one.real` and `zero.real` are `1` and `0`.""" + assert (one.real, zero.real) == (1, 0) + + def test_imag_part(self): + """`one.imag` and `zero.imag` are `0`.""" + assert (one.imag, zero.imag) == (0, 0) + + def test_conjugate(self): + """`one.conjugate()` and `zero.conjugate()` are `1 + 0j` and `0 + 0j`.""" + assert (one.conjugate(), zero.conjugate()) == (1 + 0j, 0 + 0j) + + def test_one_as_fraction(self): + """`one.numerator / one.denominator` equals `1`.""" + assert (one.numerator, one.denominator) == (1, 1) + + def test_zero_as_fraction(self): + """`one.numerator / one.denominator` equals `0`.""" + assert (zero.numerator, zero.denominator) == (0, 1) + + +class TestComparison: + """Test `one` and `zero` interact with relational operators.""" + + @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.parametrize( + ["first", "second"], + [ + (one, one), + (one, 1), + (one, 1.0), + (one, 1 + 0j), + (zero, zero), + (zero, 0), + (zero, 0.0), + (zero, 0 + 0j), + ], + ) + def test_equal_to_another(self, first, second): + """`one` and `zero` are equal to `1`-like and `0`-like numbers.""" + assert first == second + assert second == first + + @pytest.mark.parametrize( + ["first", "second"], + [ + (one, zero), + (one, 0), + (one, 0.0), + (one, 0 + 0j), + (zero, 1), + (zero, 1.0), + (zero, 1 + 0j), + ], + ) + def test_not_equal_to_another_one_or_zero_like(self, first, second): + """`one` and `zero` are not equal to `0`-like and `1`-like numbers.""" + assert first != second + assert second != first + + @pytest.mark.parametrize( + ["first", "second"], + [ + (one, 42), + (one, 42.0), + (one, 42 + 0j), + (one, 0 + 42j), + (zero, 42), + (zero, 42.0), + (zero, 42 + 0j), + (zero, 0 + 42j), + ], + ) + def test_not_equal_to_another_non_one_like(self, first, second): + """`one` and `zero` are not equal to non-`1`-or-`0`-like numbers.""" + assert first != second + assert second != first + + @pytest.mark.parametrize("operator", [operator.gt, operator.ge]) + def test_one_greater_than_or_equal_to_zero(self, operator): + """`one > zero` and `one >= zero`.""" + assert operator(one, zero) + + @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.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.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.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.parametrize("obj", [one, zero]) + def test_obj_not_strictly_smaller_than_itself(self, obj): + """`obj <= obj` but not `obj < obj`.""" + assert obj <= obj # noqa: PLR0124 + assert not obj < obj # noqa: PLR0124 + + @pytest.mark.parametrize("obj", [one, zero]) + @pytest.mark.parametrize( + "operator", + [operator.gt, operator.ge, operator.lt, operator.le], + ) + def test_compare_to_other_operand_of_wrong_type(self, obj, operator): + """`one` and `zero` may only interact with numbers.""" + with pytest.raises(TypeError): + operator(obj, "abc") + + with pytest.raises(TypeError): + operator("abc", obj) + + @pytest.mark.parametrize("obj", [one, zero]) + @pytest.mark.parametrize( + "operator", + [operator.gt, operator.ge, operator.lt, operator.le], + ) + def test_compare_to_other_operand_of_wrong_value(self, obj, operator): + """`one` and `zero` may only interact with `1`-like or `0`-like numbers.""" + with pytest.raises(ValueError, match="`1`-like or `0`-like"): + operator(obj, 42) + + with pytest.raises(ValueError, match="`1`-like or `0`-like"): + operator(42, obj) + + +class TestArithmetic: + """Test `one` and `zero` interact with arithmetic operators.""" + + @pytest.mark.parametrize("obj", [one, zero]) + @pytest.mark.parametrize("operator", [operator.pos, operator.neg]) + def test_make_obj_positive_or_negative(self, obj, operator): + """`+one` and `+zero` equal `-one` and `-zero`.""" + assert obj is operator(obj) + + @pytest.mark.parametrize( + "objs", + [ + (one, one, zero), + (one, zero, one), + (zero, zero, zero), + (one, 1, zero), + (one, 1.0, zero), + (one, 1 + 0j, zero), + (one, 0, one), + (one, 0.0, one), + (one, 0 + 0j, one), + (zero, 1, one), + (zero, 1.0, one), + (zero, 1 + 0j, one), + (zero, 0, zero), + (zero, 0.0, zero), + (zero, 0 + 0j, zero), + ], + ) + @pytest.mark.parametrize("operator", [operator.add, operator.sub]) + def test_addition_and_subtraction(self, objs, operator): + """Adding and subtracting `one` and `zero` is identical and commutative.""" + first, second, expected = objs + + result1 = operator(first, second) + assert result1 is expected + + result2 = operator(second, first) + assert result2 is expected + + 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) + assert result3 is expected + + result4 = GF2((operator(int(abs(second)), int(abs(first))) + 2) % 2) + assert result4 is expected + + @pytest.mark.parametrize( + ["first", "second", "expected"], + [ + (one, one, one), + (one, zero, zero), + (zero, zero, zero), + (one, 1, one), + (one, 1.0, one), + (one, 1 + 0j, one), + (one, 0, zero), + (one, 0.0, zero), + (one, 0 + 0j, zero), + (zero, 1, zero), + (zero, 1.0, zero), + (zero, 1 + 0j, zero), + (zero, 0, zero), + (zero, 0.0, zero), + (zero, 0 + 0j, zero), + ], + ) + def test_multiplication(self, first, second, expected): + """Multiplying `one` and `zero` is commutative.""" + result1 = first * second + assert result1 is expected + + result2 = second * first + assert result2 is expected + + if CROSS_REFERENCE: # cast `one` and `zero` as `integer`s before doing the math + + result3 = GF2(int(abs(first)) * int(abs(second))) + assert result3 is expected + + result4 = GF2(int(abs(second)) * int(abs(first))) + assert result4 is expected + + @pytest.mark.parametrize( + "objs", + [ + # In Python 3.9 we cannot floor-divide a `complex` number + (one, one, one), + (one, 1, one), + (one, 1.0, one), + (one, 1 + 0j, one), + (1, one, one), + (1.0, one, one), + (zero, one, zero), + (zero, 1, zero), + (zero, 1.0, zero), + (zero, 1 + 0j, zero), + (0, one, zero), + (0.0, one, zero), + ], + ) + @pytest.mark.parametrize( + "operator", + [operator.truediv, operator.floordiv], + ) + def test_division_by_one(self, objs, operator): + """Division by `one`.""" + first, second, expected = objs + + result1 = operator(first, second) + assert result1 is expected + + if CROSS_REFERENCE: # cast `one` and `zero` as `integer`s before doing the math + + result2 = GF2(operator(int(abs(first)), int(abs(second)))) + assert result2 is expected + + @pytest.mark.parametrize( + "objs", + [ + # In Python 3.9 we cannot modulo-divide a `complex` number + (one, one, zero), + (one, 1, zero), + (one, 1.0, zero), + (one, 1 + 0j, zero), + (1, one, zero), + (1.0, one, zero), + (zero, one, zero), + (zero, 1, zero), + (zero, 1.0, zero), + (zero, 1 + 0j, zero), + (0, one, zero), + (0.0, one, zero), + ], + ) + def test_modulo_division_by_one(self, objs): + """Division by `one`.""" + first, second, expected = objs + + result1 = first % second + assert result1 is expected + + if CROSS_REFERENCE: # cast `one` and `zero` as `integer`s before doing the math + + result2 = GF2(int(abs(first)) % int(abs(second))) + assert result2 is expected + + @pytest.mark.parametrize( + "objs", + [ + # In Python 3.9 we cannot floor-divide a `complex` number + (one, zero), + (one, 0), + (one, 0.0), + (1, zero), + (1.0, zero), + (zero, zero), + (zero, 0), + (zero, 0.0), + (0, zero), + (0.0, zero), + ], + ) + @pytest.mark.parametrize( + "operator", + [operator.truediv, operator.floordiv, operator.mod], + ) + def test_division_by_zero(self, objs, operator): + """Division by `zero` raises `ZeroDivisionError`.""" + first, second = objs + + with pytest.raises(ZeroDivisionError): + operator(first, second) + + if CROSS_REFERENCE: # cast `one` and `zero` as `integer`s before doing the math + + with pytest.raises(ZeroDivisionError): + operator(int(abs(first)), int(abs(second))) + + @pytest.mark.parametrize( + "objs", + [ + (one, one, one), + (one, 1, one), + (one, 1.0, one), + (one, 1 + 0j, one), + (1, one, one), + (1.0, one, one), + (1 + 0j, one, one), + (zero, one, zero), + (zero, 1, zero), + (zero, 1.0, zero), + (zero, 1 + 0j, zero), + (0, one, zero), + (0.0, one, zero), + (0 + 0j, one, zero), + (one, zero, one), + (one, 0, one), + (one, 0.0, one), + (one, 0 + 0j, one), + (1, zero, one), + (1.0, zero, one), + (1 + 0j, zero, one), + (zero, zero, one), + (zero, 0, one), + (zero, 0.0, one), + (zero, 0 + 0j, one), + (0, zero, one), + (0.0, zero, one), + (0 + 0j, zero, one), + ], + ) + def test_to_the_power_of(self, objs): + """Exponentiation with `one` and `zero`.""" + first, second, expected = objs + + result1 = first**second + assert result1 is expected + + if CROSS_REFERENCE: # cast `one` and `zero` as `integer`s before doing the math + + result2 = GF2(int(abs(first)) ** int(abs(second))) + assert result2 is expected + + @pytest.mark.parametrize("obj", [one, zero]) + @pytest.mark.parametrize( + "operator", + [ + operator.add, + operator.mul, + operator.truediv, + operator.floordiv, + operator.mod, + operator.pow, + ], + ) + def test_other_operand_of_wrong_type(self, obj, operator): + """`one` and `zero` may only interact with numbers.""" + # Cannot use a `str` like `"abc"` as then `%` means string formatting + with pytest.raises(TypeError): + operator(obj, ("a", "b", "c")) + + with pytest.raises(TypeError): + operator(("a", "b", "c"), obj) + + @pytest.mark.parametrize("obj", [one, zero]) + @pytest.mark.parametrize( + "operator", + [ + operator.add, + operator.mul, + operator.truediv, + operator.floordiv, + operator.mod, + operator.pow, + ], + ) + def test_other_operand_of_wrong_value(self, obj, operator): + """`one` and `zero` may only interact with `1`-like or `0`-like numbers.""" + with pytest.raises(ValueError, match="`1`-like or `0`-like"): + operator(obj, 42) + + with pytest.raises(ValueError, match="`1`-like or `0`-like"): + operator(42, obj) + + +@pytest.mark.skipif( + not sys.version_info < (3, 11), + reason='"typing-extensions" are installed to support Python 3.9 & 3.10', +) +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") + importlib.reload(package) + + assert package.Self is not None diff --git a/tests/test_docstrings.py b/tests/test_docstrings.py index 4890a36..89ae74d 100644 --- a/tests/test_docstrings.py +++ b/tests/test_docstrings.py @@ -14,6 +14,7 @@ import xdoctest "module", [ "lalib", + "lalib.elements.gf2", ], ) def test_docstrings(module): From 3cfc0db136b652d7b4515a7ecc78ca4b8db8dcd8 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Wed, 18 Sep 2024 18:29:40 +0200 Subject: [PATCH 6/9] Organize top-level imports for `lalib.elements` - make `GF2`, `one`, and `zero`, defined in the `lalib.elements.gf2` module, available as top-level imports in the `lalib.elements` sub-package via `from lalib.elements import *` - provide some code snippets in the sub-package's docstring - test the star import --- src/lalib/elements/__init__.py | 35 +++++++++++++++++++++++- tests/elements/test_top_level_imports.py | 18 ++++++++++++ tests/test_docstrings.py | 1 + 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 tests/elements/test_top_level_imports.py diff --git a/src/lalib/elements/__init__.py b/src/lalib/elements/__init__.py index ae48619..fe5dc37 100644 --- a/src/lalib/elements/__init__.py +++ b/src/lalib/elements/__init__.py @@ -1 +1,34 @@ -"""Various elements of various fields.""" +"""Various elements of various fields. + +Import the objects like so: + +>>> from lalib.elements import * + +Then, use them: + +>>> one + zero +one + +>>> GF2(0) +zero +>>> GF2(42) +Traceback (most recent call last): +... +ValueError: ... +>>> GF2(42, strict=False) +one +""" + +from lalib.elements import gf2 + + +GF2, one, zero = gf2.GF2, gf2.one, gf2.zero + +del gf2 + + +__all__ = ( + "GF2", + "one", + "zero", +) diff --git a/tests/elements/test_top_level_imports.py b/tests/elements/test_top_level_imports.py new file mode 100644 index 0000000..dd19bcd --- /dev/null +++ b/tests/elements/test_top_level_imports.py @@ -0,0 +1,18 @@ +"""Test top-level imports for `lalib.elements`.""" + +from lalib import elements as top_level + + +def test_top_level_imports(): + """Verify `from lalib.elements import *` works.""" + environment = {} + + exec("...", environment, environment) # noqa: S102 + defined_vars_before = set(environment) + + exec("from lalib.elements import *", environment, environment) # noqa: S102 + defined_vars_after = set(environment) + + new_vars = defined_vars_after - defined_vars_before + + assert new_vars == set(top_level.__all__) diff --git a/tests/test_docstrings.py b/tests/test_docstrings.py index 89ae74d..dd4ac9f 100644 --- a/tests/test_docstrings.py +++ b/tests/test_docstrings.py @@ -14,6 +14,7 @@ import xdoctest "module", [ "lalib", + "lalib.elements", "lalib.elements.gf2", ], ) From 62b25f66d90cae05a00a70bef6f7fed8f4cde833 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Wed, 18 Sep 2024 18:37:54 +0200 Subject: [PATCH 7/9] Organize top-level imports for `lalib` - make `GF2`, `one`, and `zero`, defined in the `lalib.elements.gf2` module, available as top-level imports for the `lalib` package via `from lalib import *` - provide some code snippets in the package's docstring - test the star import --- src/lalib/__init__.py | 27 +++++++++++++++++++++++++++ tests/test_top_level_imports.py | 18 ++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 tests/test_top_level_imports.py diff --git a/src/lalib/__init__.py b/src/lalib/__init__.py index 26b2482..8a1a66f 100644 --- a/src/lalib/__init__.py +++ b/src/lalib/__init__.py @@ -4,10 +4,26 @@ First, verify that your installation of `lalib` works: >>> import lalib >>> lalib.__version__ != '0.0.0' True + +`lalib` exposes its very own "words" (i.e., its public API) at the root +of the package. They can be imported all at once with: + +>>> from lalib import * + +In addition to Python's built-in numbers, `lalib` comes with a couple of +specific numeric data types, for example, `one` and `zero` representing +the two elements of the Galois field `GF2`: + +>>> one + one +zero +>>> one + zero +one """ from importlib import metadata +from lalib.elements import gf2 + try: pkg_info = metadata.metadata(__name__) @@ -24,4 +40,15 @@ else: del pkg_info +GF2, one, zero = gf2.GF2, gf2.one, gf2.zero + + +del gf2 del metadata + + +__all__ = ( + "GF2", + "one", + "zero", +) diff --git a/tests/test_top_level_imports.py b/tests/test_top_level_imports.py new file mode 100644 index 0000000..e73cd0e --- /dev/null +++ b/tests/test_top_level_imports.py @@ -0,0 +1,18 @@ +"""Test top-level imports for `lalib`.""" + +import lalib as top_level + + +def test_top_level_imports(): + """Verify `from lalib import *` works.""" + environment = {} + + exec("...", environment, environment) # noqa: S102 + defined_vars_before = set(environment) + + exec("from lalib import *", environment, environment) # noqa: S102 + defined_vars_after = set(environment) + + new_vars = defined_vars_after - defined_vars_before + + assert new_vars == set(top_level.__all__) From 51c73163e421dd49cad15773bc253cdce8a8d376 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Wed, 18 Sep 2024 20:05:49 +0200 Subject: [PATCH 8/9] Refactor `test_top_level_imports()` test cases ... ... into a unified location --- tests/elements/test_top_level_imports.py | 18 ------------------ tests/test_top_level_imports.py | 24 ++++++++++++++++++------ 2 files changed, 18 insertions(+), 24 deletions(-) delete mode 100644 tests/elements/test_top_level_imports.py diff --git a/tests/elements/test_top_level_imports.py b/tests/elements/test_top_level_imports.py deleted file mode 100644 index dd19bcd..0000000 --- a/tests/elements/test_top_level_imports.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Test top-level imports for `lalib.elements`.""" - -from lalib import elements as top_level - - -def test_top_level_imports(): - """Verify `from lalib.elements import *` works.""" - environment = {} - - exec("...", environment, environment) # noqa: S102 - defined_vars_before = set(environment) - - exec("from lalib.elements import *", environment, environment) # noqa: S102 - defined_vars_after = set(environment) - - new_vars = defined_vars_after - defined_vars_before - - assert new_vars == set(top_level.__all__) diff --git a/tests/test_top_level_imports.py b/tests/test_top_level_imports.py index e73cd0e..909e46d 100644 --- a/tests/test_top_level_imports.py +++ b/tests/test_top_level_imports.py @@ -1,18 +1,30 @@ """Test top-level imports for `lalib`.""" -import lalib as top_level +import importlib +from typing import Any + +import pytest -def test_top_level_imports(): - """Verify `from lalib import *` works.""" - environment = {} +@pytest.mark.parametrize( + "path_to_package", + [ + "lalib", + "lalib.elements", + ], +) +def test_top_level_imports(path_to_package: str): + """Verify `from {path_to_package} import *` works.""" + package = importlib.import_module(path_to_package) + + environment: dict[str, Any] = {} exec("...", environment, environment) # noqa: S102 defined_vars_before = set(environment) - exec("from lalib import *", environment, environment) # noqa: S102 + exec(f"from {path_to_package} import *", environment, environment) # noqa: S102 defined_vars_after = set(environment) new_vars = defined_vars_after - defined_vars_before - assert new_vars == set(top_level.__all__) + assert new_vars == set(package.__all__) From ea85c73933b198eed6e3d0dafd59837207e51d51 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Wed, 18 Sep 2024 22:53:33 +0200 Subject: [PATCH 9/9] Update dependencies - filelock (3.16.0 -> 3.16.1) - identify (2.6.0 -> 2.6.1) - idna (3.8 -> 3.10) - platformdirs (4.3.2 -> 4.3.6) - pytest (8.3.2 -> 8.3.3) - rich (13.8.0 -> 13.8.1) - ruff (0.6.4 -> 0.6.5) - setuptools (74.1.2 -> 75.1.0) - urllib3 (2.2.2 -> 2.2.3) - virtualenv (20.26.4 -> 20.26.5) --- poetry.lock | 121 +++++++++++++++++++++++++++------------------------- 1 file changed, 64 insertions(+), 57 deletions(-) diff --git a/poetry.lock b/poetry.lock index 0e30eaa..87fc215 100644 --- a/poetry.lock +++ b/poetry.lock @@ -431,18 +431,18 @@ test = ["pytest (>=6)"] [[package]] name = "filelock" -version = "3.16.0" +version = "3.16.1" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.16.0-py3-none-any.whl", hash = "sha256:f6ed4c963184f4c84dd5557ce8fece759a3724b37b80c6c4f20a2f63a4dc6609"}, - {file = "filelock-3.16.0.tar.gz", hash = "sha256:81de9eb8453c769b63369f87f11131a7ab04e367f8d97ad39dc230daa07e3bec"}, + {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, + {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, ] [package.extras] -docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.1.1)", "pytest (>=8.3.2)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.3)"] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] typing = ["typing-extensions (>=4.12.2)"] [[package]] @@ -706,13 +706,13 @@ flake8 = "*" [[package]] name = "identify" -version = "2.6.0" +version = "2.6.1" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, - {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, + {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"}, + {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"}, ] [package.extras] @@ -720,15 +720,18 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.8" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" files = [ - {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, - {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "imagesize" version = "1.4.1" @@ -742,22 +745,26 @@ files = [ [[package]] name = "importlib-metadata" -version = "8.4.0" +version = "8.5.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-8.4.0-py3-none-any.whl", hash = "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1"}, - {file = "importlib_metadata-8.4.0.tar.gz", hash = "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5"}, + {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, + {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, ] [package.dependencies] -zipp = ">=0.5" +zipp = ">=3.20" [package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] [[package]] name = "iniconfig" @@ -1034,13 +1041,13 @@ flake8 = ">=5.0.0" [[package]] name = "platformdirs" -version = "4.3.2" +version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.3.2-py3-none-any.whl", hash = "sha256:eb1c8582560b34ed4ba105009a4badf7f6f85768b30126f351328507b2beb617"}, - {file = "platformdirs-4.3.2.tar.gz", hash = "sha256:9e5e27a08aa095dd127b9f2e764d74254f482fef22b0970773bfba79d091ab8c"}, + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, ] [package.extras] @@ -1156,13 +1163,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pytest" -version = "8.3.2" +version = "8.3.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, - {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, ] [package.dependencies] @@ -1279,13 +1286,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rich" -version = "13.8.0" +version = "13.8.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.8.0-py3-none-any.whl", hash = "sha256:2e85306a063b9492dffc86278197a60cbece75bcb766022f3436f567cae11bdc"}, - {file = "rich-13.8.0.tar.gz", hash = "sha256:a5ac1f1cd448ade0d59cc3356f7db7a7ccda2c8cbae9c7a90c28ff463d3e91f4"}, + {file = "rich-13.8.1-py3-none-any.whl", hash = "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06"}, + {file = "rich-13.8.1.tar.gz", hash = "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a"}, ] [package.dependencies] @@ -1297,29 +1304,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.6.4" +version = "0.6.5" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.6.4-py3-none-linux_armv6l.whl", hash = "sha256:c4b153fc152af51855458e79e835fb6b933032921756cec9af7d0ba2aa01a258"}, - {file = "ruff-0.6.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:bedff9e4f004dad5f7f76a9d39c4ca98af526c9b1695068198b3bda8c085ef60"}, - {file = "ruff-0.6.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d02a4127a86de23002e694d7ff19f905c51e338c72d8e09b56bfb60e1681724f"}, - {file = "ruff-0.6.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7862f42fc1a4aca1ea3ffe8a11f67819d183a5693b228f0bb3a531f5e40336fc"}, - {file = "ruff-0.6.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eebe4ff1967c838a1a9618a5a59a3b0a00406f8d7eefee97c70411fefc353617"}, - {file = "ruff-0.6.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:932063a03bac394866683e15710c25b8690ccdca1cf192b9a98260332ca93408"}, - {file = "ruff-0.6.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:50e30b437cebef547bd5c3edf9ce81343e5dd7c737cb36ccb4fe83573f3d392e"}, - {file = "ruff-0.6.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c44536df7b93a587de690e124b89bd47306fddd59398a0fb12afd6133c7b3818"}, - {file = "ruff-0.6.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ea086601b22dc5e7693a78f3fcfc460cceabfdf3bdc36dc898792aba48fbad6"}, - {file = "ruff-0.6.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b52387d3289ccd227b62102c24714ed75fbba0b16ecc69a923a37e3b5e0aaaa"}, - {file = "ruff-0.6.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0308610470fcc82969082fc83c76c0d362f562e2f0cdab0586516f03a4e06ec6"}, - {file = "ruff-0.6.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:803b96dea21795a6c9d5bfa9e96127cc9c31a1987802ca68f35e5c95aed3fc0d"}, - {file = "ruff-0.6.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:66dbfea86b663baab8fcae56c59f190caba9398df1488164e2df53e216248baa"}, - {file = "ruff-0.6.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:34d5efad480193c046c86608dbba2bccdc1c5fd11950fb271f8086e0c763a5d1"}, - {file = "ruff-0.6.4-py3-none-win32.whl", hash = "sha256:f0f8968feea5ce3777c0d8365653d5e91c40c31a81d95824ba61d871a11b8523"}, - {file = "ruff-0.6.4-py3-none-win_amd64.whl", hash = "sha256:549daccee5227282289390b0222d0fbee0275d1db6d514550d65420053021a58"}, - {file = "ruff-0.6.4-py3-none-win_arm64.whl", hash = "sha256:ac4b75e898ed189b3708c9ab3fc70b79a433219e1e87193b4f2b77251d058d14"}, - {file = "ruff-0.6.4.tar.gz", hash = "sha256:ac3b5bfbee99973f80aa1b7cbd1c9cbce200883bdd067300c22a6cc1c7fba212"}, + {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"}, ] [[package]] @@ -1335,18 +1342,18 @@ files = [ [[package]] name = "setuptools" -version = "74.1.2" +version = "75.1.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-74.1.2-py3-none-any.whl", hash = "sha256:5f4c08aa4d3ebcb57a50c33b1b07e94315d7fc7230f7115e47fc99776c8ce308"}, - {file = "setuptools-74.1.2.tar.gz", hash = "sha256:95b40ed940a1c67eb70fc099094bd6e99c6ee7c23aa2306f4d2697ba7916f9c6"}, + {file = "setuptools-75.1.0-py3-none-any.whl", hash = "sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2"}, + {file = "setuptools-75.1.0.tar.gz", hash = "sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538"}, ] [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] @@ -1586,13 +1593,13 @@ files = [ [[package]] name = "urllib3" -version = "2.2.2" +version = "2.2.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, - {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, ] [package.extras] @@ -1603,13 +1610,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.26.4" +version = "20.26.5" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.26.4-py3-none-any.whl", hash = "sha256:48f2695d9809277003f30776d155615ffc11328e6a0a8c1f0ec80188d7874a55"}, - {file = "virtualenv-20.26.4.tar.gz", hash = "sha256:c17f4e0f3e6036e9f26700446f85c76ab11df65ff6d8a9cbfad9f71aabfcf23c"}, + {file = "virtualenv-20.26.5-py3-none-any.whl", hash = "sha256:4f3ac17b81fba3ce3bd6f4ead2749a72da5929c01774948e243db9ba41df4ff6"}, + {file = "virtualenv-20.26.5.tar.gz", hash = "sha256:ce489cac131aa58f4b25e321d6d186171f78e6cb13fafbf32a840cee67733ff4"}, ] [package.dependencies] @@ -1654,13 +1661,13 @@ tests-strict = ["pytest (==4.6.0)", "pytest (==6.2.5)", "pytest-cov (==3.0.0)"] [[package]] name = "zipp" -version = "3.20.1" +version = "3.20.2" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.20.1-py3-none-any.whl", hash = "sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064"}, - {file = "zipp-3.20.1.tar.gz", hash = "sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b"}, + {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, + {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, ] [package.extras]