diff --git a/.lastversion b/.github/.lastversion similarity index 100% rename from .lastversion rename to .github/.lastversion diff --git a/.github/dev-requirements.txt b/.github/dev-requirements.txt new file mode 100644 index 00000000..71251943 --- /dev/null +++ b/.github/dev-requirements.txt @@ -0,0 +1,60 @@ +asttokens==2.4.1 ; python_version >= "3.10" and python_version < "4.0" +click==8.1.7 ; python_version >= "3.8" and python_version < "4" +colorama==0.4.6 ; python_version >= "3.8" and sys_platform == "win32" and python_version < "4.0" or python_version >= "3.8" and python_version < "4" and platform_system == "Windows" +contourpy==1.1.1 ; python_version >= "3.8" and python_version < "4.0" +coverage-badge==1.1.1 ; python_version >= "3.8" and python_version < "4.0" +coverage==7.5.3 ; python_version >= "3.8" and python_version < "4.0" +coverage[toml]==7.5.3 ; python_version >= "3.8" and python_version < "4.0" +cycler==0.12.1 ; python_version >= "3.8" and python_version < "4.0" +decorator==5.1.1 ; python_version >= "3.10" and python_version < "4.0" +exceptiongroup==1.2.1 ; python_version >= "3.8" and python_version < "3.11" +executing==2.0.1 ; python_version >= "3.10" and python_version < "4.0" +fonttools==4.53.0 ; python_version >= "3.8" and python_version < "4.0" +importlib-metadata==7.2.0 ; python_version >= "3.8" and python_version < "3.10" +importlib-resources==6.4.0 ; python_version >= "3.8" and python_version < "3.10" +iniconfig==2.0.0 ; python_version >= "3.8" and python_version < "4.0" +ipython==8.25.0 ; python_version >= "3.10" and python_version < "4.0" +jaxtyping==0.2.19 ; python_version >= "3.8" and python_version < "4.0" +jedi==0.19.1 ; python_version >= "3.10" and python_version < "4.0" +kiwisolver==1.4.5 ; python_version >= "3.8" and python_version < "4.0" +libcst==1.1.0 ; python_version >= "3.8" and python_version < "4" +markdown-it-py==3.0.0 ; python_version >= "3.8" and python_version < "4" +matplotlib-inline==0.1.7 ; python_version >= "3.10" and python_version < "4.0" +matplotlib==3.7.5 ; python_version >= "3.8" and python_version < "4.0" +mdurl==0.1.2 ; python_version >= "3.8" and python_version < "4" +mypy-extensions==1.0.0 ; python_version >= "3.8" and python_version < "4.0" +mypy==1.10.0 ; python_version >= "3.8" and python_version < "4.0" +numpy==1.24.4 ; python_version >= "3.8" and python_version < "3.9" +numpy==1.26.4 ; python_version >= "3.9" and python_version < "4.0" +packaging==24.1 ; python_version >= "3.8" and python_version < "4.0" +parso==0.8.4 ; python_version >= "3.10" and python_version < "4.0" +pathspec==0.12.1 ; python_version >= "3.8" and python_version < "4" +pexpect==4.9.0 ; python_version >= "3.10" and python_version < "4.0" and (sys_platform != "win32" and sys_platform != "emscripten") +pillow==10.3.0 ; python_version >= "3.8" and python_version < "4.0" +plotly==5.22.0 ; python_version >= "3.8" and python_version < "4.0" +pluggy==1.5.0 ; python_version >= "3.8" and python_version < "4.0" +prompt-toolkit==3.0.47 ; python_version >= "3.10" and python_version < "4.0" +ptyprocess==0.7.0 ; python_version >= "3.10" and python_version < "4.0" and (sys_platform != "win32" and sys_platform != "emscripten") +pure-eval==0.2.2 ; python_version >= "3.10" and python_version < "4.0" +pycln==2.4.0 ; python_version >= "3.8" and python_version < "4" +pygments==2.18.0 ; python_version >= "3.8" and python_version < "4.0" +pyparsing==3.1.2 ; python_version >= "3.8" and python_version < "4.0" +pytest-cov==4.1.0 ; python_version >= "3.8" and python_version < "4.0" +pytest==8.2.2 ; python_version >= "3.8" and python_version < "4.0" +python-dateutil==2.9.0.post0 ; python_version >= "3.8" and python_version < "4.0" +pyyaml==6.0.1 ; python_version >= "3.8" and python_version < "4" +rich==13.7.1 ; python_version >= "3.8" and python_version < "4" +ruff==0.4.10 ; python_version >= "3.8" and python_version < "4.0" +shellingham==1.5.4 ; python_version >= "3.8" and python_version < "4" +six==1.16.0 ; python_version >= "3.8" and python_version < "4.0" +stack-data==0.6.3 ; python_version >= "3.10" and python_version < "4.0" +tenacity==8.4.1 ; python_version >= "3.8" and python_version < "4.0" +tomli==2.0.1 ; python_version >= "3.8" and python_full_version <= "3.11.0a6" +tomlkit==0.12.5 ; python_version >= "3.8" and python_version < "4" +traitlets==5.14.3 ; python_version >= "3.10" and python_version < "4.0" +typeguard==4.3.0 ; python_version >= "3.8" and python_version < "4.0" +typer==0.12.3 ; python_version >= "3.8" and python_version < "4" +typing-extensions==4.12.2 ; python_version >= "3.8" and python_version < "4.0" +typing-inspect==0.9.0 ; python_version >= "3.8" and python_version < "4" +wcwidth==0.2.13 ; python_version >= "3.10" and python_version < "4.0" +zipp==3.19.2 ; python_version >= "3.8" and python_version < "3.10" diff --git a/.github/lint-requirements.txt b/.github/lint-requirements.txt new file mode 100644 index 00000000..eac74f60 --- /dev/null +++ b/.github/lint-requirements.txt @@ -0,0 +1,17 @@ +click==8.1.7 ; python_version >= "3.8" and python_version < "4" +colorama==0.4.6 ; python_version >= "3.8" and python_version < "4" and platform_system == "Windows" +libcst==1.1.0 ; python_version >= "3.8" and python_version < "4" +markdown-it-py==3.0.0 ; python_version >= "3.8" and python_version < "4" +mdurl==0.1.2 ; python_version >= "3.8" and python_version < "4" +mypy-extensions==1.0.0 ; python_version >= "3.8" and python_version < "4" +pathspec==0.12.1 ; python_version >= "3.8" and python_version < "4" +pycln==2.4.0 ; python_version >= "3.8" and python_version < "4" +pygments==2.18.0 ; python_version >= "3.8" and python_version < "4" +pyyaml==6.0.1 ; python_version >= "3.8" and python_version < "4" +rich==13.7.1 ; python_version >= "3.8" and python_version < "4" +ruff==0.4.10 ; python_version >= "3.8" and python_version < "4.0" +shellingham==1.5.4 ; python_version >= "3.8" and python_version < "4" +tomlkit==0.12.5 ; python_version >= "3.8" and python_version < "4" +typer==0.12.3 ; python_version >= "3.8" and python_version < "4" +typing-extensions==4.12.2 ; python_version >= "3.8" and python_version < "4" +typing-inspect==0.9.0 ; python_version >= "3.8" and python_version < "4" diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 995dda85..2746e08a 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -14,49 +14,110 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install linters - run: pip install pycln isort black + run: make setup-format RUN_GLOBAL=1 - name: Run Format Checks - run: make check-format + run: make check-format RUN_GLOBAL=1 + + check-deps: + name: Check dependencies + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install Poetry + run: curl -sSL https://install.python-poetry.org | python3 - + + - name: Poetry Plugins + run: | + poetry self add poetry-plugin-export + poetry self show plugins + + - name: Check poetry.lock and .github/dev-requirements.txt + run: make check-dep-dev + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: uv pip install -r .github/dev-requirements.txt --system --no-deps # we already should have all dependencies exported into .github/dev-requirements.txt + + - name: Install torch (special) + run: uv pip install torch==2.3.1+cpu --system --extra-index-url https://download.pytorch.org/whl/cpu + + - name: Install muutils (local) + run: uv pip install . --system + + - name: Install zanj (requires muutils) + run: uv pip install zanj --system test: name: Test and Lint runs-on: ubuntu-latest + # needs: [lint, check-deps] strategy: matrix: versions: - - python: "3.10" - torch: "1.13.1" - - python: "3.10" - torch: "2.0.1" - - python: "3.11" - torch: "2.0.1" + - python: '3.8' + torch: '1.13.1' + - python: '3.9' + torch: '1.13.1' + - python: '3.10' + torch: '1.13.1' + - python: '3.10' + torch: '2.3.1' + - python: '3.11' + torch: '2.3.1' + - python: '3.12' + torch: '2.3.1' steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.versions.python }} + - name: Install uv + run: pip install uv + - name: Install dependencies + # install torch first to avoid pytorch index messing things up run: | - curl -sSL https://install.python-poetry.org | python3 - - poetry lock --check - export CUDA_VISIBLE_DEVICES=0 - poetry add torch@${{ matrix.versions.torch }}+cpu --source torch_cpu - poetry install --all-extras + uv pip install -r .github/dev-requirements.txt --system --no-deps + uv pip install torch==${{ matrix.versions.torch}}+cpu --system --extra-index-url https://download.pytorch.org/whl/cpu + + - name: Install muutils + run: uv pip install . --system + + - name: Install zanj (>=3.10 only) + # TODO: not yet available for python 3.8 and 3.9 + if: ${{ matrix.versions.python != '3.8' && matrix.versions.python != '3.9' }} + run: uv pip install zanj --system - name: tests - run: make test + run: make test RUN_GLOBAL=1 + + - name: tests in strict mode + # TODO: until zanj ported to 3.8 and 3.9 + if: ${{ matrix.versions.python != '3.8' && matrix.versions.python != '3.9' }} + run: make test WARN_STRICT=1 RUN_GLOBAL=1 - - name: lint - run: make lint + - name: check typing + run: make typing RUN_GLOBAL=1 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0f47f10a..71f0c4d3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,26 @@ +# local stuff (pypi token, commit log) +.github/local/** + +# this one is cursed +tests/unit/validate_type/test_validate_type_GENERATED.py +# test notebook +_test.ipynb +# junk data JUNK_DATA_PATH/ junk_data -.pypi-token -.commit_log + .vscode/ +# caches __pycache__/ **/__pycache__/ **/.mypy_cache/ **/.pytest_cache/ +# coverage .coverage htmlcov/ +# build build/ dist/ muutils.egg-info/ diff --git a/docs/coverage/coverage.svg b/docs/coverage/coverage.svg index 90299371..b750dd9c 100644 --- a/docs/coverage/coverage.svg +++ b/docs/coverage/coverage.svg @@ -15,7 +15,7 @@ coverage coverage - 76% - 76% + 77% + 77% diff --git a/docs/coverage/coverage.txt b/docs/coverage/coverage.txt index b1b1c67f..61f3bc10 100644 --- a/docs/coverage/coverage.txt +++ b/docs/coverage/coverage.txt @@ -1,55 +1,62 @@ Name Stmts Miss Cover Missing --------------------------------------------------------------------------------------------------------------- -muutils\__init__.py 0 0 100% -muutils\dictmagic.py 162 23 86% 14-19, 22-25, 177, 285, 445-449, 453, 486-498 -muutils\group_equiv.py 28 0 100% -muutils\json_serialize\__init__.py 5 5 0% 1-15 -muutils\json_serialize\array.py 80 30 62% 1-14, 18-24, 35, 74, 102-105, 114, 118, 121, 125, 129-132, 136, 143, 155, 174-179, 184, 187 -muutils\json_serialize\json_serialize.py 64 45 30% 1-80, 124, 208-234, 244, 256, 271-304 -muutils\json_serialize\serializable_dataclass.py 187 71 62% 1-35, 69, 82-102, 129, 147, 160-165, 231-248, 257-267, 288-325, 329, 339-340, 344, 353-357, 361, 370, 381-389, 417, 443, 459-460, 483, 510, 516 -muutils\json_serialize\util.py 76 48 37% 1-58, 62, 73, 77, 93, 102, 107-111, 122, 125-126 -muutils\jsonlines.py 31 31 0% 1-73 -muutils\kappa.py 14 0 100% +muutils\__init__.py 1 1 0% 1 +muutils\dictmagic.py 158 21 87% 28-33, 36-39, 297, 457-461, 465, 498-510 +muutils\errormode.py 32 13 59% 1-13, 30-33, 49 +muutils\group_equiv.py 29 0 100% +muutils\json_serialize\__init__.py 6 6 0% 1-17 +muutils\json_serialize\array.py 92 33 64% 1-24, 28-34, 45, 85, 119-122, 131, 135, 139, 142, 146, 150-153, 157, 164, 176, 203-208, 213, 216 +muutils\json_serialize\json_serialize.py 63 45 29% 1-81, 125, 209-235, 245, 257, 272-305 +muutils\json_serialize\serializable_dataclass.py 216 87 60% 1-36, 50-55, 75-83, 108, 114, 144, 148-171, 190-192, 196, 208, 220-242, 251-257, 286, 299-300, 306, 317-321, 326, 338, 355-366, 372-384, 394, 485, 493-497, 526, 550-551, 585, 627 +muutils\json_serialize\serializable_field.py 32 17 47% 1-37, 73, 79-82, 92, 103-123 +muutils\json_serialize\util.py 109 63 42% 1-43, 45, 49-61, 65, 76, 80, 96, 105, 110-114, 125, 128-133, 151, 164-169, 235-252 +muutils\jsonlines.py 32 32 0% 1-75 +muutils\kappa.py 15 0 100% muutils\logger\__init__.py 5 0 100% muutils\logger\exception_context.py 12 6 50% 24, 27, 30-43 -muutils\logger\headerfuncs.py 18 1 94% 53 +muutils\logger\headerfuncs.py 19 1 95% 55 muutils\logger\log_util.py 32 32 0% 1-80 -muutils\logger\logger.py 97 25 74% 26-34, 85, 88, 133, 153-154, 192, 225, 235, 255-259, 275-278, 293, 297, 304 -muutils\logger\loggingstream.py 39 12 69% 41-74, 79, 89-90 -muutils\logger\simplelogger.py 40 19 52% 14, 18, 22, 26, 53-63, 67-79 -muutils\logger\timing.py 39 19 51% 25-28, 41-46, 50-52, 65-68, 79-84 -muutils\misc.py 164 12 93% 155, 186, 227-229, 288, 297, 300, 320-321, 334, 373-374 -muutils\mlutils.py 66 39 41% 1-26, 29, 35-49, 54, 56, 65-73, 96, 104, 128-131, 141-142, 152-153 +muutils\logger\logger.py 98 25 74% 28-36, 87, 90, 135, 155-156, 194, 227, 237, 257-261, 277-280, 295, 299, 306 +muutils\logger\loggingstream.py 40 12 70% 43-76, 81, 91-92 +muutils\logger\simplelogger.py 41 19 54% 16, 20, 24, 28, 55-65, 69-81 +muutils\logger\timing.py 39 18 54% 27-30, 43-48, 52-54, 67-70, 81-87 +muutils\misc.py 172 11 94% 210, 241, 284, 342, 351, 354, 374-375, 388, 427-428 +muutils\mlutils.py 72 43 40% 1-28, 31, 37-51, 56, 58, 67-75, 98, 106, 128-131, 142-147, 151-152, 162-163 muutils\nbutils\__init__.py 2 2 0% 1-3 -muutils\nbutils\configure_notebook.py 132 79 40% 1-55, 73-82, 96, 106-107, 112, 115-116, 136-139, 144, 150-159, 166-171, 180, 219-230, 236-241, 264-271, 274 -muutils\nbutils\convert_ipynb_to_script.py 118 41 65% 63, 78, 91, 105-139, 228-230, 236, 263, 287-289, 296-349 +muutils\nbutils\configure_notebook.py 133 80 40% 1-57, 75-84, 106, 116-117, 122, 125-126, 146-149, 154, 160-169, 176-181, 190, 229-240, 246-251, 274-281, 284 +muutils\nbutils\convert_ipynb_to_script.py 119 41 66% 65, 80, 93, 107-141, 230-232, 238, 265, 289-291, 298-351 muutils\nbutils\mermaid.py 11 11 0% 1-18 muutils\nbutils\print_tex.py 10 10 0% 1-19 -muutils\nbutils\run_notebook_tests.py 58 20 66% 29, 31, 35, 39, 45, 53, 80-82, 86-90, 97-114 -muutils\statcounter.py 87 32 63% 24-35, 50, 70, 98, 110, 120, 136-166, 174, 183, 186, 190-195, 204 -muutils\sysinfo.py 71 18 75% 21, 60-61, 78-111, 145, 168 -muutils\tensor_utils.py 125 19 85% 83, 86, 105, 109, 119, 130, 133-136, 144, 151, 165-173 -tests\unit\json_serialize\serializable_dataclass\test_helpers.py 42 0 100% -tests\unit\json_serialize\serializable_dataclass\test_sdc_defaults.py 31 0 100% -tests\unit\json_serialize\serializable_dataclass\test_sdc_properties_nested.py 26 0 100% -tests\unit\json_serialize\serializable_dataclass\test_serializable_dataclass.py 190 0 100% +muutils\nbutils\run_notebook_tests.py 59 20 66% 29, 31, 35, 39, 45, 53, 79-81, 85-89, 96-113 +muutils\statcounter.py 89 32 64% 25-36, 51, 75, 103, 115, 125, 141-171, 179, 188, 191, 195-200, 209 +muutils\sysinfo.py 78 25 68% 20-23, 64-65, 82-115, 156-165, 177, 195-197 +muutils\tensor_utils.py 124 18 85% 86, 89, 108, 112, 129, 132-135, 143, 150, 164-172 +muutils\validate_type.py 82 19 77% 1-34, 51, 55, 190, 193, 213 +tests\unit\json_serialize\serializable_dataclass\test_helpers.py 43 0 100% +tests\unit\json_serialize\serializable_dataclass\test_sdc_defaults.py 32 0 100% +tests\unit\json_serialize\serializable_dataclass\test_sdc_properties_nested.py 44 1 98% 44 +tests\unit\json_serialize\serializable_dataclass\test_serializable_dataclass.py 204 0 100% tests\unit\json_serialize\test_array.py 40 0 100% tests\unit\json_serialize\test_util.py 49 2 96% 66, 73 -tests\unit\logger\test_logger.py 10 0 100% -tests\unit\logger\test_timer_context.py 9 0 100% -tests\unit\misc\test_freeze.py 120 0 100% -tests\unit\misc\test_misc.py 43 0 100% -tests\unit\misc\test_numerical_conversions.py 42 0 100% -tests\unit\nbutils\test_configure_notebook.py 61 0 100% -tests\unit\nbutils\test_conversion.py 26 0 100% +tests\unit\logger\test_logger.py 11 0 100% +tests\unit\logger\test_timer_context.py 11 0 100% +tests\unit\misc\test_freeze.py 121 0 100% +tests\unit\misc\test_misc.py 74 0 100% +tests\unit\misc\test_numerical_conversions.py 43 0 100% +tests\unit\nbutils\test_configure_notebook.py 70 0 100% +tests\unit\nbutils\test_conversion.py 27 0 100% tests\unit\test_chunks.py 31 0 100% -tests\unit\test_dictmagic.py 129 0 100% -tests\unit\test_group_equiv.py 12 0 100% +tests\unit\test_dictmagic.py 130 0 100% +tests\unit\test_errormode.py 69 1 99% 122 +tests\unit\test_group_equiv.py 13 0 100% tests\unit\test_import_torch.py 4 0 100% tests\unit\test_kappa.py 39 0 100% -tests\unit\test_mlutils.py 35 3 91% 31, 35, 43 -tests\unit\test_statcounter.py 13 0 100% -tests\unit\test_sysinfo.py 4 0 100% -tests\unit\test_tensor_utils.py 48 0 100% +tests\unit\test_mlutils.py 43 6 86% 35, 39, 47, 50, 57-58 +tests\unit\test_statcounter.py 14 0 100% +tests\unit\test_sysinfo.py 6 0 100% +tests\unit\test_tensor_utils.py 51 0 100% +tests\unit\validate_type\test_validate_type.py 206 45 78% 49-50, 74-75, 101-102, 124-125, 146-147, 176-177, 202-203, 235-236, 253, 272-273, 334-335, 359-360, 430-431, 459-460, 464-465, 479-497 +tests\unit\validate_type\test_validate_type_GENERATED.py 206 45 78% 50-51, 75-76, 102-103, 125-126, 147-148, 177-178, 203-204, 236-237, 254, 273-274, 335-336, 360-361, 431-432, 460-461, 465-466, 480-498 +tests\unit\validate_type\test_validate_type_special.py 15 3 80% 34-35, 57 --------------------------------------------------------------------------------------------------------------- -TOTAL 2777 655 76% +TOTAL 3618 846 77% diff --git a/makefile b/makefile index ee237e95..00abc156 100644 --- a/makefile +++ b/makefile @@ -1,34 +1,120 @@ +# configuration +# ================================================== +# MODIFY THIS FILE TO SUIT YOUR PROJECT +# it assumes that the source is in a directory named the same as the package name PACKAGE_NAME := muutils +# for checking you are on the right branch when publishing PUBLISH_BRANCH := main -PYPI_TOKEN_FILE := .pypi-token -LAST_VERSION_FILE := .lastversion +# where to put the coverage reports COVERAGE_REPORTS_DIR := docs/coverage +# where the tests are (assumes pytest) TESTS_DIR := tests/unit +# temp directory to clean up +TESTS_TEMP_DIR := tests/_temp +# dev and lint requirements.txt files +REQ_DEV := .github/dev-requirements.txt +REQ_LINT := .github/lint-requirements.txt + +# probably don't change these: +# -------------------------------------------------- +# will print this token when publishing +PYPI_TOKEN_FILE := .github/local/.pypi-token +# the last version that was auto-uploaded. will use this to create a commit log for version tag +LAST_VERSION_FILE := .github/.lastversion +# where the pyproject.toml file is PYPROJECT := pyproject.toml +# base python to use. Will add `poetry run` in front of this if `RUN_GLOBAL` is not set to 1 +PYTHON_BASE := python +# where the commit log will be stored +COMMIT_LOG_FILE := .github/local/.commit_log + + + +# reading information and command line options +# ================================================== + +# RUN_GLOBAL=1 to use global `PYTHON_BASE` instead of `poetry run $(PYTHON_BASE)` +# -------------------------------------------------- +# for formatting, we might want to run python without setting up all of poetry +RUN_GLOBAL ?= 0 +ifeq ($(RUN_GLOBAL),0) + PYTHON = poetry run $(PYTHON_BASE) +else + PYTHON = $(PYTHON_BASE) +endif + +# reading version +# -------------------------------------------------- +# assuming your pyproject.toml has a line that looks like `version = "0.0.1"`, will get the version +VERSION := NULL +# read last auto-uploaded version from file +LAST_VERSION := NULL +# get the python version, now that we have picked the python command +PYTHON_VERSION := NULL +.PHONY: gen-version-info +gen-version-info: + $(eval VERSION := $(shell python -c "import re; print(re.search(r'^version\s*=\s*\"(.+?)\"', open('$(PYPROJECT)').read(), re.MULTILINE).group(1))") ) + $(eval LAST_VERSION := $(shell [ -f $(LAST_VERSION_FILE) ] && cat $(LAST_VERSION_FILE) || echo NULL) ) + $(eval PYTHON_VERSION := $(shell $(PYTHON) -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')") ) + +# getting commit log +.PHONY: gen-commit-log +gen-commit-log: gen-version-info + if [ "$(LAST_VERSION)" = "NULL" ]; then \ + echo "LAST_VERSION is NULL, cant get commit log!"; \ + exit 1; \ + fi + $(shell python -c "import subprocess; open('$(COMMIT_LOG_FILE)', 'w').write('\n'.join(reversed(subprocess.check_output(['git', 'log', '$(LAST_VERSION)'.strip() + '..HEAD', '--pretty=format:- %s (%h)']).decode('utf-8').strip().split('\n'))))") + + +# loose typing, allow warnings for python <3.10 +# -------------------------------------------------- +TYPECHECK_ARGS ?= +# COMPATIBILITY_MODE: whether to run in compatibility mode for python <3.10 +COMPATIBILITY_MODE := $(shell $(PYTHON) -c "import sys; print(1 if sys.version_info < (3, 10) else 0)") + +# options we might want to pass to pytest +# -------------------------------------------------- +PYTEST_OPTIONS ?= # using ?= means you can pass extra options from the command line +COV ?= 1 + +ifdef VERBOSE + PYTEST_OPTIONS += --verbose +endif + +ifeq ($(COV),1) + PYTEST_OPTIONS += --cov=. +endif + +# compatibility mode for python <3.10 +# -------------------------------------------------- + +# whether to run pytest with warnings as errors +WARN_STRICT ?= 0 -VERSION := $(shell python -c "import re; print(re.search(r'^version\s*=\s*\"(.+?)\"', open('$(PYPROJECT)').read(), re.MULTILINE).group(1))") -LAST_VERSION := $(shell cat $(LAST_VERSION_FILE)) -PYPOETRY := poetry run python +ifneq ($(WARN_STRICT), 0) + PYTEST_OPTIONS += -W error +endif -# note that the commands at the end: -# 1) format the git log -# 2) replace backticks with single quotes, to avoid funny business -# 3) add a final newline, to make tac happy -# 4) reverse the order of the lines, so that the oldest commit is first -# 5) replace newlines with tabs, to prevent the newlines from being lost -COMMIT_LOG_FILE := .commit_log -COMMIT_LOG_SINCE_LAST_VERSION := $(shell (git log $(LAST_VERSION)..HEAD --pretty=format:"- %s (%h)" | tr '`' "'" ; echo) | tac | tr '\n' '\t') -# 1 2 3 4 5 +# Update the PYTEST_OPTIONS to include the conditional ignore option +ifeq ($(COMPATIBILITY_MODE), 1) + JUNK := $(info !!! WARNING !!!: Detected python version less than 3.10, some behavior will be different) + PYTEST_OPTIONS += --ignore=tests/unit/validate_type/ + TYPECHECK_ARGS += --disable-error-code misc --disable-error-code syntax --disable-error-code import-not-found +endif + + +# default target (help) +# ================================================== .PHONY: default default: help .PHONY: version -version: +version: gen-commit-log @echo "Current version is $(VERSION), last auto-uploaded version is $(LAST_VERSION)" @echo "Commit log since last version:" - @echo "$(COMMIT_LOG_SINCE_LAST_VERSION)" | tr '\t' '\n' > $(COMMIT_LOG_FILE) @cat $(COMMIT_LOG_FILE) @if [ "$(VERSION)" = "$(LAST_VERSION)" ]; then \ echo "Python package $(VERSION) is the same as last published version $(LAST_VERSION), exiting!"; \ @@ -37,55 +123,60 @@ version: # formatting -# -------------------------------------------------- +# ================================================== + +.PHONY: setup-format +setup-format: + @echo "install only packages needed for formatting, direct via pip (useful for CI)" + $(PYTHON) -m pip install -r $(REQ_LINT) + .PHONY: format format: - python -m pycln --config $(PYPROJECT) --all . - python -m isort format . - python -m black . + @echo "format the source code" + $(PYTHON) -m ruff format --config $(PYPROJECT) . + $(PYTHON) -m ruff check --fix --config $(PYPROJECT) . + $(PYTHON) -m pycln --config $(PYPROJECT) --all . .PHONY: check-format check-format: @echo "run format check" - python -m pycln --check --config $(PYPROJECT) . - python -m isort --check-only . - python -m black --check . + $(PYTHON) -m ruff check --config $(PYPROJECT) . + $(PYTHON) -m pycln --check --config $(PYPROJECT) . -# coverage reports -# -------------------------------------------------- -# whether to run pytest with coverage report generation -COV ?= 1 - -ifeq ($(COV),1) - PYTEST_OPTIONS=--cov=. -else - PYTEST_OPTIONS= -endif +# coverage +# ================================================== .PHONY: cov cov: @echo "generate coverage reports" - $(PYPOETRY) -m coverage report -m > $(COVERAGE_REPORTS_DIR)/coverage.txt - $(PYPOETRY) -m coverage_badge -f -o $(COVERAGE_REPORTS_DIR)/coverage.svg - $(PYPOETRY) -m coverage html + $(PYTHON) -m coverage report -m > $(COVERAGE_REPORTS_DIR)/coverage.txt + $(PYTHON) -m coverage_badge -f -o $(COVERAGE_REPORTS_DIR)/coverage.svg + $(PYTHON) -m coverage html # tests -# -------------------------------------------------- +# ================================================== # at some point, need to add back --check-untyped-defs to mypy call # but it complains when we specify arguments by keyword where positional is fine # not sure how to fix this # python -m pylint $(PACKAGE_NAME)/ # python -m pylint tests/ -.PHONY: lint -lint: clean - $(PYPOETRY) -m mypy --config-file $(PYPROJECT) $(PACKAGE_NAME)/ - $(PYPOETRY) -m mypy --config-file $(PYPROJECT) tests/ +.PHONY: typing +typing: clean + @echo "running type checks" + $(PYTHON) -m mypy --config-file $(PYPROJECT) $(TYPECHECK_ARGS) $(PACKAGE_NAME)/ + $(PYTHON) -m mypy --config-file $(PYPROJECT) $(TYPECHECK_ARGS) tests/ + .PHONY: test test: clean @echo "running tests" - $(PYPOETRY) -m pytest $(PYTEST_OPTIONS) $(TESTS_DIR) + + if [ $(COMPATIBILITY_MODE) -eq 0 ]; then \ + echo "converting certain tests to modern format"; \ + $(PYTHON) tests/util/replace_type_hints.py tests/unit/validate_type/test_validate_type.py "# DO NOT EDIT, GENERATED FILE" > tests/unit/validate_type/test_validate_type_GENERATED.py; \ + fi; \ + $(PYTHON) -m pytest $(PYTEST_OPTIONS) $(TESTS_DIR) .PHONY: check @@ -93,7 +184,7 @@ check: clean check-format clean test lint @echo "run format check, test, and lint" # build and publish -# -------------------------------------------------- +# ================================================== .PHONY: verify-git verify-git: @@ -107,13 +198,33 @@ verify-git: exit 1; \ fi; \ + +# no zanj, it gets special treatment because it depends on muutils +# without urls since pytorch extra index breaks things +# no torch because we install it manually in CI +EXPORT_ARGS := -E array_no_torch -E notebook --with dev --with lint --without-hashes --without-urls + +.PHONY: dep-dev +dep-dev: + @echo "exporting dev and extras deps to $(REQ_DEV), lint/format deps to $(REQ_LINT)" + poetry update + poetry export $(EXPORT_ARGS) --output $(REQ_DEV) + poetry export --only lint --without-hashes --without-urls --output $(REQ_LINT) + +.PHONY: check-dep-dev +check-dep-dev: + @echo "checking poetry lock is good, exported requirements match poetry" + poetry check --lock + poetry export $(EXPORT_ARGS) | diff - $(REQ_DEV) + poetry export --only lint --without-hashes --without-urls | diff - $(REQ_LINT) + .PHONY: build build: @echo "build via poetry, assumes checks have been run" poetry build .PHONY: publish -publish: check build verify-git version +publish: gen-commit-log check build verify-git version @echo "run all checks, build, and then publish" @echo "Enter the new version number if you want to upload to pypi and create a new tag" @@ -136,25 +247,34 @@ publish: check build verify-git version twine upload dist/* --verbose # cleanup -# -------------------------------------------------- +# ================================================== .PHONY: clean clean: @echo "cleaning up" rm -rf .mypy_cache + rm -rf .ruff_cache rm -rf .pytest_cache rm -rf .coverage rm -rf dist rm -rf build rm -rf $(PACKAGE_NAME).egg-info rm -rf tests/junk_data - python -Bc "import pathlib; [p.unlink() for p in pathlib.Path('.').rglob('*.py[co]')]" - python -Bc "import pathlib; [p.rmdir() for p in pathlib.Path('.').rglob('__pycache__')]" + $(PYTHON_BASE) -Bc "import pathlib; [p.unlink() for p in pathlib.Path('.').rglob('*.py[co]')]" + $(PYTHON_BASE) -Bc "import pathlib; [p.rmdir() for p in pathlib.Path('.').rglob('__pycache__')]" + rm -rf tests/unit/validate_type/test_validate_type_GENERATED.py # listing targets, from stackoverflow # https://stackoverflow.com/questions/4219255/how-do-you-get-the-list-of-targets-in-a-makefile .PHONY: help -help: - @echo -n "# list make targets" +help: gen-version-info + @echo -n "list make targets" @echo ":" - @cat Makefile | sed -n '/^\.PHONY: / h; /\(^\t@*echo\|^\t:\)/ {H; x; /PHONY/ s/.PHONY: \(.*\)\n.*"\(.*\)"/ make \1\t\2/p; d; x}'| sort -k2,2 |expand -t 25 \ No newline at end of file + @cat Makefile | sed -n '/^\.PHONY: / h; /\(^\t@*echo\|^\t:\)/ {H; x; /PHONY/ s/.PHONY: \(.*\)\n.*"\(.*\)"/ make \1\t\2/p; d; x}'| sort -k2,2 |expand -t 25 + @echo "# makefile variables:" + @echo " PYTHON = $(PYTHON)" + @echo " PYTHON_VERSION = $(PYTHON_VERSION)" + @echo " PACKAGE_NAME = $(PACKAGE_NAME)" + @echo " VERSION = $(VERSION)" + @echo " LAST_VERSION = $(LAST_VERSION)" + @echo " PYTEST_OPTIONS = $(PYTEST_OPTIONS)" \ No newline at end of file diff --git a/muutils/__init__.py b/muutils/__init__.py index e69de29b..9d48db4f 100644 --- a/muutils/__init__.py +++ b/muutils/__init__.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/muutils/dictmagic.py b/muutils/dictmagic.py index 1301b719..37c48dc0 100644 --- a/muutils/dictmagic.py +++ b/muutils/dictmagic.py @@ -1,13 +1,27 @@ +from __future__ import annotations + import typing import warnings from collections import defaultdict -from typing import Any, Callable, Generic, Hashable, Iterable, Literal, TypeVar +from typing import ( + Any, + Callable, + Generic, + Hashable, + Iterable, + Literal, + Optional, + TypeVar, + Union, +) + +from muutils.errormode import ErrorMode _KT = TypeVar("_KT") _VT = TypeVar("_VT") -class DefaulterDict(dict[_KT, _VT], Generic[_KT, _VT]): +class DefaulterDict(typing.Dict[_KT, _VT], Generic[_KT, _VT]): """like a defaultdict, but default_factory is passed the key as an argument""" def __init__(self, default_factory: Callable[[_KT], _VT], *args, **kwargs): @@ -29,7 +43,7 @@ def _recursive_defaultdict_ctor() -> defaultdict: return defaultdict(_recursive_defaultdict_ctor) -def defaultdict_to_dict_recursive(dd: defaultdict | DefaulterDict) -> dict: +def defaultdict_to_dict_recursive(dd: Union[defaultdict, DefaulterDict]) -> dict: """Convert a defaultdict or DefaulterDict to a normal dict, recursively""" return { key: ( @@ -41,7 +55,9 @@ def defaultdict_to_dict_recursive(dd: defaultdict | DefaulterDict) -> dict: } -def dotlist_to_nested_dict(dot_dict: dict[str, Any], sep: str = ".") -> dict[str, Any]: +def dotlist_to_nested_dict( + dot_dict: typing.Dict[str, Any], sep: str = "." +) -> typing.Dict[str, Any]: """Convert a dict with dot-separated keys to a nested dict Example: @@ -62,11 +78,11 @@ def dotlist_to_nested_dict(dot_dict: dict[str, Any], sep: str = ".") -> dict[str def nested_dict_to_dotlist( - nested_dict: dict[str, Any], + nested_dict: typing.Dict[str, Any], sep: str = ".", allow_lists: bool = False, ) -> dict[str, Any]: - def _recurse(current: Any, parent_key: str = "") -> dict[str, Any]: + def _recurse(current: Any, parent_key: str = "") -> typing.Dict[str, Any]: items: dict = dict() new_key: str @@ -129,15 +145,15 @@ def update_with_nested_dict( def kwargs_to_nested_dict( kwargs_dict: dict[str, Any], sep: str = ".", - strip_prefix: str | None = None, - when_unknown_prefix: typing.Literal["raise", "warn", "ignore"] = "warn", - transform_key: Callable[[str], str] | None = None, + strip_prefix: Optional[str] = None, + when_unknown_prefix: ErrorMode = ErrorMode.WARN, + transform_key: Optional[Callable[[str], str]] = None, ) -> dict[str, Any]: """given kwargs from fire, convert them to a nested dict if strip_prefix is not None, then all keys must start with the prefix. by default, will warn if an unknown prefix is found, but can be set to raise an error or ignore it: - `when_unknown_prefix: typing.Literal["raise", "warn", "ignore"]` + `when_unknown_prefix: ErrorMode` Example: ```python @@ -156,28 +172,24 @@ def main(**kwargs): the kwargs dict to convert - `sep: str = "."` the separator to use for nested keys - - `strip_prefix: str | None = None` + - `strip_prefix: Optional[str] = None` if not None, then all keys must start with this prefix - - `when_unknown_prefix: typing.Literal["raise", "warn", "ignore"] = "warn"` + - `when_unknown_prefix: ErrorMode = ErrorMode.WARN` what to do when an unknown prefix is found - `transform_key: Callable[[str], str] | None = None` a function to apply to each key before adding it to the dict (applied after stripping the prefix) """ + when_unknown_prefix = ErrorMode.from_any(when_unknown_prefix) filtered_kwargs: dict[str, Any] = dict() for key, value in kwargs_dict.items(): if strip_prefix is not None: if not key.startswith(strip_prefix): - if when_unknown_prefix == "raise": - raise ValueError(f"key {key} does not start with {strip_prefix}") - elif when_unknown_prefix == "warn": - warnings.warn(f"key {key} does not start with {strip_prefix}") - elif when_unknown_prefix == "ignore": - pass - else: - raise ValueError( - f"when_unknown_prefix must be one of 'raise', 'warn', or 'ignore', got {when_unknown_prefix}" - ) - key = key.removeprefix(strip_prefix) + when_unknown_prefix.process( + f"key '{key}' does not start with '{strip_prefix}'", + except_cls=ValueError, + ) + else: + key = key[len(strip_prefix) :] if transform_key is not None: key = transform_key(key) @@ -245,7 +257,7 @@ def condense_nested_dicts_numeric_keys( def condense_nested_dicts_matching_values( data: dict[str, Any], - val_condense_fallback_mapping: Callable[[Any], Hashable] | None = None, + val_condense_fallback_mapping: Optional[Callable[[Any], Hashable]] = None, ) -> dict[str, Any]: """condense a nested dict, by condensing keys with matching values @@ -301,7 +313,7 @@ def condense_nested_dicts( data: dict[str, Any], condense_numeric_keys: bool = True, condense_matching_values: bool = True, - val_condense_fallback_mapping: Callable[[Any], Hashable] | None = None, + val_condense_fallback_mapping: Optional[Callable[[Any], Hashable]] = None, ) -> dict[str, Any]: """condense a nested dict, by condensing numeric or matching keys with matching values to ranges @@ -336,16 +348,16 @@ def condense_nested_dicts( def tuple_dims_replace( - t: tuple[int, ...], dims_names_map: dict[int, str] | None = None -) -> tuple[int | str, ...]: + t: tuple[int, ...], dims_names_map: Optional[dict[int, str]] = None +) -> tuple[Union[int, str], ...]: if dims_names_map is None: return t else: return tuple(dims_names_map.get(x, x) for x in t) -TensorDict = dict[str, "torch.Tensor|np.ndarray"] # type: ignore[name-defined] -TensorIterable = Iterable[tuple[str, "torch.Tensor|np.ndarray"]] # type: ignore[name-defined] +TensorDict = typing.Dict[str, "torch.Tensor|np.ndarray"] # type: ignore[name-defined] # noqa: F821 +TensorIterable = Iterable[typing.Tuple[str, "torch.Tensor|np.ndarray"]] # type: ignore[name-defined] # noqa: F821 TensorDictFormats = Literal["dict", "json", "yaml", "yml"] @@ -360,12 +372,12 @@ def condense_tensor_dict( shapes_convert: Callable[[tuple], Any] = _default_shapes_convert, drop_batch_dims: int = 0, sep: str = ".", - dims_names_map: dict[int, str] | None = None, + dims_names_map: Optional[dict[int, str]] = None, condense_numeric_keys: bool = True, condense_matching_values: bool = True, - val_condense_fallback_mapping: Callable[[Any], Hashable] | None = None, - return_format: TensorDictFormats | None = None, -) -> str | dict[str, str | tuple[int, ...]]: + val_condense_fallback_mapping: Optional[Callable[[Any], Hashable]] = None, + return_format: Optional[TensorDictFormats] = None, +) -> Union[str, dict[str, str | tuple[int, ...]]]: """Convert a dictionary of tensors to a dictionary of shapes. by default, values are converted to strings of their shapes (for nice printing). @@ -450,15 +462,15 @@ def condense_tensor_dict( # identity function for shapes_convert if not provided if shapes_convert is None: - shapes_convert = lambda x: x + shapes_convert = lambda x: x # noqa: E731 # convert to iterable - data_items: Iterable[tuple[str, "torch.Tensor|np.ndarray"]] = ( # type: ignore + data_items: "Iterable[tuple[str, Union[torch.Tensor,np.ndarray]]]" = ( # type: ignore # noqa: F821 data.items() if hasattr(data, "items") and callable(data.items) else data # type: ignore ) # get shapes - data_shapes: dict[str, str | tuple[int, ...]] = { + data_shapes: dict[str, Union[str, tuple[int, ...]]] = { k: shapes_convert( tuple_dims_replace( tuple(v.shape)[drop_batch_dims:], @@ -472,7 +484,7 @@ def condense_tensor_dict( data_nested: dict[str, Any] = dotlist_to_nested_dict(data_shapes, sep=sep) # condense the nested dict - data_condensed: dict[str, str | tuple[int, ...]] = condense_nested_dicts( + data_condensed: dict[str, Union[str, tuple[int, ...]]] = condense_nested_dicts( data=data_nested, condense_numeric_keys=condense_numeric_keys, condense_matching_values=condense_matching_values, @@ -480,19 +492,19 @@ def condense_tensor_dict( ) # return in the specified format - match fmt.lower(): - case "dict": - return data_condensed - case "json": - import json - - return json.dumps(data_condensed, indent=2) - case "yaml" | "yml": - try: - import yaml # type: ignore[import-untyped] - - return yaml.dump(data_condensed, sort_keys=False) - except ImportError as e: - raise ValueError("PyYAML is required for YAML output") from e - case _: - raise ValueError(f"Invalid return format: {fmt}") + fmt_lower: str = fmt.lower() + if fmt_lower == "dict": + return data_condensed + elif fmt_lower == "json": + import json + + return json.dumps(data_condensed, indent=2) + elif fmt_lower in ["yaml", "yml"]: + try: + import yaml # type: ignore[import-untyped] + + return yaml.dump(data_condensed, sort_keys=False) + except ImportError as e: + raise ValueError("PyYAML is required for YAML output") from e + else: + raise ValueError(f"Invalid return format: {fmt}") diff --git a/muutils/errormode.py b/muutils/errormode.py new file mode 100644 index 00000000..4499512d --- /dev/null +++ b/muutils/errormode.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import typing +import warnings +from enum import Enum + + +class ErrorMode(Enum): + EXCEPT = "except" + WARN = "warn" + IGNORE = "ignore" + + def process( + self, + msg: str, + except_cls: typing.Type[Exception] = ValueError, + warn_cls: typing.Type[Warning] = UserWarning, + except_from: typing.Optional[Exception] = None, + ): + if self is ErrorMode.EXCEPT: + if except_from is not None: + raise except_cls(msg) from except_from + else: + raise except_cls(msg) + elif self is ErrorMode.WARN: + warnings.warn(msg, warn_cls) + elif self is ErrorMode.IGNORE: + pass + else: + raise ValueError(f"Unknown error mode {self}") + + @classmethod + def from_any(cls, mode: "str|ErrorMode", allow_aliases: bool = True) -> ErrorMode: + if isinstance(mode, ErrorMode): + return mode + elif isinstance(mode, str): + mode = mode.strip().lower() + if not allow_aliases: + try: + return ErrorMode(mode) + except ValueError as e: + raise KeyError(f"Unknown error mode {mode}") from e + else: + return ERROR_MODE_ALIASES[mode] + else: + raise TypeError(f"Expected {ErrorMode} or str, got {type(mode) = }") + + +ERROR_MODE_ALIASES: dict[str, ErrorMode] = { + # base + "except": ErrorMode.EXCEPT, + "warn": ErrorMode.WARN, + "ignore": ErrorMode.IGNORE, + # except + "e": ErrorMode.EXCEPT, + "error": ErrorMode.EXCEPT, + "err": ErrorMode.EXCEPT, + "raise": ErrorMode.EXCEPT, + # warn + "w": ErrorMode.WARN, + "warning": ErrorMode.WARN, + # ignore + "i": ErrorMode.IGNORE, + "silent": ErrorMode.IGNORE, + "quiet": ErrorMode.IGNORE, + "nothing": ErrorMode.IGNORE, +} diff --git a/muutils/group_equiv.py b/muutils/group_equiv.py index 0a45a55a..dc722235 100644 --- a/muutils/group_equiv.py +++ b/muutils/group_equiv.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from itertools import chain from typing import Callable, Sequence, TypeVar diff --git a/muutils/json_serialize/__init__.py b/muutils/json_serialize/__init__.py index 4582116d..263ed273 100644 --- a/muutils/json_serialize/__init__.py +++ b/muutils/json_serialize/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from muutils.json_serialize.array import arr_metadata, load_array from muutils.json_serialize.json_serialize import ( BASE_HANDLERS, diff --git a/muutils/json_serialize/array.py b/muutils/json_serialize/array.py index aef5a97e..d08c9bea 100644 --- a/muutils/json_serialize/array.py +++ b/muutils/json_serialize/array.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +import base64 import typing import warnings from typing import Any, Iterable, Literal, Optional, Sequence @@ -8,7 +11,14 @@ # pylint: disable=unused-argument -ArrayMode = Literal["list", "array_list_meta", "array_hex_meta", "external", "zero_dim"] +ArrayMode = Literal[ + "list", + "array_list_meta", + "array_hex_meta", + "array_b64_meta", + "external", + "zero_dim", +] def array_n_elements(arr) -> int: # type: ignore[name-defined] @@ -33,7 +43,7 @@ def arr_metadata(arr) -> dict[str, list[int] | str | int]: def serialize_array( - jser: "JsonSerializer", # type: ignore[name-defined] + jser: "JsonSerializer", # type: ignore[name-defined] # noqa: F821 arr: np.ndarray, path: str | Sequence[str | int], array_mode: ArrayMode | None = None, @@ -46,14 +56,15 @@ def serialize_array( - `list`: serialize as a list of values, no metadata (equivalent to `arr.tolist()`) - `array_list_meta`: serialize dict with metadata, actual list under the key `data` - `array_hex_meta`: serialize dict with metadata, actual hex string under the key `data` + - `array_b64_meta`: serialize dict with metadata, actual base64 string under the key `data` - for `array_list_meta` and `array_hex_meta`, the output will look like + for `array_list_meta`, `array_hex_meta`, and `array_b64_meta`, the serialized object is: ``` { "__format__": , "shape": arr.shape, "dtype": str(arr.dtype), - "data": , + "data": , } ``` @@ -98,6 +109,12 @@ def serialize_array( "data": arr_np.tobytes().hex(), **arr_metadata(arr_np), } + elif array_mode == "array_b64_meta": + return { + "__format__": f"{arr_type}:array_b64_meta", + "data": base64.b64encode(arr_np.tobytes()).decode(), + **arr_metadata(arr_np), + } else: raise KeyError(f"invalid array_mode: {array_mode}") @@ -117,6 +134,10 @@ def infer_array_mode(arr: JSONitem) -> ArrayMode: if not isinstance(arr["data"], str): raise ValueError(f"invalid hex format: {type(arr['data']) = }\t{arr}") return "array_hex_meta" + elif fmt.endswith(":array_b64_meta"): + if not isinstance(arr["data"], str): + raise ValueError(f"invalid b64 format: {type(arr['data']) = }\t{arr}") + return "array_b64_meta" elif fmt.endswith(":external"): return "external" elif fmt.endswith(":zero_dim"): @@ -163,6 +184,14 @@ def load_array(arr: JSONitem, array_mode: Optional[ArrayMode] = None) -> Any: data = np.frombuffer(bytes.fromhex(arr["data"]), dtype=arr["dtype"]) return data.reshape(arr["shape"]) + elif array_mode == "array_b64_meta": + assert isinstance( + arr, typing.Mapping + ), f"invalid list format: {type(arr) = }\n{arr = }" + + data = np.frombuffer(base64.b64decode(arr["data"]), dtype=arr["dtype"]) + return data.reshape(arr["shape"]) + elif array_mode == "list": assert isinstance( arr, typing.Sequence diff --git a/muutils/json_serialize/json_serialize.py b/muutils/json_serialize/json_serialize.py index a857765f..67b33548 100644 --- a/muutils/json_serialize/json_serialize.py +++ b/muutils/json_serialize/json_serialize.py @@ -1,15 +1,16 @@ +from __future__ import annotations + import inspect -import types import warnings from dataclasses import dataclass, is_dataclass from pathlib import Path -from typing import Any, Callable, Iterable, Mapping +from typing import Any, Callable, Iterable, Mapping, Set, Union try: from muutils.json_serialize.array import ArrayMode, serialize_array except ImportError as e: ArrayMode = str # type: ignore[misc] - serialize_array = lambda *args, **kwargs: None + serialize_array = lambda *args, **kwargs: None # noqa: E731 warnings.warn( f"muutils.json_serialize.array could not be imported probably because missing numpy, array serialization will not work: \n{e}", ImportWarning, @@ -48,12 +49,12 @@ "sourcefile": try_catch(lambda x: inspect.getsourcefile(x)), } -SERIALIZE_DIRECT_AS_STR: set[str] = { +SERIALIZE_DIRECT_AS_STR: Set[str] = { "", "", } -ObjectPath = MonoTuple[str | int] +ObjectPath = MonoTuple[Union[str, int]] @dataclass @@ -99,7 +100,7 @@ def serialize(self) -> dict: BASE_HANDLERS: MonoTuple[SerializerHandler] = ( SerializerHandler( check=lambda self, obj, path: isinstance( - obj, (bool, int, float, str, types.NoneType) + obj, (bool, int, float, str, type(None)) ), serialize_func=lambda self, obj, path: obj, uid="base types", @@ -124,7 +125,7 @@ def serialize(self) -> dict: def _serialize_override_serialize_func( self: "JsonSerializer", obj: Any, path: ObjectPath ) -> JSONitem: - obj_cls: type = type(obj) + # obj_cls: type = type(obj) # if hasattr(obj_cls, "_register_self") and callable(obj_cls._register_self): # obj_cls._register_self() diff --git a/muutils/json_serialize/serializable_dataclass.py b/muutils/json_serialize/serializable_dataclass.py index 0772f08f..47e35253 100644 --- a/muutils/json_serialize/serializable_dataclass.py +++ b/muutils/json_serialize/serializable_dataclass.py @@ -1,260 +1,29 @@ +from __future__ import annotations + import abc import dataclasses +import functools import json -import types +import sys import typing import warnings -from typing import Any, Callable, Optional, Type, TypeVar - -# pylint: disable=bad-mcs-classmethod-argument, too-many-arguments, protected-access - - -class SerializableField(dataclasses.Field): - """extension of `dataclasses.Field` with additional serialization properties""" - - __slots__ = ( - # from dataclasses.Field.__slots__ - "name", - "type", - "default", - "default_factory", - "repr", - "hash", - "init", - "compare", - "metadata", - "kw_only", - "_field_type", # Private: not to be used by user code. - # new ones - "serialize", - "serialization_fn", - "loading_fn", - "assert_type", - ) - - def __init__( - self, - default: Any | dataclasses._MISSING_TYPE = dataclasses.MISSING, - default_factory: ( - Callable[[], Any] | dataclasses._MISSING_TYPE - ) = dataclasses.MISSING, - init: bool = True, - repr: bool = True, - hash: Optional[bool] = None, - compare: bool = True, - # TODO: add field for custom comparator (such as serializing) - metadata: types.MappingProxyType | None = None, - kw_only: bool | dataclasses._MISSING_TYPE = dataclasses.MISSING, - serialize: bool = True, - serialization_fn: Optional[Callable[[Any], Any]] = None, - loading_fn: Optional[Callable[[Any], Any]] = None, - assert_type: bool = True, - ): - # TODO: should we do this check, or assume the user knows what they are doing? - if init and not serialize: - raise ValueError("Cannot have init=True and serialize=False") - - # need to assemble kwargs in this hacky way so as not to upset type checking - super_kwargs: dict[str, Any] = dict( - default=default, - default_factory=default_factory, - init=init, - repr=repr, - hash=hash, - compare=compare, - kw_only=kw_only, - ) - - if metadata is not None: - super_kwargs["metadata"] = metadata - else: - super_kwargs["metadata"] = types.MappingProxyType({}) - - # actually init the super class - super().__init__(**super_kwargs) # type: ignore[call-arg] - - # now init the new fields - self.serialize: bool = serialize - self.serialization_fn: Optional[Callable[[Any], Any]] = serialization_fn - self.loading_fn: Optional[Callable[[Any], Any]] = loading_fn - self.assert_type: bool = assert_type - - @classmethod - def from_Field(cls, field: dataclasses.Field) -> "SerializableField": - """copy all values from a `dataclasses.Field` to new `SerializableField`""" - return cls( - default=field.default, - default_factory=field.default_factory, - init=field.init, - repr=field.repr, - hash=field.hash, - compare=field.compare, - metadata=field.metadata, - kw_only=field.kw_only, - serialize=field.repr, - serialization_fn=None, - loading_fn=None, - ) - - -# Step 2: Create a serializable_field function -# no type hint to avoid confusing mypy -def serializable_field(*args, **kwargs): # -> SerializableField: - """Create a new SerializableField - - note that if not using ZANJ, and you have a class inside a container, you MUST provide - `serialization_fn` and `loading_fn` to serialize and load the container. - ZANJ will automatically do this for you. - - ``` - default: Any | dataclasses._MISSING_TYPE = dataclasses.MISSING, - default_factory: Callable[[], Any] - | dataclasses._MISSING_TYPE = dataclasses.MISSING, - init: bool = True, - repr: bool = True, - hash: Optional[bool] = None, - compare: bool = True, - metadata: types.MappingProxyType | None = None, - kw_only: bool | dataclasses._MISSING_TYPE = dataclasses.MISSING, - serialize: bool = True, - serialization_fn: Optional[Callable[[Any], Any]] = None, - loading_fn: Optional[Callable[[Any], Any]] = None, - assert_type: bool = True, - ``` - """ - return SerializableField(*args, **kwargs) - - -# credit to https://stackoverflow.com/questions/51743827/how-to-compare-equality-of-dataclasses-holding-numpy-ndarray-boola-b-raises -def array_safe_eq(a: Any, b: Any) -> bool: - """check if two objects are equal, account for if numpy arrays or torch tensors""" - if a is b: - return True - - if ( - str(type(a)) == "" - and str(type(b)) == "" - ) or ( - str(type(a)) == "" - and str(type(b)) == "" - ): - return (a == b).all() - - if ( - str(type(a)) == "" - and str(type(b)) == "" - ): - return a.equals(b) - - if isinstance(a, typing.Sequence) and isinstance(b, typing.Sequence): - return len(a) == len(b) and all(array_safe_eq(a1, b1) for a1, b1 in zip(a, b)) - - if isinstance(a, (dict, typing.Mapping)) and isinstance(b, (dict, typing.Mapping)): - return len(a) == len(b) and all( - array_safe_eq(k1, k2) and array_safe_eq(a[k1], b[k2]) - for k1, k2 in zip(a.keys(), b.keys()) - ) - - try: - return bool(a == b) - except (TypeError, ValueError) as e: - warnings.warn(f"Cannot compare {a} and {b} for equality\n{e}") - return NotImplemented # type: ignore[return-value] - - -def dc_eq( - dc1, - dc2, - except_when_class_mismatch: bool = False, - false_when_class_mismatch: bool = True, - except_when_field_mismatch: bool = False, -) -> bool: - """checks if two dataclasses which (might) hold numpy arrays are equal - - # Parameters: - - `dc1`: the first dataclass - - `dc2`: the second dataclass - - `except_when_class_mismatch: bool` - if `True`, will throw `TypeError` if the classes are different. - if not, will return false by default or attempt to compare the fields if `false_when_class_mismatch` is `False` - (default: `False`) - - `false_when_class_mismatch: bool` - only relevant if `except_when_class_mismatch` is `False`. - if `True`, will return `False` if the classes are different. - if `False`, will attempt to compare the fields. - - `except_when_field_mismatch: bool` - only relevant if `except_when_class_mismatch` is `False` and `false_when_class_mismatch` is `False`. - if `True`, will throw `TypeError` if the fields are different. - (default: `True`) - - # Returns: - - `bool`: True if the dataclasses are equal, False otherwise - - # Raises: - - `TypeError`: if the dataclasses are of different classes - - `AttributeError`: if the dataclasses have different fields - - ``` - [START] - ▼ - ┌───────────┐ ┌─────────┐ - │dc1 is dc2?├─►│ classes │ - └──┬────────┘No│ match? │ - ──── │ ├─────────┤ - (True)◄──┘Yes │No │Yes - ──── ▼ ▼ - ┌────────────────┐ ┌────────────┐ - │ except when │ │ fields keys│ - │ class mismatch?│ │ match? │ - ├───────────┬────┘ ├───────┬────┘ - │Yes │No │No │Yes - ▼ ▼ ▼ ▼ - ─────────── ┌──────────┐ ┌────────┐ - { raise } │ except │ │ field │ - { TypeError } │ when │ │ values │ - ─────────── │ field │ │ match? │ - │ mismatch?│ ├────┬───┘ - ├───────┬──┘ │ │Yes - │Yes │No │No ▼ - ▼ ▼ │ ──── - ─────────────── ───── │ (True) - { raise } (False)◄┘ ──── - { AttributeError} ───── - ─────────────── - ``` +from typing import Any, Optional, Type, TypeVar - """ - if dc1 is dc2: - return True +from muutils.errormode import ErrorMode +from muutils.validate_type import validate_type +from muutils.json_serialize.serializable_field import ( + SerializableField, + serializable_field, +) +from muutils.json_serialize.util import array_safe_eq, dc_eq - if dc1.__class__ is not dc2.__class__: - if except_when_class_mismatch: - # if the classes don't match, raise an error - raise TypeError( - f"Cannot compare dataclasses of different classes: `{dc1.__class__}` and `{dc2.__class__}`" - ) - else: - dc1_fields: set = set([fld.name for fld in dataclasses.fields(dc1)]) - dc2_fields: set = set([fld.name for fld in dataclasses.fields(dc2)]) - fields_match: bool = set(dc1_fields) == set(dc2_fields) - - if not fields_match: - # if the fields match, keep going - if except_when_field_mismatch: - raise AttributeError( - f"dataclasses {dc1} and {dc2} have different fields: `{dc1_fields}` and `{dc2_fields}`" - ) - else: - return False +# pylint: disable=bad-mcs-classmethod-argument, too-many-arguments, protected-access - return all( - array_safe_eq(getattr(dc1, fld.name), getattr(dc2, fld.name)) - for fld in dataclasses.fields(dc1) - if fld.compare - ) +T = TypeVar("T") -T = TypeVar("T") +class CantGetTypeHintsWarning(UserWarning): + pass class ZanjMissingWarning(UserWarning): @@ -264,7 +33,7 @@ class ZanjMissingWarning(UserWarning): _zanj_loading_needs_import: bool = True -def zanj_register_loader_serializable_dataclass(cls: Type[T]): +def zanj_register_loader_serializable_dataclass(cls: typing.Type[T]): """Register a serializable dataclass with the ZANJ backport @@ -303,6 +72,151 @@ def zanj_register_loader_serializable_dataclass(cls: Type[T]): return lh +_DEFAULT_ON_TYPECHECK_MISMATCH: ErrorMode = ErrorMode.WARN +_DEFAULT_ON_TYPECHECK_ERROR: ErrorMode = ErrorMode.EXCEPT + + +class FieldIsNotInitOrSerializeWarning(UserWarning): + pass + + +def SerializableDataclass__validate_field_type( + self: SerializableDataclass, + field: SerializableField | str, + on_typecheck_error: ErrorMode = _DEFAULT_ON_TYPECHECK_ERROR, +) -> bool: + """given a dataclass, check the field matches the type hint + + # Parameters: + - `self : SerializableDataclass` + `SerializableDataclass` instance + - `field : SerializableField | str` + field to validate, will get from `self.__dataclass_fields__` if an `str` + - `on_typecheck_error : ErrorMode` + what to do if type checking throws an exception (except, warn, ignore). If `ignore` and an exception is thrown, the function will return `False` + (defaults to `_DEFAULT_ON_TYPECHECK_ERROR`) + + # Returns: + - `bool` + if the field type is correct. `False` if the field type is incorrect or an exception is thrown and `on_typecheck_error` is `ignore` + """ + on_typecheck_error = ErrorMode.from_any(on_typecheck_error) + + # get field + _field: SerializableField + if isinstance(field, str): + _field = self.__dataclass_fields__[field] # type: ignore[attr-defined] + else: + _field = field + + # do nothing case + if not _field.assert_type: + return True + + # if field is not `init` or not `serialize`, skip but warn + # TODO: how to handle fields which are not `init` or `serialize`? + if not _field.init or not _field.serialize: + warnings.warn( + f"Field '{_field.name}' on class {self.__class__} is not `init` or `serialize`, so will not be type checked", + FieldIsNotInitOrSerializeWarning, + ) + return True + + assert isinstance( + _field, SerializableField + ), f"Field '{_field.name = }' on class {self.__class__ = } is not a SerializableField, but a {type(_field) = }" + + # get field type hints + field_type_hint: Any = get_cls_type_hints(self.__class__).get(_field.name, None) + + # get the value + value: Any = getattr(self, _field.name) + + # validate the type + if field_type_hint is not None: + try: + type_is_valid: bool + # validate the type with the default type validator + if _field.custom_typecheck_fn is None: + type_is_valid = validate_type(value, field_type_hint) + # validate the type with a custom type validator + else: + type_is_valid = _field.custom_typecheck_fn(field_type_hint) + + return type_is_valid + + except Exception as e: + on_typecheck_error.process( + "exception while validating type: " + + f"{_field.name = }, {field_type_hint = }, {type(field_type_hint) = }, {value = }", + except_cls=ValueError, + except_from=e, + ) + return False + else: + on_typecheck_error.process( + ( + f"Cannot get type hints for {self.__class__.__name__}, field {_field.name = } and so cannot validate." + + f"Python version is {sys.version_info = }. You can:\n" + + f" - disable `assert_type`. Currently: {_field.assert_type = }\n" + + f" - use hints like `typing.Dict` instead of `dict` in type hints (this is required on python 3.8.x). You had {_field.type = }\n" + + " - use python 3.9.x or higher\n" + + " - coming in a future release, specify custom type validation functions\n" + ), + except_cls=ValueError, + ) + return False + + +def SerializableDataclass__validate_fields_types__dict( + self: SerializableDataclass, + on_typecheck_error: ErrorMode = _DEFAULT_ON_TYPECHECK_ERROR, +) -> dict[str, bool]: + """validate the types of all the fields on a SerializableDataclass. calls `SerializableDataclass__validate_field_type` for each field + + returns a dict of field names to bools, where the bool is if the field type is valid + """ + on_typecheck_error = ErrorMode.from_any(on_typecheck_error) + + # if except, bundle the exceptions + results: dict[str, bool] = dict() + exceptions: dict[str, Exception] = dict() + + # for each field in the class + cls_fields: typing.Sequence[SerializableField] = dataclasses.fields(self) # type: ignore[arg-type, assignment] + for field in cls_fields: + try: + results[field.name] = self.validate_field_type(field, on_typecheck_error) + except Exception as e: + results[field.name] = False + exceptions[field.name] = e + + # figure out what to do with the exceptions + if len(exceptions) > 0: + on_typecheck_error.process( + f"Exceptions while validating types of fields on {self.__class__.__name__}: {[x.name for x in cls_fields]}" + + "\n\t" + + "\n\t".join([f"{k}:\t{v}" for k, v in exceptions.items()]), + except_cls=ValueError, + # HACK: ExceptionGroup not supported in py < 3.11, so get a random exception from the dict + except_from=list(exceptions.values())[0], + ) + + return results + + +def SerializableDataclass__validate_fields_types( + self: SerializableDataclass, + on_typecheck_error: ErrorMode = _DEFAULT_ON_TYPECHECK_ERROR, +) -> bool: + """validate the types of all the fields on a SerializableDataclass. calls `SerializableDataclass__validate_field_type` for each field""" + return all( + SerializableDataclass__validate_fields_types__dict( + self, on_typecheck_error=on_typecheck_error + ).values() + ) + + class SerializableDataclass(abc.ABC): """Base class for serializable dataclasses @@ -310,11 +224,29 @@ class SerializableDataclass(abc.ABC): """ def serialize(self) -> dict[str, Any]: - raise NotImplementedError + raise NotImplementedError( + f"decorate {self.__class__ = } with `@serializable_dataclass`" + ) @classmethod def load(cls: Type[T], data: dict[str, Any] | T) -> T: - raise NotImplementedError + raise NotImplementedError(f"decorate {cls = } with `@serializable_dataclass`") + + def validate_fields_types( + self, on_typecheck_error: ErrorMode = _DEFAULT_ON_TYPECHECK_ERROR + ) -> bool: + return SerializableDataclass__validate_fields_types( + self, on_typecheck_error=on_typecheck_error + ) + + def validate_field_type( + self, + field: "SerializableField|str", + on_typecheck_error: ErrorMode = _DEFAULT_ON_TYPECHECK_ERROR, + ) -> bool: + return SerializableDataclass__validate_field_type( + self, field, on_typecheck_error=on_typecheck_error + ) def __eq__(self, other: Any) -> bool: return dc_eq(self, other) @@ -325,28 +257,60 @@ def __hash__(self) -> int: def diff( self, other: "SerializableDataclass", of_serialized: bool = False ) -> dict[str, Any]: + """get a rich and recursive diff between two instances of a serializable dataclass + + ```python + >>> Myclass(a=1, b=2).diff(Myclass(a=1, b=3)) + {'b': {'self': 2, 'other': 3}} + >>> NestedClass(x="q1", y=Myclass(a=1, b=2)).diff(NestedClass(x="q2", y=Myclass(a=1, b=3))) + {'x': {'self': 'q1', 'other': 'q2'}, 'y': {'b': {'self': 2, 'other': 3}}} + ``` + + # Parameters: + - `other : SerializableDataclass` + other instance to compare against + - `of_serialized : bool` + if true, compare serialized data and not raw values + (defaults to `False`) + + # Returns: + - `dict[str, Any]` + + + # Raises: + - `ValueError` : if the instances are not of the same type + - `ValueError` : if the instances are `dataclasses.dataclass` but not `SerializableDataclass` + """ + # match types if type(self) != type(other): raise ValueError( - f"Instances must be of the same type, but got {type(self)} and {type(other)}" + f"Instances must be of the same type, but got {type(self) = } and {type(other) = }" ) + # initialize the diff result diff_result: dict = {} + # if they are the same, return the empty diff if self == other: return diff_result + # if we are working with serialized data, serialize the instances if of_serialized: ser_self: dict = self.serialize() ser_other: dict = other.serialize() + # for each field in the class for field in dataclasses.fields(self): # type: ignore[arg-type] + # skip fields that are not for comparison if not field.compare: continue + # get values field_name: str = field.name self_value = getattr(self, field_name) other_value = getattr(other, field_name) + # if the values are both serializable dataclasses, recurse if isinstance(self_value, SerializableDataclass) and isinstance( other_value, SerializableDataclass ): @@ -355,19 +319,29 @@ def diff( ) if nested_diff: diff_result[field_name] = nested_diff + # only support serializable dataclasses elif dataclasses.is_dataclass(self_value) and dataclasses.is_dataclass( other_value ): raise ValueError("Non-serializable dataclass is not supported") else: + # get the values of either the serialized or the actual values self_value_s = ser_self[field_name] if of_serialized else self_value other_value_s = ser_other[field_name] if of_serialized else other_value + # compare the values if not array_safe_eq(self_value_s, other_value_s): diff_result[field_name] = {"self": self_value, "other": other_value} + # return the diff result return diff_result def update_from_nested_dict(self, nested_dict: dict[str, Any]): + """update the instance from a nested dict, useful for configuration from command line args + + # Parameters: + - `nested_dict : dict[str, Any]` + nested dict to update the instance with + """ for field in dataclasses.fields(self): # type: ignore[arg-type] field_name: str = field.name self_value = getattr(self, field_name) @@ -379,10 +353,41 @@ def update_from_nested_dict(self, nested_dict: dict[str, Any]): setattr(self, field_name, nested_dict[field_name]) def __copy__(self) -> "SerializableDataclass": - return self.__class__.load(self.serialize()) + "deep copy by serializing and loading the instance to json" + return self.__class__.load(json.loads(json.dumps(self.serialize()))) def __deepcopy__(self, memo: dict) -> "SerializableDataclass": - return self.__class__.load(self.serialize()) + "deep copy by serializing and loading the instance to json" + return self.__class__.load(json.loads(json.dumps(self.serialize()))) + + +# cache this so we don't have to keep getting it +@functools.lru_cache(typed=True) +def get_cls_type_hints(cls: Type[T]) -> dict[str, Any]: + "cached typing.get_type_hints for a class" + # get the type hints for the class + cls_type_hints: dict[str, Any] + try: + cls_type_hints = typing.get_type_hints(cls) + except TypeError as e: + if sys.version_info < (3, 9): + warnings.warn( + f"Cannot get type hints for {cls.__name__}. Python version is {sys.version_info = }. You can:\n" + + " - use hints like `typing.Dict` instead of `dict` in type hints (this is required on python 3.8.x)\n" + + " - use python 3.9.x or higher\n" + + " - add explicit loading functions to the fields\n" + + f" {dataclasses.fields(cls) = }", # type: ignore[arg-type] + CantGetTypeHintsWarning, + ) + cls_type_hints = dict() + else: + raise TypeError( + f"Cannot get type hints for {cls.__name__}. Python version is {sys.version_info = }\n" + + f" {dataclasses.fields(cls) = }\n" # type: ignore[arg-type] + + f" {e = }" + ) from e + + return cls_type_hints # Step 3: Create a custom serializable_dataclass decorator @@ -391,16 +396,79 @@ def serializable_dataclass( _cls=None, # type: ignore *, init: bool = True, - repr: bool = True, + repr: bool = True, # this overrides the actual `repr` builtin, but we have to match the interface of `dataclasses.dataclass` eq: bool = True, order: bool = False, unsafe_hash: bool = False, frozen: bool = False, properties_to_serialize: Optional[list[str]] = None, register_handler: bool = True, + on_typecheck_error: ErrorMode = _DEFAULT_ON_TYPECHECK_ERROR, + on_typecheck_mismatch: ErrorMode = _DEFAULT_ON_TYPECHECK_MISMATCH, **kwargs, ): + """decorator to make a dataclass serializable. must also make it inherit from `SerializableDataclass` + + types will be validated (like pydantic) unless `on_typecheck_mismatch` is set to `ErrorMode.IGNORE` + + behavior of most kwargs matches that of `dataclasses.dataclass`, but with some additional kwargs + + Returns the same class as was passed in, with dunder methods added based on the fields defined in the class. + + Examines PEP 526 __annotations__ to determine fields. + + If init is true, an __init__() method is added to the class. If repr is true, a __repr__() method is added. If order is true, rich comparison dunder methods are added. If unsafe_hash is true, a __hash__() method function is added. If frozen is true, fields may not be assigned to after instance creation. + + ```python + @serializable_dataclass(kw_only=True) + class Myclass(SerializableDataclass): + a: int + b: str + ``` + ```python + >>> Myclass(a=1, b="q").serialize() + {'__format__': 'Myclass(SerializableDataclass)', 'a': 1, 'b': 'q'} + ``` + + # Parameters: + - `_cls : _type_` + class to decorate. don't pass this arg, just use this as a decorator + (defaults to `None`) + - `init : bool` + (defaults to `True`) + - `repr : bool` + (defaults to `True`) + - `order : bool` + (defaults to `False`) + - `unsafe_hash : bool` + (defaults to `False`) + - `frozen : bool` + (defaults to `False`) + - `properties_to_serialize : Optional[list[str]]` + **SerializableDataclass only:** which properties to add to the serialized data dict + (defaults to `None`) + - `register_handler : bool` + **SerializableDataclass only:** if true, register the class with ZANJ for loading + (defaults to `True`) + - `on_typecheck_error : ErrorMode` + **SerializableDataclass only:** what to do if type checking throws an exception (except, warn, ignore). If `ignore` and an exception is thrown, type validation will still return false + - `on_typecheck_mismatch : ErrorMode` + **SerializableDataclass only:** what to do if a type mismatch is found (except, warn, ignore). If `ignore`, type validation will return `True` + + # Returns: + - `_type_` + _description_ + + # Raises: + - `ValueError` : _description_ + - `ValueError` : _description_ + - `ValueError` : _description_ + - `AttributeError` : _description_ + - `ValueError` : _description_ + """ # -> Union[Callable[[Type[T]], Type[T]], Type[T]]: + on_typecheck_error = ErrorMode.from_any(on_typecheck_error) + on_typecheck_mismatch = ErrorMode.from_any(on_typecheck_mismatch) if properties_to_serialize is None: _properties_to_serialize: list = list() @@ -420,6 +488,15 @@ def wrap(cls: Type[T]) -> Type[T]: field_value = serializable_field() setattr(cls, field_name, field_value) + # special check, kw_only is not supported in python <3.9 and `dataclasses.MISSING` is truthy + if sys.version_info < (3, 10): + if "kw_only" in kwargs: + if kwargs["kw_only"] == True: # noqa: E712 + raise ValueError("kw_only is not supported in python >=3.9") + else: + del kwargs["kw_only"] + + # call `dataclasses.dataclass` to set some stuff up cls = dataclasses.dataclass( # type: ignore[call-overload] cls, init=init, @@ -431,14 +508,20 @@ def wrap(cls: Type[T]) -> Type[T]: **kwargs, ) + # copy these to the class cls._properties_to_serialize = _properties_to_serialize.copy() # type: ignore[attr-defined] + # ====================================================================== + # define `serialize` func + # done locally since it depends on args to the decorator + # ====================================================================== def serialize(self) -> dict[str, Any]: result: dict[str, Any] = { "__format__": f"{self.__class__.__name__}(SerializableDataclass)" } - - for field in dataclasses.fields(self): + # for each field in the class + for field in dataclasses.fields(self): # type: ignore[arg-type] + # need it to be our special SerializableField if not isinstance(field, SerializableField): raise ValueError( f"Field '{field.name}' on class {self.__class__.__module__}.{self.__class__.__name__} is not a SerializableField, " @@ -446,15 +529,23 @@ def serialize(self) -> dict[str, Any]: "this state should be inaccessible, please report this bug!" ) + # try to save it if field.serialize: try: + # get the val value = getattr(self, field.name) + # if it is a serializable dataclass, serialize it if isinstance(value, SerializableDataclass): value = value.serialize() + # if the value has a serialization function, use that if hasattr(value, "serialize") and callable(value.serialize): value = value.serialize() + # if the field has a serialization function, use that + # it would be nice to be able to override a class's `.serialize()`, but that could lead to some inconsistencies! elif field.serialization_fn: value = field.serialization_fn(value) + + # store the value in the result result[field.name] = value except Exception as e: raise ValueError( @@ -468,17 +559,28 @@ def serialize(self) -> dict[str, Any]: ) ) from e + # store each property if we can get it for prop in self._properties_to_serialize: if hasattr(cls, prop): value = getattr(self, prop) result[prop] = value + else: + raise AttributeError( + f"Cannot serialize property '{prop}' on class {self.__class__.__module__}.{self.__class__.__name__}" + + f"but it is in {self._properties_to_serialize = }" + + f"\n{self = }" + ) return result + # ====================================================================== + # define `load` func + # done locally since it depends on args to the decorator + # ====================================================================== # mypy thinks this isnt a classmethod @classmethod # type: ignore[misc] def load(cls, data: dict[str, Any] | T) -> Type[T]: - # TODO: this is kind of ugly, but it fixes a lot of issues for when we do recursive loading with ZANJ + # HACK: this is kind of ugly, but it fixes a lot of issues for when we do recursive loading with ZANJ if isinstance(data, cls): return data @@ -486,44 +588,71 @@ def load(cls, data: dict[str, Any] | T) -> Type[T]: data, typing.Mapping ), f"When loading {cls.__name__ = } expected a Mapping, but got {type(data) = }:\n{data = }" - cls_type_hints: dict[str, Any] = typing.get_type_hints(cls) + cls_type_hints: dict[str, Any] = get_cls_type_hints(cls) + + # initialize dict for keeping what we will pass to the constructor ctor_kwargs: dict[str, Any] = dict() + + # iterate over the fields of the class for field in dataclasses.fields(cls): + # check if the field is a SerializableField assert isinstance( field, SerializableField - ), f"Field '{field.name}' on class {cls.__name__} is not a SerializableField, but a {type(field)} this state should be inaccessible, please report this bug!" + ), f"Field '{field.name}' on class {cls.__name__} is not a SerializableField, but a {type(field)}. this state should be inaccessible, please report this bug!\nhttps://github.com/mivanit/muutils/issues/new" + # check if the field is in the data and if it should be initialized if (field.name in data) and field.init: - value = data[field.name] + # get the value, we will be processing it + value: Any = data[field.name] + # get the type hint for the field field_type_hint: Any = cls_type_hints.get(field.name, None) - if field.loading_fn: + + # we rely on the init of `SerializableField` to check that only one of `loading_fn` and `deserialize_fn` is set + if field.deserialize_fn: + # if it has a deserialization function, use that + value = field.deserialize_fn(value) + elif field.loading_fn: + # if it has a loading function, use that value = field.loading_fn(data) elif ( field_type_hint is not None and hasattr(field_type_hint, "load") and callable(field_type_hint.load) ): + # if no loading function but has a type hint with a load method, use that if isinstance(value, dict): value = field_type_hint.load(value) else: raise ValueError( f"Cannot load value into {field_type_hint}, expected {type(value) = } to be a dict\n{value = }" ) + else: + # assume no loading needs to happen, keep `value` as-is + pass - if field.assert_type: - if field.name in ctor_kwargs: - assert isinstance(ctor_kwargs[field.name], field_type_hint) - + # store the value in the constructor kwargs ctor_kwargs[field.name] = value - return cls(**ctor_kwargs) + + # create a new instance of the class with the constructor kwargs + output: cls = cls(**ctor_kwargs) + + # validate the types of the fields if needed + if on_typecheck_mismatch != ErrorMode.IGNORE: + output.validate_fields_types(on_typecheck_error=on_typecheck_error) + + # return the new instance + return output # mypy says "Type cannot be declared in assignment to non-self attribute" so thats why I've left the hints in the comments # type is `Callable[[T], dict]` cls.serialize = serialize # type: ignore[attr-defined] # type is `Callable[[dict], T]` cls.load = load # type: ignore[attr-defined] + # type is `Callable[[T, ErrorMode], bool]` + cls.validate_fields_types = SerializableDataclass__validate_fields_types # type: ignore[attr-defined] + # type is `Callable[[T, T], bool]` cls.__eq__ = lambda self, other: dc_eq(self, other) # type: ignore[assignment] # Register the class with ZANJ diff --git a/muutils/json_serialize/serializable_field.py b/muutils/json_serialize/serializable_field.py new file mode 100644 index 00000000..814188ef --- /dev/null +++ b/muutils/json_serialize/serializable_field.py @@ -0,0 +1,179 @@ +from __future__ import annotations + +import dataclasses +import sys +import types +from typing import Any, Callable, Optional, Union + + +# pylint: disable=bad-mcs-classmethod-argument, too-many-arguments, protected-access + + +class SerializableField(dataclasses.Field): + """extension of `dataclasses.Field` with additional serialization properties""" + + __slots__ = ( + # from dataclasses.Field.__slots__ + "name", + "type", + "default", + "default_factory", + "repr", + "hash", + "init", + "compare", + "metadata", + "kw_only", + "_field_type", # Private: not to be used by user code. + # new ones + "serialize", + "serialization_fn", + "loading_fn", + "deserialize_fn", # new alternative to loading_fn + "assert_type", + "custom_typecheck_fn", + ) + + def __init__( + self, + default: Union[Any, dataclasses._MISSING_TYPE] = dataclasses.MISSING, + default_factory: Union[ + Callable[[], Any], dataclasses._MISSING_TYPE + ] = dataclasses.MISSING, + init: bool = True, + repr: bool = True, + hash: Optional[bool] = None, + compare: bool = True, + # TODO: add field for custom comparator (such as serializing) + metadata: Optional[types.MappingProxyType] = None, + kw_only: Union[bool, dataclasses._MISSING_TYPE] = dataclasses.MISSING, + serialize: bool = True, + serialization_fn: Optional[Callable[[Any], Any]] = None, + loading_fn: Optional[Callable[[Any], Any]] = None, + deserialize_fn: Optional[Callable[[Any], Any]] = None, + assert_type: bool = True, + custom_typecheck_fn: Optional[Callable[[type], bool]] = None, + ): + # TODO: should we do this check, or assume the user knows what they are doing? + if init and not serialize: + raise ValueError("Cannot have init=True and serialize=False") + + # need to assemble kwargs in this hacky way so as not to upset type checking + super_kwargs: dict[str, Any] = dict( + default=default, + default_factory=default_factory, + init=init, + repr=repr, + hash=hash, + compare=compare, + kw_only=kw_only, + ) + + if metadata is not None: + super_kwargs["metadata"] = metadata + else: + super_kwargs["metadata"] = types.MappingProxyType({}) + + # special check, kw_only is not supported in python <3.9 and `dataclasses.MISSING` is truthy + if sys.version_info < (3, 10): + if super_kwargs["kw_only"] == True: # noqa: E712 + raise ValueError("kw_only is not supported in python >=3.9") + else: + del super_kwargs["kw_only"] + + # actually init the super class + super().__init__(**super_kwargs) # type: ignore[call-arg] + + # now init the new fields + self.serialize: bool = serialize + self.serialization_fn: Optional[Callable[[Any], Any]] = serialization_fn + + if loading_fn is not None and deserialize_fn is not None: + raise ValueError( + "Cannot pass both loading_fn and deserialize_fn, pass only one. ", + "`loading_fn` is the older interface and takes the dict of the class, ", + "`deserialize_fn` is the new interface and takes only the field's value.", + ) + self.loading_fn: Optional[Callable[[Any], Any]] = loading_fn + self.deserialize_fn: Optional[Callable[[Any], Any]] = deserialize_fn + + self.assert_type: bool = assert_type + self.custom_typecheck_fn: Optional[Callable[[type], bool]] = custom_typecheck_fn + + @classmethod + def from_Field(cls, field: dataclasses.Field) -> "SerializableField": + """copy all values from a `dataclasses.Field` to new `SerializableField`""" + return cls( + default=field.default, + default_factory=field.default_factory, + init=field.init, + repr=field.repr, + hash=field.hash, + compare=field.compare, + metadata=field.metadata, + kw_only=getattr(field, "kw_only", dataclasses.MISSING), # for python <3.9 + serialize=field.repr, # serialize if it's going to be repr'd + serialization_fn=None, + loading_fn=None, + deserialize_fn=None, + ) + + +# no type hint to avoid confusing mypy +def serializable_field(*args, **kwargs): # -> SerializableField: + """Create a new `SerializableField`. type hinting this func confuses mypy, so scroll down + + ``` + default: Any | dataclasses._MISSING_TYPE = dataclasses.MISSING, + default_factory: Callable[[], Any] + | dataclasses._MISSING_TYPE = dataclasses.MISSING, + init: bool = True, + repr: bool = True, + hash: Optional[bool] = None, + compare: bool = True, + metadata: types.MappingProxyType | None = None, + kw_only: bool | dataclasses._MISSING_TYPE = dataclasses.MISSING, + # ---------------------------------------------------------------------- + # new in `SerializableField`, not in `dataclasses.Field` + serialize: bool = True, + serialization_fn: Optional[Callable[[Any], Any]] = None, + loading_fn: Optional[Callable[[Any], Any]] = None, + deserialize_fn: Optional[Callable[[Any], Any]] = None, + assert_type: bool = True, + custom_typecheck_fn: Optional[Callable[[type], bool]] = None, + ``` + + # new Parameters: + - `serialize`: whether to serialize this field when serializing the class' + - `serialization_fn`: function taking the instance of the field and returning a serializable object. If not provided, will iterate through the `SerializerHandler`s defined in `muutils.json_serialize.json_serialize` + - `loading_fn`: function taking the serialized object and returning the instance of the field. If not provided, will take object as-is. + - `deserialize_fn`: new alternative to `loading_fn`. takes only the field's value, not the whole class. if both `loading_fn` and `deserialize_fn` are provided, an error will be raised. + + # Gotchas: + - `loading_fn` takes the dict of the **class**, not the field. if you wanted a `loading_fn` that does nothing, you'd write: + + ```python + class MyClass: + my_field: int = serializable_field( + serialization_fn=lambda x: str(x), + loading_fn=lambda x["my_field"]: int(x) + ) + ``` + + using `deserialize_fn` instead: + + ```python + class MyClass: + my_field: int = serializable_field( + serialization_fn=lambda x: str(x), + deserialize_fn=lambda x: int(x) + ) + ``` + + In the above code, `my_field` is an int but will be serialized as a string. + + note that if not using ZANJ, and you have a class inside a container, you MUST provide + `serialization_fn` and `loading_fn` to serialize and load the container. + ZANJ will automatically do this for you. + """ + return SerializableField(*args, **kwargs) diff --git a/muutils/json_serialize/util.py b/muutils/json_serialize/util.py index 81c7b06a..306fd6d6 100644 --- a/muutils/json_serialize/util.py +++ b/muutils/json_serialize/util.py @@ -1,6 +1,9 @@ +from __future__ import annotations + +import dataclasses import functools import inspect -import types +import sys import typing import warnings from typing import Any, Callable, Iterable, Literal, Union @@ -13,14 +16,14 @@ _NUMPY_WORKING = False ErrorMode = Literal["ignore", "warn", "except"] -TypeErrorMode = Union[ErrorMode, Literal["try_convert"]] -JSONitem = Union[bool, int, float, str, list, dict[str, Any], None] -JSONdict = dict[str, JSONitem] +JSONitem = Union[bool, int, float, str, list, typing.Dict[str, Any], None] +JSONdict = typing.Dict[str, JSONitem] Hashableitem = Union[bool, int, float, str, tuple] -if typing.TYPE_CHECKING: +# or if python version <3.9 +if typing.TYPE_CHECKING or sys.version_info < (3, 9): MonoTuple = typing.Sequence else: @@ -38,18 +41,18 @@ def __init_subclass__(cls, *args, **kwargs): # idk why mypy thinks there is no such function in typing @typing._tp_cache # type: ignore def __class_getitem__(cls, params): - if isinstance(params, (type, types.UnionType)): - return types.GenericAlias(tuple, (params, Ellipsis)) + if getattr(params, "__origin__", None) == typing.Union: + return typing.GenericAlias(tuple, (params, Ellipsis)) + elif isinstance(params, type): + typing.GenericAlias(tuple, (params, Ellipsis)) # test if has len and is iterable elif isinstance(params, Iterable): if len(params) == 0: return tuple elif len(params) == 1: - return types.GenericAlias(tuple, (params[0], Ellipsis)) + return typing.GenericAlias(tuple, (params[0], Ellipsis)) else: - raise TypeError( - f"MonoTuple expects 1 type argument, got {len(params) = } \n\t{params = }" - ) + raise TypeError(f"MonoTuple expects 1 type argument, got {params = }") class UniversalContainer: @@ -59,19 +62,19 @@ def __contains__(self, x: Any) -> bool: return True -def isinstance_namedtuple(x): +def isinstance_namedtuple(x: Any) -> bool: """checks if `x` is a `namedtuple` credit to https://stackoverflow.com/questions/2166818/how-to-check-if-an-object-is-an-instance-of-a-namedtuple """ - t = type(x) - b = t.__bases__ + t: type = type(x) + b: tuple = t.__bases__ if len(b) != 1 or b[0] != tuple: return False - f = getattr(t, "_fields", None) + f: Any = getattr(t, "_fields", None) if not isinstance(f, tuple): return False - return all(type(n) == str for n in f) + return all(isinstance(n, str) for n in f) def try_catch(func: Callable): @@ -124,3 +127,132 @@ def safe_getsource(func) -> list[str]: return string_as_lines(inspect.getsource(func)) except Exception as e: return string_as_lines(f"Error: Unable to retrieve source code:\n{e}") + + +# credit to https://stackoverflow.com/questions/51743827/how-to-compare-equality-of-dataclasses-holding-numpy-ndarray-boola-b-raises +def array_safe_eq(a: Any, b: Any) -> bool: + """check if two objects are equal, account for if numpy arrays or torch tensors""" + if a is b: + return True + + if ( + str(type(a)) == "" + and str(type(b)) == "" + ) or ( + str(type(a)) == "" + and str(type(b)) == "" + ): + return (a == b).all() + + if ( + str(type(a)) == "" + and str(type(b)) == "" + ): + return a.equals(b) + + if isinstance(a, typing.Sequence) and isinstance(b, typing.Sequence): + return len(a) == len(b) and all(array_safe_eq(a1, b1) for a1, b1 in zip(a, b)) + + if isinstance(a, (dict, typing.Mapping)) and isinstance(b, (dict, typing.Mapping)): + return len(a) == len(b) and all( + array_safe_eq(k1, k2) and array_safe_eq(a[k1], b[k2]) + for k1, k2 in zip(a.keys(), b.keys()) + ) + + try: + return bool(a == b) + except (TypeError, ValueError) as e: + warnings.warn(f"Cannot compare {a} and {b} for equality\n{e}") + return NotImplemented # type: ignore[return-value] + + +def dc_eq( + dc1, + dc2, + except_when_class_mismatch: bool = False, + false_when_class_mismatch: bool = True, + except_when_field_mismatch: bool = False, +) -> bool: + """checks if two dataclasses which (might) hold numpy arrays are equal + + # Parameters: + - `dc1`: the first dataclass + - `dc2`: the second dataclass + - `except_when_class_mismatch: bool` + if `True`, will throw `TypeError` if the classes are different. + if not, will return false by default or attempt to compare the fields if `false_when_class_mismatch` is `False` + (default: `False`) + - `false_when_class_mismatch: bool` + only relevant if `except_when_class_mismatch` is `False`. + if `True`, will return `False` if the classes are different. + if `False`, will attempt to compare the fields. + - `except_when_field_mismatch: bool` + only relevant if `except_when_class_mismatch` is `False` and `false_when_class_mismatch` is `False`. + if `True`, will throw `TypeError` if the fields are different. + (default: `True`) + + # Returns: + - `bool`: True if the dataclasses are equal, False otherwise + + # Raises: + - `TypeError`: if the dataclasses are of different classes + - `AttributeError`: if the dataclasses have different fields + + ``` + [START] + ▼ + ┌───────────┐ ┌─────────┐ + │dc1 is dc2?├─►│ classes │ + └──┬────────┘No│ match? │ + ──── │ ├─────────┤ + (True)◄──┘Yes │No │Yes + ──── ▼ ▼ + ┌────────────────┐ ┌────────────┐ + │ except when │ │ fields keys│ + │ class mismatch?│ │ match? │ + ├───────────┬────┘ ├───────┬────┘ + │Yes │No │No │Yes + ▼ ▼ ▼ ▼ + ─────────── ┌──────────┐ ┌────────┐ + { raise } │ except │ │ field │ + { TypeError } │ when │ │ values │ + ─────────── │ field │ │ match? │ + │ mismatch?│ ├────┬───┘ + ├───────┬──┘ │ │Yes + │Yes │No │No ▼ + ▼ ▼ │ ──── + ─────────────── ───── │ (True) + { raise } (False)◄┘ ──── + { AttributeError} ───── + ─────────────── + ``` + + """ + if dc1 is dc2: + return True + + if dc1.__class__ is not dc2.__class__: + if except_when_class_mismatch: + # if the classes don't match, raise an error + raise TypeError( + f"Cannot compare dataclasses of different classes: `{dc1.__class__}` and `{dc2.__class__}`" + ) + else: + dc1_fields: set = set([fld.name for fld in dataclasses.fields(dc1)]) + dc2_fields: set = set([fld.name for fld in dataclasses.fields(dc2)]) + fields_match: bool = set(dc1_fields) == set(dc2_fields) + + if not fields_match: + # if the fields match, keep going + if except_when_field_mismatch: + raise AttributeError( + f"dataclasses {dc1} and {dc2} have different fields: `{dc1_fields}` and `{dc2_fields}`" + ) + else: + return False + + return all( + array_safe_eq(getattr(dc1, fld.name), getattr(dc2, fld.name)) + for fld in dataclasses.fields(dc1) + if fld.compare + ) diff --git a/muutils/jsonlines.py b/muutils/jsonlines.py index 4cae67c9..2d9fca68 100644 --- a/muutils/jsonlines.py +++ b/muutils/jsonlines.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import gzip import json from typing import Callable, Sequence diff --git a/muutils/kappa.py b/muutils/kappa.py index b53b420e..819f1ad6 100644 --- a/muutils/kappa.py +++ b/muutils/kappa.py @@ -1,10 +1,12 @@ """anonymous getitem class - + util for constructing a class which has a getitem method which just calls a function a `lambda` is an anonymous function: kappa is the letter before lambda in the greek alphabet, hence the name of this class""" +from __future__ import annotations + from typing import Callable, Mapping, TypeVar _kappa_K = TypeVar("_kappa_K") diff --git a/muutils/logger/headerfuncs.py b/muutils/logger/headerfuncs.py index 11b97c1a..8f327268 100644 --- a/muutils/logger/headerfuncs.py +++ b/muutils/logger/headerfuncs.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json from typing import Any, Mapping, Protocol diff --git a/muutils/logger/logger.py b/muutils/logger/logger.py index 03280db0..b4165f95 100644 --- a/muutils/logger/logger.py +++ b/muutils/logger/logger.py @@ -2,11 +2,13 @@ - `SimpleLogger` is an extremely simple logger that can write to both console and a file - `Logger` class handles levels in a slightly different way than default python `logging`, - and also has "streams" which allow for different sorts of output in the same logger - this was mostly made with training models in mind and storing both metadata and loss + and also has "streams" which allow for different sorts of output in the same logger + this was mostly made with training models in mind and storing both metadata and loss - `TimerContext` is a context manager that can be used to time the duration of a block of code """ +from __future__ import annotations + import json import time import typing @@ -193,7 +195,7 @@ def log( # type: ignore # yes, the signatures are different here. else: lvl = self._default_level - assert not lvl is None, "lvl should not be None at this point" + assert lvl is not None, "lvl should not be None at this point" # print to console with formatting # ======================================== diff --git a/muutils/logger/loggingstream.py b/muutils/logger/loggingstream.py index 16dd742f..77cad982 100644 --- a/muutils/logger/loggingstream.py +++ b/muutils/logger/loggingstream.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import time from dataclasses import dataclass, field from typing import Any, Callable diff --git a/muutils/logger/simplelogger.py b/muutils/logger/simplelogger.py index bce0046b..07f1a306 100644 --- a/muutils/logger/simplelogger.py +++ b/muutils/logger/simplelogger.py @@ -1,8 +1,10 @@ +from __future__ import annotations + import json import sys import time import typing -from typing import TextIO +from typing import TextIO, Union from muutils.json_serialize import JSONitem, json_serialize @@ -26,7 +28,7 @@ def close(self) -> None: pass -AnyIO = TextIO | NullIO +AnyIO = Union[TextIO, NullIO] class SimpleLogger: diff --git a/muutils/logger/timing.py b/muutils/logger/timing.py index ba8e43bd..1b7b2e99 100644 --- a/muutils/logger/timing.py +++ b/muutils/logger/timing.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import time from typing import Literal @@ -79,6 +81,7 @@ def get_progress_default(self, i: int) -> str: timing_raw: dict[str, float] = self.get_timing_raw(i) percent_str: str = str(int(timing_raw["percent"] * 100)).ljust(2) - iters_str: str = f"{str(i).ljust(self.total_str_len)}/{self.n_total}" - timing_str: str + # TODO: get_progress_default + # iters_str: str = f"{str(i).ljust(self.total_str_len)}/{self.n_total}" + # timing_str: str return f"{percent_str}% {self.get_pbar(i)}" diff --git a/muutils/misc.py b/muutils/misc.py index cc2ea074..7382bdba 100644 --- a/muutils/misc.py +++ b/muutils/misc.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import hashlib import typing @@ -61,25 +63,78 @@ def list_join(lst: list, factory: typing.Callable) -> list: return output -# filename stuff +# name stuff # ================================================================================ -def sanitize_fname(fname: str | None) -> str: - """sanitize a filename for use in a path""" - if fname is None: - return "_None_" +def sanitize_name( + name: str | None, + additional_allowed_chars: str = "", + replace_invalid: str = "", + when_none: str | None = "_None_", + leading_digit_prefix: str = "", +) -> str: + """sanitize a string, leaving only alphanumerics and `additional_allowed_chars` + + # Parameters: + - `name : str | None` + input string + - `additional_allowed_chars : str` + additional characters to allow, none by default + (defaults to `""`) + - `replace_invalid : str` + character to replace invalid characters with + (defaults to `""`) + - `when_none : str | None` + string to return if `name` is `None`. if `None`, raises an exception + (defaults to `"_None_"`) + - `leading_digit_prefix : str` + character to prefix the string with if it starts with a digit + (defaults to `""`) + + # Returns: + - `str` + sanitized string + """ + + if name is None: + if when_none is None: + raise ValueError("name is None") + else: + return when_none - fname_sanitized: str = "" - for char in fname: + sanitized: str = "" + for char in name: if char.isalnum(): - fname_sanitized += char - elif char in ("-", "_", "."): - fname_sanitized += char + sanitized += char + elif char in additional_allowed_chars: + sanitized += char else: - fname_sanitized += "" + sanitized += replace_invalid + + if sanitized[0].isdigit(): + sanitized = leading_digit_prefix + sanitized + + return sanitized + + +def sanitize_fname(fname: str | None, **kwargs) -> str: + """sanitize a filename to posix standards - return fname_sanitized + - leave only alphanumerics, `_` (underscore), '-' (dash) and `.` (period) + """ + return sanitize_name(fname, additional_allowed_chars="._-", **kwargs) + + +def sanitize_identifier(fname: str | None, **kwargs) -> str: + """sanitize an identifier (variable or function name) + + - leave only alphanumerics and `_` (underscore) + - prefix with `_` if it starts with a digit + """ + return sanitize_name( + fname, additional_allowed_chars="_", leading_digit_prefix="_", **kwargs + ) def dict_to_filename( @@ -210,28 +265,28 @@ def str_to_numeric( # detect if it has a suffix suffixes_detected: list[bool] = [suffix in quantity for suffix in _mapping] - match sum(suffixes_detected): - case 0: - # no suffix - pass - case 1: - # find multiplier - for suffix, mult in _mapping.items(): - if quantity.endswith(suffix): - # remove suffix, store multiplier, and break - quantity = quantity.removesuffix(suffix).strip() - multiplier = mult - break - else: - raise ValueError(f"Invalid suffix in {quantity_original}") - case _: - # multiple suffixes - raise ValueError(f"Multiple suffixes detected in {quantity_original}") + n_suffixes_detected: int = sum(suffixes_detected) + if n_suffixes_detected == 0: + # no suffix + pass + elif n_suffixes_detected == 1: + # find multiplier + for suffix, mult in _mapping.items(): + if quantity.endswith(suffix): + # remove suffix, store multiplier, and break + quantity = quantity[: -len(suffix)].strip() + multiplier = mult + break + else: + raise ValueError(f"Invalid suffix in {quantity_original}") + else: + # multiple suffixes + raise ValueError(f"Multiple suffixes detected in {quantity_original}") # fractions if "/" in quantity: try: - assert quantity.count("/") == 1, f"too many '/'" + assert quantity.count("/") == 1, "too many '/'" # split and strip num, den = quantity.split("/") num = num.strip() @@ -244,7 +299,7 @@ def str_to_numeric( # assert that both are digits assert ( num.isdigit() and den.isdigit() - ), f"numerator and denominator must be digits" + ), "numerator and denominator must be digits" # return the fraction result = num_sign * ( int(num) / int(den) @@ -254,7 +309,6 @@ def str_to_numeric( # decimals else: - try: result = int(quantity) except ValueError: diff --git a/muutils/mlutils.py b/muutils/mlutils.py index 6f21a85c..b08592a9 100644 --- a/muutils/mlutils.py +++ b/muutils/mlutils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json import os import random @@ -5,7 +7,7 @@ import warnings from itertools import islice from pathlib import Path -from typing import Any, Callable, TypeVar +from typing import Any, Callable, Optional, TypeVar, Union ARRAY_IMPORTS: bool try: @@ -23,7 +25,7 @@ GLOBAL_SEED: int = DEFAULT_SEED -def get_device(device: "str|torch.device|None" = None) -> "torch.device": +def get_device(device: "Union[str,torch.device,None]" = None) -> "torch.device": """Get the torch.device instance on which `torch.Tensor`s should be allocated.""" if not ARRAY_IMPORTS: raise ImportError( @@ -113,9 +115,7 @@ def get_checkpoint_paths_for_run( - a wildcard for the iteration number """ - assert ( - run_path.is_dir() - ), f"Model path {run_path} is not a directory (expect run directory, not model files)" + assert run_path.is_dir(), f"Model path {run_path} is not a directory (expect run directory, not model files)" return [ (int(checkpoint_path.stem.split("_")[-1].split(".")[0]), checkpoint_path) @@ -130,13 +130,23 @@ def get_checkpoint_paths_for_run( def register_method( method_dict: dict[str, Callable[..., Any]], - custom_name: str | None = None, + custom_name: Optional[str] = None, ) -> Callable[[F], F]: """Decorator to add a method to the method_dict""" def decorator(method: F) -> F: + method_name: str if custom_name is None: - method_name: str = method.__name__ + method_name_orig: str | None = getattr(method, "__name__", None) + if method_name_orig is None: + warnings.warn( + f"Method {method} does not have a name, using sanitized repr" + ) + from muutils.misc import sanitize_identifier + + method_name = sanitize_identifier(repr(method)) + else: + method_name = method_name_orig else: method_name = custom_name method.__name__ = custom_name diff --git a/muutils/nbutils/configure_notebook.py b/muutils/nbutils/configure_notebook.py index ab77e37d..62352f50 100644 --- a/muutils/nbutils/configure_notebook.py +++ b/muutils/nbutils/configure_notebook.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import typing import warnings @@ -31,7 +33,7 @@ class PlotlyNotInstalledWarning(UserWarning): IN_JUPYTER = False # muutils imports -from muutils.mlutils import get_device, set_reproducibility +from muutils.mlutils import get_device, set_reproducibility # noqa: E402 # handling figures PlottingMode = typing.Literal["ignore", "inline", "widget", "save"] @@ -88,7 +90,15 @@ def setup_plots( close_after_plotshow: bool = False, ) -> None: """Set up plot saving/rendering options""" - global PLOT_MODE, CONVERSION_PLOTMODE_OVERRIDE, FIG_COUNTER, FIG_OUTPUT_FMT, FIG_NUMBERED_FNAME, FIG_CONFIG, FIG_BASEPATH, CLOSE_AFTER_PLOTSHOW + global \ + PLOT_MODE, \ + CONVERSION_PLOTMODE_OVERRIDE, \ + FIG_COUNTER, \ + FIG_OUTPUT_FMT, \ + FIG_NUMBERED_FNAME, \ + FIG_CONFIG, \ + FIG_BASEPATH, \ + CLOSE_AFTER_PLOTSHOW # set plot mode, handling override if CONVERSION_PLOTMODE_OVERRIDE is not None: @@ -134,7 +144,7 @@ def setup_plots( plt.rcParams["savefig.format"] = FIG_OUTPUT_FMT if FIG_OUTPUT_FMT in TIKZPLOTLIB_FORMATS: try: - import tikzplotlib # type: ignore[import] + import tikzplotlib # type: ignore[import] # noqa: F401 except ImportError: warnings.warn( f"Tikzplotlib not installed. Cannot save figures in Tikz format '{FIG_OUTPUT_FMT}', things might break." @@ -188,7 +198,7 @@ def configure_notebook( fig_config: dict | None = None, fig_basepath: str | None = None, close_after_plotshow: bool = False, -) -> "torch.device|None": # type: ignore[name-defined] +) -> "torch.device|None": # type: ignore[name-defined] # noqa: F821 """Shared Jupyter notebook setup steps: - Set random seeds and library reproducibility settings - Set device based on availability diff --git a/muutils/nbutils/convert_ipynb_to_script.py b/muutils/nbutils/convert_ipynb_to_script.py index 130d0750..8b65bcaf 100644 --- a/muutils/nbutils/convert_ipynb_to_script.py +++ b/muutils/nbutils/convert_ipynb_to_script.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import argparse import json import os diff --git a/muutils/nbutils/run_notebook_tests.py b/muutils/nbutils/run_notebook_tests.py index 74a44fd1..7dd8b37b 100644 --- a/muutils/nbutils/run_notebook_tests.py +++ b/muutils/nbutils/run_notebook_tests.py @@ -67,9 +67,7 @@ def run_notebook_tests( output_file: Path = file.with_suffix(CI_output_suffix) print(f" Output in {output_file}") - command: str = ( - f"{run_python_cmd} {root_relative_to_notebooks / file} > {root_relative_to_notebooks / output_file} 2>&1" - ) + command: str = f"{run_python_cmd} {root_relative_to_notebooks / file} > {root_relative_to_notebooks / output_file} 2>&1" process: subprocess.CompletedProcess = subprocess.run( command, shell=True, text=True ) diff --git a/muutils/statcounter.py b/muutils/statcounter.py index 53e466d0..99edc86b 100644 --- a/muutils/statcounter.py +++ b/muutils/statcounter.py @@ -1,9 +1,10 @@ +from __future__ import annotations + import json import math from collections import Counter from functools import cached_property from itertools import chain -from types import NoneType from typing import Callable, Optional, Sequence, Union # _GeneralArray = Union[np.ndarray, "torch.Tensor"] @@ -16,7 +17,7 @@ def universal_flatten( - arr: NumericSequence | float | int, require_rectangular: bool = True + arr: Union[NumericSequence, float, int], require_rectangular: bool = True ) -> NumericSequence: """flattens any iterable""" @@ -47,7 +48,7 @@ class StatCounter(Counter): def validate(self) -> bool: """validate the counter as being all floats or ints""" - return all(isinstance(k, (bool, int, float, NoneType)) for k in self.keys()) + return all(isinstance(k, (bool, int, float, type(None))) for k in self.keys()) def min(self): return min(x for x, v in self.items() if v > 0) @@ -55,6 +56,10 @@ def min(self): def max(self): return max(x for x, v in self.items() if v > 0) + def total(self): + """Sum of the counts""" + return sum(self.values()) + @cached_property def keys_sorted(self) -> list: """return the keys""" diff --git a/muutils/sysinfo.py b/muutils/sysinfo.py index 4efa6325..a19c9fea 100644 --- a/muutils/sysinfo.py +++ b/muutils/sysinfo.py @@ -1,9 +1,9 @@ -import os +from __future__ import annotations + import subprocess import sys import typing - -from pip._internal.operations.freeze import freeze as pip_freeze +from importlib.metadata import distributions def _popen(cmd: list[str], split_out: bool = False) -> dict[str, typing.Any]: @@ -11,9 +11,11 @@ def _popen(cmd: list[str], split_out: bool = False) -> dict[str, typing.Any]: cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) - p_out: str | list[str] | None - if p.stdout is not None: - p_out = p.stdout.read().decode("utf-8") + stdout, stderr = p.communicate() + + p_out: typing.Union[str, list[str], None] + if stdout: + p_out = stdout.decode("utf-8") if split_out: assert isinstance(p_out, str) p_out = p_out.strip().split("\n") @@ -22,7 +24,7 @@ def _popen(cmd: list[str], split_out: bool = False) -> dict[str, typing.Any]: return { "stdout": p_out, - "stderr": (None if p.stderr is None else p.stderr.read().decode("utf-8")), + "stderr": stderr.decode("utf-8") if stderr else None, "returncode": p.returncode if p.returncode is None else int(p.returncode), } @@ -36,6 +38,7 @@ def python() -> dict: ver_tup = sys.version_info return { "version": sys.version, + "version_info": ver_tup, "major": ver_tup[0], "minor": ver_tup[1], "micro": ver_tup[2], @@ -46,7 +49,8 @@ def python() -> dict: @staticmethod def pip() -> dict: """installed packages info""" - pckgs: list[str] = [x for x in pip_freeze(local_only=True)] + # for some reason, python 3.8 thinks `Distribution` has no attribute `name`? + pckgs: list[tuple[str, str]] = [(x.name, x.version) for x in distributions()] # type: ignore[attr-defined] return { "n_packages": len(pckgs), "packages": pckgs, @@ -100,7 +104,7 @@ def pytorch() -> dict: "device": current_device, "name": dev_prop.name, "version": { - f"major": dev_prop.major, + "major": dev_prop.major, "minor": dev_prop.minor, }, "total_memory": dev_prop.total_memory, @@ -138,27 +142,32 @@ def platform() -> dict: return {x: getattr(platform, x)() for x in items} @staticmethod - def git_info() -> dict: + def git_info(with_log: bool = False) -> dict: git_version: dict = _popen(["git", "version"]) git_status: dict = _popen(["git", "status"]) - if git_status["stderr"].startswith("fatal: not a git repository"): + if not git_status["stderr"] or git_status["stderr"].startswith( + "fatal: not a git repository" + ): return { "git version": git_version["stdout"], "git status": git_status, } else: - return { + output: dict = { "git version": git_version["stdout"], "git status": git_status, "git branch": _popen(["git", "branch"], split_out=True), "git remote -v": _popen(["git", "remote", "-v"], split_out=True), - "git log": _popen(["git", "log"]), } + if with_log: + output["git log"] = _popen(["git", "log"], split_out=False) + + return output @classmethod def get_all( cls, - include: tuple[str, ...] | None = None, + include: typing.Optional[tuple[str, ...]] = None, exclude: tuple[str, ...] = tuple(), ) -> dict: include_meta: tuple[str, ...] @@ -180,3 +189,9 @@ def get_all( ] ) } + + +if __name__ == "__main__": + import pprint + + pprint.pprint(SysInfo.get_all()) diff --git a/muutils/tensor_utils.py b/muutils/tensor_utils.py index 97e8446b..c15b7730 100644 --- a/muutils/tensor_utils.py +++ b/muutils/tensor_utils.py @@ -1,11 +1,13 @@ +from __future__ import annotations + import json import typing -import warnings import jaxtyping import numpy as np import torch +from muutils.errormode import ErrorMode from muutils.dictmagic import dotlist_to_nested_dict # pylint: disable=missing-class-docstring @@ -62,7 +64,7 @@ def jaxtype_factory( name: str, array_type: type, default_jax_dtype=jaxtyping.Float, - legacy_mode: typing.Literal["error", "warn", "ignore"] = "warn", + legacy_mode: ErrorMode = ErrorMode.WARN, ) -> type: """usage: ``` @@ -70,6 +72,7 @@ def jaxtype_factory( x: ATensor["dim1 dim2", np.float32] ``` """ + legacy_mode = ErrorMode.from_any(legacy_mode) class _BaseArray: """jaxtyping shorthand @@ -99,7 +102,7 @@ def param_info(cls, params) -> str: ) @typing._tp_cache # type: ignore - def __class_getitem__(cls, params: str | tuple) -> type: + def __class_getitem__(cls, params: typing.Union[str, tuple]) -> type: # MyTensor["dim1 dim2"] if isinstance(params, str): return default_jax_dtype[array_type, params] @@ -115,14 +118,10 @@ def __class_getitem__(cls, params: str | tuple) -> type: return TYPE_TO_JAX_DTYPE[params[1]][array_type, params[0]] elif isinstance(params[0], tuple): - if legacy_mode == "error": - raise Exception( - f"legacy mode is set to error, but legacy type was used:\n{cls.param_info(params)}" - ) - elif legacy_mode == "warn": - warnings.warn( - f"legacy type annotation was used:\n{cls.param_info(params)}" - ) + legacy_mode.process( + f"legacy type annotation was used:\n{cls.param_info(params) = }", + except_cls=Exception, + ) # MyTensor[("dim1", "dim2"), int] shape_anot: list[str] = list() for x in params[0]: @@ -178,7 +177,7 @@ def __class_getitem__(cls, params): NDArray = jaxtype_factory("NDArray", np.ndarray, jaxtyping.Float) # type: ignore[misc, assignment] -def numpy_to_torch_dtype(dtype: np.dtype | torch.dtype) -> torch.dtype: +def numpy_to_torch_dtype(dtype: typing.Union[np.dtype, torch.dtype]) -> torch.dtype: """convert numpy dtype to torch dtype""" if isinstance(dtype, torch.dtype): return dtype @@ -278,11 +277,11 @@ def numpy_to_torch_dtype(dtype: np.dtype | torch.dtype) -> torch.dtype: def pad_tensor( - tensor: jaxtyping.Shaped[torch.Tensor, "dim1"], + tensor: jaxtyping.Shaped[torch.Tensor, "dim1"], # noqa: F821 padded_length: int, pad_value: float = 0.0, rpad: bool = False, -) -> jaxtyping.Shaped[torch.Tensor, "padded_length"]: +) -> jaxtyping.Shaped[torch.Tensor, "padded_length"]: # noqa: F821 """pad a 1-d tensor on the left with pad_value to length `padded_length` set `rpad = True` to pad on the right instead""" @@ -317,11 +316,11 @@ def rpad_tensor( def pad_array( - array: jaxtyping.Shaped[np.ndarray, "dim1"], + array: jaxtyping.Shaped[np.ndarray, "dim1"], # noqa: F821 padded_length: int, pad_value: float = 0.0, rpad: bool = False, -) -> jaxtyping.Shaped[np.ndarray, "padded_length"]: +) -> jaxtyping.Shaped[np.ndarray, "padded_length"]: # noqa: F821 """pad a 1-d array on the left with pad_value to length `padded_length` set `rpad = True` to pad on the right instead""" diff --git a/muutils/validate_type.py b/muutils/validate_type.py new file mode 100644 index 00000000..cb5a29fd --- /dev/null +++ b/muutils/validate_type.py @@ -0,0 +1,217 @@ +from __future__ import annotations + +import types +import typing +import functools + +# this is also for python <3.10 compatibility +_GenericAliasTypeNames: typing.List[str] = [ + "GenericAlias", + "_GenericAlias", + "_UnionGenericAlias", + "_BaseGenericAlias", +] + +_GenericAliasTypesList: list = [ + getattr(typing, name, None) for name in _GenericAliasTypeNames +] + +GenericAliasTypes: tuple = tuple([t for t in _GenericAliasTypesList if t is not None]) + + +class IncorrectTypeException(TypeError): + pass + + +class TypeHintNotImplementedError(NotImplementedError): + pass + + +class InvalidGenericAliasError(TypeError): + pass + + +def _return_validation_except( + return_val: bool, value: typing.Any, expected_type: typing.Any +) -> bool: + if return_val: + return True + else: + raise IncorrectTypeException( + f"Expected {expected_type = } for {value = }", + f"{type(value) = }", + f"{type(value).__mro__ = }", + f"{typing.get_origin(expected_type) = }", + f"{typing.get_args(expected_type) = }", + "\ndo --tb=long in pytest to see full trace", + ) + return False + + +def _return_validation_bool(return_val: bool) -> bool: + return return_val + + +def validate_type( + value: typing.Any, expected_type: typing.Any, do_except: bool = False +) -> bool: + """Validate that a `value` is of the `expected_type` + + # Parameters + - `value`: the value to check the type of + - `expected_type`: the type to check against. Not all types are supported + - `do_except`: if `True`, raise an exception if the type is incorrect (instead of returning `False`) + (default: `False`) + + # Returns + - `bool`: `True` if the value is of the expected type, `False` otherwise. + + # Raises + - `IncorrectTypeException(TypeError)`: if the type is incorrect and `do_except` is `True` + - `TypeHintNotImplementedError(NotImplementedError)`: if the type hint is not implemented + - `InvalidGenericAliasError(TypeError)`: if the generic alias is invalid + + use `typeguard` for a more robust solution: https://github.com/agronholm/typeguard + """ + if expected_type is typing.Any: + return True + + # set up the return function depending on `do_except` + _return_func: typing.Callable[[bool], bool] = ( + # functools.partial doesn't hint the function signature + functools.partial( # type: ignore[assignment] + _return_validation_except, value=value, expected_type=expected_type + ) + if do_except + else _return_validation_bool + ) + + # base type without args + if isinstance(expected_type, type): + try: + # if you use args on a type like `dict[str, int]`, this will fail + return _return_func(isinstance(value, expected_type)) + except TypeError as e: + if isinstance(e, IncorrectTypeException): + raise e + + origin: typing.Any = typing.get_origin(expected_type) + args: tuple = typing.get_args(expected_type) + + # useful for debugging + # print(f"{value = }, {expected_type = }, {origin = }, {args = }") + UnionType = getattr(types, "UnionType", None) + + if (origin is typing.Union) or ( # this works in python <3.10 + False + if UnionType is None # return False if UnionType is not available + else origin is UnionType # return True if UnionType is available + ): + return _return_func(any(validate_type(value, arg) for arg in args)) + + # generic alias, more complicated + item_type: type + if isinstance(expected_type, GenericAliasTypes): + if origin is list: + # no args + if len(args) == 0: + return _return_func(isinstance(value, list)) + # incorrect number of args + if len(args) != 1: + raise InvalidGenericAliasError( + f"Too many arguments for list expected 1, got {args = }, {expected_type = }, {value = }, {origin = }", + f"{GenericAliasTypes = }", + ) + # check is list + if not isinstance(value, list): + return _return_func(False) + # check all items in list are of the correct type + item_type = args[0] + return all(validate_type(item, item_type) for item in value) + + if origin is dict: + # no args + if len(args) == 0: + return _return_func(isinstance(value, dict)) + # incorrect number of args + if len(args) != 2: + raise InvalidGenericAliasError( + f"Expected 2 arguments for dict, expected 2, got {args = }, {expected_type = }, {value = }, {origin = }", + f"{GenericAliasTypes = }", + ) + # check is dict + if not isinstance(value, dict): + return _return_func(False) + # check all items in dict are of the correct type + key_type: type = args[0] + value_type: type = args[1] + return _return_func( + all( + validate_type(key, key_type) and validate_type(val, value_type) + for key, val in value.items() + ) + ) + + if origin is set: + # no args + if len(args) == 0: + return _return_func(isinstance(value, set)) + # incorrect number of args + if len(args) != 1: + raise InvalidGenericAliasError( + f"Expected 1 argument for Set, got {args = }, {expected_type = }, {value = }, {origin = }", + f"{GenericAliasTypes = }", + ) + # check is set + if not isinstance(value, set): + return _return_func(False) + # check all items in set are of the correct type + item_type = args[0] + return _return_func(all(validate_type(item, item_type) for item in value)) + + if origin is tuple: + # no args + if len(args) == 0: + return _return_func(isinstance(value, tuple)) + # check is tuple + if not isinstance(value, tuple): + return _return_func(False) + # check correct number of items in tuple + if len(value) != len(args): + return _return_func(False) + # check all items in tuple are of the correct type + return _return_func( + all(validate_type(item, arg) for item, arg in zip(value, args)) + ) + + if origin is type: + # no args + if len(args) == 0: + return _return_func(isinstance(value, type)) + # incorrect number of args + if len(args) != 1: + raise InvalidGenericAliasError( + f"Expected 1 argument for Type, got {args = }, {expected_type = }, {value = }, {origin = }", + f"{GenericAliasTypes = }", + ) + # check is type + item_type = args[0] + if item_type in value.__mro__: + return _return_func(True) + else: + return _return_func(False) + + # TODO: Callables, etc. + + raise TypeHintNotImplementedError( + f"Unsupported generic alias {expected_type = } for {value = }, {origin = }, {args = }", + f"{origin = }, {args = }", + f"\n{GenericAliasTypes = }", + ) + + else: + raise TypeHintNotImplementedError( + f"Unsupported type hint {expected_type = } for {value = }", + f"{origin = }, {args = }", + f"\n{GenericAliasTypes = }", + ) diff --git a/poetry.lock b/poetry.lock index 3e8eddd8..d3e33158 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,29 +1,10 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. - -[[package]] -name = "astroid" -version = "2.15.8" -description = "An abstract syntax tree for Python with inference support." -optional = false -python-versions = ">=3.7.2" -files = [ - {file = "astroid-2.15.8-py3-none-any.whl", hash = "sha256:1aa149fc5c6589e3d0ece885b4491acd80af4f087baafa3fb5203b113e68cd3c"}, - {file = "astroid-2.15.8.tar.gz", hash = "sha256:6c107453dffee9055899705de3c9ead36e74119cee151e5a9aaf7f0b0e020a6a"}, -] - -[package.dependencies] -lazy-object-proxy = ">=1.4.0" -typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} -wrapt = [ - {version = ">=1.11,<2", markers = "python_version < \"3.11\""}, - {version = ">=1.14,<2", markers = "python_version >= \"3.11\""}, -] +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "asttokens" version = "2.4.1" description = "Annotate AST trees with source code positions" -optional = false +optional = true python-versions = "*" files = [ {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, @@ -37,52 +18,6 @@ six = ">=1.12.0" astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] -[[package]] -name = "black" -version = "24.4.2" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.8" -files = [ - {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, - {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, - {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, - {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, - {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, - {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, - {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, - {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, - {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, - {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, - {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, - {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, - {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, - {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, - {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, - {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, - {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, - {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, - {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, - {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, - {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, - {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - [[package]] name = "click" version = "8.1.7" @@ -110,66 +45,77 @@ files = [ [[package]] name = "contourpy" -version = "1.2.1" +version = "1.1.1" description = "Python library for calculating contours of 2D quadrilateral grids" optional = false -python-versions = ">=3.9" +python-versions = ">=3.8" files = [ - {file = "contourpy-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd7c23df857d488f418439686d3b10ae2fbf9bc256cd045b37a8c16575ea1040"}, - {file = "contourpy-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5b9eb0ca724a241683c9685a484da9d35c872fd42756574a7cfbf58af26677fd"}, - {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c75507d0a55378240f781599c30e7776674dbaf883a46d1c90f37e563453480"}, - {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11959f0ce4a6f7b76ec578576a0b61a28bdc0696194b6347ba3f1c53827178b9"}, - {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb3315a8a236ee19b6df481fc5f997436e8ade24a9f03dfdc6bd490fea20c6da"}, - {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39f3ecaf76cd98e802f094e0d4fbc6dc9c45a8d0c4d185f0f6c2234e14e5f75b"}, - {file = "contourpy-1.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:94b34f32646ca0414237168d68a9157cb3889f06b096612afdd296003fdd32fd"}, - {file = "contourpy-1.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:457499c79fa84593f22454bbd27670227874cd2ff5d6c84e60575c8b50a69619"}, - {file = "contourpy-1.2.1-cp310-cp310-win32.whl", hash = "sha256:ac58bdee53cbeba2ecad824fa8159493f0bf3b8ea4e93feb06c9a465d6c87da8"}, - {file = "contourpy-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9cffe0f850e89d7c0012a1fb8730f75edd4320a0a731ed0c183904fe6ecfc3a9"}, - {file = "contourpy-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6022cecf8f44e36af10bd9118ca71f371078b4c168b6e0fab43d4a889985dbb5"}, - {file = "contourpy-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef5adb9a3b1d0c645ff694f9bca7702ec2c70f4d734f9922ea34de02294fdf72"}, - {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6150ffa5c767bc6332df27157d95442c379b7dce3a38dff89c0f39b63275696f"}, - {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c863140fafc615c14a4bf4efd0f4425c02230eb8ef02784c9a156461e62c965"}, - {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00e5388f71c1a0610e6fe56b5c44ab7ba14165cdd6d695429c5cd94021e390b2"}, - {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4492d82b3bc7fbb7e3610747b159869468079fe149ec5c4d771fa1f614a14df"}, - {file = "contourpy-1.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:49e70d111fee47284d9dd867c9bb9a7058a3c617274900780c43e38d90fe1205"}, - {file = "contourpy-1.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b59c0ffceff8d4d3996a45f2bb6f4c207f94684a96bf3d9728dbb77428dd8cb8"}, - {file = "contourpy-1.2.1-cp311-cp311-win32.whl", hash = "sha256:7b4182299f251060996af5249c286bae9361fa8c6a9cda5efc29fe8bfd6062ec"}, - {file = "contourpy-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2855c8b0b55958265e8b5888d6a615ba02883b225f2227461aa9127c578a4922"}, - {file = "contourpy-1.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:62828cada4a2b850dbef89c81f5a33741898b305db244904de418cc957ff05dc"}, - {file = "contourpy-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:309be79c0a354afff9ff7da4aaed7c3257e77edf6c1b448a779329431ee79d7e"}, - {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e785e0f2ef0d567099b9ff92cbfb958d71c2d5b9259981cd9bee81bd194c9a4"}, - {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cac0a8f71a041aa587410424ad46dfa6a11f6149ceb219ce7dd48f6b02b87a7"}, - {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af3f4485884750dddd9c25cb7e3915d83c2db92488b38ccb77dd594eac84c4a0"}, - {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ce6889abac9a42afd07a562c2d6d4b2b7134f83f18571d859b25624a331c90b"}, - {file = "contourpy-1.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a1eea9aecf761c661d096d39ed9026574de8adb2ae1c5bd7b33558af884fb2ce"}, - {file = "contourpy-1.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:187fa1d4c6acc06adb0fae5544c59898ad781409e61a926ac7e84b8f276dcef4"}, - {file = "contourpy-1.2.1-cp312-cp312-win32.whl", hash = "sha256:c2528d60e398c7c4c799d56f907664673a807635b857df18f7ae64d3e6ce2d9f"}, - {file = "contourpy-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:1a07fc092a4088ee952ddae19a2b2a85757b923217b7eed584fdf25f53a6e7ce"}, - {file = "contourpy-1.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bb6834cbd983b19f06908b45bfc2dad6ac9479ae04abe923a275b5f48f1a186b"}, - {file = "contourpy-1.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1d59e739ab0e3520e62a26c60707cc3ab0365d2f8fecea74bfe4de72dc56388f"}, - {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd3db01f59fdcbce5b22afad19e390260d6d0222f35a1023d9adc5690a889364"}, - {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a12a813949e5066148712a0626895c26b2578874e4cc63160bb007e6df3436fe"}, - {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe0ccca550bb8e5abc22f530ec0466136379c01321fd94f30a22231e8a48d985"}, - {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1d59258c3c67c865435d8fbeb35f8c59b8bef3d6f46c1f29f6123556af28445"}, - {file = "contourpy-1.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f32c38afb74bd98ce26de7cc74a67b40afb7b05aae7b42924ea990d51e4dac02"}, - {file = "contourpy-1.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d31a63bc6e6d87f77d71e1abbd7387ab817a66733734883d1fc0021ed9bfa083"}, - {file = "contourpy-1.2.1-cp39-cp39-win32.whl", hash = "sha256:ddcb8581510311e13421b1f544403c16e901c4e8f09083c881fab2be80ee31ba"}, - {file = "contourpy-1.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:10a37ae557aabf2509c79715cd20b62e4c7c28b8cd62dd7d99e5ed3ce28c3fd9"}, - {file = "contourpy-1.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a31f94983fecbac95e58388210427d68cd30fe8a36927980fab9c20062645609"}, - {file = "contourpy-1.2.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef2b055471c0eb466033760a521efb9d8a32b99ab907fc8358481a1dd29e3bd3"}, - {file = "contourpy-1.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b33d2bc4f69caedcd0a275329eb2198f560b325605810895627be5d4b876bf7f"}, - {file = "contourpy-1.2.1.tar.gz", hash = "sha256:4d8908b3bee1c889e547867ca4cdc54e5ab6be6d3e078556814a22457f49423c"}, + {file = "contourpy-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:46e24f5412c948d81736509377e255f6040e94216bf1a9b5ea1eaa9d29f6ec1b"}, + {file = "contourpy-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e48694d6a9c5a26ee85b10130c77a011a4fedf50a7279fa0bdaf44bafb4299d"}, + {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a66045af6cf00e19d02191ab578a50cb93b2028c3eefed999793698e9ea768ae"}, + {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ebf42695f75ee1a952f98ce9775c873e4971732a87334b099dde90b6af6a916"}, + {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6aec19457617ef468ff091669cca01fa7ea557b12b59a7908b9474bb9674cf0"}, + {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:462c59914dc6d81e0b11f37e560b8a7c2dbab6aca4f38be31519d442d6cde1a1"}, + {file = "contourpy-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6d0a8efc258659edc5299f9ef32d8d81de8b53b45d67bf4bfa3067f31366764d"}, + {file = "contourpy-1.1.1-cp310-cp310-win32.whl", hash = "sha256:d6ab42f223e58b7dac1bb0af32194a7b9311065583cc75ff59dcf301afd8a431"}, + {file = "contourpy-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:549174b0713d49871c6dee90a4b499d3f12f5e5f69641cd23c50a4542e2ca1eb"}, + {file = "contourpy-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:407d864db716a067cc696d61fa1ef6637fedf03606e8417fe2aeed20a061e6b2"}, + {file = "contourpy-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe80c017973e6a4c367e037cb31601044dd55e6bfacd57370674867d15a899b"}, + {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e30aaf2b8a2bac57eb7e1650df1b3a4130e8d0c66fc2f861039d507a11760e1b"}, + {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3de23ca4f381c3770dee6d10ead6fff524d540c0f662e763ad1530bde5112532"}, + {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:566f0e41df06dfef2431defcfaa155f0acfa1ca4acbf8fd80895b1e7e2ada40e"}, + {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b04c2f0adaf255bf756cf08ebef1be132d3c7a06fe6f9877d55640c5e60c72c5"}, + {file = "contourpy-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d0c188ae66b772d9d61d43c6030500344c13e3f73a00d1dc241da896f379bb62"}, + {file = "contourpy-1.1.1-cp311-cp311-win32.whl", hash = "sha256:0683e1ae20dc038075d92e0e0148f09ffcefab120e57f6b4c9c0f477ec171f33"}, + {file = "contourpy-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:8636cd2fc5da0fb102a2504fa2c4bea3cbc149533b345d72cdf0e7a924decc45"}, + {file = "contourpy-1.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:560f1d68a33e89c62da5da4077ba98137a5e4d3a271b29f2f195d0fba2adcb6a"}, + {file = "contourpy-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24216552104ae8f3b34120ef84825400b16eb6133af2e27a190fdc13529f023e"}, + {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56de98a2fb23025882a18b60c7f0ea2d2d70bbbcfcf878f9067234b1c4818442"}, + {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:07d6f11dfaf80a84c97f1a5ba50d129d9303c5b4206f776e94037332e298dda8"}, + {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1eaac5257a8f8a047248d60e8f9315c6cff58f7803971170d952555ef6344a7"}, + {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19557fa407e70f20bfaba7d55b4d97b14f9480856c4fb65812e8a05fe1c6f9bf"}, + {file = "contourpy-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:081f3c0880712e40effc5f4c3b08feca6d064cb8cfbb372ca548105b86fd6c3d"}, + {file = "contourpy-1.1.1-cp312-cp312-win32.whl", hash = "sha256:059c3d2a94b930f4dafe8105bcdc1b21de99b30b51b5bce74c753686de858cb6"}, + {file = "contourpy-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f44d78b61740e4e8c71db1cf1fd56d9050a4747681c59ec1094750a658ceb970"}, + {file = "contourpy-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:70e5a10f8093d228bb2b552beeb318b8928b8a94763ef03b858ef3612b29395d"}, + {file = "contourpy-1.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8394e652925a18ef0091115e3cc191fef350ab6dc3cc417f06da66bf98071ae9"}, + {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5bd5680f844c3ff0008523a71949a3ff5e4953eb7701b28760805bc9bcff217"}, + {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66544f853bfa85c0d07a68f6c648b2ec81dafd30f272565c37ab47a33b220684"}, + {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0c02b75acfea5cab07585d25069207e478d12309557f90a61b5a3b4f77f46ce"}, + {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41339b24471c58dc1499e56783fedc1afa4bb018bcd035cfb0ee2ad2a7501ef8"}, + {file = "contourpy-1.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f29fb0b3f1217dfe9362ec55440d0743fe868497359f2cf93293f4b2701b8251"}, + {file = "contourpy-1.1.1-cp38-cp38-win32.whl", hash = "sha256:f9dc7f933975367251c1b34da882c4f0e0b2e24bb35dc906d2f598a40b72bfc7"}, + {file = "contourpy-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:498e53573e8b94b1caeb9e62d7c2d053c263ebb6aa259c81050766beb50ff8d9"}, + {file = "contourpy-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ba42e3810999a0ddd0439e6e5dbf6d034055cdc72b7c5c839f37a7c274cb4eba"}, + {file = "contourpy-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c06e4c6e234fcc65435223c7b2a90f286b7f1b2733058bdf1345d218cc59e34"}, + {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca6fab080484e419528e98624fb5c4282148b847e3602dc8dbe0cb0669469887"}, + {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93df44ab351119d14cd1e6b52a5063d3336f0754b72736cc63db59307dabb718"}, + {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eafbef886566dc1047d7b3d4b14db0d5b7deb99638d8e1be4e23a7c7ac59ff0f"}, + {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efe0fab26d598e1ec07d72cf03eaeeba8e42b4ecf6b9ccb5a356fde60ff08b85"}, + {file = "contourpy-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f08e469821a5e4751c97fcd34bcb586bc243c39c2e39321822060ba902eac49e"}, + {file = "contourpy-1.1.1-cp39-cp39-win32.whl", hash = "sha256:bfc8a5e9238232a45ebc5cb3bfee71f1167064c8d382cadd6076f0d51cff1da0"}, + {file = "contourpy-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:c84fdf3da00c2827d634de4fcf17e3e067490c4aea82833625c4c8e6cdea0887"}, + {file = "contourpy-1.1.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:229a25f68046c5cf8067d6d6351c8b99e40da11b04d8416bf8d2b1d75922521e"}, + {file = "contourpy-1.1.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a10dab5ea1bd4401c9483450b5b0ba5416be799bbd50fc7a6cc5e2a15e03e8a3"}, + {file = "contourpy-1.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4f9147051cb8fdb29a51dc2482d792b3b23e50f8f57e3720ca2e3d438b7adf23"}, + {file = "contourpy-1.1.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a75cc163a5f4531a256f2c523bd80db509a49fc23721b36dd1ef2f60ff41c3cb"}, + {file = "contourpy-1.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b53d5769aa1f2d4ea407c65f2d1d08002952fac1d9e9d307aa2e1023554a163"}, + {file = "contourpy-1.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11b836b7dbfb74e049c302bbf74b4b8f6cb9d0b6ca1bf86cfa8ba144aedadd9c"}, + {file = "contourpy-1.1.1.tar.gz", hash = "sha256:96ba37c2e24b7212a77da85004c38e7c4d155d3e72a45eeaf22c1f03f607e8ab"}, ] [package.dependencies] -numpy = ">=1.20" +numpy = [ + {version = ">=1.16,<2.0", markers = "python_version <= \"3.11\""}, + {version = ">=1.26.0rc1,<2.0", markers = "python_version >= \"3.12\""}, +] [package.extras] bokeh = ["bokeh", "selenium"] docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] -mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.8.0)", "types-Pillow"] +mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.4.1)", "types-Pillow"] test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] -test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"] +test-no-images = ["pytest", "pytest-cov", "wurlitzer"] [[package]] name = "coverage" @@ -271,28 +217,13 @@ tests = ["pytest", "pytest-cov", "pytest-xdist"] name = "decorator" version = "5.1.1" description = "Decorators for Humans" -optional = false +optional = true python-versions = ">=3.5" files = [ {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, ] -[[package]] -name = "dill" -version = "0.3.8" -description = "serialize all of Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, - {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, -] - -[package.extras] -graph = ["objgraph (>=1.7.2)"] -profile = ["gprof2dot (>=2022.7.29)"] - [[package]] name = "exceptiongroup" version = "1.2.1" @@ -311,7 +242,7 @@ test = ["pytest (>=6)"] name = "executing" version = "2.0.1" description = "Get the currently executing AST node of a frame, and other information" -optional = false +optional = true python-versions = ">=3.5" files = [ {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, @@ -323,18 +254,18 @@ tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipyth [[package]] name = "filelock" -version = "3.14.0" +version = "3.15.3" description = "A platform independent file lock." optional = true python-versions = ">=3.8" files = [ - {file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"}, - {file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"}, + {file = "filelock-3.15.3-py3-none-any.whl", hash = "sha256:0151273e5b5d6cf753a61ec83b3a9b7d8821c39ae9af9d7ecf2f9e2f17404103"}, + {file = "filelock-3.15.3.tar.gz", hash = "sha256:e1199bf5194a2277273dacd50269f0d87d0682088a3c561c15674ea9005d8635"}, ] [package.extras] docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] typing = ["typing-extensions (>=4.8)"] [[package]] @@ -441,6 +372,43 @@ test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask-expr", "dask[dataframe, test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard"] tqdm = ["tqdm"] +[[package]] +name = "importlib-metadata" +version = "7.2.0" +description = "Read metadata from Python packages" +optional = true +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-7.2.0-py3-none-any.whl", hash = "sha256:04e4aad329b8b948a5711d394fa8759cb80f009225441b4f2a02bd4d8e5f426c"}, + {file = "importlib_metadata-7.2.0.tar.gz", hash = "sha256:3ff4519071ed42740522d494d04819b666541b9752c43012f85afb2cc220fcc6"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +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)"] + +[[package]] +name = "importlib-resources" +version = "6.4.0" +description = "Read resources from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c"}, + {file = "importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145"}, +] + +[package.dependencies] +zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"] + [[package]] name = "iniconfig" version = "2.0.0" @@ -470,7 +438,7 @@ files = [ name = "ipython" version = "8.25.0" description = "IPython: Productive Interactive Computing" -optional = false +optional = true python-versions = ">=3.10" files = [ {file = "ipython-8.25.0-py3-none-any.whl", hash = "sha256:53eee7ad44df903a06655871cbab66d156a051fd86f3ec6750470ac9604ac1ab"}, @@ -504,39 +472,27 @@ qtconsole = ["qtconsole"] test = ["pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"] test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"] -[[package]] -name = "isort" -version = "5.13.2" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, - {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, -] - -[package.extras] -colors = ["colorama (>=0.4.6)"] - [[package]] name = "jaxtyping" -version = "0.2.29" +version = "0.2.19" description = "Type annotations and runtime checking for shape and dtype of JAX arrays, and PyTrees." optional = true -python-versions = "~=3.9" +python-versions = "~=3.8" files = [ - {file = "jaxtyping-0.2.29-py3-none-any.whl", hash = "sha256:3580fc4dfef4c98ef2372c2c81314d89b98a186eb78d69d925fd0546025d556f"}, - {file = "jaxtyping-0.2.29.tar.gz", hash = "sha256:e1cd916ed0196e40402b0638449e7d051571562b2cd68d8b94961a383faeb409"}, + {file = "jaxtyping-0.2.19-py3-none-any.whl", hash = "sha256:651352032799d422987e783fd1b77699b53c3bb28ffa644bbca5f75ec4fbb843"}, + {file = "jaxtyping-0.2.19.tar.gz", hash = "sha256:21ff4c3caec6781cadfe980b019dde856c1011e17d11dfe8589298040056325a"}, ] [package.dependencies] -typeguard = "2.13.3" +numpy = ">=1.20.0" +typeguard = ">=2.13.3" +typing-extensions = ">=3.7.4.1" [[package]] name = "jedi" version = "0.19.1" description = "An autocompletion tool for Python that can be used for text editors." -optional = false +optional = true python-versions = ">=3.6" files = [ {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, @@ -681,91 +637,53 @@ files = [ {file = "kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec"}, ] -[[package]] -name = "lazy-object-proxy" -version = "1.10.0" -description = "A fast and thorough lazy object proxy." -optional = false -python-versions = ">=3.8" -files = [ - {file = "lazy-object-proxy-1.10.0.tar.gz", hash = "sha256:78247b6d45f43a52ef35c25b5581459e85117225408a4128a3daf8bf9648ac69"}, - {file = "lazy_object_proxy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:855e068b0358ab916454464a884779c7ffa312b8925c6f7401e952dcf3b89977"}, - {file = "lazy_object_proxy-1.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab7004cf2e59f7c2e4345604a3e6ea0d92ac44e1c2375527d56492014e690c3"}, - {file = "lazy_object_proxy-1.10.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc0d2fc424e54c70c4bc06787e4072c4f3b1aa2f897dfdc34ce1013cf3ceef05"}, - {file = "lazy_object_proxy-1.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e2adb09778797da09d2b5ebdbceebf7dd32e2c96f79da9052b2e87b6ea495895"}, - {file = "lazy_object_proxy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b1f711e2c6dcd4edd372cf5dec5c5a30d23bba06ee012093267b3376c079ec83"}, - {file = "lazy_object_proxy-1.10.0-cp310-cp310-win32.whl", hash = "sha256:76a095cfe6045c7d0ca77db9934e8f7b71b14645f0094ffcd842349ada5c5fb9"}, - {file = "lazy_object_proxy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:b4f87d4ed9064b2628da63830986c3d2dca7501e6018347798313fcf028e2fd4"}, - {file = "lazy_object_proxy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fec03caabbc6b59ea4a638bee5fce7117be8e99a4103d9d5ad77f15d6f81020c"}, - {file = "lazy_object_proxy-1.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02c83f957782cbbe8136bee26416686a6ae998c7b6191711a04da776dc9e47d4"}, - {file = "lazy_object_proxy-1.10.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009e6bb1f1935a62889ddc8541514b6a9e1fcf302667dcb049a0be5c8f613e56"}, - {file = "lazy_object_proxy-1.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75fc59fc450050b1b3c203c35020bc41bd2695ed692a392924c6ce180c6f1dc9"}, - {file = "lazy_object_proxy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:782e2c9b2aab1708ffb07d4bf377d12901d7a1d99e5e410d648d892f8967ab1f"}, - {file = "lazy_object_proxy-1.10.0-cp311-cp311-win32.whl", hash = "sha256:edb45bb8278574710e68a6b021599a10ce730d156e5b254941754a9cc0b17d03"}, - {file = "lazy_object_proxy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:e271058822765ad5e3bca7f05f2ace0de58a3f4e62045a8c90a0dfd2f8ad8cc6"}, - {file = "lazy_object_proxy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e98c8af98d5707dcdecc9ab0863c0ea6e88545d42ca7c3feffb6b4d1e370c7ba"}, - {file = "lazy_object_proxy-1.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:952c81d415b9b80ea261d2372d2a4a2332a3890c2b83e0535f263ddfe43f0d43"}, - {file = "lazy_object_proxy-1.10.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80b39d3a151309efc8cc48675918891b865bdf742a8616a337cb0090791a0de9"}, - {file = "lazy_object_proxy-1.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e221060b701e2aa2ea991542900dd13907a5c90fa80e199dbf5a03359019e7a3"}, - {file = "lazy_object_proxy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:92f09ff65ecff3108e56526f9e2481b8116c0b9e1425325e13245abfd79bdb1b"}, - {file = "lazy_object_proxy-1.10.0-cp312-cp312-win32.whl", hash = "sha256:3ad54b9ddbe20ae9f7c1b29e52f123120772b06dbb18ec6be9101369d63a4074"}, - {file = "lazy_object_proxy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:127a789c75151db6af398b8972178afe6bda7d6f68730c057fbbc2e96b08d282"}, - {file = "lazy_object_proxy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e4ed0518a14dd26092614412936920ad081a424bdcb54cc13349a8e2c6d106a"}, - {file = "lazy_object_proxy-1.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ad9e6ed739285919aa9661a5bbed0aaf410aa60231373c5579c6b4801bd883c"}, - {file = "lazy_object_proxy-1.10.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fc0a92c02fa1ca1e84fc60fa258458e5bf89d90a1ddaeb8ed9cc3147f417255"}, - {file = "lazy_object_proxy-1.10.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0aefc7591920bbd360d57ea03c995cebc204b424524a5bd78406f6e1b8b2a5d8"}, - {file = "lazy_object_proxy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5faf03a7d8942bb4476e3b62fd0f4cf94eaf4618e304a19865abf89a35c0bbee"}, - {file = "lazy_object_proxy-1.10.0-cp38-cp38-win32.whl", hash = "sha256:e333e2324307a7b5d86adfa835bb500ee70bfcd1447384a822e96495796b0ca4"}, - {file = "lazy_object_proxy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:cb73507defd385b7705c599a94474b1d5222a508e502553ef94114a143ec6696"}, - {file = "lazy_object_proxy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:366c32fe5355ef5fc8a232c5436f4cc66e9d3e8967c01fb2e6302fd6627e3d94"}, - {file = "lazy_object_proxy-1.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2297f08f08a2bb0d32a4265e98a006643cd7233fb7983032bd61ac7a02956b3b"}, - {file = "lazy_object_proxy-1.10.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18dd842b49456aaa9a7cf535b04ca4571a302ff72ed8740d06b5adcd41fe0757"}, - {file = "lazy_object_proxy-1.10.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:217138197c170a2a74ca0e05bddcd5f1796c735c37d0eee33e43259b192aa424"}, - {file = "lazy_object_proxy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9a3a87cf1e133e5b1994144c12ca4aa3d9698517fe1e2ca82977781b16955658"}, - {file = "lazy_object_proxy-1.10.0-cp39-cp39-win32.whl", hash = "sha256:30b339b2a743c5288405aa79a69e706a06e02958eab31859f7f3c04980853b70"}, - {file = "lazy_object_proxy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:a899b10e17743683b293a729d3a11f2f399e8a90c73b089e29f5d0fe3509f0dd"}, - {file = "lazy_object_proxy-1.10.0-pp310.pp311.pp312.pp38.pp39-none-any.whl", hash = "sha256:80fa48bd89c8f2f456fc0765c11c23bf5af827febacd2f523ca5bc1893fcc09d"}, -] - [[package]] name = "libcst" -version = "1.4.0" -description = "A concrete syntax tree with AST-like properties for Python 3.0 through 3.12 programs." +version = "1.1.0" +description = "A concrete syntax tree with AST-like properties for Python 3.5, 3.6, 3.7, 3.8, 3.9, and 3.10 programs." optional = false -python-versions = ">=3.9" +python-versions = ">=3.8" files = [ - {file = "libcst-1.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:279b54568ea1f25add50ea4ba3d76d4f5835500c82f24d54daae4c5095b986aa"}, - {file = "libcst-1.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3401dae41fe24565387a65baee3887e31a44e3e58066b0250bc3f3ccf85b1b5a"}, - {file = "libcst-1.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1989fa12d3cd79118ebd29ebe2a6976d23d509b1a4226bc3d66fcb7cb50bd5d"}, - {file = "libcst-1.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:addc6d585141a7677591868886f6bda0577529401a59d210aa8112114340e129"}, - {file = "libcst-1.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:17d71001cb25e94cfe8c3d997095741a8c4aa7a6d234c0f972bc42818c88dfaf"}, - {file = "libcst-1.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:2d47de16d105e7dd5f4e01a428d9f4dc1e71efd74f79766daf54528ce37f23c3"}, - {file = "libcst-1.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e6227562fc5c9c1efd15dfe90b0971ae254461b8b6b23c1b617139b6003de1c1"}, - {file = "libcst-1.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3399e6c95df89921511b44d8c5bf6a75bcbc2d51f1f6429763609ba005c10f6b"}, - {file = "libcst-1.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48601e3e590e2d6a7ab8c019cf3937c70511a78d778ab3333764531253acdb33"}, - {file = "libcst-1.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f42797309bb725f0f000510d5463175ccd7155395f09b5e7723971b0007a976d"}, - {file = "libcst-1.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb4e42ea107a37bff7f9fdbee9532d39f9ea77b89caa5c5112b37057b12e0838"}, - {file = "libcst-1.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:9d0cc3c5a2a51fa7e1d579a828c0a2e46b2170024fd8b1a0691c8a52f3abb2d9"}, - {file = "libcst-1.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7ece51d935bc9bf60b528473d2e5cc67cbb88e2f8146297e40ee2c7d80be6f13"}, - {file = "libcst-1.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:81653dea1cdfa4c6520a7c5ffb95fa4d220cbd242e446c7a06d42d8636bfcbba"}, - {file = "libcst-1.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6abce0e66bba2babfadc20530fd3688f672d565674336595b4623cd800b91ef"}, - {file = "libcst-1.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5da9d7dc83801aba3b8d911f82dc1a375db0d508318bad79d9fb245374afe068"}, - {file = "libcst-1.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c54aa66c86d8ece9c93156a2cf5ca512b0dce40142fe9e072c86af2bf892411"}, - {file = "libcst-1.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:62e2682ee1567b6a89c91853865372bf34f178bfd237853d84df2b87b446e654"}, - {file = "libcst-1.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b8ecdba8934632b4dadacb666cd3816627a6ead831b806336972ccc4ba7ca0e9"}, - {file = "libcst-1.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8e54c777b8d27339b70f304d16fc8bc8674ef1bd34ed05ea874bf4921eb5a313"}, - {file = "libcst-1.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:061d6855ef30efe38b8a292b7e5d57c8e820e71fc9ec9846678b60a934b53bbb"}, - {file = "libcst-1.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb0abf627ee14903d05d0ad9b2c6865f1b21eb4081e2c7bea1033f85db2b8bae"}, - {file = "libcst-1.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d024f44059a853b4b852cfc04fec33e346659d851371e46fc8e7c19de24d3da9"}, - {file = "libcst-1.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:3c6a8faab9da48c5b371557d0999b4ca51f4f2cbd37ee8c2c4df0ac01c781465"}, - {file = "libcst-1.4.0.tar.gz", hash = "sha256:449e0b16604f054fa7f27c3ffe86ea7ef6c409836fe68fe4e752a1894175db00"}, + {file = "libcst-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:63f75656fd733dc20354c46253fde3cf155613e37643c3eaf6f8818e95b7a3d1"}, + {file = "libcst-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ae11eb1ea55a16dc0cdc61b41b29ac347da70fec14cc4381248e141ee2fbe6c"}, + {file = "libcst-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bc745d0c06420fe2644c28d6ddccea9474fb68a2135904043676deb4fa1e6bc"}, + {file = "libcst-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c1f2da45f1c45634090fd8672c15e0159fdc46853336686959b2d093b6e10fa"}, + {file = "libcst-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:003e5e83a12eed23542c4ea20fdc8de830887cc03662432bb36f84f8c4841b81"}, + {file = "libcst-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:3ebbb9732ae3cc4ae7a0e97890bed0a57c11d6df28790c2b9c869f7da653c7c7"}, + {file = "libcst-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d68c34e3038d3d1d6324eb47744cbf13f2c65e1214cf49db6ff2a6603c1cd838"}, + {file = "libcst-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9dffa1795c2804d183efb01c0f1efd20a7831db6a21a0311edf90b4100d67436"}, + {file = "libcst-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc9b6ac36d7ec9db2f053014ea488086ca2ed9c322be104fbe2c71ca759da4bb"}, + {file = "libcst-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b7a38ec4c1c009ac39027d51558b52851fb9234669ba5ba62283185963a31c"}, + {file = "libcst-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5297a16e575be8173185e936b7765c89a3ca69d4ae217a4af161814a0f9745a7"}, + {file = "libcst-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:7ccaf53925f81118aeaadb068a911fac8abaff608817d7343da280616a5ca9c1"}, + {file = "libcst-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:75816647736f7e09c6120bdbf408456f99b248d6272277eed9a58cf50fb8bc7d"}, + {file = "libcst-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c8f26250f87ca849a7303ed7a4fd6b2c7ac4dec16b7d7e68ca6a476d7c9bfcdb"}, + {file = "libcst-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d37326bd6f379c64190a28947a586b949de3a76be00176b0732c8ee87d67ebe"}, + {file = "libcst-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3d8cf974cfa2487b28f23f56c4bff90d550ef16505e58b0dca0493d5293784b"}, + {file = "libcst-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82d1271403509b0a4ee6ff7917c2d33b5a015f44d1e208abb1da06ba93b2a378"}, + {file = "libcst-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:bca1841693941fdd18371824bb19a9702d5784cd347cb8231317dbdc7062c5bc"}, + {file = "libcst-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f36f592e035ef84f312a12b75989dde6a5f6767fe99146cdae6a9ee9aff40dd0"}, + {file = "libcst-1.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f561c9a84eca18be92f4ad90aa9bd873111efbea995449301719a1a7805dbc5c"}, + {file = "libcst-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97fbc73c87e9040e148881041fd5ffa2a6ebf11f64b4ccb5b52e574b95df1a15"}, + {file = "libcst-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99fdc1929703fd9e7408aed2e03f58701c5280b05c8911753a8d8619f7dfdda5"}, + {file = "libcst-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bf69cbbab5016d938aac4d3ae70ba9ccb3f90363c588b3b97be434e6ba95403"}, + {file = "libcst-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:fe41b33aa73635b1651f64633f429f7aa21f86d2db5748659a99d9b7b1ed2a90"}, + {file = "libcst-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:73c086705ed34dbad16c62c9adca4249a556c1b022993d511da70ea85feaf669"}, + {file = "libcst-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3a07ecfabbbb8b93209f952a365549e65e658831e9231649f4f4e4263cad24b1"}, + {file = "libcst-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c653d9121d6572d8b7f8abf20f88b0a41aab77ff5a6a36e5a0ec0f19af0072e8"}, + {file = "libcst-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f1cd308a4c2f71d5e4eec6ee693819933a03b78edb2e4cc5e3ad1afd5fb3f07"}, + {file = "libcst-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8afb6101b8b3c86c5f9cec6b90ab4da16c3c236fe7396f88e8b93542bb341f7c"}, + {file = "libcst-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:d22d1abfe49aa60fc61fa867e10875a9b3024ba5a801112f4d7ba42d8d53242e"}, + {file = "libcst-1.1.0.tar.gz", hash = "sha256:0acbacb9a170455701845b7e940e2d7b9519db35a86768d86330a0b0deae1086"}, ] [package.dependencies] pyyaml = ">=5.2" +typing-extensions = ">=3.7.4.2" +typing-inspect = ">=0.4.0" [package.extras] -dev = ["Sphinx (>=5.1.1)", "black (==23.12.1)", "build (>=0.10.0)", "coverage (>=4.5.4)", "fixit (==2.1.0)", "flake8 (==7.0.0)", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "jinja2 (==3.1.4)", "jupyter (>=1.0.0)", "maturin (>=0.8.3,<1.6)", "nbsphinx (>=0.4.2)", "prompt-toolkit (>=2.0.9)", "pyre-check (==0.9.18)", "setuptools-rust (>=1.5.2)", "setuptools-scm (>=6.0.1)", "slotscheck (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "ufmt (==2.6.0)", "usort (==1.0.8.post1)"] +dev = ["Sphinx (>=5.1.1)", "black (==23.9.1)", "build (>=0.10.0)", "coverage (>=4.5.4)", "fixit (==2.0.0.post1)", "flake8 (>=3.7.8,<5)", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "jinja2 (==3.1.2)", "jupyter (>=1.0.0)", "maturin (>=0.8.3,<0.16)", "nbsphinx (>=0.4.2)", "prompt-toolkit (>=2.0.9)", "pyre-check (==0.9.18)", "setuptools-rust (>=1.5.2)", "setuptools-scm (>=6.0.1)", "slotscheck (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "ufmt (==2.2.0)", "usort (==1.0.7)"] [[package]] name = "markdown-it-py" @@ -862,61 +780,77 @@ files = [ [[package]] name = "matplotlib" -version = "3.9.0" +version = "3.7.5" description = "Python plotting package" optional = false -python-versions = ">=3.9" +python-versions = ">=3.8" files = [ - {file = "matplotlib-3.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2bcee1dffaf60fe7656183ac2190bd630842ff87b3153afb3e384d966b57fe56"}, - {file = "matplotlib-3.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3f988bafb0fa39d1074ddd5bacd958c853e11def40800c5824556eb630f94d3b"}, - {file = "matplotlib-3.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe428e191ea016bb278758c8ee82a8129c51d81d8c4bc0846c09e7e8e9057241"}, - {file = "matplotlib-3.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaf3978060a106fab40c328778b148f590e27f6fa3cd15a19d6892575bce387d"}, - {file = "matplotlib-3.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2e7f03e5cbbfacdd48c8ea394d365d91ee8f3cae7e6ec611409927b5ed997ee4"}, - {file = "matplotlib-3.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:13beb4840317d45ffd4183a778685e215939be7b08616f431c7795276e067463"}, - {file = "matplotlib-3.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:063af8587fceeac13b0936c42a2b6c732c2ab1c98d38abc3337e430e1ff75e38"}, - {file = "matplotlib-3.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a2fa6d899e17ddca6d6526cf6e7ba677738bf2a6a9590d702c277204a7c6152"}, - {file = "matplotlib-3.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550cdda3adbd596078cca7d13ed50b77879104e2e46392dcd7c75259d8f00e85"}, - {file = "matplotlib-3.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76cce0f31b351e3551d1f3779420cf8f6ec0d4a8cf9c0237a3b549fd28eb4abb"}, - {file = "matplotlib-3.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c53aeb514ccbbcbab55a27f912d79ea30ab21ee0531ee2c09f13800efb272674"}, - {file = "matplotlib-3.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5be985db2596d761cdf0c2eaf52396f26e6a64ab46bd8cd810c48972349d1be"}, - {file = "matplotlib-3.9.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c79f3a585f1368da6049318bdf1f85568d8d04b2e89fc24b7e02cc9b62017382"}, - {file = "matplotlib-3.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bdd1ecbe268eb3e7653e04f451635f0fb0f77f07fd070242b44c076c9106da84"}, - {file = "matplotlib-3.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d38e85a1a6d732f645f1403ce5e6727fd9418cd4574521d5803d3d94911038e5"}, - {file = "matplotlib-3.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a490715b3b9984fa609116481b22178348c1a220a4499cda79132000a79b4db"}, - {file = "matplotlib-3.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8146ce83cbc5dc71c223a74a1996d446cd35cfb6a04b683e1446b7e6c73603b7"}, - {file = "matplotlib-3.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:d91a4ffc587bacf5c4ce4ecfe4bcd23a4b675e76315f2866e588686cc97fccdf"}, - {file = "matplotlib-3.9.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:616fabf4981a3b3c5a15cd95eba359c8489c4e20e03717aea42866d8d0465956"}, - {file = "matplotlib-3.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cd53c79fd02f1c1808d2cfc87dd3cf4dbc63c5244a58ee7944497107469c8d8a"}, - {file = "matplotlib-3.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06a478f0d67636554fa78558cfbcd7b9dba85b51f5c3b5a0c9be49010cf5f321"}, - {file = "matplotlib-3.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81c40af649d19c85f8073e25e5806926986806fa6d54be506fbf02aef47d5a89"}, - {file = "matplotlib-3.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52146fc3bd7813cc784562cb93a15788be0b2875c4655e2cc6ea646bfa30344b"}, - {file = "matplotlib-3.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:0fc51eaa5262553868461c083d9adadb11a6017315f3a757fc45ec6ec5f02888"}, - {file = "matplotlib-3.9.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bd4f2831168afac55b881db82a7730992aa41c4f007f1913465fb182d6fb20c0"}, - {file = "matplotlib-3.9.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:290d304e59be2b33ef5c2d768d0237f5bd132986bdcc66f80bc9bcc300066a03"}, - {file = "matplotlib-3.9.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff2e239c26be4f24bfa45860c20ffccd118d270c5b5d081fa4ea409b5469fcd"}, - {file = "matplotlib-3.9.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:af4001b7cae70f7eaacfb063db605280058246de590fa7874f00f62259f2df7e"}, - {file = "matplotlib-3.9.0.tar.gz", hash = "sha256:e6d29ea6c19e34b30fb7d88b7081f869a03014f66fe06d62cc77d5a6ea88ed7a"}, + {file = "matplotlib-3.7.5-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:4a87b69cb1cb20943010f63feb0b2901c17a3b435f75349fd9865713bfa63925"}, + {file = "matplotlib-3.7.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:d3ce45010fefb028359accebb852ca0c21bd77ec0f281952831d235228f15810"}, + {file = "matplotlib-3.7.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fbea1e762b28400393d71be1a02144aa16692a3c4c676ba0178ce83fc2928fdd"}, + {file = "matplotlib-3.7.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec0e1adc0ad70ba8227e957551e25a9d2995e319c29f94a97575bb90fa1d4469"}, + {file = "matplotlib-3.7.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6738c89a635ced486c8a20e20111d33f6398a9cbebce1ced59c211e12cd61455"}, + {file = "matplotlib-3.7.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1210b7919b4ed94b5573870f316bca26de3e3b07ffdb563e79327dc0e6bba515"}, + {file = "matplotlib-3.7.5-cp310-cp310-win32.whl", hash = "sha256:068ebcc59c072781d9dcdb82f0d3f1458271c2de7ca9c78f5bd672141091e9e1"}, + {file = "matplotlib-3.7.5-cp310-cp310-win_amd64.whl", hash = "sha256:f098ffbaab9df1e3ef04e5a5586a1e6b1791380698e84938d8640961c79b1fc0"}, + {file = "matplotlib-3.7.5-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:f65342c147572673f02a4abec2d5a23ad9c3898167df9b47c149f32ce61ca078"}, + {file = "matplotlib-3.7.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4ddf7fc0e0dc553891a117aa083039088d8a07686d4c93fb8a810adca68810af"}, + {file = "matplotlib-3.7.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0ccb830fc29442360d91be48527809f23a5dcaee8da5f4d9b2d5b867c1b087b8"}, + {file = "matplotlib-3.7.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efc6bb28178e844d1f408dd4d6341ee8a2e906fc9e0fa3dae497da4e0cab775d"}, + {file = "matplotlib-3.7.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b15c4c2d374f249f324f46e883340d494c01768dd5287f8bc00b65b625ab56c"}, + {file = "matplotlib-3.7.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d028555421912307845e59e3de328260b26d055c5dac9b182cc9783854e98fb"}, + {file = "matplotlib-3.7.5-cp311-cp311-win32.whl", hash = "sha256:fe184b4625b4052fa88ef350b815559dd90cc6cc8e97b62f966e1ca84074aafa"}, + {file = "matplotlib-3.7.5-cp311-cp311-win_amd64.whl", hash = "sha256:084f1f0f2f1010868c6f1f50b4e1c6f2fb201c58475494f1e5b66fed66093647"}, + {file = "matplotlib-3.7.5-cp312-cp312-macosx_10_12_universal2.whl", hash = "sha256:34bceb9d8ddb142055ff27cd7135f539f2f01be2ce0bafbace4117abe58f8fe4"}, + {file = "matplotlib-3.7.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c5a2134162273eb8cdfd320ae907bf84d171de948e62180fa372a3ca7cf0f433"}, + {file = "matplotlib-3.7.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:039ad54683a814002ff37bf7981aa1faa40b91f4ff84149beb53d1eb64617980"}, + {file = "matplotlib-3.7.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d742ccd1b09e863b4ca58291728db645b51dab343eebb08d5d4b31b308296ce"}, + {file = "matplotlib-3.7.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:743b1c488ca6a2bc7f56079d282e44d236bf375968bfd1b7ba701fd4d0fa32d6"}, + {file = "matplotlib-3.7.5-cp312-cp312-win_amd64.whl", hash = "sha256:fbf730fca3e1f23713bc1fae0a57db386e39dc81ea57dc305c67f628c1d7a342"}, + {file = "matplotlib-3.7.5-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:cfff9b838531698ee40e40ea1a8a9dc2c01edb400b27d38de6ba44c1f9a8e3d2"}, + {file = "matplotlib-3.7.5-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:1dbcca4508bca7847fe2d64a05b237a3dcaec1f959aedb756d5b1c67b770c5ee"}, + {file = "matplotlib-3.7.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4cdf4ef46c2a1609a50411b66940b31778db1e4b73d4ecc2eaa40bd588979b13"}, + {file = "matplotlib-3.7.5-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:167200ccfefd1674b60e957186dfd9baf58b324562ad1a28e5d0a6b3bea77905"}, + {file = "matplotlib-3.7.5-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:53e64522934df6e1818b25fd48cf3b645b11740d78e6ef765fbb5fa5ce080d02"}, + {file = "matplotlib-3.7.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3e3bc79b2d7d615067bd010caff9243ead1fc95cf735c16e4b2583173f717eb"}, + {file = "matplotlib-3.7.5-cp38-cp38-win32.whl", hash = "sha256:6b641b48c6819726ed47c55835cdd330e53747d4efff574109fd79b2d8a13748"}, + {file = "matplotlib-3.7.5-cp38-cp38-win_amd64.whl", hash = "sha256:f0b60993ed3488b4532ec6b697059897891927cbfc2b8d458a891b60ec03d9d7"}, + {file = "matplotlib-3.7.5-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:090964d0afaff9c90e4d8de7836757e72ecfb252fb02884016d809239f715651"}, + {file = "matplotlib-3.7.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:9fc6fcfbc55cd719bc0bfa60bde248eb68cf43876d4c22864603bdd23962ba25"}, + {file = "matplotlib-3.7.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7cc3078b019bb863752b8b60e8b269423000f1603cb2299608231996bd9d54"}, + {file = "matplotlib-3.7.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e4e9a868e8163abaaa8259842d85f949a919e1ead17644fb77a60427c90473c"}, + {file = "matplotlib-3.7.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fa7ebc995a7d747dacf0a717d0eb3aa0f0c6a0e9ea88b0194d3a3cd241a1500f"}, + {file = "matplotlib-3.7.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3785bfd83b05fc0e0c2ae4c4a90034fe693ef96c679634756c50fe6efcc09856"}, + {file = "matplotlib-3.7.5-cp39-cp39-win32.whl", hash = "sha256:29b058738c104d0ca8806395f1c9089dfe4d4f0f78ea765c6c704469f3fffc81"}, + {file = "matplotlib-3.7.5-cp39-cp39-win_amd64.whl", hash = "sha256:fd4028d570fa4b31b7b165d4a685942ae9cdc669f33741e388c01857d9723eab"}, + {file = "matplotlib-3.7.5-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2a9a3f4d6a7f88a62a6a18c7e6a84aedcaf4faf0708b4ca46d87b19f1b526f88"}, + {file = "matplotlib-3.7.5-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9b3fd853d4a7f008a938df909b96db0b454225f935d3917520305b90680579c"}, + {file = "matplotlib-3.7.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0ad550da9f160737d7890217c5eeed4337d07e83ca1b2ca6535078f354e7675"}, + {file = "matplotlib-3.7.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:20da7924a08306a861b3f2d1da0d1aa9a6678e480cf8eacffe18b565af2813e7"}, + {file = "matplotlib-3.7.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b45c9798ea6bb920cb77eb7306409756a7fab9db9b463e462618e0559aecb30e"}, + {file = "matplotlib-3.7.5-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a99866267da1e561c7776fe12bf4442174b79aac1a47bd7e627c7e4d077ebd83"}, + {file = "matplotlib-3.7.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b6aa62adb6c268fc87d80f963aca39c64615c31830b02697743c95590ce3fbb"}, + {file = "matplotlib-3.7.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e530ab6a0afd082d2e9c17eb1eb064a63c5b09bb607b2b74fa41adbe3e162286"}, + {file = "matplotlib-3.7.5.tar.gz", hash = "sha256:1e5c971558ebc811aa07f54c7b7c677d78aa518ef4c390e14673a09e0860184a"}, ] [package.dependencies] contourpy = ">=1.0.1" cycler = ">=0.10" fonttools = ">=4.22.0" -kiwisolver = ">=1.3.1" -numpy = ">=1.23" +importlib-resources = {version = ">=3.2.0", markers = "python_version < \"3.10\""} +kiwisolver = ">=1.0.1" +numpy = ">=1.20,<2" packaging = ">=20.0" -pillow = ">=8" +pillow = ">=6.2.0" pyparsing = ">=2.3.1" python-dateutil = ">=2.7" -[package.extras] -dev = ["meson-python (>=0.13.1)", "numpy (>=1.25)", "pybind11 (>=2.6)", "setuptools (>=64)", "setuptools_scm (>=7)"] - [[package]] name = "matplotlib-inline" version = "0.1.7" description = "Inline Matplotlib backend for Jupyter" -optional = false +optional = true python-versions = ">=3.8" files = [ {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, @@ -926,17 +860,6 @@ files = [ [package.dependencies] traitlets = "*" -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - [[package]] name = "mdurl" version = "0.1.2" @@ -1043,21 +966,58 @@ files = [ [[package]] name = "networkx" -version = "3.3" +version = "3.1" description = "Python package for creating and manipulating graphs and networks" optional = true -python-versions = ">=3.10" +python-versions = ">=3.8" files = [ - {file = "networkx-3.3-py3-none-any.whl", hash = "sha256:28575580c6ebdaf4505b22c6256a2b9de86b316dc63ba9e93abde3d78dfdbcf2"}, - {file = "networkx-3.3.tar.gz", hash = "sha256:0c127d8b2f4865f59ae9cb8aafcd60b5c70f3241ebd66f7defad7c4ab90126c9"}, + {file = "networkx-3.1-py3-none-any.whl", hash = "sha256:4f33f68cb2afcf86f28a45f43efc27a9386b535d567d2127f8f61d51dec58d36"}, + {file = "networkx-3.1.tar.gz", hash = "sha256:de346335408f84de0eada6ff9fafafff9bcda11f0a0dfaa931133debb146ab61"}, ] [package.extras] -default = ["matplotlib (>=3.6)", "numpy (>=1.23)", "pandas (>=1.4)", "scipy (>=1.9,!=1.11.0,!=1.11.1)"] -developer = ["changelist (==0.5)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"] -doc = ["myst-nb (>=1.0)", "numpydoc (>=1.7)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.14)", "sphinx (>=7)", "sphinx-gallery (>=0.14)", "texext (>=0.6.7)"] -extra = ["lxml (>=4.6)", "pydot (>=2.0)", "pygraphviz (>=1.12)", "sympy (>=1.10)"] -test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] +default = ["matplotlib (>=3.4)", "numpy (>=1.20)", "pandas (>=1.3)", "scipy (>=1.8)"] +developer = ["mypy (>=1.1)", "pre-commit (>=3.2)"] +doc = ["nb2plots (>=0.6)", "numpydoc (>=1.5)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.13)", "sphinx (>=6.1)", "sphinx-gallery (>=0.12)", "texext (>=0.6.7)"] +extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.10)", "sympy (>=1.10)"] +test = ["codecov (>=2.1)", "pytest (>=7.2)", "pytest-cov (>=4.0)"] + +[[package]] +name = "numpy" +version = "1.24.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64"}, + {file = "numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1"}, + {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4"}, + {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6"}, + {file = "numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc"}, + {file = "numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e"}, + {file = "numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810"}, + {file = "numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254"}, + {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7"}, + {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5"}, + {file = "numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d"}, + {file = "numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694"}, + {file = "numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61"}, + {file = "numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f"}, + {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e"}, + {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc"}, + {file = "numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2"}, + {file = "numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706"}, + {file = "numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400"}, + {file = "numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f"}, + {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9"}, + {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d"}, + {file = "numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835"}, + {file = "numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2"}, + {file = "numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463"}, +] [[package]] name = "numpy" @@ -1261,7 +1221,7 @@ files = [ name = "parso" version = "0.8.4" description = "A Python Parser" -optional = false +optional = true python-versions = ">=3.6" files = [ {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, @@ -1287,7 +1247,7 @@ files = [ name = "pexpect" version = "4.9.0" description = "Pexpect allows easy control of interactive console applications." -optional = false +optional = true python-versions = "*" files = [ {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, @@ -1384,20 +1344,19 @@ typing = ["typing-extensions"] xmp = ["defusedxml"] [[package]] -name = "platformdirs" -version = "4.2.2" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +name = "plotly" +version = "5.22.0" +description = "An open-source, interactive data visualization library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, + {file = "plotly-5.22.0-py3-none-any.whl", hash = "sha256:68fc1901f098daeb233cc3dd44ec9dc31fb3ca4f4e53189344199c43496ed006"}, + {file = "plotly-5.22.0.tar.gz", hash = "sha256:859fdadbd86b5770ae2466e542b761b247d1c6b49daed765b95bb8c7063e7469"}, ] -[package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] -type = ["mypy (>=1.8)"] +[package.dependencies] +packaging = "*" +tenacity = ">=6.2.0" [[package]] name = "pluggy" @@ -1418,7 +1377,7 @@ testing = ["pytest", "pytest-benchmark"] name = "prompt-toolkit" version = "3.0.47" description = "Library for building powerful interactive command lines in Python" -optional = false +optional = true python-versions = ">=3.7.0" files = [ {file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"}, @@ -1432,7 +1391,7 @@ wcwidth = "*" name = "ptyprocess" version = "0.7.0" description = "Run a subprocess in a pseudo terminal" -optional = false +optional = true python-versions = "*" files = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, @@ -1443,7 +1402,7 @@ files = [ name = "pure-eval" version = "0.2.2" description = "Safely evaluate AST nodes without side effects" -optional = false +optional = true python-versions = "*" files = [ {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, @@ -1485,34 +1444,6 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] -[[package]] -name = "pylint" -version = "2.17.7" -description = "python code static checker" -optional = false -python-versions = ">=3.7.2" -files = [ - {file = "pylint-2.17.7-py3-none-any.whl", hash = "sha256:27a8d4c7ddc8c2f8c18aa0050148f89ffc09838142193fdbe98f172781a3ff87"}, - {file = "pylint-2.17.7.tar.gz", hash = "sha256:f4fcac7ae74cfe36bc8451e931d8438e4a476c20314b1101c458ad0f05191fad"}, -] - -[package.dependencies] -astroid = ">=2.15.8,<=2.17.0-dev0" -colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -dill = [ - {version = ">=0.2", markers = "python_version < \"3.11\""}, - {version = ">=0.3.6", markers = "python_version >= \"3.11\""}, -] -isort = ">=4.2.5,<6" -mccabe = ">=0.6,<0.8" -platformdirs = ">=2.2.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -tomlkit = ">=0.10.1" - -[package.extras] -spelling = ["pyenchant (>=3.2,<4.0)"] -testutils = ["gitpython (>3)"] - [[package]] name = "pyparsing" version = "3.1.2" @@ -1529,13 +1460,13 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "7.4.4" +version = "8.2.2" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, - {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, + {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, + {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, ] [package.dependencies] @@ -1543,11 +1474,11 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +pluggy = ">=1.5,<2.0" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" @@ -1654,10 +1585,37 @@ files = [ [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] +[[package]] +name = "ruff" +version = "0.4.10" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.4.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c2c4d0859305ac5a16310eec40e4e9a9dec5dcdfbe92697acd99624e8638dac"}, + {file = "ruff-0.4.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a79489607d1495685cdd911a323a35871abfb7a95d4f98fc6f85e799227ac46e"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1dd1681dfa90a41b8376a61af05cc4dc5ff32c8f14f5fe20dba9ff5deb80cd6"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c75c53bb79d71310dc79fb69eb4902fba804a81f374bc86a9b117a8d077a1784"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18238c80ee3d9100d3535d8eb15a59c4a0753b45cc55f8bf38f38d6a597b9739"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d8f71885bce242da344989cae08e263de29752f094233f932d4f5cfb4ef36a81"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:330421543bd3222cdfec481e8ff3460e8702ed1e58b494cf9d9e4bf90db52b9d"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e9b6fb3a37b772628415b00c4fc892f97954275394ed611056a4b8a2631365e"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f54c481b39a762d48f64d97351048e842861c6662d63ec599f67d515cb417f6"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:67fe086b433b965c22de0b4259ddfe6fa541c95bf418499bedb9ad5fb8d1c631"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:acfaaab59543382085f9eb51f8e87bac26bf96b164839955f244d07125a982ef"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3cea07079962b2941244191569cf3a05541477286f5cafea638cd3aa94b56815"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:338a64ef0748f8c3a80d7f05785930f7965d71ca260904a9321d13be24b79695"}, + {file = "ruff-0.4.10-py3-none-win32.whl", hash = "sha256:ffe3cd2f89cb54561c62e5fa20e8f182c0a444934bf430515a4b422f1ab7b7ca"}, + {file = "ruff-0.4.10-py3-none-win_amd64.whl", hash = "sha256:67f67cef43c55ffc8cc59e8e0b97e9e60b4837c8f21e8ab5ffd5d66e196e25f7"}, + {file = "ruff-0.4.10-py3-none-win_arm64.whl", hash = "sha256:dd1fcee327c20addac7916ca4e2653fbbf2e8388d8a6477ce5b4e986b68ae6c0"}, + {file = "ruff-0.4.10.tar.gz", hash = "sha256:3aa4f2bc388a30d346c56524f7cacca85945ba124945fe489952aadb6b5cd804"}, +] + [[package]] name = "shellingham" version = "1.5.4" @@ -1684,7 +1642,7 @@ files = [ name = "stack-data" version = "0.6.3" description = "Extract data from python stack frames and tracebacks for informative displays" -optional = false +optional = true python-versions = "*" files = [ {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, @@ -1715,17 +1673,32 @@ mpmath = ">=1.1.0,<1.4.0" [[package]] name = "tbb" -version = "2021.12.0" +version = "2021.13.0" description = "Intel® oneAPI Threading Building Blocks (oneTBB)" optional = true python-versions = "*" files = [ - {file = "tbb-2021.12.0-py2.py3-none-manylinux1_i686.whl", hash = "sha256:f2cc9a7f8ababaa506cbff796ce97c3bf91062ba521e15054394f773375d81d8"}, - {file = "tbb-2021.12.0-py2.py3-none-manylinux1_x86_64.whl", hash = "sha256:a925e9a7c77d3a46ae31c34b0bb7f801c4118e857d137b68f68a8e458fcf2bd7"}, - {file = "tbb-2021.12.0-py3-none-win32.whl", hash = "sha256:b1725b30c174048edc8be70bd43bb95473f396ce895d91151a474d0fa9f450a8"}, - {file = "tbb-2021.12.0-py3-none-win_amd64.whl", hash = "sha256:fc2772d850229f2f3df85f1109c4844c495a2db7433d38200959ee9265b34789"}, + {file = "tbb-2021.13.0-py2.py3-none-manylinux1_i686.whl", hash = "sha256:a2567725329639519d46d92a2634cf61e76601dac2f777a05686fea546c4fe4f"}, + {file = "tbb-2021.13.0-py2.py3-none-manylinux1_x86_64.whl", hash = "sha256:aaf667e92849adb012b8874d6393282afc318aca4407fc62f912ee30a22da46a"}, + {file = "tbb-2021.13.0-py3-none-win32.whl", hash = "sha256:6669d26703e9943f6164c6407bd4a237a45007e79b8d3832fe6999576eaaa9ef"}, + {file = "tbb-2021.13.0-py3-none-win_amd64.whl", hash = "sha256:3528a53e4bbe64b07a6112b4c5a00ff3c61924ee46c9c68e004a1ac7ad1f09c3"}, +] + +[[package]] +name = "tenacity" +version = "8.4.1" +description = "Retry code until it succeeds" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tenacity-8.4.1-py3-none-any.whl", hash = "sha256:28522e692eda3e1b8f5e99c51464efcc0b9fc86933da92415168bc1c4e2308fa"}, + {file = "tenacity-8.4.1.tar.gz", hash = "sha256:54b1412b878ddf7e1f1577cd49527bad8cdef32421bd599beac0c6c3f10582fd"}, ] +[package.extras] +doc = ["reno", "sphinx"] +test = ["pytest", "tornado (>=4.5)", "typeguard"] + [[package]] name = "tomli" version = "2.0.1" @@ -1806,7 +1779,7 @@ optree = ["optree (>=0.9.1)"] name = "traitlets" version = "5.14.3" description = "Traitlets Python configuration system" -optional = false +optional = true python-versions = ">=3.8" files = [ {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, @@ -1842,18 +1815,22 @@ tutorials = ["matplotlib", "pandas", "tabulate", "torch"] [[package]] name = "typeguard" -version = "2.13.3" +version = "4.3.0" description = "Run-time type checker for Python" optional = true -python-versions = ">=3.5.3" +python-versions = ">=3.8" files = [ - {file = "typeguard-2.13.3-py3-none-any.whl", hash = "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1"}, - {file = "typeguard-2.13.3.tar.gz", hash = "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4"}, + {file = "typeguard-4.3.0-py3-none-any.whl", hash = "sha256:4d24c5b39a117f8a895b9da7a9b3114f04eb63bade45a4492de49b175b6f7dfa"}, + {file = "typeguard-4.3.0.tar.gz", hash = "sha256:92ee6a0aec9135181eae6067ebd617fd9de8d75d714fb548728a4933b1dea651"}, ] +[package.dependencies] +importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} +typing-extensions = ">=4.10.0" + [package.extras] -doc = ["sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["mypy", "pytest", "typing-extensions"] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.3.0)"] +test = ["coverage[toml] (>=7)", "mypy (>=1.2.0)", "pytest (>=7)"] [[package]] name = "typer" @@ -1883,11 +1860,26 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +[[package]] +name = "typing-inspect" +version = "0.9.0" +description = "Runtime inspection utilities for typing module." +optional = false +python-versions = "*" +files = [ + {file = "typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f"}, + {file = "typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78"}, +] + +[package.dependencies] +mypy-extensions = ">=0.3.0" +typing-extensions = ">=3.7.4" + [[package]] name = "wcwidth" version = "0.2.13" description = "Measures the displayed width of unicode strings in a terminal" -optional = false +optional = true python-versions = "*" files = [ {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, @@ -1895,88 +1887,44 @@ files = [ ] [[package]] -name = "wrapt" -version = "1.16.0" -description = "Module for decorators, wrappers and monkey patching." +name = "zanj" +version = "0.2.3" +description = "save and load complex objects to disk without pickling" +optional = true +python-versions = "<4.0,>=3.10" +files = [ + {file = "zanj-0.2.3-py3-none-any.whl", hash = "sha256:4992eba4b7b48264da9a243922d69f9689c1d6b11e7915b49e84aeda61b43297"}, + {file = "zanj-0.2.3.tar.gz", hash = "sha256:d0d45ddda07911723d2c2e57db84b1613b1498f66eb418af197d192c89fb87e9"}, +] + +[package.dependencies] +muutils = {version = ">=0.5.1,<0.7.0", extras = ["array"]} + +[package.extras] +pandas = ["pandas (>=1.5.3)"] + +[[package]] +name = "zipp" +version = "3.19.2" +description = "Backport of pathlib-compatible object wrapper for zip files" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, - {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, - {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, - {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, - {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, - {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, - {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, - {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, - {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, - {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, - {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, - {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, - {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, - {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, - {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, - {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, - {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, - {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, - {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, - {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, - {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, - {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, - {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, - {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, + {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, + {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, ] +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + [extras] -array = ["jaxtyping", "numpy", "torch"] +array = ["jaxtyping", "numpy", "numpy", "torch"] +array-no-torch = ["jaxtyping", "numpy", "numpy"] +notebook = ["ipython"] +zanj = ["zanj"] [metadata] lock-version = "2.0" -python-versions = "^3.10" -content-hash = "b103697ab0f6424f1007107b22e004c99ebe010859e44340308feaa642bc9824" +python-versions = "^3.8" +content-hash = "5d222b1676114ac943bd9abb6048a1eed168c24d32be9ca026bde19f10371702" diff --git a/pyproject.toml b/pyproject.toml index dd445a01..19abb2d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,51 +1,68 @@ [tool.poetry] name = "muutils" -version = "0.5.12" -description = "A collection of miscellaneous python utilities" +version = "0.6.0" +description = "miscellaneous python utilities" license = "GPL-3.0-only" authors = ["mivanit "] readme = "README.md" classifiers=[ + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Development Status :: 4 - Beta", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Operating System :: OS Independent", + "Topic :: Utilities", + "Typing :: Typed", ] repository = "https://github.com/mivanit/muutils" [tool.poetry.dependencies] -python = "^3.10" -numpy = { version = "^1.22.4", optional = true } +python = "^3.8" +# [array] +numpy = [ + { version = "^1.24.4", optional = true, markers = "python_version < '3.9'" }, + { version = "^1.26.4", optional = true, markers = "python_version >= '3.9'" }, +] torch = { version = ">=1.13.1", optional = true } jaxtyping = { version = "^0.2.12", optional = true } - -[tool.poetry.extras] -array = ["numpy", "torch", "jaxtyping"] +# [notebook] +ipython = { version = "^8.20.0", optional = true, python = "^3.10" } +# [zanj] +zanj = { version = "^0.2.3", optional = true, python = "^3.10" } [tool.poetry.group.dev.dependencies] -pytest = "^7.2.1" -black = "^24.1.1" -pylint = "^2.16.4" -pycln = "^2.1.3" -isort = "^5.12.0" +# typing mypy = "^1.0.1" +# tests & coverage +pytest = "^8.2.2" pytest-cov = "^4.1.0" coverage-badge = "^1.1.0" +# for testing plotting matplotlib = "^3.0.0" -ipython = "^8.20.0" +plotly = "^5.0.0" + +[tool.poetry.group.lint.dependencies] +pycln = "^2.1.3" +ruff = "^0.4.8" + +[tool.poetry.extras] +array = ["numpy", "torch", "jaxtyping"] +# special group for CI, where we install cpu torch separately +array_no_torch = ["numpy", "jaxtyping"] +notebook = ["ipython"] +zanj = ["zanj"] [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" -[[tool.poetry.source]] -name = "torch_cpu" -url = "https://download.pytorch.org/whl/cpu" -priority = "explicit" - -# TODO: make all of the following ignored across all formatting/linting -# tests/input_data, tests/junk_data, muutils/_wip +# [[tool.poetry.source]] +# name = "torch_cpu" +# url = "https://download.pytorch.org/whl/cpu" +# priority = "explicit" [tool.pytest.ini_options] filterwarnings = [ @@ -54,18 +71,14 @@ filterwarnings = [ "ignore::muutils.json_serialize.serializable_dataclass.ZanjMissingWarning", # don't show warning for missing zanj (can't have as a dep since zanj depends on muutils) ] +[tool.ruff] +# Exclude the directories specified in the global excludes +exclude = ["tests/input_data", "tests/junk_data", "muutils/_wip"] + [tool.pycln] all = true -exclude = "tests/input_data" - -[tool.isort] -profile = "black" -ignore_comments = false -extend_skip = "tests/input_data" - -[tool.black] -extend-exclude = "tests/input_data" +exclude = ["tests/input_data", "tests/junk_data", "muutils/_wip"] [tool.mypy] -exclude = ['_wip', "tests/input_data", "tests/junk_data"] -show_error_codes = true +exclude = ["tests/input_data", "tests/junk_data", "muutils/_wip"] +show_error_codes = true \ No newline at end of file diff --git a/tests/unit/json_serialize/serializable_dataclass/test_helpers.py b/tests/unit/json_serialize/serializable_dataclass/test_helpers.py index 3184ba7f..81c92b2d 100644 --- a/tests/unit/json_serialize/serializable_dataclass/test_helpers.py +++ b/tests/unit/json_serialize/serializable_dataclass/test_helpers.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dataclasses import dataclass import numpy as np diff --git a/tests/unit/json_serialize/serializable_dataclass/test_sdc_defaults.py b/tests/unit/json_serialize/serializable_dataclass/test_sdc_defaults.py index 1272fc46..8a1bf203 100644 --- a/tests/unit/json_serialize/serializable_dataclass/test_sdc_defaults.py +++ b/tests/unit/json_serialize/serializable_dataclass/test_sdc_defaults.py @@ -1,3 +1,6 @@ +from __future__ import annotations + + from muutils.json_serialize import ( JsonSerializer, SerializableDataclass, diff --git a/tests/unit/json_serialize/serializable_dataclass/test_sdc_properties_nested.py b/tests/unit/json_serialize/serializable_dataclass/test_sdc_properties_nested.py index 5e951b01..091ef291 100644 --- a/tests/unit/json_serialize/serializable_dataclass/test_sdc_properties_nested.py +++ b/tests/unit/json_serialize/serializable_dataclass/test_sdc_properties_nested.py @@ -1,5 +1,15 @@ +from __future__ import annotations + +import sys + +import pytest + from muutils.json_serialize import SerializableDataclass, serializable_dataclass +SUPPORTS_KW_ONLY: bool = sys.version_info >= (3, 10) + +print(f"{SUPPORTS_KW_ONLY = }") + @serializable_dataclass class Person(SerializableDataclass): @@ -12,7 +22,7 @@ def full_name(self) -> str: @serializable_dataclass( - kw_only=True, properties_to_serialize=["full_name", "full_title"] + kw_only=SUPPORTS_KW_ONLY, properties_to_serialize=["full_name", "full_title"] ) class TitledPerson(Person): title: str @@ -22,8 +32,27 @@ def full_title(self) -> str: return f"{self.title} {self.full_name}" +@serializable_dataclass( + kw_only=SUPPORTS_KW_ONLY, + properties_to_serialize=["full_name", "not_a_real_property"], +) +class AgedPerson_not_valid(Person): + title: str + + @property + def full_title(self) -> str: + return f"{self.title} {self.full_name}" + + +def test_invalid_properties_to_serialize(): + instance = AgedPerson_not_valid(first_name="Jane", last_name="Smith", title="Dr.") + + with pytest.raises(AttributeError): + instance.serialize() + + def test_serialize_person(): - instance = Person("John", "Doe") + instance = Person(first_name="John", last_name="Doe") serialized = instance.serialize() @@ -41,6 +70,10 @@ def test_serialize_person(): def test_serialize_titled_person(): instance = TitledPerson(first_name="Jane", last_name="Smith", title="Dr.") + if SUPPORTS_KW_ONLY: + with pytest.raises(TypeError): + TitledPerson("Jane", "Smith", "Dr.") + serialized = instance.serialize() assert serialized == { diff --git a/tests/unit/json_serialize/serializable_dataclass/test_serializable_dataclass.py b/tests/unit/json_serialize/serializable_dataclass/test_serializable_dataclass.py index 2ebcd2fb..7479a82a 100644 --- a/tests/unit/json_serialize/serializable_dataclass/test_serializable_dataclass.py +++ b/tests/unit/json_serialize/serializable_dataclass/test_serializable_dataclass.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +import typing from typing import Any import pytest @@ -8,6 +11,10 @@ serializable_field, ) +from muutils.json_serialize.serializable_dataclass import ( + FieldIsNotInitOrSerializeWarning, +) + # pylint: disable=missing-class-docstring, unused-variable @@ -15,7 +22,7 @@ class BasicAutofields(SerializableDataclass): a: str b: int - c: list[int] + c: typing.List[int] def test_basic_auto_fields(): @@ -51,7 +58,7 @@ def test_basic_diff(): class SimpleFields(SerializableDataclass): d: str e: int = 42 - f: list[int] = serializable_field(default_factory=list) + f: typing.List[int] = serializable_field(default_factory=list) # noqa: F821 @serializable_dataclass @@ -105,7 +112,9 @@ def test_simple_fields_serialization(simple_fields_instance): def test_simple_fields_loading(simple_fields_instance): serialized = simple_fields_instance.serialize() + loaded = SimpleFields.load(serialized) + assert loaded == simple_fields_instance assert loaded.diff(simple_fields_instance) == {} assert simple_fields_instance.diff(loaded) == {} @@ -122,8 +131,10 @@ def test_field_options_serialization(field_options_instance): def test_field_options_loading(field_options_instance): + # ignore a `FieldIsNotInitOrSerializeWarning` serialized = field_options_instance.serialize() - loaded = FieldOptions.load(serialized) + with pytest.warns(FieldIsNotInitOrSerializeWarning): + loaded = FieldOptions.load(serialized) assert loaded == field_options_instance @@ -243,7 +254,7 @@ def test_person_serialization(): class FullPerson(SerializableDataclass): name: str = serializable_field() age: int = serializable_field(default=-1) - items: list[str] = serializable_field(default_factory=list) + items: typing.List[str] = serializable_field(default_factory=list) @property def full_name(self) -> str: @@ -261,6 +272,7 @@ def full_name(self) -> str: assert serialized == expected_ser, f"Expected {expected_ser}, got {serialized}" loaded = FullPerson.load(serialized) + assert loaded == person @@ -286,7 +298,7 @@ class CustomSerialization(SerializableDataclass): class Nested_with_Container(SerializableDataclass): val_int: int val_str: str - val_list: list[BasicAutofields] = serializable_field( + val_list: typing.List[BasicAutofields] = serializable_field( default_factory=list, serialization_fn=lambda x: [y.serialize() for y in x], loading_fn=lambda x: [BasicAutofields.load(y) for y in x["val_list"]], @@ -325,7 +337,9 @@ def test_nested_with_container(): } assert serialized == expected_ser + loaded = Nested_with_Container.load(serialized) + assert loaded == instance @@ -353,7 +367,7 @@ class nested_custom(SerializableDataclass): data1: Custom_class_with_serialization -def test_nested_custom(): +def test_nested_custom(recwarn): # this will send some warnings but whatever instance = nested_custom( value=42.0, data1=Custom_class_with_serialization(1, "hello") ) @@ -366,3 +380,23 @@ def test_nested_custom(): assert serialized == expected_ser loaded = nested_custom.load(serialized) assert loaded == instance + + +def test_deserialize_fn(): + @serializable_dataclass + class DeserializeFn(SerializableDataclass): + data: int = serializable_field( + serialization_fn=lambda x: str(x), + deserialize_fn=lambda x: int(x), + ) + + instance = DeserializeFn(data=5) + serialized = instance.serialize() + assert serialized == { + "data": "5", + "__format__": "DeserializeFn(SerializableDataclass)", + } + + loaded = DeserializeFn.load(serialized) + assert loaded == instance + assert loaded.data == 5 diff --git a/tests/unit/json_serialize/test_array.py b/tests/unit/json_serialize/test_array.py index dc3d767a..91e9b445 100644 --- a/tests/unit/json_serialize/test_array.py +++ b/tests/unit/json_serialize/test_array.py @@ -39,6 +39,7 @@ def test_arr_metadata(self): ("list", list), ("array_list_meta", dict), ("array_hex_meta", dict), + ("array_b64_meta", dict), ], ) def test_serialize_array(self, array_mode: ArrayMode, expected_type: type): @@ -55,7 +56,12 @@ def test_load_array(self): assert np.array_equal(loaded_array, self.array_3d) def test_serialize_load_integration(self): - for array_mode in ["list", "array_list_meta", "array_hex_meta"]: + for array_mode in [ + "list", + "array_list_meta", + "array_hex_meta", + "array_b64_meta", + ]: for array in [self.array_1d, self.array_2d, self.array_3d]: serialized_array = serialize_array( self.jser, array, "test_path", array_mode=array_mode @@ -64,7 +70,12 @@ def test_serialize_load_integration(self): assert np.array_equal(loaded_array, array) def test_serialize_load_zero_dim(self): - for array_mode in ["list", "array_list_meta", "array_hex_meta"]: + for array_mode in [ + "list", + "array_list_meta", + "array_hex_meta", + "array_b64_meta", + ]: serialized_array = serialize_array( self.jser, self.array_zero_dim, "test_path", array_mode=array_mode ) diff --git a/tests/unit/logger/test_logger.py b/tests/unit/logger/test_logger.py index 29340d29..15be01dc 100644 --- a/tests/unit/logger/test_logger.py +++ b/tests/unit/logger/test_logger.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from muutils.logger import Logger diff --git a/tests/unit/logger/test_timer_context.py b/tests/unit/logger/test_timer_context.py index 63e48786..5d164abd 100644 --- a/tests/unit/logger/test_timer_context.py +++ b/tests/unit/logger/test_timer_context.py @@ -1,9 +1,12 @@ +from __future__ import annotations + from muutils.logger.timing import TimerContext def test_timer_context() -> None: with TimerContext() as timer: x: float = 1.0 + print(x) assert isinstance(timer.start_time, float) assert isinstance(timer.end_time, float) diff --git a/tests/unit/misc/test_freeze.py b/tests/unit/misc/test_freeze.py index a0a2bd99..819e1478 100644 --- a/tests/unit/misc/test_freeze.py +++ b/tests/unit/misc/test_freeze.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from muutils.misc import freeze diff --git a/tests/unit/misc/test_misc.py b/tests/unit/misc/test_misc.py index 01c0aec8..ac5c4302 100644 --- a/tests/unit/misc/test_misc.py +++ b/tests/unit/misc/test_misc.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from muutils.misc import ( @@ -6,6 +8,8 @@ list_join, list_split, sanitize_fname, + sanitize_identifier, + sanitize_name, stable_hash, ) @@ -33,6 +37,42 @@ def test_sanitize_fname(): assert sanitize_fname(None) == "_None_", "None input should return '_None_'" +def test_sanitize_name(): + assert sanitize_name("Hello World") == "HelloWorld" + assert sanitize_name("Hello_World", additional_allowed_chars="_") == "Hello_World" + assert sanitize_name("Hello!World", replace_invalid="-") == "Hello-World" + assert sanitize_name(None) == "_None_" + assert sanitize_name(None, when_none="Empty") == "Empty" + with pytest.raises(ValueError): + sanitize_name(None, when_none=None) + assert sanitize_name("123abc") == "123abc" + assert sanitize_name("123abc", leading_digit_prefix="_") == "_123abc" + + +def test_sanitize_fname_2(): + assert sanitize_fname("file name.txt") == "filename.txt" + assert sanitize_fname("file_name.txt") == "file_name.txt" + assert sanitize_fname("file-name.txt") == "file-name.txt" + assert sanitize_fname("file!name.txt") == "filename.txt" + assert sanitize_fname(None) == "_None_" + assert sanitize_fname(None, when_none="Empty") == "Empty" + with pytest.raises(ValueError): + sanitize_fname(None, when_none=None) + assert sanitize_fname("123file.txt") == "123file.txt" + assert sanitize_fname("123file.txt", leading_digit_prefix="_") == "_123file.txt" + + +def test_sanitize_identifier(): + assert sanitize_identifier("variable_name") == "variable_name" + assert sanitize_identifier("VariableName") == "VariableName" + assert sanitize_identifier("variable!name") == "variablename" + assert sanitize_identifier("123variable") == "_123variable" + assert sanitize_identifier(None) == "_None_" + assert sanitize_identifier(None, when_none="Empty") == "Empty" + with pytest.raises(ValueError): + sanitize_identifier(None, when_none=None) + + def test_dict_to_filename(): data = {"key1": "value1", "key2": "value2"} assert ( diff --git a/tests/unit/misc/test_numerical_conversions.py b/tests/unit/misc/test_numerical_conversions.py index f1b0242b..15c39ba5 100644 --- a/tests/unit/misc/test_numerical_conversions.py +++ b/tests/unit/misc/test_numerical_conversions.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import random from math import isclose, isinf, isnan diff --git a/tests/unit/nbutils/test_configure_notebook.py b/tests/unit/nbutils/test_configure_notebook.py index 293820d9..e9116a31 100644 --- a/tests/unit/nbutils/test_configure_notebook.py +++ b/tests/unit/nbutils/test_configure_notebook.py @@ -44,11 +44,13 @@ def test_configure_notebook(): def test_plotshow_save(): setup_plots(plot_mode="save", fig_basepath=JUNK_DATA_PATH) - plt.plot([1, 2, 3], [1, 2, 3]) - plotshow() + with pytest.warns(UnknownFigureFormatWarning): + plt.plot([1, 2, 3], [1, 2, 3]) + plotshow() assert os.path.exists(os.path.join(JUNK_DATA_PATH, "figure-1.pdf")) - plt.plot([3, 6, 9], [2, 4, 8]) - plotshow() + with pytest.warns(UnknownFigureFormatWarning): + plt.plot([3, 6, 9], [2, 4, 8]) + plotshow() assert os.path.exists(os.path.join(JUNK_DATA_PATH, "figure-2.pdf")) @@ -68,14 +70,16 @@ def test_plotshow_save_mixed(): fig_basepath=JUNK_DATA_PATH, fig_numbered_fname="mixedfig-{num}", ) - plt.plot([1, 2, 3], [1, 2, 3]) - plotshow() + with pytest.warns(UnknownFigureFormatWarning): + plt.plot([1, 2, 3], [1, 2, 3]) + plotshow() assert os.path.exists(os.path.join(JUNK_DATA_PATH, "mixedfig-1.pdf")) plt.plot([3, 6, 9], [2, 4, 8]) plotshow(fname="mixed-test.pdf") assert os.path.exists(os.path.join(JUNK_DATA_PATH, "mixed-test.pdf")) - plt.plot([1, 1, 1], [1, 9, 9]) - plotshow() + with pytest.warns(UnknownFigureFormatWarning): + plt.plot([1, 1, 1], [1, 9, 9]) + plotshow() assert os.path.exists(os.path.join(JUNK_DATA_PATH, "mixedfig-3.pdf")) @@ -90,6 +94,17 @@ def test_warn_unknown_format(): plotshow() +def test_no_warn_unknown_format_2(): + with pytest.warns(UnknownFigureFormatWarning): + setup_plots( + plot_mode="save", + fig_basepath=JUNK_DATA_PATH, + fig_numbered_fname="mixedfig-{num}", + ) + plt.plot([1, 2, 3], [1, 2, 3]) + plotshow("no-format") + + def test_no_warn_pdf_format(): with warnings.catch_warnings(): warnings.simplefilter("error") diff --git a/tests/unit/nbutils/test_conversion.py b/tests/unit/nbutils/test_conversion.py index 52f9f01f..f984432f 100644 --- a/tests/unit/nbutils/test_conversion.py +++ b/tests/unit/nbutils/test_conversion.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import itertools import os diff --git a/tests/unit/test_dictmagic.py b/tests/unit/test_dictmagic.py index 1375a029..d95f6691 100644 --- a/tests/unit/test_dictmagic.py +++ b/tests/unit/test_dictmagic.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from muutils.dictmagic import ( @@ -260,7 +262,8 @@ def test_condense_tensor_dict_basic(tensor_data): def test_condense_tensor_dict_shapes_convert(tensor_data): - shapes_convert = lambda x: x # Returning the actual shape tuple + # Returning the actual shape tuple + shapes_convert = lambda x: x # noqa: E731 assert condense_tensor_dict( tensor_data, shapes_convert=shapes_convert, diff --git a/tests/unit/test_errormode.py b/tests/unit/test_errormode.py new file mode 100644 index 00000000..a6d4e272 --- /dev/null +++ b/tests/unit/test_errormode.py @@ -0,0 +1,211 @@ +from __future__ import annotations + +import warnings + +from muutils.errormode import ErrorMode, ERROR_MODE_ALIASES + +import pytest + +""" +import typing +import warnings +from enum import Enum + +class ErrorMode(Enum): + EXCEPT = "except" + WARN = "warn" + IGNORE = "ignore" + + def process( + self, + msg: str, + except_cls: typing.Type[Exception] = ValueError, + warn_cls: typing.Type[Warning] = UserWarning, + except_from: typing.Optional[typing.Type[Exception]] = None, + ): + if self is ErrorMode.EXCEPT: + if except_from is not None: + raise except_from(msg) from except_from + else: + raise except_cls(msg) + elif self is ErrorMode.WARN: + warnings.warn(msg, warn_cls) + elif self is ErrorMode.IGNORE: + pass + else: + raise ValueError(f"Unknown error mode {self}") +""" + + +def test_except(): + with pytest.raises(ValueError): + ErrorMode.EXCEPT.process("test-except", except_cls=ValueError) + + with pytest.raises(TypeError): + ErrorMode.EXCEPT.process("test-except", except_cls=TypeError) + + with pytest.raises(RuntimeError): + ErrorMode.EXCEPT.process("test-except", except_cls=RuntimeError) + + with pytest.raises(KeyError): + ErrorMode.EXCEPT.process("test-except", except_cls=KeyError) + + with pytest.raises(KeyError): + ErrorMode.EXCEPT.process( + "test-except", except_cls=KeyError, except_from=ValueError("base exception") + ) + + +def test_warn(): + with pytest.warns(UserWarning): + ErrorMode.WARN.process("test-warn", warn_cls=UserWarning) + + with pytest.warns(Warning): + ErrorMode.WARN.process("test-warn", warn_cls=Warning) + + with pytest.warns(DeprecationWarning): + ErrorMode.WARN.process("test-warn", warn_cls=DeprecationWarning) + + +def test_ignore(): + with warnings.catch_warnings(record=True) as w: + ErrorMode.IGNORE.process("test-ignore") + + ErrorMode.IGNORE.process("test-ignore", except_cls=ValueError) + ErrorMode.IGNORE.process("test-ignore", except_from=TypeError) + + ErrorMode.IGNORE.process("test-ignore", warn_cls=UserWarning) + + assert len(w) == 0, f"There should be no warnings: {w}" + + +def test_except_custom(): + class MyCustomError(ValueError): + pass + + with pytest.raises(MyCustomError): + ErrorMode.EXCEPT.process("test-except", except_cls=MyCustomError) + + +def test_warn_custom(): + class MyCustomWarning(Warning): + pass + + with pytest.warns(MyCustomWarning): + ErrorMode.WARN.process("test-warn", warn_cls=MyCustomWarning) + + +def test_invalid_mode(): + with pytest.raises(ValueError): + ErrorMode("invalid") + + +def test_except_mode_chained_exception(): + try: + # set up the base exception + try: + raise KeyError("base exception") + except Exception as base_exception: + # catch it, raise another exception with it as the cause + ErrorMode.EXCEPT.process( + "Test chained exception", + except_cls=RuntimeError, + except_from=base_exception, + ) + # catch the outer exception + except RuntimeError as e: + assert str(e) == "Test chained exception" + # check that the cause is the base exception + assert isinstance(e.__cause__, KeyError) + assert repr(e.__cause__) == "KeyError('base exception')" + else: + assert False, "Expected RuntimeError with cause KeyError" + + +@pytest.mark.parametrize( + "mode, expected_mode", + [ + ("except", ErrorMode.EXCEPT), + ("warn", ErrorMode.WARN), + ("ignore", ErrorMode.IGNORE), + ("Except", ErrorMode.EXCEPT), + ("Warn", ErrorMode.WARN), + ("Ignore", ErrorMode.IGNORE), + (" \teXcEpT \n", ErrorMode.EXCEPT), + ("WaRn \t", ErrorMode.WARN), + (" \tIGNORE", ErrorMode.IGNORE), + ], +) +def test_from_any_strict_ok(mode, expected_mode): + assert ErrorMode.from_any(mode, allow_aliases=False) == expected_mode + + +@pytest.mark.parametrize( + "mode, excepted_error", + [ + (42, TypeError), + (42.0, TypeError), + (None, TypeError), + (object(), TypeError), + (True, TypeError), + (False, TypeError), + (["except"], TypeError), + ("invalid", KeyError), + (" \tinvalid", KeyError), + ("e", KeyError), + (" E", KeyError), + ("w", KeyError), + ("W", KeyError), + ("i", KeyError), + ("I", KeyError), + ("silent", KeyError), + ("Silent", KeyError), + ("quiet", KeyError), + ("Quiet", KeyError), + ("raise", KeyError), + ("Raise", KeyError), + ("error", KeyError), + ("Error", KeyError), + ("err", KeyError), + ("ErR\t", KeyError), + ("warning", KeyError), + ("Warning", KeyError), + ], +) +def test_from_any_strict_error(mode, excepted_error): + with pytest.raises(excepted_error): + ErrorMode.from_any(mode, allow_aliases=False) + + +@pytest.mark.parametrize( + "mode, expected_mode", + [ + *list(ERROR_MODE_ALIASES.items()), + *list((a.upper(), b) for a, b in ERROR_MODE_ALIASES.items()), + *list((a.title(), b) for a, b in ERROR_MODE_ALIASES.items()), + *list((a.capitalize(), b) for a, b in ERROR_MODE_ALIASES.items()), + *list((f" \t{a} \t", b) for a, b in ERROR_MODE_ALIASES.items()), + ], +) +def test_from_any_aliases_ok(mode, expected_mode): + assert ErrorMode.from_any(mode) == expected_mode + assert ErrorMode.from_any(mode, allow_aliases=True) == expected_mode + + +@pytest.mark.parametrize( + "mode, excepted_error", + [ + (42, TypeError), + (42.0, TypeError), + (None, TypeError), + (object(), TypeError), + (True, TypeError), + (False, TypeError), + (["except"], TypeError), + ("invalid", KeyError), + (" \tinvalid", KeyError), + ], +) +def test_from_any_aliases_error(mode, excepted_error): + with pytest.raises(excepted_error): + ErrorMode.from_any(mode, allow_aliases=True) diff --git a/tests/unit/test_group_equiv.py b/tests/unit/test_group_equiv.py index 80e8bc3b..a2499fc3 100644 --- a/tests/unit/test_group_equiv.py +++ b/tests/unit/test_group_equiv.py @@ -1,4 +1,5 @@ -# Assuming your functions are in a file named `group_by.py` +from __future__ import annotations + from muutils.group_equiv import group_by_equivalence diff --git a/tests/unit/test_kappa.py b/tests/unit/test_kappa.py index ab8b169c..860bbfd1 100644 --- a/tests/unit/test_kappa.py +++ b/tests/unit/test_kappa.py @@ -6,50 +6,50 @@ def test_Kappa_returns_Kappa_instance(): - func = lambda x: x**2 + func = lambda x: x**2 # noqa: E731 result = Kappa(func) assert isinstance(result, Mapping), "Kappa did not return a Mapping instance" def test_Kappa_getitem_calls_func(): - func = lambda x: x**2 + func = lambda x: x**2 # noqa: E731 result = Kappa(func) assert result[2] == 4, "__getitem__ did not correctly call the input function" def test_Kappa_doc_is_correctly_formatted(): - func = lambda x: x**2 + func = lambda x: x**2 # noqa: E731 result = Kappa(func) expected_doc = _BASE_DOC + "None" assert result.doc == expected_doc, "doc was not correctly formatted" def test_Kappa_getitem_works_with_different_functions(): - func = lambda x: x + 1 + func = lambda x: x + 1 # noqa: E731 result = Kappa(func) assert result[2] == 3, "__getitem__ did not correctly call the input function" - func = lambda x: str(x) + func = lambda x: str(x) # noqa: E731 result = Kappa(func) assert result[2] == "2", "__getitem__ did not correctly call the input function" def test_Kappa_iter_raises_NotImplementedError(): - func = lambda x: x**2 + func = lambda x: x**2 # noqa: E731 result = Kappa(func) with pytest.raises(NotImplementedError): iter(result) def test_Kappa_len_raises_NotImplementedError(): - func = lambda x: x**2 + func = lambda x: x**2 # noqa: E731 result = Kappa(func) with pytest.raises(NotImplementedError): len(result) def test_Kappa_doc_works_with_function_with_docstring(): - func = lambda x: x**2 + func = lambda x: x**2 # noqa: E731 func.__doc__ = "This is a test function" result = Kappa(func) expected_doc = _BASE_DOC + "This is a test function" diff --git a/tests/unit/test_mlutils.py b/tests/unit/test_mlutils.py index a3dbca14..f1fb1e63 100644 --- a/tests/unit/test_mlutils.py +++ b/tests/unit/test_mlutils.py @@ -1,3 +1,4 @@ +import sys from pathlib import Path from muutils.mlutils import get_checkpoint_paths_for_run, register_method @@ -21,7 +22,10 @@ def test_get_checkpoint_paths_for_run(): assert checkpoint_paths == [(123, checkpoint1_path), (456, checkpoint2_path)] -def test_register_method(): +BELOW_PY_3_10: bool = sys.version_info < (3, 10) + + +def test_register_method(recwarn): class TestEvalsA: evals = {} @@ -42,7 +46,16 @@ class TestEvalsB: def other_eval_function(): pass + if BELOW_PY_3_10: + assert len(recwarn) == 2 + else: + assert len(recwarn) == 0 + evalsA = TestEvalsA.evals evalsB = TestEvalsB.evals - assert list(evalsA.keys()) == ["eval_function"] - assert list(evalsB.keys()) == ["other_eval_function"] + if BELOW_PY_3_10: + assert len(evalsA) == 1 + assert len(evalsB) == 1 + else: + assert list(evalsA.keys()) == ["eval_function"] + assert list(evalsB.keys()) == ["other_eval_function"] diff --git a/tests/unit/test_statcounter.py b/tests/unit/test_statcounter.py index d10f1f04..cab9fbc2 100644 --- a/tests/unit/test_statcounter.py +++ b/tests/unit/test_statcounter.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import numpy as np from muutils.statcounter import StatCounter @@ -47,7 +49,6 @@ def test_statcounter() -> None: # arrs.append(np.random.randint(i, j, size=1000)) for a in arrs: - r = _compare_np_custom(a) assert all( diff --git a/tests/unit/test_sysinfo.py b/tests/unit/test_sysinfo.py index 023d7460..2bc9ec44 100644 --- a/tests/unit/test_sysinfo.py +++ b/tests/unit/test_sysinfo.py @@ -1,6 +1,9 @@ +import pytest + from muutils.sysinfo import SysInfo +@pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning") def test_sysinfo(): sysinfo = SysInfo.get_all() # we can't test the output because it's different on every machine diff --git a/tests/unit/test_tensor_utils.py b/tests/unit/test_tensor_utils.py index 1574edba..89e9e398 100644 --- a/tests/unit/test_tensor_utils.py +++ b/tests/unit/test_tensor_utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import jaxtyping import numpy as np import pytest @@ -22,13 +24,17 @@ def test_jaxtype_factory(): - ATensor = jaxtype_factory("ATensor", torch.Tensor, jaxtyping.Float) + ATensor = jaxtype_factory( + "ATensor", torch.Tensor, jaxtyping.Float, legacy_mode="ignore" + ) assert ATensor.__name__ == "ATensor" assert "default_jax_dtype = " in ATensor.__doc__ x = ATensor[(1, 2, 3), np.float32] - x = ATensor["dim1 dim2", np.float32] + print(x) + y = ATensor["dim1 dim2", np.float32] + print(y) def test_numpy_to_torch_dtype(): @@ -77,7 +83,6 @@ def test_compare_state_dicts(): def test_get_dict_shapes(): - x = {"a": torch.rand(2, 3), "b": torch.rand(1, 3, 5), "c": torch.rand(2)} x_shapes = get_dict_shapes(x) assert x_shapes == {"a": (2, 3), "b": (1, 3, 5), "c": (2,)} diff --git a/tests/unit/validate_type/test_validate_type.py b/tests/unit/validate_type/test_validate_type.py new file mode 100644 index 00000000..0bb8de63 --- /dev/null +++ b/tests/unit/validate_type/test_validate_type.py @@ -0,0 +1,556 @@ +from __future__ import annotations + +import typing +from typing import Any, Optional, Union + +import pytest + +from muutils.validate_type import IncorrectTypeException, validate_type + + +# Tests for basic types and common use cases +@pytest.mark.parametrize( + "value, expected_type, expected_result", + [ + (42, int, True), + (3.14, float, True), + (5, int, True), + (5.0, int, False), + ("hello", str, True), + (True, bool, True), + (None, type(None), True), + (None, int, False), + ([1, 2, 3], typing.List, True), + ([1, 2, 3], typing.List, True), + ({"a": 1, "b": 2}, typing.Dict, True), + ({"a": 1, "b": 2}, typing.Dict, True), + ({1, 2, 3}, typing.Set, True), + ({1, 2, 3}, typing.Set, True), + ((1, 2, 3), typing.Tuple, True), + ((1, 2, 3), typing.Tuple, True), + (b"bytes", bytes, True), + (b"bytes", str, False), + ("3.14", float, False), + ("hello", Any, True), + (5, Any, True), + (3.14, Any, True), + # ints + (int(0), int, True), + (int(1), int, True), + (int(-1), int, True), + # bools + (True, bool, True), + (False, bool, True), + ], +) +def test_validate_type_basic(value, expected_type, expected_result): + try: + assert validate_type(value, expected_type) == expected_result + except Exception as e: + raise Exception( + f"{value = }, {expected_type = }, {expected_result = }, {e}" + ) from e + + +@pytest.mark.parametrize( + "value", + [ + 42, + "hello", + 3.14, + True, + None, + [1, 2, 3], + {"a": 1, "b": 2}, + {1, 2, 3}, + (1, 2, 3), + b"bytes", + "3.14", + ], +) +def test_validate_type_any(value): + try: + assert validate_type(value, Any) + except Exception as e: + raise Exception(f"{value = }, expected `Any`, {e}") from e + + +@pytest.mark.parametrize( + "value, expected_type, expected_result", + [ + (42, Union[int, str], True), + ("hello", Union[int, str], True), + (3.14, Union[int, float], True), + (True, Union[int, str], True), + (None, Union[int, type(None)], True), + (None, Union[int, str], False), + (5, Union[int, str], True), + (5.0, Union[int, str], False), + ("hello", Union[int, str], True), + (5, typing.Union[int, str], True), + ("hello", typing.Union[int, str], True), + (5.0, typing.Union[int, str], False), + (5, Union[int, str], True), + ("hello", Union[int, str], True), + (5.0, Union[int, str], False), + ], +) +def test_validate_type_union(value, expected_type, expected_result): + try: + assert validate_type(value, expected_type) == expected_result + except Exception as e: + raise Exception( + f"{value = }, {expected_type = }, {expected_result = }, {e}" + ) from e + + +@pytest.mark.parametrize( + "value, expected_type, expected_result", + [ + (42, Optional[int], True), + ("hello", Optional[int], False), + (3.14, Optional[int], False), + ([1], Optional[typing.List[int]], True), + (None, Optional[int], True), + (None, Optional[str], True), + (None, Optional[int], True), + (None, Optional[None], True), + (None, Optional[typing.List[typing.Dict[str, int]]], True), + ], +) +def test_validate_type_optional(value, expected_type, expected_result): + try: + assert validate_type(value, expected_type) == expected_result + except Exception as e: + raise Exception( + f"{value = }, {expected_type = }, {expected_result = }, {e}" + ) from e + + +@pytest.mark.parametrize( + "value, expected_type, expected_result", + [ + (42, typing.List[int], False), + ([1, 2, 3], typing.List[int], True), + ([1, 2, 3], typing.List[str], False), + (["a", "b", "c"], typing.List[str], True), + ([1, "a", 3], typing.List[int], False), + (42, typing.List[int], False), + ([1, 2, 3], typing.List[int], True), + ([1, "2", 3], typing.List[int], False), + ], +) +def test_validate_type_list(value, expected_type, expected_result): + try: + assert validate_type(value, expected_type) == expected_result + except Exception as e: + raise Exception( + f"{value = }, {expected_type = }, {expected_result = }, {e}" + ) from e + + +@pytest.mark.parametrize( + "value, expected_type, expected_result", + [ + (42, typing.Dict[str, int], False), + ({"a": 1, "b": 2}, typing.Dict[str, int], True), + ({"a": 1, "b": 2}, typing.Dict[int, str], False), + (42, typing.Dict[str, int], False), + ({"a": 1, "b": 2}, typing.Dict[str, int], True), + ({"a": 1, "b": 2}, typing.Dict[int, str], False), + ({1: "a", 2: "b"}, typing.Dict[int, str], True), + ({1: "a", 2: "b"}, typing.Dict[str, int], False), + ({"a": 1, "b": "c"}, typing.Dict[str, int], False), + ([("a", 1), ("b", 2)], typing.Dict[str, int], False), + ({"key": "value"}, typing.Dict[str, str], True), + ({"key": 2}, typing.Dict[str, str], False), + ({"key": 2}, typing.Dict[str, int], True), + ({"key": 2.0}, typing.Dict[str, int], False), + ({"a": 1, "b": 2}, typing.Dict[str, int], True), + ({"a": 1, "b": "2"}, typing.Dict[str, int], False), + ], +) +def test_validate_type_dict(value, expected_type, expected_result): + try: + assert validate_type(value, expected_type) == expected_result + except Exception as e: + raise Exception( + f"{value = }, {expected_type = }, {expected_result = }, {e}" + ) from e + + +@pytest.mark.parametrize( + "value, expected_type, expected_result", + [ + (42, typing.Set[int], False), + ({1, 2, 3}, typing.Set[int], True), + (42, typing.Set[int], False), + ({1, 2, 3}, typing.Set[int], True), + ({1, 2, 3}, typing.Set[str], False), + ({"a", "b", "c"}, typing.Set[str], True), + ({1, "a", 3}, typing.Set[int], False), + (42, typing.Set[int], False), + ({1, 2, 3}, typing.Set[int], True), + ({1, "2", 3}, typing.Set[int], False), + ([1, 2, 3], typing.Set[int], False), + ("hello", typing.Set[str], False), + ], +) +def test_validate_type_set(value, expected_type, expected_result): + try: + assert validate_type(value, expected_type) == expected_result + except Exception as e: + raise Exception( + f"{value = }, {expected_type = }, {expected_result = }, {e}" + ) from e + + +@pytest.mark.parametrize( + "value, expected_type, expected_result", + [ + (42, typing.Tuple[int, str], False), + ((1, "a"), typing.Tuple[int, str], True), + (42, typing.Tuple[int, str], False), + ((1, "a"), typing.Tuple[int, str], True), + ((1, 2), typing.Tuple[int, str], False), + ((1, 2), typing.Tuple[int, int], True), + ((1, 2, 3), typing.Tuple[int, int], False), + ((1, "a", 3.14), typing.Tuple[int, str, float], True), + (("a", "b", "c"), typing.Tuple[str, str, str], True), + ((1, "a", 3.14), typing.Tuple[int, str], False), + ((1, "a", 3.14), typing.Tuple[int, str, float], True), + ([1, "a", 3.14], typing.Tuple[int, str, float], False), + ( + (1, "a", 3.14, "b", True, None, (1, 2, 3)), + typing.Tuple[ + int, str, float, str, bool, type(None), typing.Tuple[int, int, int] + ], + True, + ), + ], +) +def test_validate_type_tuple(value, expected_type, expected_result): + try: + assert validate_type(value, expected_type) == expected_result + except Exception as e: + raise Exception( + f"{value = }, {expected_type = }, {expected_result = }, {e}" + ) from e + + +@pytest.mark.parametrize( + "value, expected_type", + [ + (43, typing.Callable), + (lambda x: x, typing.Callable), + (42, typing.Callable[[], None]), + (42, typing.Callable[[int, str], typing.List]), + ], +) +def test_validate_type_unsupported_type_hint(value, expected_type): + with pytest.raises(NotImplementedError): + validate_type(value, expected_type) + print(f"Failed to except: {value = }, {expected_type = }") + + +@pytest.mark.parametrize( + "value, expected_type, expected_result", + [ + ([1, 2, 3], typing.List[int], True), + (["a", "b", "c"], typing.List[str], True), + ([1, "a", 3], typing.List[int], False), + ([1, 2, [3, 4]], typing.List[Union[int, typing.List[int]]], True), + ([(1, 2), (3, 4)], typing.List[typing.Tuple[int, int]], True), + ([(1, 2), (3, "4")], typing.List[typing.Tuple[int, int]], False), + ({1: [1, 2], 2: [3, 4]}, typing.Dict[int, typing.List[int]], True), + ({1: [1, 2], 2: [3, "4"]}, typing.Dict[int, typing.List[int]], False), + ], +) +def test_validate_type_collections(value, expected_type, expected_result): + try: + assert validate_type(value, expected_type) == expected_result + except Exception as e: + raise Exception( + f"{value = }, {expected_type = }, {expected_result = }, {e}" + ) from e + + +@pytest.mark.parametrize( + "value, expected_type, expected_result", + [ + # empty lists + ([], typing.List[int], True), + ([], typing.List[typing.Dict], True), + ( + [], + typing.List[typing.Tuple[typing.Dict[typing.Tuple, str], str, None]], + True, + ), + # empty dicts + ({}, typing.Dict[str, int], True), + ({}, typing.Dict[str, typing.Dict], True), + ({}, typing.Dict[str, typing.Dict[str, int]], True), + ({}, typing.Dict[str, typing.Dict[str, int]], True), + # empty sets + (set(), typing.Set[int], True), + (set(), typing.Set[typing.Dict], True), + ( + set(), + typing.Set[typing.Tuple[typing.Dict[typing.Tuple, str], str, None]], + True, + ), + # empty tuple + (tuple(), typing.Tuple, True), + # empty string + ("", str, True), + # empty bytes + (b"", bytes, True), + # None + (None, type(None), True), + # bools are ints, ints are not floats + (True, int, True), + (False, int, True), + (True, float, False), + (False, float, False), + (1, int, True), + (0, int, True), + (1, float, False), + (0, float, False), + (0, bool, False), + (1, bool, False), + # weird floats + (float("nan"), float, True), + (float("inf"), float, True), + (float("-inf"), float, True), + (float(0), float, True), + # list/tuple + ([1], typing.Tuple[int, int], False), + ((1, 2), typing.List[int], False), + ], +) +def test_validate_type_edge_cases(value, expected_type, expected_result): + try: + assert validate_type(value, expected_type) == expected_result + except Exception as e: + raise Exception( + f"{value = }, {expected_type = }, {expected_result = }, {e}" + ) from e + + +@pytest.mark.parametrize( + "value, expected_type, expected_result", + [ + (42, typing.List[int], False), + ([1, 2, 3], int, False), + (3.14, typing.Tuple[float], False), + (3.14, typing.Tuple[float, float], False), + (3.14, typing.Tuple[bool, str], False), + (False, typing.Tuple[bool, str], False), + (False, typing.Tuple[bool], False), + ((False,), typing.Tuple[bool], True), + (("abc",), typing.Tuple[str], True), + ("test-dict", typing.Dict[str, int], False), + ("test-dict", typing.Dict, False), + ], +) +def test_validate_type_wrong_type(value, expected_type, expected_result): + try: + assert validate_type(value, expected_type) == expected_result + except Exception as e: + raise Exception( + f"{value = }, {expected_type = }, {expected_result = }, {e}" + ) from e + + +def test_validate_type_complex(): + assert validate_type([1, 2, [3, 4]], typing.List[Union[int, typing.List[int]]]) + assert validate_type( + {"a": 1, "b": {"c": 2}}, typing.Dict[str, Union[int, typing.Dict[str, int]]] + ) + assert validate_type({1, (2, 3)}, typing.Set[Union[int, typing.Tuple[int, int]]]) + assert validate_type((1, ("a", "b")), typing.Tuple[int, typing.Tuple[str, str]]) + assert validate_type([{"key": "value"}], typing.List[typing.Dict[str, str]]) + assert validate_type([{"key": 2}], typing.List[typing.Dict[str, str]]) is False + assert validate_type([[1, 2], [3, 4]], typing.List[typing.List[int]]) + assert validate_type([[1, 2], [3, "4"]], typing.List[typing.List[int]]) is False + assert validate_type([(1, 2), (3, 4)], typing.List[typing.Tuple[int, int]]) + assert ( + validate_type([(1, 2), (3, "4")], typing.List[typing.Tuple[int, int]]) is False + ) + assert validate_type({1: "one", 2: "two"}, typing.Dict[int, str]) + assert validate_type({1: "one", 2: 2}, typing.Dict[int, str]) is False + assert validate_type([(1, "one"), (2, "two")], typing.List[typing.Tuple[int, str]]) + assert ( + validate_type([(1, "one"), (2, 2)], typing.List[typing.Tuple[int, str]]) + is False + ) + assert validate_type({1: [1, 2], 2: [3, 4]}, typing.Dict[int, typing.List[int]]) + assert ( + validate_type({1: [1, 2], 2: [3, "4"]}, typing.Dict[int, typing.List[int]]) + is False + ) + assert validate_type([(1, "a"), (2, "b")], typing.List[typing.Tuple[int, str]]) + assert ( + validate_type([(1, "a"), (2, 2)], typing.List[typing.Tuple[int, str]]) is False + ) + + +@pytest.mark.parametrize( + "value, expected_type, expected_result", + [ + ([[[[1]]]], typing.List[typing.List[typing.List[typing.List[int]]]], True), + ([[[[1]]]], typing.List[typing.List[typing.List[typing.List[str]]]], False), + ( + {"a": {"b": {"c": 1}}}, + typing.Dict[str, typing.Dict[str, typing.Dict[str, int]]], + True, + ), + ( + {"a": {"b": {"c": 1}}}, + typing.Dict[str, typing.Dict[str, typing.Dict[str, str]]], + False, + ), + ({1, 2, 3}, typing.Set[int], True), + ({1, 2, 3}, typing.Set[str], False), + ( + ((1, 2), (3, 4)), + typing.Tuple[typing.Tuple[int, int], typing.Tuple[int, int]], + True, + ), + ( + ((1, 2), (3, 4)), + typing.Tuple[typing.Tuple[int, int], typing.Tuple[int, str]], + False, + ), + ], +) +def test_validate_type_nested(value, expected_type, expected_result): + try: + assert validate_type(value, expected_type) == expected_result + except Exception as e: + raise Exception( + f"{value = }, {expected_type = }, {expected_result = }, {e}" + ) from e + + +def test_validate_type_inheritance(): + class Parent: + def __init__(self, a: int, b: str): + self.a: int = a + self.b: str = b + + class Child(Parent): + def __init__(self, a: int, b: str): + self.a: int = 2 * a + self.b: str = b + + assert validate_type(Parent(1, "a"), Parent) + validate_type(Child(1, "a"), Parent, do_except=True) + assert validate_type(Child(1, "a"), Child) + assert not validate_type(Parent(1, "a"), Child) + + with pytest.raises(IncorrectTypeException): + validate_type(Parent(1, "a"), Child, do_except=True) + + +def test_validate_type_class(): + class Parent: + def __init__(self, a: int, b: str): + self.a: int = a + self.b: str = b + + class Child(Parent): + def __init__(self, a: int, b: str): + self.a: int = 2 * a + self.b: str = b + + assert validate_type(Parent, type) + assert validate_type(Child, type) + assert validate_type(Parent, typing.Type[Parent], do_except=True) + assert validate_type(Child, typing.Type[Child]) + assert not validate_type(Parent, typing.Type[Child]) + + assert validate_type(Child, typing.Union[typing.Type[Child], typing.Type[Parent]]) + assert validate_type(Child, typing.Union[typing.Type[Child], int]) + + +@pytest.mark.skip(reason="Not implemented") +def test_validate_type_class_union(): + class Parent: + def __init__(self, a: int, b: str): + self.a: int = a + self.b: str = b + + class Child(Parent): + def __init__(self, a: int, b: str): + self.a: int = 2 * a + self.b: str = b + + class Other: + def __init__(self, x: int, y: str): + self.x: int = x + self.y: str = y + + assert validate_type(Child, typing.Type[typing.Union[Child, Parent]]) + assert validate_type(Child, typing.Type[typing.Union[Child, Other]]) + assert validate_type(Parent, typing.Type[typing.Union[Child, Other]]) + assert validate_type(Parent, typing.Type[typing.Union[Parent, Other]]) + + +def test_validate_type_aliases(): + AliasInt = int + AliasStr = str + AliasListInt = typing.List[int] + AliasListStr = typing.List[str] + AliasDictIntStr = typing.Dict[int, str] + AliasDictStrInt = typing.Dict[str, int] + AliasTupleIntStr = typing.Tuple[int, str] + AliasTupleStrInt = typing.Tuple[str, int] + AliasSetInt = typing.Set[int] + AliasSetStr = typing.Set[str] + AliasUnionIntStr = typing.Union[int, str] + AliasUnionStrInt = typing.Union[str, int] + AliasOptionalInt = typing.Optional[int] + AliasOptionalStr = typing.Optional[str] + AliasOptionalListInt = typing.Optional[typing.List[int]] + AliasDictStrListInt = typing.Dict[str, typing.List[int]] + + assert validate_type(42, AliasInt) + assert not validate_type("42", AliasInt) + assert validate_type(42, AliasInt) + assert not validate_type("42", AliasInt) + assert validate_type("hello", AliasStr) + assert not validate_type(42, AliasStr) + assert validate_type([1, 2, 3], AliasListInt) + assert not validate_type([1, "2", 3], AliasListInt) + assert validate_type(["hello", "world"], AliasListStr) + assert not validate_type(["hello", 42], AliasListStr) + assert validate_type({1: "a", 2: "b"}, AliasDictIntStr) + assert not validate_type({1: 2, 3: 4}, AliasDictIntStr) + assert validate_type({"one": 1, "two": 2}, AliasDictStrInt) + assert not validate_type({1: "one", 2: "two"}, AliasDictStrInt) + assert validate_type((1, "a"), AliasTupleIntStr) + assert not validate_type(("a", 1), AliasTupleIntStr) + assert validate_type(("a", 1), AliasTupleStrInt) + assert not validate_type((1, "a"), AliasTupleStrInt) + assert validate_type({1, 2, 3}, AliasSetInt) + assert not validate_type({1, "two", 3}, AliasSetInt) + assert validate_type({"one", "two"}, AliasSetStr) + assert not validate_type({"one", 2}, AliasSetStr) + assert validate_type(42, AliasUnionIntStr) + assert validate_type("hello", AliasUnionIntStr) + assert not validate_type(3.14, AliasUnionIntStr) + assert validate_type("hello", AliasUnionStrInt) + assert validate_type(42, AliasUnionStrInt) + assert not validate_type(3.14, AliasUnionStrInt) + assert validate_type(42, AliasOptionalInt) + assert validate_type(None, AliasOptionalInt) + assert not validate_type("42", AliasOptionalInt) + assert validate_type("hello", AliasOptionalStr) + assert validate_type(None, AliasOptionalStr) + assert not validate_type(42, AliasOptionalStr) + assert validate_type([1, 2, 3], AliasOptionalListInt) + assert validate_type(None, AliasOptionalListInt) + assert not validate_type(["1", "2", "3"], AliasOptionalListInt) + assert validate_type({"key": [1, 2, 3]}, AliasDictStrListInt) + assert not validate_type({"key": [1, "2", 3]}, AliasDictStrListInt) diff --git a/tests/unit/validate_type/test_validate_type_special.py b/tests/unit/validate_type/test_validate_type_special.py new file mode 100644 index 00000000..cd33500a --- /dev/null +++ b/tests/unit/validate_type/test_validate_type_special.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import typing + +import pytest + +from muutils.validate_type import validate_type + + +@pytest.mark.parametrize( + "value, expected_type, expected_result", + [ + (5, int | str, True), # type: ignore[operator] + ("hello", int | str, True), # type: ignore[operator] + (5.0, int | str, False), # type: ignore[operator] + (None, typing.Union[int, type(None)], True), # type: ignore[operator] + (None, typing.Union[int, str], False), # type: ignore[operator] + (None, int | str, False), # type: ignore[operator] + (42, int | None, True), # type: ignore[operator] + ("hello", int | None, False), # type: ignore[operator] + (3.14, int | None, False), # type: ignore[operator] + ([1], typing.List[int] | None, True), # type: ignore[operator] + (None, int | None, True), # type: ignore[operator] + (None, str | None, True), # type: ignore[operator] + (None, None | str, True), # type: ignore[operator] + (None, None | int, True), # type: ignore[operator] + (None, str | int, False), # type: ignore[operator] + (None, None | typing.List[typing.Dict[str, int]], True), # type: ignore[operator] + ], +) +def test_validate_type_union(value, expected_type, expected_result): + try: + assert validate_type(value, expected_type) == expected_result + except Exception as e: + raise Exception( + f"{value = }, {expected_type = }, {expected_result = }, {e}" + ) from e + + +@pytest.mark.parametrize( + "value, expected_type", + [ + (42, list[int, str]), # type: ignore[misc] + ([1, 2, 3], list[int, str]), # type: ignore[misc] + ({"a": 1, "b": 2}, set[str, int]), # type: ignore[misc] + ({1: "a", 2: "b"}, set[int, str]), # type: ignore[misc] + ({"a": 1, "b": 2}, set[str, int, str]), # type: ignore[misc] + ({1: "a", 2: "b"}, set[int, str, int]), # type: ignore[misc] + ({1, 2, 3}, set[int, str]), # type: ignore[misc] + ({"a"}, set[int, str]), # type: ignore[misc] + (42, dict[int, str, bool]), # type: ignore[misc] + ], +) +def test_validate_type_unsupported_generic_alias(value, expected_type): + with pytest.raises(TypeError): + validate_type(value, expected_type) + print(f"Failed to except: {value = }, {expected_type = }") diff --git a/tests/util/replace_type_hints.py b/tests/util/replace_type_hints.py new file mode 100644 index 00000000..64abb247 --- /dev/null +++ b/tests/util/replace_type_hints.py @@ -0,0 +1,30 @@ +def replace_typing_aliases(filename: str) -> str: + # Dictionary to map old types from the typing module to the new built-in types + replacements = { + "typing.List": "list", + "typing.Dict": "dict", + "typing.Set": "set", + "typing.Tuple": "tuple", + } + + # Read the file content + with open(filename, "r") as file: + content = file.read() + + # Replace all occurrences of the typing module types with built-in types + for old, new in replacements.items(): + content = content.replace(old, new) + + # return the modified content + return content + + +if __name__ == "__main__": + import sys + + file: str = sys.argv[1] + prefix: str = "" + if len(sys.argv) > 1: + prefix = "\n".join(sys.argv[2:]) + + print(prefix + "\n" + replace_typing_aliases(file)) diff --git a/tests/manual/test_fire.py b/tests/util/test_fire.py similarity index 100% rename from tests/manual/test_fire.py rename to tests/util/test_fire.py