Merge branch 'gf2-type' into 'develop'
This commit is contained in:
commit
4c47ca1b17
10 changed files with 1548 additions and 58 deletions
|
@ -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 ...
|
||||
|
|
123
poetry.lock
generated
123
poetry.lock
generated
|
@ -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]
|
||||
|
@ -1674,4 +1681,4 @@ type = ["pytest-mypy"]
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.9"
|
||||
content-hash = "3a34bd29eb4226a6054fe5ddba556605fcd621ceff7661334edf429d715c320f"
|
||||
content-hash = "e9e490a864511852844926112978e57c1421328f6231437f8f280ddfc88cecde"
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
@ -78,6 +80,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"
|
||||
|
@ -121,6 +124,16 @@ show_missing = true
|
|||
skip_covered = true
|
||||
skip_empty = true
|
||||
|
||||
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}",
|
||||
|
||||
]
|
||||
|
||||
|
||||
[tool.coverage.run]
|
||||
|
||||
|
@ -177,6 +190,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",
|
||||
|
|
|
@ -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",
|
||||
)
|
||||
|
|
34
src/lalib/elements/__init__.py
Normal file
34
src/lalib/elements/__init__.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
"""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",
|
||||
)
|
535
src/lalib/elements/gf2.py
Normal file
535
src/lalib/elements/gf2.py
Normal file
|
@ -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()
|
1
tests/elements/__init__.py
Normal file
1
tests/elements/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""Tests for the `lalib.elements` sub-package."""
|
832
tests/elements/test_gf2.py
Normal file
832
tests/elements/test_gf2.py
Normal file
|
@ -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
|
|
@ -14,6 +14,8 @@ import xdoctest
|
|||
"module",
|
||||
[
|
||||
"lalib",
|
||||
"lalib.elements",
|
||||
"lalib.elements.gf2",
|
||||
],
|
||||
)
|
||||
def test_docstrings(module):
|
||||
|
|
30
tests/test_top_level_imports.py
Normal file
30
tests/test_top_level_imports.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
"""Test top-level imports for `lalib`."""
|
||||
|
||||
import importlib
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@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(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(package.__all__)
|
Loading…
Reference in a new issue