From 74c1506a1a7d3f77ff5a65aa9a0c7e4aa6e542a9 Mon Sep 17 00:00:00 2001 From: davidteather <34144122+davidteather@users.noreply.github.com> Date: Fri, 3 Apr 2026 11:22:22 -0400 Subject: [PATCH 01/11] chore: swap to poetry --- .github/workflows/package-test.yml | 23 +- .github/workflows/publish.yml | 44 ++ .github/workflows/python-publish.yml | 31 - .gitignore | 4 +- .pre-commit-config.yaml | 40 ++ poetry.lock | 845 +++++++++++++++++++++++++++ pyproject.toml | 52 ++ python_obfuscator/__init__.py | 3 + python_obfuscator/version.py | 1 + setup.cfg | 10 + setup.py | 33 -- 11 files changed, 1012 insertions(+), 74 deletions(-) create mode 100644 .github/workflows/publish.yml delete mode 100644 .github/workflows/python-publish.yml create mode 100644 .pre-commit-config.yaml create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 python_obfuscator/version.py create mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.github/workflows/package-test.yml b/.github/workflows/package-test.yml index 7944761..a8bba47 100644 --- a/.github/workflows/package-test.yml +++ b/.github/workflows/package-test.yml @@ -7,7 +7,7 @@ on: branches: - main - nightly - - 'releases/*' + - "releases/*" jobs: Unit-Tests: @@ -17,18 +17,23 @@ jobs: fail-fast: false matrix: os: [macos-latest, windows-latest, ubuntu-latest] - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Setup dependencies + + - name: Install Poetry run: | - python -m pip install --upgrade pip - pip install pytest - python setup.py install + curl -sSL https://install.python-poetry.org | python - -y + + - name: Update PATH + run: echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: Install dependencies + run: poetry install --no-interaction - name: Run Tests - run: pytest tests \ No newline at end of file + run: poetry run pytest tests diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..1af736a --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,44 @@ +name: Publish Package + +on: + release: + types: [created] + workflow_dispatch: + +permissions: + contents: read + id-token: write + +jobs: + pypi-publish: + name: Upload release to PyPI + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Python 3.x + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Install Poetry + run: | + curl -sSL https://install.python-poetry.org | python - -y + + - name: Update PATH + run: echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: Update Poetry configuration + run: poetry config virtualenvs.create false + + - name: Install dependencies + run: poetry install --sync --no-interaction + + - name: Package project + run: poetry build + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml deleted file mode 100644 index d9ec21f..0000000 --- a/.github/workflows/python-publish.yml +++ /dev/null @@ -1,31 +0,0 @@ -# This workflows will upload a Python Package using Twine when a release is created -# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries - -name: Upload Python Package - -on: - release: - types: [created] - -jobs: - deploy: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - - name: Build and publish - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - python setup.py sdist - twine upload dist/* \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7e390b2..f23068a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ __pycache__ dist build python_obfuscator.egg-info -.pytest_cache \ No newline at end of file +.pytest_cache +.mypy_cache +.ruff_cache \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..d037397 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,40 @@ +repos: + - repo: https://github.com/psf/black + rev: 25.1.0 + hooks: + - id: black + language_version: python3 + args: [--line-length=88] + + - repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort + args: [--profile=black] + + - repo: https://github.com/pycqa/flake8 + rev: 7.0.0 + hooks: + - id: flake8 + args: + [ + "--max-line-length=88", + "--extend-ignore=E203,W503,E501,F401,F541", + ] + + - repo: local + hooks: + - id: pytest-unit + name: Run unit tests + entry: poetry run pytest tests/ -v + language: system + pass_filenames: false + always_run: true + stages: [commit] + +default_language_version: + python: python3 + +fail_fast: true + +default_stages: [commit] diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..b16eb5d --- /dev/null +++ b/poetry.lock @@ -0,0 +1,845 @@ +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. + +[[package]] +name = "black" +version = "25.11.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "black-25.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec311e22458eec32a807f029b2646f661e6859c3f61bc6d9ffb67958779f392e"}, + {file = "black-25.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1032639c90208c15711334d681de2e24821af0575573db2810b0763bcd62e0f0"}, + {file = "black-25.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0f7c461df55cf32929b002335883946a4893d759f2df343389c4396f3b6b37"}, + {file = "black-25.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:f9786c24d8e9bd5f20dc7a7f0cdd742644656987f6ea6947629306f937726c03"}, + {file = "black-25.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:895571922a35434a9d8ca67ef926da6bc9ad464522a5fe0db99b394ef1c0675a"}, + {file = "black-25.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb4f4b65d717062191bdec8e4a442539a8ea065e6af1c4f4d36f0cdb5f71e170"}, + {file = "black-25.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d81a44cbc7e4f73a9d6ae449ec2317ad81512d1e7dce7d57f6333fd6259737bc"}, + {file = "black-25.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7eebd4744dfe92ef1ee349dc532defbf012a88b087bb7ddd688ff59a447b080e"}, + {file = "black-25.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac"}, + {file = "black-25.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96"}, + {file = "black-25.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd"}, + {file = "black-25.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae263af2f496940438e5be1a0c1020e13b09154f3af4df0835ea7f9fe7bfa409"}, + {file = "black-25.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a1d40348b6621cc20d3d7530a5b8d67e9714906dfd7346338249ad9c6cedf2b"}, + {file = "black-25.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51c65d7d60bb25429ea2bf0731c32b2a2442eb4bd3b2afcb47830f0b13e58bfd"}, + {file = "black-25.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:936c4dd07669269f40b497440159a221ee435e3fddcf668e0c05244a9be71993"}, + {file = "black-25.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:f42c0ea7f59994490f4dccd64e6b2dd49ac57c7c84f38b8faab50f8759db245c"}, + {file = "black-25.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35690a383f22dd3e468c85dc4b915217f87667ad9cce781d7b42678ce63c4170"}, + {file = "black-25.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dae49ef7369c6caa1a1833fd5efb7c3024bb7e4499bf64833f65ad27791b1545"}, + {file = "black-25.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bd4a22a0b37401c8e492e994bce79e614f91b14d9ea911f44f36e262195fdda"}, + {file = "black-25.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa211411e94fdf86519996b7f5f05e71ba34835d8f0c0f03c00a26271da02664"}, + {file = "black-25.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3bb5ce32daa9ff0605d73b6f19da0b0e6c1f8f2d75594db539fdfed722f2b06"}, + {file = "black-25.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9815ccee1e55717fe9a4b924cae1646ef7f54e0f990da39a34fc7b264fcf80a2"}, + {file = "black-25.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92285c37b93a1698dcbc34581867b480f1ba3a7b92acf1fe0467b04d7a4da0dc"}, + {file = "black-25.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:43945853a31099c7c0ff8dface53b4de56c41294fa6783c0441a8b1d9bf668bc"}, + {file = "black-25.11.0-py3-none-any.whl", hash = "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b"}, + {file = "black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +pytokens = ">=0.3.0" +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.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "click" +version = "8.1.8" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] +markers = "sys_platform == \"win32\" or platform_system == \"Windows\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.10.7" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a"}, + {file = "coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5"}, + {file = "coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17"}, + {file = "coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b"}, + {file = "coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87"}, + {file = "coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e"}, + {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e"}, + {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df"}, + {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0"}, + {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13"}, + {file = "coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b"}, + {file = "coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807"}, + {file = "coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59"}, + {file = "coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a"}, + {file = "coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699"}, + {file = "coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d"}, + {file = "coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e"}, + {file = "coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23"}, + {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab"}, + {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82"}, + {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2"}, + {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61"}, + {file = "coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14"}, + {file = "coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2"}, + {file = "coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a"}, + {file = "coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417"}, + {file = "coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973"}, + {file = "coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c"}, + {file = "coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7"}, + {file = "coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6"}, + {file = "coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59"}, + {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b"}, + {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a"}, + {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb"}, + {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1"}, + {file = "coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256"}, + {file = "coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba"}, + {file = "coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf"}, + {file = "coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d"}, + {file = "coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b"}, + {file = "coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e"}, + {file = "coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b"}, + {file = "coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49"}, + {file = "coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911"}, + {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0"}, + {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f"}, + {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c"}, + {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f"}, + {file = "coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698"}, + {file = "coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843"}, + {file = "coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546"}, + {file = "coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c"}, + {file = "coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15"}, + {file = "coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4"}, + {file = "coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0"}, + {file = "coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0"}, + {file = "coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65"}, + {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541"}, + {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6"}, + {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999"}, + {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2"}, + {file = "coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a"}, + {file = "coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb"}, + {file = "coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb"}, + {file = "coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520"}, + {file = "coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32"}, + {file = "coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f"}, + {file = "coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a"}, + {file = "coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360"}, + {file = "coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69"}, + {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14"}, + {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe"}, + {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e"}, + {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd"}, + {file = "coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2"}, + {file = "coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681"}, + {file = "coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880"}, + {file = "coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63"}, + {file = "coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2"}, + {file = "coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d"}, + {file = "coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0"}, + {file = "coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699"}, + {file = "coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9"}, + {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f"}, + {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1"}, + {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0"}, + {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399"}, + {file = "coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235"}, + {file = "coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d"}, + {file = "coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a"}, + {file = "coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3"}, + {file = "coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c"}, + {file = "coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396"}, + {file = "coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40"}, + {file = "coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594"}, + {file = "coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a"}, + {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b"}, + {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3"}, + {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0"}, + {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f"}, + {file = "coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431"}, + {file = "coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07"}, + {file = "coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260"}, + {file = "coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239"}, +] + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +markers = "python_version < \"3.11\"" +files = [ + {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, + {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +markers = "python_version < \"3.10\"" +files = [ + {file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}, + {file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +perf = ["ipython"] +test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] + +[[package]] +name = "iniconfig" +version = "2.1.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, +] + +[[package]] +name = "isort" +version = "6.1.0" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.9.0" +groups = ["main", "dev"] +files = [ + {file = "isort-6.1.0-py3-none-any.whl", hash = "sha256:58d8927ecce74e5087aef019f778d4081a3b6c98f15a80ba35782ca8a2097784"}, + {file = "isort-6.1.0.tar.gz", hash = "sha256:9b8f96a14cfee0677e78e941ff62f03769a06d412aabb9e2a90487b3b7e8d481"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.6.0", markers = "python_version < \"3.10\""} + +[package.extras] +colors = ["colorama"] +plugins = ["setuptools"] + +[[package]] +name = "librt" +version = "0.8.1" +description = "Mypyc runtime library" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "librt-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc"}, + {file = "librt-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7"}, + {file = "librt-0.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6"}, + {file = "librt-0.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0"}, + {file = "librt-0.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b"}, + {file = "librt-0.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891"}, + {file = "librt-0.8.1-cp310-cp310-win32.whl", hash = "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7"}, + {file = "librt-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2"}, + {file = "librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd"}, + {file = "librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965"}, + {file = "librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da"}, + {file = "librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0"}, + {file = "librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e"}, + {file = "librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe"}, + {file = "librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb"}, + {file = "librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b"}, + {file = "librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9"}, + {file = "librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a"}, + {file = "librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9"}, + {file = "librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb"}, + {file = "librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d"}, + {file = "librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7"}, + {file = "librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0"}, + {file = "librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a"}, + {file = "librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444"}, + {file = "librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d"}, + {file = "librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35"}, + {file = "librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583"}, + {file = "librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c"}, + {file = "librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04"}, + {file = "librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363"}, + {file = "librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d"}, + {file = "librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a"}, + {file = "librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79"}, + {file = "librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0"}, + {file = "librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f"}, + {file = "librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c"}, + {file = "librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc"}, + {file = "librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c"}, + {file = "librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3"}, + {file = "librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78"}, + {file = "librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023"}, + {file = "librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730"}, + {file = "librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3"}, + {file = "librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1"}, + {file = "librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994"}, + {file = "librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a"}, + {file = "librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4"}, + {file = "librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61"}, + {file = "librt-0.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3dff3d3ca8db20e783b1bc7de49c0a2ab0b8387f31236d6a026597d07fcd68ac"}, + {file = "librt-0.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08eec3a1fc435f0d09c87b6bf1ec798986a3544f446b864e4099633a56fcd9ed"}, + {file = "librt-0.8.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e3f0a41487fd5fad7e760b9e8a90e251e27c2816fbc2cff36a22a0e6bcbbd9dd"}, + {file = "librt-0.8.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bacdb58d9939d95cc557b4dbaa86527c9db2ac1ed76a18bc8d26f6dc8647d851"}, + {file = "librt-0.8.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d7ab1f01aa753188605b09a51faa44a3327400b00b8cce424c71910fc0a128"}, + {file = "librt-0.8.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4998009e7cb9e896569f4be7004f09d0ed70d386fa99d42b6d363f6d200501ac"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2cc68eeeef5e906839c7bb0815748b5b0a974ec27125beefc0f942715785b551"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0bf69d79a23f4f40b8673a947a234baeeb133b5078b483b7297c5916539cf5d5"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:22b46eabd76c1986ee7d231b0765ad387d7673bbd996aa0d0d054b38ac65d8f6"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:237796479f4d0637d6b9cbcb926ff424a97735e68ade6facf402df4ec93375ed"}, + {file = "librt-0.8.1-cp39-cp39-win32.whl", hash = "sha256:4beb04b8c66c6ae62f8c1e0b2f097c1ebad9295c929a8d5286c05eae7c2fc7dc"}, + {file = "librt-0.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:64548cde61b692dc0dc379f4b5f59a2f582c2ebe7890d09c1ae3b9e66fa015b7"}, + {file = "librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73"}, +] + +[[package]] +name = "mypy" +version = "1.19.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec"}, + {file = "mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b"}, + {file = "mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6"}, + {file = "mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74"}, + {file = "mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1"}, + {file = "mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac"}, + {file = "mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288"}, + {file = "mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab"}, + {file = "mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6"}, + {file = "mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331"}, + {file = "mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925"}, + {file = "mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042"}, + {file = "mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1"}, + {file = "mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e"}, + {file = "mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2"}, + {file = "mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8"}, + {file = "mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a"}, + {file = "mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13"}, + {file = "mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250"}, + {file = "mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b"}, + {file = "mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e"}, + {file = "mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef"}, + {file = "mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75"}, + {file = "mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd"}, + {file = "mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1"}, + {file = "mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718"}, + {file = "mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b"}, + {file = "mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045"}, + {file = "mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957"}, + {file = "mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f"}, + {file = "mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3"}, + {file = "mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a"}, + {file = "mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67"}, + {file = "mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e"}, + {file = "mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376"}, + {file = "mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24"}, + {file = "mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247"}, + {file = "mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba"}, +] + +[package.dependencies] +librt = {version = ">=0.6.2", markers = "platform_python_implementation != \"PyPy\""} +mypy_extensions = ">=1.0.0" +pathspec = ">=0.9.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "packaging" +version = "26.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, + {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"}, + {file = "pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"}, +] + +[package.extras] +hyperscan = ["hyperscan (>=0.7)"] +optional = ["typing-extensions (>=4)"] +re2 = ["google-re2 (>=1.1)"] +tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] + +[[package]] +name = "platformdirs" +version = "4.4.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"}, + {file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pygments" +version = "2.20.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pytest" +version = "8.4.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, + {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1" +packaging = ">=20" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytokens" +version = "0.4.1" +description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older Pythons." +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5"}, + {file = "pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe"}, + {file = "pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c"}, + {file = "pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7"}, + {file = "pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2"}, + {file = "pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440"}, + {file = "pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc"}, + {file = "pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d"}, + {file = "pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16"}, + {file = "pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6"}, + {file = "pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083"}, + {file = "pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1"}, + {file = "pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1"}, + {file = "pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9"}, + {file = "pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68"}, + {file = "pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b"}, + {file = "pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f"}, + {file = "pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1"}, + {file = "pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4"}, + {file = "pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78"}, + {file = "pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321"}, + {file = "pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa"}, + {file = "pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d"}, + {file = "pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324"}, + {file = "pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9"}, + {file = "pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb"}, + {file = "pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3"}, + {file = "pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975"}, + {file = "pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a"}, + {file = "pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918"}, + {file = "pytokens-0.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:da5baeaf7116dced9c6bb76dc31ba04a2dc3695f3d9f74741d7910122b456edc"}, + {file = "pytokens-0.4.1-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11edda0942da80ff58c4408407616a310adecae1ddd22eef8c692fe266fa5009"}, + {file = "pytokens-0.4.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0fc71786e629cef478cbf29d7ea1923299181d0699dbe7c3c0f4a583811d9fc1"}, + {file = "pytokens-0.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dcafc12c30dbaf1e2af0490978352e0c4041a7cde31f4f81435c2a5e8b9cabb6"}, + {file = "pytokens-0.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:42f144f3aafa5d92bad964d471a581651e28b24434d184871bd02e3a0d956037"}, + {file = "pytokens-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:34bcc734bd2f2d5fe3b34e7b3c0116bfb2397f2d9666139988e7a3eb5f7400e3"}, + {file = "pytokens-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941d4343bf27b605e9213b26bfa1c4bf197c9c599a9627eb7305b0defcfe40c1"}, + {file = "pytokens-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3ad72b851e781478366288743198101e5eb34a414f1d5627cdd585ca3b25f1db"}, + {file = "pytokens-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:682fa37ff4d8e95f7df6fe6fe6a431e8ed8e788023c6bcc0f0880a12eab80ad1"}, + {file = "pytokens-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:30f51edd9bb7f85c748979384165601d028b84f7bd13fe14d3e065304093916a"}, + {file = "pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de"}, + {file = "pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a"}, +] + +[package.extras] +dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"] + +[[package]] +name = "regex" +version = "2026.1.15" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e"}, + {file = "regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f"}, + {file = "regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b"}, + {file = "regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c"}, + {file = "regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9"}, + {file = "regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c"}, + {file = "regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106"}, + {file = "regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618"}, + {file = "regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4"}, + {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79"}, + {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9"}, + {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220"}, + {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13"}, + {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3"}, + {file = "regex-2026.1.15-cp310-cp310-win32.whl", hash = "sha256:21ca32c28c30d5d65fc9886ff576fc9b59bbca08933e844fa2363e530f4c8218"}, + {file = "regex-2026.1.15-cp310-cp310-win_amd64.whl", hash = "sha256:3038a62fc7d6e5547b8915a3d927a0fbeef84cdbe0b1deb8c99bbd4a8961b52a"}, + {file = "regex-2026.1.15-cp310-cp310-win_arm64.whl", hash = "sha256:505831646c945e3e63552cc1b1b9b514f0e93232972a2d5bedbcc32f15bc82e3"}, + {file = "regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a"}, + {file = "regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f"}, + {file = "regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1"}, + {file = "regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b"}, + {file = "regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8"}, + {file = "regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413"}, + {file = "regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026"}, + {file = "regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785"}, + {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e"}, + {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763"}, + {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb"}, + {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2"}, + {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1"}, + {file = "regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569"}, + {file = "regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7"}, + {file = "regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec"}, + {file = "regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1"}, + {file = "regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681"}, + {file = "regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f"}, + {file = "regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa"}, + {file = "regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804"}, + {file = "regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c"}, + {file = "regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5"}, + {file = "regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3"}, + {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb"}, + {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410"}, + {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4"}, + {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d"}, + {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22"}, + {file = "regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913"}, + {file = "regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a"}, + {file = "regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056"}, + {file = "regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e"}, + {file = "regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10"}, + {file = "regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc"}, + {file = "regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599"}, + {file = "regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae"}, + {file = "regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5"}, + {file = "regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6"}, + {file = "regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788"}, + {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714"}, + {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d"}, + {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3"}, + {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31"}, + {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3"}, + {file = "regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f"}, + {file = "regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e"}, + {file = "regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337"}, + {file = "regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be"}, + {file = "regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8"}, + {file = "regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd"}, + {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a"}, + {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93"}, + {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af"}, + {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09"}, + {file = "regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5"}, + {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794"}, + {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a"}, + {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80"}, + {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2"}, + {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60"}, + {file = "regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952"}, + {file = "regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10"}, + {file = "regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829"}, + {file = "regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac"}, + {file = "regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6"}, + {file = "regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2"}, + {file = "regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846"}, + {file = "regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b"}, + {file = "regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e"}, + {file = "regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde"}, + {file = "regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5"}, + {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34"}, + {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75"}, + {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e"}, + {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160"}, + {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1"}, + {file = "regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1"}, + {file = "regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903"}, + {file = "regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705"}, + {file = "regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8"}, + {file = "regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf"}, + {file = "regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d"}, + {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84"}, + {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df"}, + {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434"}, + {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a"}, + {file = "regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10"}, + {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac"}, + {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea"}, + {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e"}, + {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521"}, + {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db"}, + {file = "regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e"}, + {file = "regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf"}, + {file = "regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70"}, + {file = "regex-2026.1.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:55b4ea996a8e4458dd7b584a2f89863b1655dd3d17b88b46cbb9becc495a0ec5"}, + {file = "regex-2026.1.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e1e28be779884189cdd57735e997f282b64fd7ccf6e2eef3e16e57d7a34a815"}, + {file = "regex-2026.1.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0057de9eaef45783ff69fa94ae9f0fd906d629d0bd4c3217048f46d1daa32e9b"}, + {file = "regex-2026.1.15-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc7cd0b2be0f0269283a45c0d8b2c35e149d1319dcb4a43c9c3689fa935c1ee6"}, + {file = "regex-2026.1.15-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8db052bbd981e1666f09e957f3790ed74080c2229007c1dd67afdbf0b469c48b"}, + {file = "regex-2026.1.15-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:343db82cb3712c31ddf720f097ef17c11dab2f67f7a3e7be976c4f82eba4e6df"}, + {file = "regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55e9d0118d97794367309635df398bdfd7c33b93e2fdfa0b239661cd74b4c14e"}, + {file = "regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:008b185f235acd1e53787333e5690082e4f156c44c87d894f880056089e9bc7c"}, + {file = "regex-2026.1.15-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fd65af65e2aaf9474e468f9e571bd7b189e1df3a61caa59dcbabd0000e4ea839"}, + {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f42e68301ff4afee63e365a5fc302b81bb8ba31af625a671d7acb19d10168a8c"}, + {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f7792f27d3ee6e0244ea4697d92b825f9a329ab5230a78c1a68bd274e64b5077"}, + {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:dbaf3c3c37ef190439981648ccbf0c02ed99ae066087dd117fcb616d80b010a4"}, + {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:adc97a9077c2696501443d8ad3fa1b4fc6d131fc8fd7dfefd1a723f89071cf0a"}, + {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:069f56a7bf71d286a6ff932a9e6fb878f151c998ebb2519a9f6d1cee4bffdba3"}, + {file = "regex-2026.1.15-cp39-cp39-win32.whl", hash = "sha256:ea4e6b3566127fda5e007e90a8fd5a4169f0cf0619506ed426db647f19c8454a"}, + {file = "regex-2026.1.15-cp39-cp39-win_amd64.whl", hash = "sha256:cda1ed70d2b264952e88adaa52eea653a33a1b98ac907ae2f86508eb44f65cdc"}, + {file = "regex-2026.1.15-cp39-cp39-win_arm64.whl", hash = "sha256:b325d4714c3c48277bfea1accd94e193ad6ed42b4bad79ad64f3b8f8a31260a5"}, + {file = "regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5"}, +] + +[[package]] +name = "tomli" +version = "2.4.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +markers = "python_version < \"3.11\"" +files = [ + {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, + {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"}, + {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"}, + {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"}, + {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"}, + {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"}, + {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"}, + {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"}, + {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"}, + {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"}, + {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"}, + {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"}, + {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"}, + {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"}, + {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"}, + {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"}, + {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"}, + {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, + {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "zipp" +version = "3.23.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +markers = "python_version < \"3.10\"" +files = [ + {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, + {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + +[extras] +dev = ["black", "coverage", "isort", "mypy", "pytest"] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.9" +content-hash = "c90af77d35ececd229a2df152846aff6449bcd157ed8e3ad81d3f0285612258f" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ecbcc14 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,52 @@ +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry] +packages = [{ include = "python_obfuscator" }] + +[tool.poetry.dependencies] +python = ">=3.9" +regex = "*" + +[tool.poetry.group.dev.dependencies] +mypy = "^1.15.0" +black = "^25.1.0" +isort = "^6.0.0" +pytest = "^8.3.4" +coverage = "^7.6.12" + +[project] +name = "python_obfuscator" +version = "0.0.2" +description = "It's a python obfuscator." +readme = "README.md" +authors = [{ name = "David Teather", email = "contact.davidteather@gmail.com" }] +license = { file = "LICENSE" } +dependencies = ["regex"] +requires-python = ">=3.9" +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Topic :: Software Development :: Build Tools", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", +] + +[project.optional-dependencies] +dev = ["pytest", "black", "mypy", "coverage", "isort"] + +[project.scripts] +pyobfuscate = "python_obfuscator.cli:cli" + +[tool.isort] +profile = "black" +line_length = 88 + +[tool.pytest.ini_options] +testpaths = ["tests"] + +[tool.coverage.run] +branch = true +source = ["python_obfuscator"] +omit = ["tests/*"] diff --git a/python_obfuscator/__init__.py b/python_obfuscator/__init__.py index 53747a1..a08606e 100644 --- a/python_obfuscator/__init__.py +++ b/python_obfuscator/__init__.py @@ -1 +1,4 @@ from .obfuscator import obfuscator +from .version import __version__ + +__all__ = ["obfuscator", "__version__"] diff --git a/python_obfuscator/version.py b/python_obfuscator/version.py new file mode 100644 index 0000000..3b93d0b --- /dev/null +++ b/python_obfuscator/version.py @@ -0,0 +1 @@ +__version__ = "0.0.2" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..7ac5da9 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,10 @@ +[bumpversion] +current_version = 0.0.2 + +[bumpversion:file:pyproject.toml] +search = version = "{current_version}" +replace = version = "{new_version}" + +[bumpversion:file:python_obfuscator/version.py] +search = __version__ = "{current_version}" +replace = __version__ = "{new_version}" diff --git a/setup.py b/setup.py deleted file mode 100644 index aceb9e9..0000000 --- a/setup.py +++ /dev/null @@ -1,33 +0,0 @@ -from distutils.core import setup -import os.path -import setuptools - -with open("README.md", "r") as fh: - long_description = fh.read() - -setuptools.setup( - name="python_obfuscator", - packages=setuptools.find_packages(), - version="0.0.2", - license="MIT", - description="It's a python obfuscator.", - author="David Teather", - author_email="contact.davidteather@gmail.com", - url="https://github.com/davidteather/python-obfuscator", - long_description=long_description, - long_description_content_type="text/markdown", - download_url="https://github.com/davidteather/python-obfuscator/tarball/master", - keywords=["obfuscator"], - install_requires=["regex"], - classifiers=[ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "Topic :: Software Development :: Build Tools", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - ], - entry_points={"console_scripts": ["pyobfuscate=python_obfuscator.cli:cli"]}, -) From 6f12d6d733f872f01f27a1cabc06e294dc85a3e6 Mon Sep 17 00:00:00 2001 From: davidteather <34144122+davidteather@users.noreply.github.com> Date: Fri, 3 Apr 2026 11:30:13 -0400 Subject: [PATCH 02/11] swap obfuscator to typer --- .github/workflows/package-test.yml | 2 +- poetry.lock | 409 ++++++++++++++++------------- pyproject.toml | 9 +- python_obfuscator/cli/__init__.py | 90 ++++--- 4 files changed, 273 insertions(+), 237 deletions(-) diff --git a/.github/workflows/package-test.yml b/.github/workflows/package-test.yml index a8bba47..2622ecd 100644 --- a/.github/workflows/package-test.yml +++ b/.github/workflows/package-test.yml @@ -17,7 +17,7 @@ jobs: fail-fast: false matrix: os: [macos-latest, windows-latest, ubuntu-latest] - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 - name: Install Python ${{ matrix.python-version }} diff --git a/poetry.lock b/poetry.lock index b16eb5d..6d4369a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,17 @@ # This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +[[package]] +name = "annotated-doc" +version = "0.0.4" +description = "Document parameters, class attributes, return types, and variables inline, with Annotated." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320"}, + {file = "annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4"}, +] + [[package]] name = "black" version = "25.11.0" @@ -54,14 +66,14 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "click" -version = "8.1.8" +version = "8.3.1" description = "Composable command line interface toolkit" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, - {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, + {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, + {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, ] [package.dependencies] @@ -74,7 +86,7 @@ description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["main", "dev"] -markers = "sys_platform == \"win32\" or platform_system == \"Windows\"" +markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -216,31 +228,6 @@ typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} [package.extras] test = ["pytest (>=6)"] -[[package]] -name = "importlib-metadata" -version = "8.7.1" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -markers = "python_version < \"3.10\"" -files = [ - {file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}, - {file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}, -] - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=3.4)"] -perf = ["ipython"] -test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] - [[package]] name = "iniconfig" version = "2.1.0" @@ -265,9 +252,6 @@ files = [ {file = "isort-6.1.0.tar.gz", hash = "sha256:9b8f96a14cfee0677e78e941ff62f03769a06d412aabb9e2a90487b3b7e8d481"}, ] -[package.dependencies] -importlib-metadata = {version = ">=4.6.0", markers = "python_version < \"3.10\""} - [package.extras] colors = ["colorama"] plugins = ["setuptools"] @@ -373,6 +357,42 @@ files = [ {file = "librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73"}, ] +[[package]] +name = "markdown-it-py" +version = "4.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, + {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins (>=0.5.0)"] +profiling = ["gprof2dot"] +rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + [[package]] name = "mypy" version = "1.19.1" @@ -606,143 +626,157 @@ dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "t [[package]] name = "regex" -version = "2026.1.15" +version = "2026.3.32" description = "Alternative regular expression module, to replace re." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "regex-2026.3.32-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:462a041d2160090553572f6bb0be417ab9bb912a08de54cb692829c871ee88c1"}, + {file = "regex-2026.3.32-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c3c6f6b027d10f84bfe65049028892b5740878edd9eae5fea0d1710b09b1d257"}, + {file = "regex-2026.3.32-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:879ae91f2928a13f01a55cfa168acedd2b02b11b4cd8b5bb9223e8cde777ca52"}, + {file = "regex-2026.3.32-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:887a9fa74418d74d645281ee0edcf60694053bd1bc2ebc49eb5e66bfffc6d107"}, + {file = "regex-2026.3.32-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d571f0b2eec3513734ea31a16ce0f7840c0b85a98e7edfa0e328ed144f9ef78f"}, + {file = "regex-2026.3.32-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ada7bd5bb6511d12177a7b00416ce55caee49fbf8c268f26b909497b534cacb"}, + {file = "regex-2026.3.32-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:918db4e34a7ef3d0beee913fa54b34231cc3424676f1c19bdb85f01828d3cd37"}, + {file = "regex-2026.3.32-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:69a847a6ffaa86e8af7b9e7037606e05a6f663deec516ad851e8e05d9908d16a"}, + {file = "regex-2026.3.32-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2c8d402ea3dfe674288fe3962016affd33b5b27213d2b5db1823ffa4de524c57"}, + {file = "regex-2026.3.32-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d6b39a2cc5625bbc4fda18919a891eab9aab934eecf83660a90ce20c53621a9a"}, + {file = "regex-2026.3.32-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f7cc00089b4c21847852c0ad76fb3680f9833b855a0d30bcec94211c435bff6b"}, + {file = "regex-2026.3.32-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:fd03e38068faeef937cc6761a250a4aaa015564bd0d61481fefcf15586d31825"}, + {file = "regex-2026.3.32-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e006ea703d5c0f3d112b51ba18af73b58209b954acfe3d8da42eacc9a00e4be6"}, + {file = "regex-2026.3.32-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6980ceb5c1049d4878632f08ba0bf7234c30e741b0dc9081da0f86eca13189d3"}, + {file = "regex-2026.3.32-cp310-cp310-win32.whl", hash = "sha256:6128dd0793a87287ea1d8bf16b4250dd96316c464ee15953d5b98875a284d41e"}, + {file = "regex-2026.3.32-cp310-cp310-win_amd64.whl", hash = "sha256:5aa78c857c1731bdd9863923ffadc816d823edf475c7db6d230c28b53b7bdb5e"}, + {file = "regex-2026.3.32-cp310-cp310-win_arm64.whl", hash = "sha256:34c905a721ddee0f84c99e3e3b59dd4a5564a6fe338222bc89dd4d4df166115c"}, + {file = "regex-2026.3.32-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d7855f5e59fcf91d0c9f4a51dc5d8847813832a2230c3e8e35912ccf20baaa2"}, + {file = "regex-2026.3.32-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:18eb45f711e942c27dbed4109830bd070d8d618e008d0db39705f3f57070a4c6"}, + {file = "regex-2026.3.32-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed3b8281c5d0944d939c82db4ec2300409dd69ee087f7a75a94f2e301e855fb4"}, + {file = "regex-2026.3.32-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad5c53f2e8fcae9144009435ebe3d9832003508cf8935c04542a1b3b8deefa15"}, + {file = "regex-2026.3.32-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70c634e39c5cda0da05c93d6747fdc957599f7743543662b6dbabdd8d3ba8a96"}, + {file = "regex-2026.3.32-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e0f6648fd48f4c73d801c55ab976cd602e2da87de99c07bff005b131f269c6a"}, + {file = "regex-2026.3.32-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5e0fdb5744caf1036dec5510f543164f2144cb64932251f6dfd42fa872b7f9c"}, + {file = "regex-2026.3.32-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dab4178a0bc1ef13178832b12db7bc7f562e8f028b2b5be186e370090dc50652"}, + {file = "regex-2026.3.32-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f95bd07f301135771559101c060f558e2cf896c7df00bec050ca7f93bf11585a"}, + {file = "regex-2026.3.32-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2dcca2bceb823c9cc610e57b86a265d7ffc30e9fe98548c609eba8bd3c0c2488"}, + {file = "regex-2026.3.32-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:567b57eb987547a23306444e4f6f85d4314f83e65c71d320d898aa7550550443"}, + {file = "regex-2026.3.32-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:b6acb765e7c1f2fa08ac9057a33595e26104d7d67046becae184a8f100932dd9"}, + {file = "regex-2026.3.32-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1ed17104d1be7f807fdec35ec99777168dd793a09510d753f8710590ba54cdd"}, + {file = "regex-2026.3.32-cp311-cp311-win32.whl", hash = "sha256:c60f1de066eb5a0fd8ee5974de4194bb1c2e7692941458807162ffbc39887303"}, + {file = "regex-2026.3.32-cp311-cp311-win_amd64.whl", hash = "sha256:8fe14e24124ef41220e5992a0f09432f890037df6f93fd3d6b7a0feff2db16b2"}, + {file = "regex-2026.3.32-cp311-cp311-win_arm64.whl", hash = "sha256:ded4fc0edf3de792850cb8b04bbf3c5bd725eeaf9df4c27aad510f6eed9c4e19"}, + {file = "regex-2026.3.32-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ad8d372587e659940568afd009afeb72be939c769c552c9b28773d0337251391"}, + {file = "regex-2026.3.32-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3f5747501b69299c6b0b047853771e4ed390510bada68cb16da9c9c2078343f7"}, + {file = "regex-2026.3.32-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:db976be51375bca900e008941639448d148c655c9545071965d0571ecc04f5d0"}, + {file = "regex-2026.3.32-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66a5083c3ffe5a5a95f8281ea47a88072d4f24001d562d1d9d28d4cdc005fec5"}, + {file = "regex-2026.3.32-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e83ce8008b48762be296f1401f19afd9ea29f3d035d1974e0cecb74e9afbd1df"}, + {file = "regex-2026.3.32-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3aa21bad31db904e0b9055e12c8282df62d43169c4a9d2929407060066ebc74"}, + {file = "regex-2026.3.32-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f54840bea73541652f1170dc63402a5b776fc851ad36a842da9e5163c1f504a0"}, + {file = "regex-2026.3.32-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2ffbadc647325dd4e3118269bda93ded1eb5f5b0c3b7ba79a3da9fbd04f248e9"}, + {file = "regex-2026.3.32-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:66d3126afe7eac41759cd5f0b3b246598086e88e70527c0d68c9e615b81771c4"}, + {file = "regex-2026.3.32-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f785f44a44702dea89b28bce5bc82552490694ce4e144e21a4f0545e364d2150"}, + {file = "regex-2026.3.32-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b7836aa13721dbdef658aebd11f60d00de633a95726521860fe1f6be75fa225a"}, + {file = "regex-2026.3.32-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5336b1506142eb0f23c96fb4a34b37c4fefd4fed2a7042069f3c8058efe17855"}, + {file = "regex-2026.3.32-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b56993a7aeb4140c4770f4f7965c9e5af4f024457d06e23c01b0d47501cb18ed"}, + {file = "regex-2026.3.32-cp312-cp312-win32.whl", hash = "sha256:d363660f9ef8c734495598d2f3e527fb41f745c73159dc0d743402f049fb6836"}, + {file = "regex-2026.3.32-cp312-cp312-win_amd64.whl", hash = "sha256:c9f261ad3cd97257dc1d9355bfbaa7dd703e06574bffa0fa8fe1e31da915ee38"}, + {file = "regex-2026.3.32-cp312-cp312-win_arm64.whl", hash = "sha256:89e50667e7e8c0e7903e4d644a2764fffe9a3a5d6578f72ab7a7b4205bf204b7"}, + {file = "regex-2026.3.32-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c6d9c6e783b348f719b6118bb3f187b2e138e3112576c9679eb458cc8b2e164b"}, + {file = "regex-2026.3.32-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f21ae18dfd15752cdd98d03cbd7a3640be826bfd58482a93f730dbd24d7b9fb"}, + {file = "regex-2026.3.32-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:844d88509c968dd44b30daeefac72b038b1bf31ac372d5106358ab01d393c48b"}, + {file = "regex-2026.3.32-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8fc918cd003ba0d066bf0003deb05a259baaaab4dc9bd4f1207bbbe64224857a"}, + {file = "regex-2026.3.32-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bbc458a292aee57d572075f22c035fa32969cdb7987d454e3e34d45a40a0a8b4"}, + {file = "regex-2026.3.32-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:987cdfcfb97a249abc3601ad53c7de5c370529f1981e4c8c46793e4a1e1bfe8e"}, + {file = "regex-2026.3.32-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5d88fa37ba5e8a80ca8d956b9ea03805cfa460223ac94b7d4854ee5e30f3173"}, + {file = "regex-2026.3.32-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d082be64e51671dd5ee1c208c92da2ddda0f2f20d8ef387e57634f7e97b6aae"}, + {file = "regex-2026.3.32-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c1d7fa44aece1fa02b8927441614c96520253a5cad6a96994e3a81e060feed55"}, + {file = "regex-2026.3.32-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d478a2ca902b6ef28ffc9521e5f0f728d036abe35c0b250ee8ae78cfe7c5e44e"}, + {file = "regex-2026.3.32-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2820d2231885e97aff0fcf230a19ebd5d2b5b8a1ba338c20deb34f16db1c7897"}, + {file = "regex-2026.3.32-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc8ced733d6cd9af5e412f256a32f7c61cd2d7371280a65c689939ac4572499f"}, + {file = "regex-2026.3.32-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:847087abe98b3c1ebf1eb49d6ef320dbba75a83ee4f83c94704580f1df007dd4"}, + {file = "regex-2026.3.32-cp313-cp313-win32.whl", hash = "sha256:d21a07edddb3e0ca12a8b8712abc8452481c3d3db19ae87fc94e9842d005964b"}, + {file = "regex-2026.3.32-cp313-cp313-win_amd64.whl", hash = "sha256:3c054e39a9f85a3d76c62a1d50c626c5e9306964eaa675c53f61ff7ec1204bbb"}, + {file = "regex-2026.3.32-cp313-cp313-win_arm64.whl", hash = "sha256:b2e9c2ea2e93223579308263f359eab8837dc340530b860cb59b713651889f14"}, + {file = "regex-2026.3.32-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5d86e3fb08c94f084a625c8dc2132a79a3a111c8bf6e2bc59351fa61753c2f6e"}, + {file = "regex-2026.3.32-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b6f366a5ef66a2df4d9e68035cfe9f0eb8473cdfb922c37fac1d169b468607b0"}, + {file = "regex-2026.3.32-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b8fca73e16c49dd972ce3a88278dfa5b93bf91ddef332a46e9443abe21ca2f7c"}, + {file = "regex-2026.3.32-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b953d9d496d19786f4d46e6ba4b386c6e493e81e40f9c5392332458183b0599d"}, + {file = "regex-2026.3.32-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b565f25171e04d4fad950d1fa837133e3af6ea6f509d96166eed745eb0cf63bc"}, + {file = "regex-2026.3.32-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f28eac18a8733a124444643a66ac96fef2c0ad65f50034e0a043b90333dc677f"}, + {file = "regex-2026.3.32-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cdd508664430dd51b8888deb6c5b416d8de046b2e11837254378d31febe4a98"}, + {file = "regex-2026.3.32-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5c35d097f509cf7e40d20d5bee548d35d6049b36eb9965e8d43e4659923405b9"}, + {file = "regex-2026.3.32-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:85c9b0c131427470a6423baa0a9330be6fd8c3630cc3ee6fdee03360724cbec5"}, + {file = "regex-2026.3.32-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:e50af656c15e2723eeb7279c0837e07accc594b95ec18b86821a4d44b51b24bf"}, + {file = "regex-2026.3.32-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4bc32b4dbdb4f9f300cf9f38f8ea2ce9511a068ffaa45ac1373ee7a943f1d810"}, + {file = "regex-2026.3.32-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e3e5d1802cba785210a4a800e63fcee7a228649a880f3bf7f2aadccb151a834b"}, + {file = "regex-2026.3.32-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ef250a3f5e93182193f5c927c5e9575b2cb14b80d03e258bc0b89cc5de076b60"}, + {file = "regex-2026.3.32-cp313-cp313t-win32.whl", hash = "sha256:9cf7036dfa2370ccc8651521fcbb40391974841119e9982fa312b552929e6c85"}, + {file = "regex-2026.3.32-cp313-cp313t-win_amd64.whl", hash = "sha256:c940e00e8d3d10932c929d4b8657c2ea47d2560f31874c3e174c0d3488e8b865"}, + {file = "regex-2026.3.32-cp313-cp313t-win_arm64.whl", hash = "sha256:ace48c5e157c1e58b7de633c5e257285ce85e567ac500c833349c363b3df69d4"}, + {file = "regex-2026.3.32-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a416ee898ecbc5d8b283223b4cf4d560f93244f6f7615c1bd67359744b00c166"}, + {file = "regex-2026.3.32-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d76d62909bfb14521c3f7cfd5b94c0c75ec94b0a11f647d2f604998962ec7b6c"}, + {file = "regex-2026.3.32-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:631f7d95c83f42bccfe18946a38ad27ff6b6717fb4807e60cf24860b5eb277fc"}, + {file = "regex-2026.3.32-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12917c6c6813ffcdfb11680a04e4d63c5532b88cf089f844721c5f41f41a63ad"}, + {file = "regex-2026.3.32-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e221b615f83b15887636fcb90ed21f1a19541366f8b7ba14ba1ad8304f4ded4"}, + {file = "regex-2026.3.32-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f9ae4755fa90f1dc2d0d393d572ebc134c0fe30fcfc0ab7e67c1db15f192041"}, + {file = "regex-2026.3.32-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a094e9dcafedfb9d333db5cf880304946683f43a6582bb86688f123335122929"}, + {file = "regex-2026.3.32-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c1cecea3e477af105f32ef2119b8d895f297492e41d317e60d474bc4bffd62ff"}, + {file = "regex-2026.3.32-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f26262900edd16272b6360014495e8d68379c6c6e95983f9b7b322dc928a1194"}, + {file = "regex-2026.3.32-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1cb22fa9ee6a0acb22fc9aecce5f9995fe4d2426ed849357d499d62608fbd7f9"}, + {file = "regex-2026.3.32-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:9b9118a78e031a2e4709cd2fcc3028432e89b718db70073a8da574c249b5b249"}, + {file = "regex-2026.3.32-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b193ed199848aa96618cd5959c1582a0bf23cd698b0b900cb0ffe81b02c8659c"}, + {file = "regex-2026.3.32-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:10fb2aaae1aaadf7d43c9f3c2450404253697bf8b9ce360bd5418d1d16292298"}, + {file = "regex-2026.3.32-cp314-cp314-win32.whl", hash = "sha256:110ba4920721374d16c4c8ea7ce27b09546d43e16aea1d7f43681b5b8f80ba61"}, + {file = "regex-2026.3.32-cp314-cp314-win_amd64.whl", hash = "sha256:245667ad430745bae6a1e41081872d25819d86fbd9e0eec485ba00d9f78ad43d"}, + {file = "regex-2026.3.32-cp314-cp314-win_arm64.whl", hash = "sha256:1ca02ff0ef33e9d8276a1fcd6d90ff6ea055a32c9149c0050b5b67e26c6d2c51"}, + {file = "regex-2026.3.32-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:51fb7e26f91f9091fd8ec6a946f99b15d3bc3667cb5ddc73dd6cb2222dd4a1cc"}, + {file = "regex-2026.3.32-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:51a93452034d671b0e21b883d48ea66c5d6a05620ee16a9d3f229e828568f3f0"}, + {file = "regex-2026.3.32-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:03c2ebd15ff51e7b13bb3dc28dd5ac18cd39e59ebb40430b14ae1a19e833cff1"}, + {file = "regex-2026.3.32-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5bf2f3c2c5bd8360d335c7dcd4a9006cf1dabae063ee2558ee1b07bbc8a20d88"}, + {file = "regex-2026.3.32-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a4a3189a99ecdd1c13f42513ab3fc7fa8311b38ba7596dd98537acb8cd9acc3"}, + {file = "regex-2026.3.32-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3c0bbfbd38506e1ea96a85da6782577f06239cb9fcf9696f1ea537c980c0680b"}, + {file = "regex-2026.3.32-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8aaf8ee8f34b677f90742ca089b9c83d64bdc410528767273c816a863ed57327"}, + {file = "regex-2026.3.32-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ea568832eca219c2be1721afa073c1c9eb8f98a9733fdedd0a9747639fc22a5"}, + {file = "regex-2026.3.32-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e4c8fa46aad1a11ae2f8fcd1c90b9d55e18925829ac0d98c5bb107f93351745"}, + {file = "regex-2026.3.32-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cec365d44835b043d7b3266487797639d07d621bec9dc0ea224b00775797cc1"}, + {file = "regex-2026.3.32-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:09e26cad1544d856da85881ad292797289e4406338afe98163f3db9f7fac816c"}, + {file = "regex-2026.3.32-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:6062c4ef581a3e9e503dccf4e1b7f2d33fdc1c13ad510b287741ac73bc4c6b27"}, + {file = "regex-2026.3.32-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88ebc0783907468f17fca3d7821b30f9c21865a721144eb498cb0ff99a67bcac"}, + {file = "regex-2026.3.32-cp314-cp314t-win32.whl", hash = "sha256:e480d3dac06c89bc2e0fd87524cc38c546ac8b4a38177650745e64acbbcfdeba"}, + {file = "regex-2026.3.32-cp314-cp314t-win_amd64.whl", hash = "sha256:67015a8162d413af9e3309d9a24e385816666fbf09e48e3ec43342c8536f7df6"}, + {file = "regex-2026.3.32-cp314-cp314t-win_arm64.whl", hash = "sha256:1a6ac1ed758902e664e0d95c1ee5991aa6fb355423f378ed184c6ec47a1ec0e9"}, + {file = "regex-2026.3.32.tar.gz", hash = "sha256:f1574566457161678297a116fa5d1556c5a4159d64c5ff7c760e7c564bf66f16"}, +] + +[[package]] +name = "rich" +version = "14.3.3" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +groups = ["main"] +files = [ + {file = "rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d"}, + {file = "rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "shellingham" +version = "1.5.4" +description = "Tool to Detect Surrounding Shell" +optional = false +python-versions = ">=3.7" groups = ["main"] files = [ - {file = "regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e"}, - {file = "regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f"}, - {file = "regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4"}, - {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79"}, - {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9"}, - {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220"}, - {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13"}, - {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3"}, - {file = "regex-2026.1.15-cp310-cp310-win32.whl", hash = "sha256:21ca32c28c30d5d65fc9886ff576fc9b59bbca08933e844fa2363e530f4c8218"}, - {file = "regex-2026.1.15-cp310-cp310-win_amd64.whl", hash = "sha256:3038a62fc7d6e5547b8915a3d927a0fbeef84cdbe0b1deb8c99bbd4a8961b52a"}, - {file = "regex-2026.1.15-cp310-cp310-win_arm64.whl", hash = "sha256:505831646c945e3e63552cc1b1b9b514f0e93232972a2d5bedbcc32f15bc82e3"}, - {file = "regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a"}, - {file = "regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f"}, - {file = "regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1"}, - {file = "regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569"}, - {file = "regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7"}, - {file = "regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec"}, - {file = "regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1"}, - {file = "regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681"}, - {file = "regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22"}, - {file = "regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913"}, - {file = "regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a"}, - {file = "regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056"}, - {file = "regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e"}, - {file = "regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10"}, - {file = "regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3"}, - {file = "regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f"}, - {file = "regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e"}, - {file = "regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337"}, - {file = "regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be"}, - {file = "regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8"}, - {file = "regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60"}, - {file = "regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952"}, - {file = "regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10"}, - {file = "regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829"}, - {file = "regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac"}, - {file = "regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6"}, - {file = "regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1"}, - {file = "regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1"}, - {file = "regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903"}, - {file = "regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705"}, - {file = "regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8"}, - {file = "regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf"}, - {file = "regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db"}, - {file = "regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e"}, - {file = "regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf"}, - {file = "regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70"}, - {file = "regex-2026.1.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:55b4ea996a8e4458dd7b584a2f89863b1655dd3d17b88b46cbb9becc495a0ec5"}, - {file = "regex-2026.1.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e1e28be779884189cdd57735e997f282b64fd7ccf6e2eef3e16e57d7a34a815"}, - {file = "regex-2026.1.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0057de9eaef45783ff69fa94ae9f0fd906d629d0bd4c3217048f46d1daa32e9b"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc7cd0b2be0f0269283a45c0d8b2c35e149d1319dcb4a43c9c3689fa935c1ee6"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8db052bbd981e1666f09e957f3790ed74080c2229007c1dd67afdbf0b469c48b"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:343db82cb3712c31ddf720f097ef17c11dab2f67f7a3e7be976c4f82eba4e6df"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55e9d0118d97794367309635df398bdfd7c33b93e2fdfa0b239661cd74b4c14e"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:008b185f235acd1e53787333e5690082e4f156c44c87d894f880056089e9bc7c"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fd65af65e2aaf9474e468f9e571bd7b189e1df3a61caa59dcbabd0000e4ea839"}, - {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f42e68301ff4afee63e365a5fc302b81bb8ba31af625a671d7acb19d10168a8c"}, - {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f7792f27d3ee6e0244ea4697d92b825f9a329ab5230a78c1a68bd274e64b5077"}, - {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:dbaf3c3c37ef190439981648ccbf0c02ed99ae066087dd117fcb616d80b010a4"}, - {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:adc97a9077c2696501443d8ad3fa1b4fc6d131fc8fd7dfefd1a723f89071cf0a"}, - {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:069f56a7bf71d286a6ff932a9e6fb878f151c998ebb2519a9f6d1cee4bffdba3"}, - {file = "regex-2026.1.15-cp39-cp39-win32.whl", hash = "sha256:ea4e6b3566127fda5e007e90a8fd5a4169f0cf0619506ed426db647f19c8454a"}, - {file = "regex-2026.1.15-cp39-cp39-win_amd64.whl", hash = "sha256:cda1ed70d2b264952e88adaa52eea653a33a1b98ac907ae2f86508eb44f65cdc"}, - {file = "regex-2026.1.15-cp39-cp39-win_arm64.whl", hash = "sha256:b325d4714c3c48277bfea1accd94e193ad6ed42b4bad79ad64f3b8f8a31260a5"}, - {file = "regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5"}, + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, ] [[package]] @@ -804,42 +838,39 @@ files = [ ] [[package]] -name = "typing-extensions" -version = "4.15.0" -description = "Backported and Experimental Type Hints for Python 3.9+" +name = "typer" +version = "0.24.1" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] +python-versions = ">=3.10" +groups = ["main"] files = [ - {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, - {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, + {file = "typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e"}, + {file = "typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45"}, ] +[package.dependencies] +annotated-doc = ">=0.0.2" +click = ">=8.2.1" +rich = ">=12.3.0" +shellingham = ">=1.3.0" + [[package]] -name = "zipp" -version = "3.23.0" -description = "Backport of pathlib-compatible object wrapper for zip files" +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" groups = ["main", "dev"] -markers = "python_version < \"3.10\"" files = [ - {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, - {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - [extras] dev = ["black", "coverage", "isort", "mypy", "pytest"] [metadata] lock-version = "2.1" -python-versions = ">=3.9" -content-hash = "c90af77d35ececd229a2df152846aff6449bcd157ed8e3ad81d3f0285612258f" +python-versions = ">=3.10" +content-hash = "bc25dd44a3fefc544c08799909967e9edaae633d64a71735caa1026b03e93e67" diff --git a/pyproject.toml b/pyproject.toml index ecbcc14..05bff8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,8 +6,9 @@ build-backend = "poetry.core.masonry.api" packages = [{ include = "python_obfuscator" }] [tool.poetry.dependencies] -python = ">=3.9" -regex = "*" +python = ">=3.10" +regex = ">=2026.3.32" +typer = "^0.24.1" [tool.poetry.group.dev.dependencies] mypy = "^1.15.0" @@ -23,8 +24,8 @@ description = "It's a python obfuscator." readme = "README.md" authors = [{ name = "David Teather", email = "contact.davidteather@gmail.com" }] license = { file = "LICENSE" } -dependencies = ["regex"] -requires-python = ">=3.9" +dependencies = ["regex>=2026.3.32", "typer>=0.24.1,<0.25.0"] +requires-python = ">=3.10" classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", diff --git a/python_obfuscator/cli/__init__.py b/python_obfuscator/cli/__init__.py index 1db187c..81fd37e 100644 --- a/python_obfuscator/cli/__init__.py +++ b/python_obfuscator/cli/__init__.py @@ -1,50 +1,54 @@ -import sys -import python_obfuscator -from python_obfuscator.techniques import one_liner -import argparse +from pathlib import Path +from typing import Annotated +import typer -def convert_file(args): - file_path = args.input +import python_obfuscator +from python_obfuscator.techniques import one_liner as one_liner_technique + + +def main( + input_path: Annotated[ + Path, + typer.Option( + "--input", + "-i", + help="File to obfuscate", + exists=True, + dir_okay=False, + readable=True, + ), + ], + replace: Annotated[ + bool, + typer.Option( + "--replace", + "-r", + help="Overwrite the input file with obfuscated code.", + ), + ] = False, + include_one_liner: Annotated[ + bool, + typer.Option( + "--one-liner", + "-ol", + help="Include the one-liner obfuscation technique.", + ), + ] = False, +) -> None: obfuscate = python_obfuscator.obfuscator() - - # removed techniques remove = [] - if not args.one_liner: - remove.append(one_liner) + if not include_one_liner: + remove.append(one_liner_technique) - with open(file_path, "r") as f: - data = f.read() - obfuscated_data = obfuscate.obfuscate(data, remove_techniques=remove) + data = input_path.read_text() + obfuscated_data = obfuscate.obfuscate(data, remove_techniques=remove) - if args.replace: - with open(file_path, "w+") as f: - f.write(obfuscated_data) + if replace: + input_path.write_text(obfuscated_data) else: - print(obfuscated_data) - - -def cli(): - parser = argparse.ArgumentParser(description="Process CLI args") - parser.add_argument( - "-i", "--input", help="File to obfuscate", required=True, type=str - ) - parser.add_argument( - "-r", - "--replace", - help="Replace the file specified", - required=False, - default=False, - type=bool, - ) - parser.add_argument( - "-ol", - "--one-liner", - help="Add the one liner technique", - required=False, - default=False, - type=bool, - ) - args = parser.parse_args() - - convert_file(args) + typer.echo(obfuscated_data) + + +def cli() -> None: + typer.run(main) From e8f6713cf58299dbe6f38b96f8a2ffe31c64ae42 Mon Sep 17 00:00:00 2001 From: davidteather <34144122+davidteather@users.noreply.github.com> Date: Fri, 3 Apr 2026 11:33:31 -0400 Subject: [PATCH 03/11] change default cli behavior, remove replace directive --- .gitignore | 3 ++- README.md | 10 ++++++---- python_obfuscator/cli/__init__.py | 29 ++++++++++++++++++++++------- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index f23068a..610e75f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ build python_obfuscator.egg-info .pytest_cache .mypy_cache -.ruff_cache \ No newline at end of file +.ruff_cache +obfuscated/ \ No newline at end of file diff --git a/README.md b/README.md index fd18f6f..ac8983d 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,16 @@ pip install python-obfuscator ## Quickstart -Print out obfuscated code +By default, obfuscated code is written under an **`obfuscated/`** directory (created next to your current working directory). Paths under the current directory are preserved (e.g. `src/app.py` → `obfuscated/src/app.py`). + ``` pyobfuscate -i your_file.py ``` -Apply changes to the input file +Print to stdout instead (e.g. for piping): + ``` -pyobfuscate -i your_file.py -r True +pyobfuscate -i your_file.py --stdout ``` ## More Detailed Documentation @@ -62,7 +64,7 @@ print("{} that's a great number!".format(user_value)) [With `pyobfuscate -i file.py`](https://gist.github.com/davidteather/b6ff932140d8c174b9c6f50c9b42fdaf) -[With `--one-liner True`](https://gist.github.com/davidteather/75e48c04bf74f0262fe2919239a74295) +[With `--one-liner`](https://gist.github.com/davidteather/75e48c04bf74f0262fe2919239a74295) ## Authors diff --git a/python_obfuscator/cli/__init__.py b/python_obfuscator/cli/__init__.py index 81fd37e..cab8855 100644 --- a/python_obfuscator/cli/__init__.py +++ b/python_obfuscator/cli/__init__.py @@ -6,6 +6,20 @@ import python_obfuscator from python_obfuscator.techniques import one_liner as one_liner_technique +DEFAULT_OUTPUT_DIR = "obfuscated" + + +def _resolved_output_path(input_path: Path) -> Path: + """Write under ./obfuscated/, preserving path relative to cwd when possible.""" + cwd = Path.cwd().resolve() + try: + rel = input_path.resolve().relative_to(cwd) + except ValueError: + rel = Path(input_path.name) + out = cwd / DEFAULT_OUTPUT_DIR / rel + out.parent.mkdir(parents=True, exist_ok=True) + return out + def main( input_path: Annotated[ @@ -19,12 +33,11 @@ def main( readable=True, ), ], - replace: Annotated[ + stdout: Annotated[ bool, typer.Option( - "--replace", - "-r", - help="Overwrite the input file with obfuscated code.", + "--stdout", + help="Print obfuscated code to stdout instead of writing a file.", ), ] = False, include_one_liner: Annotated[ @@ -44,10 +57,12 @@ def main( data = input_path.read_text() obfuscated_data = obfuscate.obfuscate(data, remove_techniques=remove) - if replace: - input_path.write_text(obfuscated_data) - else: + if stdout: typer.echo(obfuscated_data) + else: + out_path = _resolved_output_path(input_path) + out_path.write_text(obfuscated_data) + typer.secho(f"Wrote {out_path}", err=True) def cli() -> None: From c2a52ad9d53046cdbcc098a46aeb0653eecba252 Mon Sep 17 00:00:00 2001 From: davidteather <34144122+davidteather@users.noreply.github.com> Date: Fri, 3 Apr 2026 11:36:50 -0400 Subject: [PATCH 04/11] add --version --- README.md | 6 ++++++ python_obfuscator/cli/__init__.py | 12 ++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ac8983d..6cee937 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,12 @@ Print to stdout instead (e.g. for piping): pyobfuscate -i your_file.py --stdout ``` +Print the installed version (from `python_obfuscator.version`): + +``` +pyobfuscate --version +``` + ## More Detailed Documentation You can use this as a module if you want diff --git a/python_obfuscator/cli/__init__.py b/python_obfuscator/cli/__init__.py index cab8855..5a5bdea 100644 --- a/python_obfuscator/cli/__init__.py +++ b/python_obfuscator/cli/__init__.py @@ -1,3 +1,4 @@ +import sys from pathlib import Path from typing import Annotated @@ -5,6 +6,7 @@ import python_obfuscator from python_obfuscator.techniques import one_liner as one_liner_technique +from python_obfuscator.version import __version__ DEFAULT_OUTPUT_DIR = "obfuscated" @@ -25,6 +27,7 @@ def main( input_path: Annotated[ Path, typer.Option( + ..., "--input", "-i", help="File to obfuscate", @@ -49,21 +52,26 @@ def main( ), ] = False, ) -> None: + resolved = input_path.expanduser().resolve() + obfuscate = python_obfuscator.obfuscator() remove = [] if not include_one_liner: remove.append(one_liner_technique) - data = input_path.read_text() + data = resolved.read_text() obfuscated_data = obfuscate.obfuscate(data, remove_techniques=remove) if stdout: typer.echo(obfuscated_data) else: - out_path = _resolved_output_path(input_path) + out_path = _resolved_output_path(resolved) out_path.write_text(obfuscated_data) typer.secho(f"Wrote {out_path}", err=True) def cli() -> None: + if len(sys.argv) == 2 and sys.argv[1] in ("--version", "-V"): + typer.echo(__version__) + raise SystemExit(0) typer.run(main) From 1741df9aad4b283b47a347400760f5edff203ba3 Mon Sep 17 00:00:00 2001 From: davidteather <34144122+davidteather@users.noreply.github.com> Date: Fri, 3 Apr 2026 11:54:20 -0400 Subject: [PATCH 05/11] test mypy, unit test coverage --- .github/workflows/package-test.yml | 24 ++++++++++++++--- .gitignore | 5 +++- .pre-commit-config.yaml | 16 ++++++++++++ poetry.lock | 41 +++++++++++++++++++++++++++--- pyproject.toml | 39 +++++++++++++++++++++++++++- 5 files changed, 116 insertions(+), 9 deletions(-) diff --git a/.github/workflows/package-test.yml b/.github/workflows/package-test.yml index 2622ecd..bcd7ed0 100644 --- a/.github/workflows/package-test.yml +++ b/.github/workflows/package-test.yml @@ -9,14 +9,16 @@ on: - nightly - "releases/*" +permissions: + contents: read + jobs: Unit-Tests: timeout-minutes: 10 - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest strategy: fail-fast: false matrix: - os: [macos-latest, windows-latest, ubuntu-latest] python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 @@ -35,5 +37,19 @@ jobs: - name: Install dependencies run: poetry install --no-interaction - - name: Run Tests - run: poetry run pytest tests + - name: Type check (mypy) + run: poetry run mypy python_obfuscator + + - name: Run unit tests with coverage + run: | + poetry run coverage run -m pytest + poetry run coverage report + poetry run coverage xml + + - name: Upload coverage reports to Codecov + if: matrix.python-version == '3.12' + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + slug: davidteather/python-obfuscator + files: ./coverage.xml diff --git a/.gitignore b/.gitignore index 610e75f..f01447f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,7 @@ python_obfuscator.egg-info .pytest_cache .mypy_cache .ruff_cache -obfuscated/ \ No newline at end of file +obfuscated/ +.coverage +coverage.xml +htmlcov/ \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d037397..6bbc00d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,6 +24,14 @@ repos: - repo: local hooks: + - id: mypy + name: mypy + entry: poetry run mypy python_obfuscator + language: system + pass_filenames: false + always_run: true + stages: [commit] + - id: pytest-unit name: Run unit tests entry: poetry run pytest tests/ -v @@ -32,6 +40,14 @@ repos: always_run: true stages: [commit] + - id: pytest-coverage-report + name: pytest coverage report (manual) + entry: poetry run pytest tests --cov=python_obfuscator --cov-report=term-missing --cov-report=html + language: system + pass_filenames: false + always_run: true + stages: [manual] + default_language_version: python: python3 diff --git a/poetry.lock b/poetry.lock index 6d4369a..a2d6556 100644 --- a/poetry.lock +++ b/poetry.lock @@ -206,6 +206,9 @@ files = [ {file = "coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239"}, ] +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] @@ -569,6 +572,26 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-cov" +version = "7.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678"}, + {file = "pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2"}, +] + +[package.dependencies] +coverage = {version = ">=7.10.6", extras = ["toml"]} +pluggy = ">=1.2" +pytest = ">=7" + +[package.extras] +testing = ["process-tests", "pytest-xdist", "virtualenv"] + [[package]] name = "pytokens" version = "0.4.1" @@ -786,7 +809,7 @@ description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["main", "dev"] -markers = "python_version < \"3.11\"" +markers = "python_full_version <= \"3.11.0a6\"" files = [ {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, @@ -855,6 +878,18 @@ click = ">=8.2.1" rich = ">=12.3.0" shellingham = ">=1.3.0" +[[package]] +name = "types-regex" +version = "2026.3.32.20260329" +description = "Typing stubs for regex" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "types_regex-2026.3.32.20260329-py3-none-any.whl", hash = "sha256:861d0893bcfe08a57eb7486a502014e29dc2721d46dd5130798fbccafdb31cc0"}, + {file = "types_regex-2026.3.32.20260329.tar.gz", hash = "sha256:12653e44694cb3e3ccdc39bab3d433d2a83fec1c01220e6871fd6f3cf434675c"}, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -868,9 +903,9 @@ files = [ ] [extras] -dev = ["black", "coverage", "isort", "mypy", "pytest"] +dev = ["black", "coverage", "isort", "mypy", "pytest", "pytest-cov", "types-regex"] [metadata] lock-version = "2.1" python-versions = ">=3.10" -content-hash = "bc25dd44a3fefc544c08799909967e9edaae633d64a71735caa1026b03e93e67" +content-hash = "b7f8f0f24b8bf0b821951e3c916c0e6e3dd77ef5c98823995f17a28ff4d946f9" diff --git a/pyproject.toml b/pyproject.toml index 05bff8d..85832ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,8 @@ black = "^25.1.0" isort = "^6.0.0" pytest = "^8.3.4" coverage = "^7.6.12" +pytest-cov = "^7.1.0" +types-regex = "^2026.3.32.20260329" [project] name = "python_obfuscator" @@ -35,7 +37,15 @@ classifiers = [ ] [project.optional-dependencies] -dev = ["pytest", "black", "mypy", "coverage", "isort"] +dev = [ + "pytest", + "pytest-cov", + "black", + "mypy", + "types-regex", + "coverage", + "isort", +] [project.scripts] pyobfuscate = "python_obfuscator.cli:cli" @@ -47,7 +57,34 @@ line_length = 88 [tool.pytest.ini_options] testpaths = ["tests"] +[tool.mypy] +python_version = "3.10" +packages = ["python_obfuscator"] +explicit_package_bases = true +warn_unused_ignores = true +warn_return_any = true +show_error_codes = true +strict = false +disallow_untyped_defs = false +# TODO: Enable when more of the codebase is annotated: +# check_untyped_defs = true +ignore_missing_imports = false + +[tool.pyright] +pythonVersion = "3.10" +include = ["python_obfuscator"] +exclude = ["tests", "build", "dist", ".venv"] +typeCheckingMode = "basic" +reportMissingImports = true +reportMissingTypeStubs = false + [tool.coverage.run] branch = true source = ["python_obfuscator"] omit = ["tests/*"] + +[tool.coverage.report] +show_missing = true +skip_empty = true +precision = 1 +fail_under = 95 From 718a6bff7543889c4d824d0d989824e0a746a628 Mon Sep 17 00:00:00 2001 From: davidteather <34144122+davidteather@users.noreply.github.com> Date: Fri, 3 Apr 2026 13:52:14 -0400 Subject: [PATCH 06/11] swap techniques to ast-based --- pyproject.toml | 2 +- python_obfuscator/__init__.py | 5 +- python_obfuscator/cli/__init__.py | 27 +- python_obfuscator/config.py | 42 ++ python_obfuscator/helpers/random_datatype.py | 37 +- .../helpers/variable_name_generator.py | 56 +- python_obfuscator/obfuscator.py | 57 +- python_obfuscator/techniques.py | 87 --- python_obfuscator/techniques/__init__.py | 20 + .../techniques/ast_transforms/__init__.py | 6 + .../ast_transforms/dead_code_injector.py | 222 ++++++ .../techniques/ast_transforms/exec_wrapper.py | 45 ++ .../ast_transforms/string_hex_encoder.py | 62 ++ .../ast_transforms/variable_renamer.py | 164 +++++ python_obfuscator/techniques/base.py | 35 + python_obfuscator/techniques/registry.py | 32 + tests/e2e/__init__.py | 0 tests/e2e/files/algorithms.py | 166 +++++ tests/e2e/files/cipher.py | 180 +++++ tests/e2e/files/data_structures.py | 238 ++++++ tests/e2e/files/functional.py | 221 ++++++ tests/e2e/files/number_theory.py | 179 +++++ tests/e2e/files/oop_simulation.py | 174 +++++ tests/e2e/test_e2e.py | 152 ++++ tests/techniques/__init__.py | 0 tests/techniques/test_base.py | 57 ++ tests/techniques/test_dead_code_injector.py | 381 ++++++++++ tests/techniques/test_exec_wrapper.py | 101 +++ tests/techniques/test_string_hex_encoder.py | 167 +++++ tests/techniques/test_variable_renamer.py | 313 ++++++++ tests/test_config.py | 75 ++ tests/test_helpers.py | 78 ++ tests/test_integration.py | 679 ++++++++++++++++++ tests/test_module.py | 44 +- tests/test_obfuscator.py | 78 ++ tests/test_registry.py | 72 ++ 36 files changed, 4094 insertions(+), 160 deletions(-) create mode 100644 python_obfuscator/config.py delete mode 100644 python_obfuscator/techniques.py create mode 100644 python_obfuscator/techniques/__init__.py create mode 100644 python_obfuscator/techniques/ast_transforms/__init__.py create mode 100644 python_obfuscator/techniques/ast_transforms/dead_code_injector.py create mode 100644 python_obfuscator/techniques/ast_transforms/exec_wrapper.py create mode 100644 python_obfuscator/techniques/ast_transforms/string_hex_encoder.py create mode 100644 python_obfuscator/techniques/ast_transforms/variable_renamer.py create mode 100644 python_obfuscator/techniques/base.py create mode 100644 python_obfuscator/techniques/registry.py create mode 100644 tests/e2e/__init__.py create mode 100644 tests/e2e/files/algorithms.py create mode 100644 tests/e2e/files/cipher.py create mode 100644 tests/e2e/files/data_structures.py create mode 100644 tests/e2e/files/functional.py create mode 100644 tests/e2e/files/number_theory.py create mode 100644 tests/e2e/files/oop_simulation.py create mode 100644 tests/e2e/test_e2e.py create mode 100644 tests/techniques/__init__.py create mode 100644 tests/techniques/test_base.py create mode 100644 tests/techniques/test_dead_code_injector.py create mode 100644 tests/techniques/test_exec_wrapper.py create mode 100644 tests/techniques/test_string_hex_encoder.py create mode 100644 tests/techniques/test_variable_renamer.py create mode 100644 tests/test_config.py create mode 100644 tests/test_helpers.py create mode 100644 tests/test_integration.py create mode 100644 tests/test_obfuscator.py create mode 100644 tests/test_registry.py diff --git a/pyproject.toml b/pyproject.toml index 85832ed..9dde95c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,7 +81,7 @@ reportMissingTypeStubs = false [tool.coverage.run] branch = true source = ["python_obfuscator"] -omit = ["tests/*"] +omit = ["tests/*", "python_obfuscator/cli/*"] [tool.coverage.report] show_missing = true diff --git a/python_obfuscator/__init__.py b/python_obfuscator/__init__.py index a08606e..75bf479 100644 --- a/python_obfuscator/__init__.py +++ b/python_obfuscator/__init__.py @@ -1,4 +1,5 @@ -from .obfuscator import obfuscator +from .config import ObfuscationConfig +from .obfuscator import Obfuscator, obfuscate from .version import __version__ -__all__ = ["obfuscator", "__version__"] +__all__ = ["Obfuscator", "ObfuscationConfig", "obfuscate", "__version__"] diff --git a/python_obfuscator/cli/__init__.py b/python_obfuscator/cli/__init__.py index 5a5bdea..905f787 100644 --- a/python_obfuscator/cli/__init__.py +++ b/python_obfuscator/cli/__init__.py @@ -5,7 +5,9 @@ import typer import python_obfuscator -from python_obfuscator.techniques import one_liner as one_liner_technique +from python_obfuscator.config import ObfuscationConfig +from python_obfuscator.obfuscator import Obfuscator +from python_obfuscator.techniques import all_technique_names from python_obfuscator.version import __version__ DEFAULT_OUTPUT_DIR = "obfuscated" @@ -43,24 +45,25 @@ def main( help="Print obfuscated code to stdout instead of writing a file.", ), ] = False, - include_one_liner: Annotated[ - bool, + disable: Annotated[ + list[str], typer.Option( - "--one-liner", - "-ol", - help="Include the one-liner obfuscation technique.", + "--disable", + "-d", + help=( + "Disable a technique by name. May be repeated. " + f"Available: {', '.join(sorted(all_technique_names()))}" + ), ), - ] = False, + ] = [], ) -> None: resolved = input_path.expanduser().resolve() - obfuscate = python_obfuscator.obfuscator() - remove = [] - if not include_one_liner: - remove.append(one_liner_technique) + config = ObfuscationConfig.all_enabled().without(*disable) + obfuscator = Obfuscator(config) data = resolved.read_text() - obfuscated_data = obfuscate.obfuscate(data, remove_techniques=remove) + obfuscated_data = obfuscator.obfuscate(data) if stdout: typer.echo(obfuscated_data) diff --git a/python_obfuscator/config.py b/python_obfuscator/config.py new file mode 100644 index 0000000..9d2785f --- /dev/null +++ b/python_obfuscator/config.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class ObfuscationConfig: + """Immutable configuration describing which techniques are enabled. + + Prefer the factory classmethods over constructing directly:: + + # Enable everything registered + cfg = ObfuscationConfig.all_enabled() + + # Enable a specific subset + cfg = ObfuscationConfig.only("variable_renamer", "string_hex_encoder") + + # Start from all-enabled and exclude one + cfg = ObfuscationConfig.all_enabled().without("string_hex_encoder") + """ + + enabled_techniques: frozenset[str] + + @classmethod + def all_enabled(cls) -> ObfuscationConfig: + from .techniques.registry import all_technique_names + + return cls(enabled_techniques=all_technique_names()) + + @classmethod + def only(cls, *names: str) -> ObfuscationConfig: + return cls(enabled_techniques=frozenset(names)) + + def without(self, *names: str) -> ObfuscationConfig: + return ObfuscationConfig( + enabled_techniques=self.enabled_techniques - frozenset(names) + ) + + def with_added(self, *names: str) -> ObfuscationConfig: + return ObfuscationConfig( + enabled_techniques=self.enabled_techniques | frozenset(names) + ) diff --git a/python_obfuscator/helpers/random_datatype.py b/python_obfuscator/helpers/random_datatype.py index 6aa31c6..fd2fa67 100644 --- a/python_obfuscator/helpers/random_datatype.py +++ b/python_obfuscator/helpers/random_datatype.py @@ -1,23 +1,34 @@ +from __future__ import annotations + import random import string -import time +from typing import Callable class RandomDataTypeGenerator: - def __init__(self): - self.generator_options = [self.random_string, self.random_int] + """Generates random Python literal values (str or int). + + Pass a seeded :class:`random.Random` instance for reproducible output:: + + gen = RandomDataTypeGenerator(rng=random.Random(42)) + """ + + def __init__(self, rng: random.Random | None = None) -> None: + self._rng = rng or random.Random() + self._generator_options: list[Callable[[], str | int]] = [ + self.random_string, + self.random_int, + ] - def get_random(self): - return random.choice(self.generator_options)() + def get_random(self) -> str | int: + return self._rng.choice(self._generator_options)() - def random_string(self, length=79): - # Why is it 79 by default? - # See: https://stackoverflow.com/a/16920876/11472374 - # As kirelagin commented readability is very important + def random_string(self, length: int = 79) -> str: + # 79 chars: see https://stackoverflow.com/a/16920876/11472374 return "".join( - random.choice(string.ascii_lowercase + string.ascii_uppercase) - for i in range(length) + self._rng.choice(string.ascii_lowercase + string.ascii_uppercase) + for _ in range(length) ) - def random_int(self): - return random.randint(random.randint(0, 300), random.randint(300, 999)) + def random_int(self) -> int: + return self._rng.randint(self._rng.randint(0, 300), self._rng.randint(300, 999)) diff --git a/python_obfuscator/helpers/variable_name_generator.py b/python_obfuscator/helpers/variable_name_generator.py index 81013fb..ef3d70c 100644 --- a/python_obfuscator/helpers/variable_name_generator.py +++ b/python_obfuscator/helpers/variable_name_generator.py @@ -1,11 +1,21 @@ +from __future__ import annotations + import random import string -import time +from typing import Callable class VariableNameGenerator: - def __init__(self): - self.generator_options = [ + """Generates obfuscated-looking variable names. + + Pass a seeded :class:`random.Random` instance for reproducible output:: + + gen = VariableNameGenerator(rng=random.Random(42)) + """ + + def __init__(self, rng: random.Random | None = None) -> None: + self._rng = rng or random.Random() + self._generator_options: list[Callable[[int], str]] = [ self.random_string, self.l_and_i, self.time_based, @@ -14,33 +24,29 @@ def __init__(self): self.single_letter_a_lot, ] - def get_random(self, id): - return random.choice(self.generator_options)(id) + def get_random(self, id: int) -> str: + return self._rng.choice(self._generator_options)(id) - def random_string(self, id, length=79): - # Why is it 79 by default? - # See: https://stackoverflow.com/a/16920876/11472374 - # As kirelagin commented readability is very important + def random_string(self, id: int, length: int = 79) -> str: + # 79 chars: see https://stackoverflow.com/a/16920876/11472374 return "".join( - random.choice(string.ascii_letters) for i in range(length) + self._rng.choice(string.ascii_letters) for _ in range(length) ) + str(id) - def l_and_i(self, id): - return "".join(random.choice("Il") for i in range(id)) + def l_and_i(self, id: int) -> str: + return "".join(self._rng.choice("Il") for _ in range(id)) - def time_based(self, id): - return ( - random.choice(string.ascii_letters) - + str(time.time()).replace(".", "") - + str(id) - ) + def time_based(self, id: int) -> str: + # Use the rng to produce a large pseudo-time value so that this + # generator is fully deterministic when the rng is seeded. + pseudo_time = str(self._rng.randint(10**12, 10**13)) + return self._rng.choice(string.ascii_letters) + pseudo_time + str(id) - def just_id(self, id): - # python doesn't work with numbers for variable names - return random.choice(string.ascii_letters) + str(id) + def just_id(self, id: int) -> str: + return self._rng.choice(string.ascii_letters) + str(id) - def scream(self, id): - return "".join(random.choice("Aa") for i in range(id)) + def scream(self, id: int) -> str: + return "".join(self._rng.choice("Aa") for _ in range(id)) - def single_letter_a_lot(self, id): - return random.choice(string.ascii_letters) * id + def single_letter_a_lot(self, id: int) -> str: + return self._rng.choice(string.ascii_letters) * id diff --git a/python_obfuscator/obfuscator.py b/python_obfuscator/obfuscator.py index f5ff383..1590e39 100644 --- a/python_obfuscator/obfuscator.py +++ b/python_obfuscator/obfuscator.py @@ -1,10 +1,53 @@ -import logging -from .techniques import obfuscate +from __future__ import annotations +import ast -class obfuscator: - def __init__(self, logging_level=logging.error): - pass +from .config import ObfuscationConfig +from .techniques.registry import all_technique_names, get_transforms - def obfuscate(self, code, remove_techniques=[]): - return obfuscate(code, remove_techniques) + +def _validate_config(config: ObfuscationConfig) -> None: + unknown = config.enabled_techniques - all_technique_names() + if unknown: + raise ValueError( + f"Unknown technique(s): {sorted(unknown)}. " + f"Available: {sorted(all_technique_names())}" + ) + + +def obfuscate(source: str, config: ObfuscationConfig | None = None) -> str: + """Obfuscate *source* using the techniques described by *config*. + + When *config* is ``None`` all registered techniques are applied. + """ + resolved = config if config is not None else ObfuscationConfig.all_enabled() + _validate_config(resolved) + + tree = ast.parse(source) + for transform_cls in get_transforms(resolved.enabled_techniques): + tree = transform_cls().apply(tree) + return ast.unparse(tree) + + +class Obfuscator: + """Stateful wrapper that pre-builds and caches the transform pipeline. + + Prefer this over the module-level :func:`obfuscate` when processing many + files with the same configuration, since the pipeline is validated and + sorted once at construction time. + """ + + def __init__(self, config: ObfuscationConfig | None = None) -> None: + self._config = config if config is not None else ObfuscationConfig.all_enabled() + _validate_config(self._config) + self._transforms = get_transforms(self._config.enabled_techniques) + + @property + def config(self) -> ObfuscationConfig: + return self._config + + def obfuscate(self, source: str) -> str: + tree = ast.parse(source) + for transform_cls in self._transforms: + tree = transform_cls().apply(tree) + return ast.unparse(tree) diff --git a/python_obfuscator/techniques.py b/python_obfuscator/techniques.py deleted file mode 100644 index 9ada11f..0000000 --- a/python_obfuscator/techniques.py +++ /dev/null @@ -1,87 +0,0 @@ -import re -import ast -import random -import time -from .helpers import VariableNameGenerator, RandomDataTypeGenerator -import regex - - -def one_liner(code): - # TODO: strings with \n at top - formatted_code = re.sub( - r"(;)\1+", - ";", - """exec(\"\"\"{};\"\"\")""".format( - code.replace("\n", ";").replace('"""', '\\"\\"\\"') - ), - ) - - if formatted_code[0] == ';': - return formatted_code[1:] - return formatted_code - -def variable_renamer(code): - # add \n so regex picks it up - code = "\n" + code - variable_names = re.findall(r"(\w+)(?=( |)=( |))", code) - name_generator = VariableNameGenerator() - for i in range(len(variable_names)): - obfuscated_name = name_generator.get_random(i + 1) - code = re.sub( - r"(?<=[^.])(\b{}\b)".format(variable_names[i][0]), obfuscated_name, code - ) - return code - - -def add_random_variables(code): - useless_variables_to_add = random.randint(100, 400) - name_generator = VariableNameGenerator() - data_generator = RandomDataTypeGenerator() - for v in range(1, useless_variables_to_add): - rand_data = data_generator.get_random() - if type(rand_data) == str: - rand_data = '"{}"'.format(rand_data) - if v % 2 == 0: - code = "{} = {}\n".format(name_generator.get_random(v), rand_data) + code - else: - code = code + "\n{} = {}".format(name_generator.get_random(v), rand_data) - return code - - -def str_to_hex_bytes(code): - # ((?<=(( | |)\w+( |)=( |))("""|"|'))[\W\w]*?(?=("""|"|'))) - # TODO: Fix still buggy and kinda just wanna publish this to github rn - python_string_decoraters = ['"""', "'''", '"', "'"] - - for s in python_string_decoraters: - pattern = r"((?<=(( | |\n)\w+( |)=( |))({}))[\W\w]*?(?=({})))".format(s, s) - t = regex.findall(pattern, code) - for v in t: - string_contents = v[0] - if s == '"' and string_contents == '"': - continue - if s == "'" and string_contents == "'": - continue - hex_bytes = "\\" + "\\".join( - x.encode("utf-8").hex() for x in string_contents - ) - code = regex.sub(pattern, str(hex_bytes).replace("\\", "\\\\"), code) - - return code - - -def obfuscate(code, remove_techniques=[]): - if len(remove_techniques) == 0: - methods = all_methods - else: - methods = all_methods.copy() - for technique in remove_techniques: - methods.remove(technique) - - for technique in methods: - code = technique(code) - - return code - - -all_methods = [variable_renamer, add_random_variables, one_liner] diff --git a/python_obfuscator/techniques/__init__.py b/python_obfuscator/techniques/__init__.py new file mode 100644 index 0000000..a5eb466 --- /dev/null +++ b/python_obfuscator/techniques/__init__.py @@ -0,0 +1,20 @@ +"""Obfuscation techniques package. + +Importing this package triggers registration of all built-in techniques via +the ``@register`` decorator. Third-party techniques can be registered by +importing their module before calling :func:`python_obfuscator.obfuscate`. +""" + +# Side-effect import: populates the registry +from . import ast_transforms +from .base import ASTTransform, TechniqueMetadata +from .registry import all_technique_names, get_transforms, register + +__all__ = [ + "ASTTransform", + "TechniqueMetadata", + "register", + "all_technique_names", + "get_transforms", + "ast_transforms", +] diff --git a/python_obfuscator/techniques/ast_transforms/__init__.py b/python_obfuscator/techniques/ast_transforms/__init__.py new file mode 100644 index 0000000..7f16989 --- /dev/null +++ b/python_obfuscator/techniques/ast_transforms/__init__.py @@ -0,0 +1,6 @@ +from .dead_code_injector import DeadCodeInjector +from .exec_wrapper import ExecWrapper +from .string_hex_encoder import StringHexEncoder +from .variable_renamer import VariableRenamer + +__all__ = ["DeadCodeInjector", "ExecWrapper", "StringHexEncoder", "VariableRenamer"] diff --git a/python_obfuscator/techniques/ast_transforms/dead_code_injector.py b/python_obfuscator/techniques/ast_transforms/dead_code_injector.py new file mode 100644 index 0000000..ab9fb74 --- /dev/null +++ b/python_obfuscator/techniques/ast_transforms/dead_code_injector.py @@ -0,0 +1,222 @@ +from __future__ import annotations + +import ast +import random +from dataclasses import dataclass +from typing import ClassVar + +from ...helpers import RandomDataTypeGenerator, VariableNameGenerator +from ..base import ASTTransform, TechniqueMetadata +from ..registry import register +from .variable_renamer import _NameCollector + +_MAX_NAME_ATTEMPTS = 200 + + +def _interleave( + original: list[ast.stmt], + injected: list[ast.stmt], + rng: random.Random, +) -> list[ast.stmt]: + """Return a copy of *original* with *injected* statements scattered throughout. + + Each injected statement is assigned a random slot (gap between real + statements) independently. Critically, the *relative order* of injected + statements is preserved: if injected[i] references a variable defined by + injected[j] where j < i, injected[j] will always appear first, preventing + NameErrors when cross-scope references are used. + """ + if not injected: + return list(original) + n = len(original) + # Sort slots to preserve relative order among injected stmts. + slots = sorted(rng.randint(0, n) for _ in injected) + + junk_by_slot: list[list[ast.stmt]] = [[] for _ in range(n + 1)] + for stmt, slot in zip(injected, slots): + junk_by_slot[slot].append(stmt) + + result: list[ast.stmt] = [] + for i, real_stmt in enumerate(original): + result.extend(junk_by_slot[i]) + result.append(real_stmt) + result.extend(junk_by_slot[n]) + return result + + +@dataclass(frozen=True) +class InjectionParams: + """Controls dead code injection density and cross-reference behaviour. + + ``stmts_per_scope_min`` / ``stmts_per_scope_max`` + Range of junk statements injected into each discovered scope + (module body, function body, if-branch, for-body, …). + + ``cross_ref_probability`` + Probability [0, 1] that a new junk assignment reads from an already- + created junk variable (from the same scope *or* an outer scope) rather + than a fresh literal. This creates convincing inter-variable "flow" + that is still entirely dead. + """ + + stmts_per_scope_min: int = 3 + stmts_per_scope_max: int = 10 + cross_ref_probability: float = 0.35 + + +@register +class DeadCodeInjector(ASTTransform): + """Recursively injects dead variable assignments at every scope level. + + Unlike a flat module-level approach this transform walks the entire AST + and injects into every statement list it finds — function bodies, class + bodies, if/elif/else branches, for/while bodies, with-blocks, and + try/except/finally handlers. + + Outer-scope junk names are passed *into* nested scopes so that inner dead + code can reference them (read-only), creating the illusion that values flow + through the program. Junk names themselves are globally unique across all + scopes to avoid any accidental shadowing. + + Pass a seeded :class:`random.Random` and/or custom :class:`InjectionParams` + for reproducible output or tuned density:: + + injector = DeadCodeInjector( + rng=random.Random(42), + params=InjectionParams(stmts_per_scope_min=1, stmts_per_scope_max=4), + ) + """ + + metadata: ClassVar[TechniqueMetadata] = TechniqueMetadata( + name="dead_code_injector", + description="Recursively injects dead code at every scope level.", + priority=30, + ) + + def __init__( + self, + rng: random.Random | None = None, + params: InjectionParams | None = None, + ) -> None: + self._rng = rng or random.Random() + self._params = params or InjectionParams() + + # ------------------------------------------------------------------ + # ASTTransform entry point + # ------------------------------------------------------------------ + + def apply(self, tree: ast.Module) -> ast.Module: + collector = _NameCollector() + collector.visit(tree) + + # Initialise per-apply state (a new instance is created per pipeline + # run, so this is safe to store on self). + self._excluded: frozenset[str] = collector.all_bound_names + self._seen: set[str] = set() + self._counter: int = 0 + self._name_gen = VariableNameGenerator(rng=self._rng) + self._data_gen = RandomDataTypeGenerator(rng=self._rng) + + self._inject_stmts(tree.body, outer_junk=frozenset()) + return super().apply(tree) + + # ------------------------------------------------------------------ + # Core injection helpers + # ------------------------------------------------------------------ + + def _fresh_name(self) -> str | None: + """Return a name not already used anywhere in the module.""" + for _ in range(_MAX_NAME_ATTEMPTS): + name = self._name_gen.get_random(self._counter + 1) + self._counter += 1 + if name not in self._excluded and name not in self._seen: + self._seen.add(name) + return name + return None # safety guard — essentially unreachable in practice + + def _make_junk_stmt( + self, + local_junk: set[str], + ) -> ast.stmt | None: + """Build one dead assignment, optionally referencing existing local junk. + + Cross-references are intentionally limited to ``local_junk`` (names + already injected in this same scope during the current call to + ``_inject_stmts``). Referencing ``outer_junk`` names would risk a + ``NameError`` at runtime: module-level junk is randomly interleaved + with real statements, so it may not yet be defined at the point where + an inner scope (function, if-branch, …) reads it. Since + ``_interleave`` uses sorted slots, local junk stmts always appear in + generation order, making intra-scope cross-references safe. + """ + name = self._fresh_name() + if name is None: + return None + + available = list(local_junk) + if available and self._rng.random() < self._params.cross_ref_probability: + ref = self._rng.choice(available) + value: ast.expr = ast.Name(id=ref, ctx=ast.Load()) + else: + value = ast.Constant(value=self._data_gen.get_random()) + + local_junk.add(name) + return ast.Assign( + targets=[ast.Name(id=name, ctx=ast.Store())], + value=value, + ) + + def _inject_stmts( + self, + stmts: list[ast.stmt], + outer_junk: frozenset[str], + ) -> None: + """Inject junk into *stmts* in-place, then recurse into nested scopes.""" + count = self._rng.randint( + self._params.stmts_per_scope_min, + self._params.stmts_per_scope_max, + ) + local_junk: set[str] = set() + new_stmts: list[ast.stmt] = [] + + for _ in range(count * 10): + if len(new_stmts) >= count: + break + stmt = self._make_junk_stmt(local_junk) + if stmt is None: + break + new_stmts.append(stmt) + + stmts[:] = _interleave(stmts, new_stmts, self._rng) + + # Pass combined pool into nested scopes (read-only for outer names). + combined = outer_junk | frozenset(local_junk) + for stmt in stmts: + self._recurse_into(stmt, combined) + + def _recurse_into(self, stmt: ast.stmt, outer_junk: frozenset[str]) -> None: + """Dispatch injection into all statement lists nested inside *stmt*.""" + if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + self._inject_stmts(stmt.body, outer_junk) + + elif isinstance(stmt, ast.If): + self._inject_stmts(stmt.body, outer_junk) + if stmt.orelse: + self._inject_stmts(stmt.orelse, outer_junk) + + elif isinstance(stmt, (ast.For, ast.While)): + self._inject_stmts(stmt.body, outer_junk) + if stmt.orelse: + self._inject_stmts(stmt.orelse, outer_junk) + + elif isinstance(stmt, ast.With): + self._inject_stmts(stmt.body, outer_junk) + + elif isinstance(stmt, ast.Try): + self._inject_stmts(stmt.body, outer_junk) + for handler in stmt.handlers: + self._inject_stmts(handler.body, outer_junk) + if stmt.orelse: + self._inject_stmts(stmt.orelse, outer_junk) + if stmt.finalbody: + self._inject_stmts(stmt.finalbody, outer_junk) diff --git a/python_obfuscator/techniques/ast_transforms/exec_wrapper.py b/python_obfuscator/techniques/ast_transforms/exec_wrapper.py new file mode 100644 index 0000000..c3b9eee --- /dev/null +++ b/python_obfuscator/techniques/ast_transforms/exec_wrapper.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import ast +from typing import ClassVar + +from ..base import ASTTransform, TechniqueMetadata +from ..registry import register + + +@register +class ExecWrapper(ASTTransform): + """Replaces the module body with a single ``exec('...')`` call. + + This reduces the program to one top-level statement. Because the inner + source is stored as an ``ast.Constant``, all quoting and escaping is + handled by ``ast.unparse`` — no manual string escaping is needed. + + Priority 100 ensures this runs last so that all other AST transforms + (variable renaming, string encoding, etc.) have already been applied to + the inner code before it is embedded in the exec call. + """ + + metadata: ClassVar[TechniqueMetadata] = TechniqueMetadata( + name="exec_wrapper", + description="Replaces the module body with a single exec('...') call.", + priority=100, + ) + + def apply(self, tree: ast.Module) -> ast.Module: + inner_source = ast.unparse(tree) + + new_tree = ast.Module( + body=[ + ast.Expr( + value=ast.Call( + func=ast.Name(id="exec", ctx=ast.Load()), + args=[ast.Constant(value=inner_source)], + keywords=[], + ) + ) + ], + type_ignores=[], + ) + ast.fix_missing_locations(new_tree) + return new_tree diff --git a/python_obfuscator/techniques/ast_transforms/string_hex_encoder.py b/python_obfuscator/techniques/ast_transforms/string_hex_encoder.py new file mode 100644 index 0000000..4e8d9e6 --- /dev/null +++ b/python_obfuscator/techniques/ast_transforms/string_hex_encoder.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +import ast +from typing import ClassVar + +from ..base import ASTTransform, TechniqueMetadata +from ..registry import register + + +def _str_to_hex_call(value: str) -> ast.Call: + """Build a ``bytes.fromhex('...').decode('utf-8')`` AST node. + + This is the canonical replacement for a string literal: it produces + valid Python that evaluates to the original string while making the + content unreadable without execution. + """ + hex_value = value.encode("utf-8").hex() + return ast.Call( + func=ast.Attribute( + value=ast.Call( + func=ast.Attribute( + value=ast.Name(id="bytes", ctx=ast.Load()), + attr="fromhex", + ctx=ast.Load(), + ), + args=[ast.Constant(value=hex_value)], + keywords=[], + ), + attr="decode", + ctx=ast.Load(), + ), + args=[ast.Constant(value="utf-8")], + keywords=[], + ) + + +@register +class StringHexEncoder(ASTTransform): + """Replaces string literals with ``bytes.fromhex(...).decode('utf-8')``.""" + + metadata: ClassVar[TechniqueMetadata] = TechniqueMetadata( + name="string_hex_encoder", + description=( + "Encodes string literals to bytes.fromhex(...).decode('utf-8') " + "calls, hiding their content." + ), + priority=20, + ) + + def visit_Constant(self, node: ast.Constant) -> ast.AST: + if not isinstance(node.value, str): + return node + return ast.copy_location(_str_to_hex_call(node.value), node) + + def visit_JoinedStr(self, node: ast.JoinedStr) -> ast.JoinedStr: + # f-strings embed Constant nodes for their literal parts, but the + # semantics differ from top-level string constants. Replacing them + # with Call nodes would produce invalid AST inside a JoinedStr, so we + # skip f-strings entirely. + # TODO: we could split this out to a new technique in the future. + # consider also over-doing f strings wiht some random strings/args places around + return node diff --git a/python_obfuscator/techniques/ast_transforms/variable_renamer.py b/python_obfuscator/techniques/ast_transforms/variable_renamer.py new file mode 100644 index 0000000..ec33a7b --- /dev/null +++ b/python_obfuscator/techniques/ast_transforms/variable_renamer.py @@ -0,0 +1,164 @@ +from __future__ import annotations + +import ast +import builtins +import re +from typing import ClassVar + +from ...helpers import VariableNameGenerator +from ..base import ASTTransform, TechniqueMetadata +from ..registry import register + +_BUILTIN_NAMES: frozenset[str] = frozenset(dir(builtins)) +_DUNDER_RE: re.Pattern[str] = re.compile(r"^__\w+__$") + + +class _NameCollector(ast.NodeVisitor): + """First-pass visitor that identifies every name eligible for renaming. + + A name is *ineligible* if it is: + - a Python builtin (``print``, ``len``, etc.) + - bound by an import statement + - a dunder (``__name__``, ``__doc__``, etc.) + + Limitation: scope is not tracked. The same identifier appearing in + different scopes will map to the same obfuscated name. This is + semantically correct for non-shadowing code but may produce surprising + results when the same name is reused in sibling scopes. + """ + + def __init__(self) -> None: + self._imported: set[str] = set() + self._assigned: set[str] = set() + self._attr_accessed: set[str] = set() + + @property + def renameable(self) -> frozenset[str]: + return frozenset( + name + for name in self._assigned + if name not in _BUILTIN_NAMES + and name not in self._imported + and not _DUNDER_RE.match(name) + and name not in self._attr_accessed + ) + + @property + def all_bound_names(self) -> frozenset[str]: + """Every name that is assigned or imported, plus all builtins. + + Use this as the exclusion set when generating junk names so that + injected variables never shadow real ones. + """ + return frozenset(self._assigned) | frozenset(self._imported) | _BUILTIN_NAMES + + def visit_Import(self, node: ast.Import) -> None: + for alias in node.names: + bound = alias.asname or alias.name.split(".")[0] + self._imported.add(bound) + self.generic_visit(node) + + def visit_ImportFrom(self, node: ast.ImportFrom) -> None: + for alias in node.names: + if alias.name == "*": + continue # can't know statically what names are introduced + bound = alias.asname or alias.name + self._imported.add(bound) + self.generic_visit(node) + + def visit_Name(self, node: ast.Name) -> None: + if isinstance(node.ctx, ast.Store): + self._assigned.add(node.id) + + def visit_FunctionDef(self, node: ast.FunctionDef) -> None: + self._assigned.add(node.name) + self.generic_visit(node) + + def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: + self._assigned.add(node.name) + self.generic_visit(node) + + def visit_ClassDef(self, node: ast.ClassDef) -> None: + self._assigned.add(node.name) + self.generic_visit(node) + + def visit_Attribute(self, node: ast.Attribute) -> None: + # Attribute names (obj.name) are stored as bare strings in the AST. + # We cannot update call sites without full type analysis, so any name + # that appears as an attribute accessor is excluded from renaming. + self._attr_accessed.add(node.attr) + self.generic_visit(node) + + def visit_arg(self, node: ast.arg) -> None: + self._assigned.add(node.arg) + # Do not recurse: we intentionally skip renaming annotation names + + +@register +class VariableRenamer(ASTTransform): + """Renames variables, functions, classes, and parameters to unreadable names. + + Uses a two-pass approach: + 1. :class:`_NameCollector` walks the tree to build the rename mapping. + 2. The :class:`ast.NodeTransformer` machinery applies the mapping. + """ + + metadata: ClassVar[TechniqueMetadata] = TechniqueMetadata( + name="variable_renamer", + description="Renames local variables, functions, and parameters to hard-to-read identifiers.", + priority=10, + ) + + def __init__(self) -> None: + self._rename_map: dict[str, str] = {} + + def apply(self, tree: ast.Module) -> ast.Module: + collector = _NameCollector() + collector.visit(tree) + + name_gen = VariableNameGenerator() + # Sort for deterministic ordering given the same input + self._rename_map = { + name: name_gen.get_random(i + 1) + for i, name in enumerate(sorted(collector.renameable)) + } + + return super().apply(tree) + + def visit_Name(self, node: ast.Name) -> ast.Name: + if node.id in self._rename_map: + node.id = self._rename_map[node.id] + return node + + def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef: + if node.name in self._rename_map: + node.name = self._rename_map[node.name] + self.generic_visit(node) + return node + + def visit_AsyncFunctionDef( + self, node: ast.AsyncFunctionDef + ) -> ast.AsyncFunctionDef: + if node.name in self._rename_map: + node.name = self._rename_map[node.name] + self.generic_visit(node) + return node + + def visit_ClassDef(self, node: ast.ClassDef) -> ast.ClassDef: + if node.name in self._rename_map: + node.name = self._rename_map[node.name] + self.generic_visit(node) + return node + + def visit_arg(self, node: ast.arg) -> ast.arg: + if node.arg in self._rename_map: + node.arg = self._rename_map[node.arg] + return node + + def visit_Nonlocal(self, node: ast.Nonlocal) -> ast.Nonlocal: + node.names = [self._rename_map.get(n, n) for n in node.names] + return node + + def visit_Global(self, node: ast.Global) -> ast.Global: + node.names = [self._rename_map.get(n, n) for n in node.names] + return node diff --git a/python_obfuscator/techniques/base.py b/python_obfuscator/techniques/base.py new file mode 100644 index 0000000..f01a512 --- /dev/null +++ b/python_obfuscator/techniques/base.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import ast +from abc import ABC +from dataclasses import dataclass +from typing import ClassVar + + +@dataclass(frozen=True) +class TechniqueMetadata: + name: str + description: str + priority: int # lower numbers run first + + +class ASTTransform(ast.NodeTransformer, ABC): + """Base for all obfuscation transforms. + + Subclasses must define a class-level ``metadata`` attribute and implement + one or more ``visit_*`` methods. The entry point is :meth:`apply`, not + :meth:`visit` directly — this ensures ``ast.fix_missing_locations`` is + always called after the transform. + """ + + metadata: ClassVar[TechniqueMetadata] + + def apply(self, tree: ast.Module) -> ast.Module: + result = self.visit(tree) + if not isinstance(result, ast.Module): + raise RuntimeError( + f"{type(self).__name__}.visit() returned a non-Module node; " + "transforms must not replace the top-level Module" + ) + ast.fix_missing_locations(result) + return result diff --git a/python_obfuscator/techniques/registry.py b/python_obfuscator/techniques/registry.py new file mode 100644 index 0000000..5f9f012 --- /dev/null +++ b/python_obfuscator/techniques/registry.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from .base import ASTTransform + +_REGISTRY: dict[str, type[ASTTransform]] = {} + + +def register(cls: type[ASTTransform]) -> type[ASTTransform]: + """Class decorator that registers a transform with the global registry. + + Usage:: + + @register + class MyTransform(ASTTransform): + metadata = TechniqueMetadata(name="my_transform", ...) + ... + """ + if not (isinstance(cls, type) and issubclass(cls, ASTTransform)): + raise TypeError(f"{cls!r} must be a subclass of ASTTransform") + _REGISTRY[cls.metadata.name] = cls + return cls + + +def all_technique_names() -> frozenset[str]: + """Return the names of every registered technique.""" + return frozenset(_REGISTRY) + + +def get_transforms(enabled: frozenset[str]) -> list[type[ASTTransform]]: + """Return enabled transforms sorted by ascending priority.""" + transforms = [_REGISTRY[name] for name in enabled if name in _REGISTRY] + return sorted(transforms, key=lambda cls: cls.metadata.priority) diff --git a/tests/e2e/__init__.py b/tests/e2e/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/files/algorithms.py b/tests/e2e/files/algorithms.py new file mode 100644 index 0000000..03113ac --- /dev/null +++ b/tests/e2e/files/algorithms.py @@ -0,0 +1,166 @@ +"""Sorting and searching algorithms on a variety of datasets.""" + + +def merge_sort(arr): + if len(arr) <= 1: + return list(arr) + mid = len(arr) // 2 + left = merge_sort(arr[:mid]) + right = merge_sort(arr[mid:]) + merged = [] + i = j = 0 + while i < len(left) and j < len(right): + if left[i] <= right[j]: + merged.append(left[i]) + i += 1 + else: + merged.append(right[j]) + j += 1 + merged.extend(left[i:]) + merged.extend(right[j:]) + return merged + + +def heap_sort(arr): + lst = list(arr) + n = len(lst) + + def sift_down(i, end): + while True: + largest = i + left = 2 * i + 1 + right = 2 * i + 2 + if left <= end and lst[left] > lst[largest]: + largest = left + if right <= end and lst[right] > lst[largest]: + largest = right + if largest == i: + break + lst[i], lst[largest] = lst[largest], lst[i] + i = largest + + for i in range((n - 1) // 2, -1, -1): + sift_down(i, n - 1) + for end in range(n - 1, 0, -1): + lst[0], lst[end] = lst[end], lst[0] + sift_down(0, end - 1) + return lst + + +def quick_sort(arr): + if len(arr) <= 1: + return list(arr) + pivot = arr[len(arr) // 2] + left = [x for x in arr if x < pivot] + mid = [x for x in arr if x == pivot] + right = [x for x in arr if x > pivot] + return quick_sort(left) + mid + quick_sort(right) + + +def counting_sort(arr, key=None): + if not arr: + return [] + mapped = [key(x) for x in arr] if key else list(arr) + lo, hi = min(mapped), max(mapped) + counts = [0] * (hi - lo + 1) + for v in mapped: + counts[v - lo] += 1 + result = [] + for i, c in enumerate(counts): + result.extend([i + lo] * c) + return result + + +def binary_search(arr, target): + lo, hi = 0, len(arr) - 1 + while lo <= hi: + mid = (lo + hi) // 2 + if arr[mid] == target: + return mid + elif arr[mid] < target: + lo = mid + 1 + else: + hi = mid - 1 + return -1 + + +def interpolation_search(arr, target): + lo, hi = 0, len(arr) - 1 + while lo <= hi and arr[lo] <= target <= arr[hi]: + if arr[lo] == arr[hi]: + return lo if arr[lo] == target else -1 + pos = lo + (target - arr[lo]) * (hi - lo) // (arr[hi] - arr[lo]) + if arr[pos] == target: + return pos + elif arr[pos] < target: + lo = pos + 1 + else: + hi = pos - 1 + return -1 + + +# ── correctness tests ──────────────────────────────────────────────────────── + +DATA = [ + 82, + 3, + 47, + 15, + 91, + 64, + 28, + 73, + 9, + 56, + 40, + 17, + 85, + 32, + 60, + 7, + 99, + 21, + 44, + 68, + 1, + 77, + 53, + 36, + 12, + 89, + 24, + 71, + 49, + 5, +] + +EXPECTED = sorted(DATA) + +for label, fn in [ + ("merge", merge_sort), + ("heap", heap_sort), + ("quick", quick_sort), + ("counting", counting_sort), +]: + result = fn(DATA) + ok = "OK" if result == EXPECTED else "FAIL" + print(f"{label:10s} {ok} first={result[:5]}") + +sorted_data = EXPECTED +for target in [1, 12, 49, 99, 100, 3, 50]: + bi = binary_search(sorted_data, target) + ii = interpolation_search(sorted_data, target) + match = "match" if bi == ii else f"MISMATCH bi={bi} ii={ii}" + found = "found" if bi != -1 else "absent" + print(f"search({target:3d}): {found:6s} {match}") + +# ── multi-pass on larger data ───────────────────────────────────────────────── + +big = list(range(200, 0, -1)) # 200 down to 1 +big_sorted = sorted(big) + +for fn in (merge_sort, heap_sort, quick_sort): + assert fn(big) == big_sorted + +print(f"large-data: all three sorts agree, n={len(big)}") +print(f"sum of sorted: {sum(big_sorted)}") diff --git a/tests/e2e/files/cipher.py b/tests/e2e/files/cipher.py new file mode 100644 index 0000000..d16baf9 --- /dev/null +++ b/tests/e2e/files/cipher.py @@ -0,0 +1,180 @@ +"""Classical ciphers: Caesar, Vigenère, Playfair, and Rail-fence.""" + +import string + +# ── Caesar ──────────────────────────────────────────────────────────────────── + + +def caesar_encrypt(text, shift): + result = [] + for ch in text: + if ch.isupper(): + result.append(chr((ord(ch) - ord("A") + shift) % 26 + ord("A"))) + elif ch.islower(): + result.append(chr((ord(ch) - ord("a") + shift) % 26 + ord("a"))) + else: + result.append(ch) + return "".join(result) + + +def caesar_decrypt(text, shift): + return caesar_encrypt(text, -shift) + + +# ── Vigenère ────────────────────────────────────────────────────────────────── + + +def vigenere_encrypt(text, key): + key = key.upper() + key_len = len(key) + result = [] + ki = 0 + for ch in text: + if ch.isalpha(): + base = ord("A") if ch.isupper() else ord("a") + shift = ord(key[ki % key_len]) - ord("A") + result.append(chr((ord(ch) - base + shift) % 26 + base)) + ki += 1 + else: + result.append(ch) + return "".join(result) + + +def vigenere_decrypt(text, key): + key = key.upper() + key_len = len(key) + result = [] + ki = 0 + for ch in text: + if ch.isalpha(): + base = ord("A") if ch.isupper() else ord("a") + shift = ord(key[ki % key_len]) - ord("A") + result.append(chr((ord(ch) - base - shift) % 26 + base)) + ki += 1 + else: + result.append(ch) + return "".join(result) + + +# ── Rail-fence ──────────────────────────────────────────────────────────────── + + +def rail_fence_encrypt(text, rails): + fence = [[] for _ in range(rails)] + rail = 0 + direction = 1 + for ch in text: + fence[rail].append(ch) + if rail == 0: + direction = 1 + elif rail == rails - 1: + direction = -1 + rail += direction + return "".join(ch for row in fence for ch in row) + + +def rail_fence_decrypt(text, rails): + n = len(text) + pattern = [] + rail = 0 + direction = 1 + for i in range(n): + pattern.append(rail) + if rail == 0: + direction = 1 + elif rail == rails - 1: + direction = -1 + rail += direction + indices = sorted(range(n), key=lambda i: (pattern[i], i)) + result = [""] * n + for pos, ch in zip(indices, text): + result[pos] = ch + return "".join(result) + + +# ── Frequency analysis ──────────────────────────────────────────────────────── + + +def letter_freq(text): + counts = {c: 0 for c in string.ascii_lowercase} + for ch in text.lower(): + if ch in counts: + counts[ch] += 1 + total = sum(counts.values()) or 1 + return {c: round(v / total * 100, 2) for c, v in counts.items() if v > 0} + + +def coincidence_index(text): + """Index of coincidence — useful for identifying poly-alphabetic ciphers.""" + filtered = [ch for ch in text.lower() if ch.isalpha()] + n = len(filtered) + if n < 2: + return 0.0 + freq = {} + for ch in filtered: + freq[ch] = freq.get(ch, 0) + 1 + return sum(f * (f - 1) for f in freq.values()) / (n * (n - 1)) + + +# ── Brute-force Caesar cracker ───────────────────────────────────────────────── + +ENGLISH_FREQ_ORDER = "etaoinshrdlcumwfgypbvkjxqz" + + +def crack_caesar(ciphertext): + """Return most-likely plaintext by frequency analysis.""" + best_shift = 0 + best_score = float("inf") + for shift in range(26): + candidate = caesar_decrypt(ciphertext, shift) + freq = letter_freq(candidate) + top = sorted(freq, key=freq.get, reverse=True)[:6] + score = sum(ENGLISH_FREQ_ORDER.index(c) for c in top if c in ENGLISH_FREQ_ORDER) + if score < best_score: + best_score = score + best_shift = shift + return caesar_decrypt(ciphertext, best_shift), best_shift + + +# ── Outputs ──────────────────────────────────────────────────────────────────── + +PLAINTEXT = "The quick brown fox jumps over the lazy dog" +print("original:", PLAINTEXT) + +for shift in [3, 13, 25]: + enc = caesar_encrypt(PLAINTEXT, shift) + dec = caesar_decrypt(enc, shift) + ok = "OK" if dec == PLAINTEXT else "FAIL" + print(f"caesar({shift:2d}): {enc[:30]}... round-trip={ok}") + +KEY = "LEMON" +vig_enc = vigenere_encrypt(PLAINTEXT, KEY) +vig_dec = vigenere_decrypt(vig_enc, KEY) +print(f"vigenere enc: {vig_enc}") +print(f"vigenere dec: {vig_dec}") +print(f"vigenere round-trip: {'OK' if vig_dec == PLAINTEXT else 'FAIL'}") + +for rails in [2, 3, 4]: + enc = rail_fence_encrypt(PLAINTEXT, rails) + dec = rail_fence_decrypt(enc, rails) + ok = "OK" if dec == PLAINTEXT else "FAIL" + print(f"rail-fence({rails}): {enc[:30]}... round-trip={ok}") + +MESSAGES = [ + "Hello World from Python", + "Attack at dawn", + "Never gonna give you up", +] +for msg in MESSAGES: + shift = 7 + enc = caesar_encrypt(msg, shift) + cracked, found_shift = crack_caesar(enc) + ok = "OK" if cracked == msg else "FAIL" + print(f"crack shift={found_shift}: {ok} '{cracked}'") + +sample = "secretmessagehiddeninplainsight" * 3 +ic = coincidence_index(sample) +print(f"IC(sample): {ic:.4f}") +freq = letter_freq(sample) +top5 = sorted(freq, key=freq.get, reverse=True)[:5] +print(f"top-5 letters: {top5}") diff --git a/tests/e2e/files/data_structures.py b/tests/e2e/files/data_structures.py new file mode 100644 index 0000000..7d97720 --- /dev/null +++ b/tests/e2e/files/data_structures.py @@ -0,0 +1,238 @@ +"""Linked list, binary search tree, and deque implemented from scratch.""" + +import random as _rnd + +# ── Doubly-linked list ──────────────────────────────────────────────────────── + + +class Node: + def __init__(self, value): + self.value = value + self.prev = None + self.next = None + + +class DoublyLinkedList: + def __init__(self): + self.head = None + self.tail = None + self.size = 0 + + def append(self, value): + node = Node(value) + if self.tail is None: + self.head = self.tail = node + else: + node.prev = self.tail + self.tail.next = node + self.tail = node + self.size += 1 + + def prepend(self, value): + node = Node(value) + if self.head is None: + self.head = self.tail = node + else: + node.next = self.head + self.head.prev = node + self.head = node + self.size += 1 + + def remove(self, value): + cur = self.head + while cur: + if cur.value == value: + if cur.prev: + cur.prev.next = cur.next + else: + self.head = cur.next + if cur.next: + cur.next.prev = cur.prev + else: + self.tail = cur.prev + self.size -= 1 + return True + cur = cur.next + return False + + def to_list(self): + result = [] + cur = self.head + while cur: + result.append(cur.value) + cur = cur.next + return result + + def reverse(self): + cur = self.head + while cur: + cur.prev, cur.next = cur.next, cur.prev + cur = cur.prev + self.head, self.tail = self.tail, self.head + + +# ── Binary search tree ──────────────────────────────────────────────────────── + + +class BSTNode: + def __init__(self, key): + self.key = key + self.left = None + self.right = None + self.height = 1 + + +class BST: + def __init__(self): + self.root = None + + def insert(self, key): + self.root = self._insert(self.root, key) + + def _insert(self, node, key): + if node is None: + return BSTNode(key) + if key < node.key: + node.left = self._insert(node.left, key) + elif key > node.key: + node.right = self._insert(node.right, key) + return node + + def search(self, key): + node = self.root + while node: + if key == node.key: + return True + node = node.left if key < node.key else node.right + return False + + def inorder(self): + result = [] + stack = [] + cur = self.root + while cur or stack: + while cur: + stack.append(cur) + cur = cur.left + cur = stack.pop() + result.append(cur.key) + cur = cur.right + return result + + def height(self): + def _h(node): + if node is None: + return 0 + return 1 + max(_h(node.left), _h(node.right)) + + return _h(self.root) + + def min_value(self): + node = self.root + while node and node.left: + node = node.left + return node.key if node else None + + def max_value(self): + node = self.root + while node and node.right: + node = node.right + return node.key if node else None + + +# ── Circular deque ──────────────────────────────────────────────────────────── + + +class Deque: + def __init__(self, capacity=16): + self._buf = [None] * capacity + self._head = 0 + self._size = 0 + + def _grow(self): + cap = len(self._buf) + new_buf = [None] * (cap * 2) + for i in range(self._size): + new_buf[i] = self._buf[(self._head + i) % cap] + self._buf = new_buf + self._head = 0 + + def push_back(self, item): + if self._size == len(self._buf): + self._grow() + tail = (self._head + self._size) % len(self._buf) + self._buf[tail] = item + self._size += 1 + + def push_front(self, item): + if self._size == len(self._buf): + self._grow() + self._head = (self._head - 1) % len(self._buf) + self._buf[self._head] = item + self._size += 1 + + def pop_back(self): + if self._size == 0: + raise IndexError("deque is empty") + tail = (self._head + self._size - 1) % len(self._buf) + val = self._buf[tail] + self._size -= 1 + return val + + def pop_front(self): + if self._size == 0: + raise IndexError("deque is empty") + val = self._buf[self._head] + self._head = (self._head + 1) % len(self._buf) + self._size -= 1 + return val + + def __len__(self): + return self._size + + def to_list(self): + return [self._buf[(self._head + i) % len(self._buf)] for i in range(self._size)] + + +# ── Exercises ───────────────────────────────────────────────────────────────── + +dll = DoublyLinkedList() +for v in [10, 20, 30, 40, 50]: + dll.append(v) +dll.prepend(5) +dll.remove(30) +print("dll forward:", dll.to_list()) +dll.reverse() +print("dll reversed:", dll.to_list()) +print("dll size:", dll.size) + +bst = BST() +keys = [50, 30, 70, 20, 40, 60, 80, 10, 25, 35, 45] +for k in keys: + bst.insert(k) +print("bst inorder:", bst.inorder()) +print("bst height:", bst.height()) +print("bst min:", bst.min_value(), "max:", bst.max_value()) +for k in [25, 55, 80, 1]: + print(f" search({k}): {bst.search(k)}") + +dq = Deque(4) +for v in range(1, 9): + dq.push_back(v) +for v in [0, -1]: + dq.push_front(v) +print("deque:", dq.to_list()) +print("pop_back:", dq.pop_back()) +print("pop_front:", dq.pop_front()) +print("deque after pops:", dq.to_list()) + +# stress: sort using BST (tree sort) +_rnd.seed(42) +random_vals = [_rnd.randint(0, 1000) for _ in range(50)] +tree = BST() +for v in random_vals: + tree.insert(v) +tree_sorted = tree.inorder() +direct_sorted = sorted(set(random_vals)) +print("tree-sort matches sorted(set):", tree_sorted == direct_sorted) +print("tree-sort length:", len(tree_sorted)) diff --git a/tests/e2e/files/functional.py b/tests/e2e/files/functional.py new file mode 100644 index 0000000..3558967 --- /dev/null +++ b/tests/e2e/files/functional.py @@ -0,0 +1,221 @@ +"""Functional programming patterns: higher-order functions, generators, lazy pipelines.""" + +from functools import reduce + +# ── Core combinators ────────────────────────────────────────────────────────── + + +def compose(*fns): + """Right-to-left function composition.""" + + def composed(x): + for fn in reversed(fns): + x = fn(x) + return x + + return composed + + +def pipe(*fns): + """Left-to-right function composition.""" + return compose(*reversed(fns)) + + +def curry(fn): + """Simple two-argument currying.""" + + def curried(a): + def inner(b): + return fn(a, b) + + return inner + + return curried + + +def memoize(fn): + cache = {} + + def wrapper(*args): + if args not in cache: + cache[args] = fn(*args) + return cache[args] + + return wrapper + + +def trampoline(fn): + """Run a thunk-returning function iteratively to avoid stack overflow.""" + + def run(*args): + result = fn(*args) + while callable(result): + result = result() + return result + + return run + + +# ── Lazy sequences ───────────────────────────────────────────────────────────── + + +def take(n, iterable): + result = [] + for i, item in enumerate(iterable): + if i >= n: + break + result.append(item) + return result + + +def drop(n, iterable): + it = iter(iterable) + for _ in range(n): + next(it, None) + return list(it) + + +def naturals(start=0): + n = start + while True: + yield n + n += 1 + + +def fibs(): + a, b = 0, 1 + while True: + yield a + a, b = b, a + b + + +def primes_gen(): + """Infinite prime generator using a sieve dict.""" + composites = {} + n = 2 + while True: + if n not in composites: + yield n + composites[n * n] = [n] + else: + for p in composites[n]: + composites.setdefault(n + p, []).append(p) + del composites[n] + n += 1 + + +def scan(fn, iterable, initial=None): + """Running accumulation (like Haskell's scanl).""" + acc = initial + result = [] if initial is None else [initial] + for item in iterable: + acc = fn(acc, item) if acc is not None else item + result.append(acc) + return result + + +def zip_with(fn, *iterables): + return [fn(*args) for args in zip(*iterables)] + + +def flatten(nested, depth=1): + result = [] + for item in nested: + if isinstance(item, (list, tuple)) and depth > 0: + result.extend(flatten(item, depth - 1)) + else: + result.append(item) + return result + + +def group_by(fn, iterable): + groups = {} + for item in iterable: + key = fn(item) + groups.setdefault(key, []).append(item) + return groups + + +def partition(pred, iterable): + yes, no = [], [] + for item in iterable: + (yes if pred(item) else no).append(item) + return yes, no + + +# ── Memoized recursive algorithms ───────────────────────────────────────────── + + +@memoize +def fib(n): + if n < 2: + return n + return fib(n - 1) + fib(n - 2) + + +@memoize +def catalan(n): + if n == 0: + return 1 + return sum(catalan(i) * catalan(n - 1 - i) for i in range(n)) + + +# ── Outputs ──────────────────────────────────────────────────────────────────── + +double = curry(lambda a, b: a * b)(2) +add10 = curry(lambda a, b: a + b)(10) + + +def square(x): + return x * x + + +pipeline = pipe(double, add10, square) +inputs = [1, 2, 3, 4, 5] +print("pipeline results:", [pipeline(x) for x in inputs]) + +composed = compose(str, square, double) +print("composed:", [composed(x) for x in inputs]) + +fib_list = take(15, fibs()) +print("fibs:", fib_list) +print("fib(30):", fib(30)) + +prime_list = take(20, primes_gen()) +print("primes:", prime_list) +print("50th prime:", take(50, primes_gen())[-1]) + +evens, odds = partition(lambda x: x % 2 == 0, range(20)) +print("evens:", evens) +print("odds:", odds) + +by_mod = group_by(lambda x: x % 3, range(15)) +for k in sorted(by_mod): + print(f"mod3={k}: {by_mod[k]}") + +running_sum = scan(lambda a, b: a + b, range(1, 11)) +print("running sum:", running_sum) + +pairs = zip_with(lambda a, b: a * b, range(1, 6), range(6, 11)) +print("zip_with mul:", pairs) + +nested = [[1, [2, 3]], [4, [5, [6]]]] +print("flatten(depth=1):", flatten(nested, 1)) +print("flatten(depth=2):", flatten(nested, 2)) + +catalan_list = [catalan(n) for n in range(10)] +print("catalan:", catalan_list) + +# Transducer-style pipeline using reduce +data = list(range(1, 21)) +result = reduce( + lambda acc, x: acc + [x * x] if x % 2 == 0 else acc, + data, + [], +) +print("squares of evens:", result) +print("sum:", sum(result)) + +# Triangle numbers via scan over a finite range +triangles = scan(lambda a, b: a + b, range(1, 12)) +print("triangle numbers:", triangles) diff --git a/tests/e2e/files/number_theory.py b/tests/e2e/files/number_theory.py new file mode 100644 index 0000000..e0392f3 --- /dev/null +++ b/tests/e2e/files/number_theory.py @@ -0,0 +1,179 @@ +"""Number theory: primes, factorisation, modular arithmetic, combinatorics.""" + + +def sieve(limit): + """Return all primes up to limit via Sieve of Eratosthenes.""" + if limit < 2: + return [] + is_prime = bytearray([1]) * (limit + 1) + is_prime[0] = is_prime[1] = 0 + for i in range(2, int(limit**0.5) + 1): + if is_prime[i]: + is_prime[i * i :: i] = bytearray(len(is_prime[i * i :: i])) + return [i for i in range(2, limit + 1) if is_prime[i]] + + +def factorize(n): + """Return prime factorisation as list (with repetition).""" + factors = [] + d = 2 + while d * d <= n: + while n % d == 0: + factors.append(d) + n //= d + d += 1 + if n > 1: + factors.append(n) + return factors + + +def gcd(a, b): + while b: + a, b = b, a % b + return a + + +def lcm(a, b): + return a * b // gcd(a, b) + + +def extended_gcd(a, b): + """Return (g, x, y) such that a*x + b*y = g = gcd(a, b).""" + if b == 0: + return a, 1, 0 + g, x1, y1 = extended_gcd(b, a % b) + return g, y1, x1 - (a // b) * y1 + + +def mod_inverse(a, m): + g, x, _ = extended_gcd(a % m, m) + if g != 1: + return None + return x % m + + +def pow_mod(base, exp, mod): + """Fast modular exponentiation.""" + result = 1 + base %= mod + while exp > 0: + if exp % 2 == 1: + result = result * base % mod + exp //= 2 + base = base * base % mod + return result + + +def miller_rabin(n, witnesses=None): + """Deterministic Miller-Rabin for n < 3_215_031_751.""" + if n < 2: + return False + if n in (2, 3, 5, 7): + return True + if n % 2 == 0: + return False + d, r = n - 1, 0 + while d % 2 == 0: + d //= 2 + r += 1 + for a in witnesses or [2, 3, 5, 7]: + if a >= n: + continue + x = pow_mod(a, d, n) + if x in (1, n - 1): + continue + for _ in range(r - 1): + x = pow_mod(x, 2, n) + if x == n - 1: + break + else: + return False + return True + + +def goldbach(n): + """Express even n >= 4 as sum of two primes (Goldbach).""" + primes = set(sieve(n)) + for p in sorted(primes): + if n - p in primes: + return p, n - p + return None + + +def totient(n): + """Euler's totient function phi(n).""" + result = n + p = 2 + temp = n + while p * p <= temp: + if temp % p == 0: + while temp % p == 0: + temp //= p + result -= result // p + p += 1 + if temp > 1: + result -= result // temp + return result + + +def factorial(n): + result = 1 + for i in range(2, n + 1): + result *= i + return result + + +def choose(n, k): + if k > n: + return 0 + k = min(k, n - k) + result = 1 + for i in range(k): + result = result * (n - i) // (i + 1) + return result + + +# ── outputs ─────────────────────────────────────────────────────────────────── + +primes_100 = sieve(100) +print(f"primes<=100: {len(primes_100)} primes, sum={sum(primes_100)}") +print(f"first 10: {primes_100[:10]}") + +for n in [12, 360, 1024, 9973]: + facs = factorize(n) + product = 1 + for f in facs: + product *= f + print(f"factors({n}): {facs} product={product}") + +pairs = [(12, 8), (100, 75), (17, 5), (1001, 77)] +for a, b in pairs: + print(f"gcd({a},{b})={gcd(a,b)} lcm={lcm(a,b)}") + +print("miller-rabin:") +for n in [2, 17, 100, 997, 998, 7919]: + result = miller_rabin(n) + naive = n in set(sieve(8000)) + match = "OK" if result == naive else "FAIL" + print(f" {n}: {result} {match}") + +print("goldbach:") +for n in [28, 100, 200]: + p, q = goldbach(n) + print(f" {n} = {p} + {q}") + +print("totient:") +for n in [1, 6, 10, 36, 100]: + print(f" phi({n}) = {totient(n)}") + +print("mod-inverse:") +for a, m in [(3, 7), (10, 17), (6, 35)]: + inv = mod_inverse(a, m) + print(f" {a}^-1 mod {m} = {inv}") + +print("binomial:") +for n, k in [(5, 2), (10, 3), (20, 10)]: + print(f" C({n},{k}) = {choose(n,k)}") + +print(f"10! = {factorial(10)}") +print(f"pow_mod(2,100,1000000007) = {pow_mod(2, 100, 1000000007)}") diff --git a/tests/e2e/files/oop_simulation.py b/tests/e2e/files/oop_simulation.py new file mode 100644 index 0000000..5c0757c --- /dev/null +++ b/tests/e2e/files/oop_simulation.py @@ -0,0 +1,174 @@ +"""Bank account simulation exercising OOP: inheritance, properties, decorators.""" + + +def validate_positive(fn): + def wrapper(self, amount): + if amount <= 0: + raise ValueError(f"Amount must be positive, got {amount}") + return fn(self, amount) + + return wrapper + + +class Transaction: + def __init__(self, kind, amount, balance_after): + self.kind = kind + self.amount = amount + self.balance_after = balance_after + + def __repr__(self): + return ( + f"Transaction({self.kind}, {self.amount:.2f}, bal={self.balance_after:.2f})" + ) + + +class Account: + _next_id = 1000 + + def __init__(self, owner, initial_balance=0.0): + self.owner = owner + self._balance = float(initial_balance) + self._history = [] + self.account_id = Account._next_id + Account._next_id += 1 + + @property + def balance(self): + return self._balance + + @validate_positive + def deposit(self, amount): + self._balance += amount + self._history.append(Transaction("deposit", amount, self._balance)) + + @validate_positive + def withdraw(self, amount): + if amount > self._balance: + raise ValueError( + f"Insufficient funds: need {amount:.2f}, have {self._balance:.2f}" + ) + self._balance -= amount + self._history.append(Transaction("withdraw", amount, self._balance)) + + def statement(self): + lines = [f"Account #{self.account_id} ({self.owner})"] + for tx in self._history: + lines.append(f" {tx.kind:10s} {tx.amount:8.2f} → {tx.balance_after:.2f}") + lines.append(f" Balance: {self._balance:.2f}") + return "\n".join(lines) + + def __repr__(self): + return f"Account({self.owner!r}, balance={self._balance:.2f})" + + +class SavingsAccount(Account): + def __init__(self, owner, initial_balance=0.0, rate=0.05): + super().__init__(owner, initial_balance) + self.rate = rate + + def apply_interest(self): + interest = round(self._balance * self.rate, 2) + if interest > 0: + self.deposit(interest) + return interest + + @validate_positive + def withdraw(self, amount): + penalty = 0.0 + if amount > self._balance * 0.5: + penalty = round(amount * 0.02, 2) + super().withdraw(amount) + if penalty > 0: + super().withdraw(penalty) + + +class CheckingAccount(Account): + def __init__(self, owner, initial_balance=0.0, overdraft_limit=100.0): + super().__init__(owner, initial_balance) + self.overdraft_limit = overdraft_limit + + @validate_positive + def withdraw(self, amount): + if amount > self._balance + self.overdraft_limit: + raise ValueError("Exceeds overdraft limit") + self._balance -= amount + self._history.append(Transaction("withdraw", amount, self._balance)) + + +class Bank: + def __init__(self, name): + self.name = name + self._accounts = {} + + def open(self, account): + self._accounts[account.account_id] = account + return account + + def transfer(self, from_id, to_id, amount): + src = self._accounts[from_id] + dst = self._accounts[to_id] + src.withdraw(amount) + dst.deposit(amount) + + def total_deposits(self): + return sum(a.balance for a in self._accounts.values()) + + def richest(self): + return max(self._accounts.values(), key=lambda a: a.balance) + + def summary(self): + lines = [f"Bank: {self.name} ({len(self._accounts)} accounts)"] + for acc in sorted(self._accounts.values(), key=lambda a: a.account_id): + lines.append(f" {acc}") + lines.append(f" Total deposits: {self.total_deposits():.2f}") + return "\n".join(lines) + + +# ── Simulation ───────────────────────────────────────────────────────────────── + +bank = Bank("PyBank") + +alice = bank.open(Account("Alice", 500)) +bob = bank.open(SavingsAccount("Bob", 1000, rate=0.10)) +carol = bank.open(CheckingAccount("Carol", 200, overdraft_limit=150)) + +alice.deposit(300) +alice.withdraw(100) +bob.deposit(500) +interest = bob.apply_interest() +print(f"Bob interest: {interest:.2f}") +carol.deposit(50) +carol.withdraw(380) # uses overdraft + +bank.transfer(alice.account_id, bob.account_id, 200) +bank.transfer(bob.account_id, carol.account_id, 100) + +print(bank.summary()) +print("richest:", bank.richest().owner) + +# Test error paths +errors = [] +for desc, fn in [ + ("deposit zero", lambda: alice.deposit(0)), + ("withdraw overdraft", lambda: carol.withdraw(500)), +]: + try: + fn() + except ValueError: + errors.append(f"{desc}: caught ValueError") +print("errors caught:", len(errors)) +for e in errors: + print(" ", e) + +print(alice.statement()) +print(bob.statement()) + +# Batch operations +accounts = [bank.open(Account(f"User{i}", i * 100)) for i in range(1, 6)] +for acc in accounts: + acc.deposit(50) + if acc.balance > 200: + acc.withdraw(80) +totals = [acc.balance for acc in accounts] +print("batch balances:", totals) +print("batch total:", sum(totals)) diff --git a/tests/e2e/test_e2e.py b/tests/e2e/test_e2e.py new file mode 100644 index 0000000..2741eb3 --- /dev/null +++ b/tests/e2e/test_e2e.py @@ -0,0 +1,152 @@ +"""End-to-end tests: run each file in tests/e2e/files/ before and after full +obfuscation and assert that stdout is identical. + +A separate benchmark test measures the runtime overhead introduced by +obfuscation. Benchmark tests always pass — they are informational and are +printed to the terminal when pytest is run with ``-s``. + +Usage:: + + pytest tests/e2e/ -v # correctness only (fast) + pytest tests/e2e/ -v -s # correctness + benchmark output + pytest tests/e2e/ -v -s -k benchmark # benchmarks only +""" + +from __future__ import annotations + +import time +from pathlib import Path +from typing import TYPE_CHECKING + +import pytest + +from python_obfuscator import ObfuscationConfig, obfuscate + +if TYPE_CHECKING: + pass + +# ── configuration ───────────────────────────────────────────────────────────── + +E2E_DIR = Path(__file__).parent / "files" +E2E_FILES = sorted(E2E_DIR.glob("*.py")) + +_BASELINE = ObfuscationConfig.only() # no transforms — plain re-parse/unparse +_ALL = ObfuscationConfig.all_enabled() + +#: Number of timed repetitions per file. Enough for stable means without +#: making the suite slow under CI. +_BENCHMARK_RUNS = 20 + +# ── helpers ─────────────────────────────────────────────────────────────────── + + +def _capture(code: str) -> list[str]: + """Execute *code* and return every string passed to print(), in order. + + The fake ``print`` is injected into the exec namespace so it works even + when ``exec_wrapper`` is active (the inner exec inherits the outer globals). + """ + captured: list[str] = [] + + def fake_print(*args: object, sep: str = " ", end: str = "\n", **_: object) -> None: + captured.append(sep.join(str(a) for a in args)) + + exec(code, {"print": fake_print}) # noqa: S102 + return captured + + +def _time_runs(code: str, n: int) -> float: + """Execute *code* n times and return the total wall-clock seconds.""" + captured: list[str] = [] + + def fake_print(*args: object, **_: object) -> None: + captured.append(str(args)) + + start = time.perf_counter() + for _ in range(n): + exec(code, {"print": fake_print}) # noqa: S102 + return time.perf_counter() - start + + +# ── correctness ─────────────────────────────────────────────────────────────── + + +@pytest.mark.parametrize("py_file", E2E_FILES, ids=[f.stem for f in E2E_FILES]) +def test_e2e_correctness(py_file: Path) -> None: + """Obfuscated output must be byte-for-byte identical to the original.""" + source = py_file.read_text() + expected = _capture(obfuscate(source, config=_BASELINE)) + actual = _capture(obfuscate(source, config=_ALL)) + assert actual == expected, ( + f"\nOutput mismatch for {py_file.name}:\n" + f" expected {len(expected)} line(s), got {len(actual)}\n" + f" first expected: {expected[:5]}\n" + f" first actual: {actual[:5]}" + ) + + +# ── benchmark ───────────────────────────────────────────────────────────────── + + +@pytest.mark.parametrize("py_file", E2E_FILES, ids=[f.stem for f in E2E_FILES]) +def test_e2e_benchmark(py_file: Path) -> None: + """Measure runtime overhead introduced by all obfuscation techniques. + + This test always passes. Run with ``-s`` to see the timing table printed + to stdout. + """ + source = py_file.read_text() + original_code = obfuscate(source, config=_BASELINE) + obfuscated_code = obfuscate(source, config=_ALL) + + orig_t = _time_runs(original_code, _BENCHMARK_RUNS) + obf_t = _time_runs(obfuscated_code, _BENCHMARK_RUNS) + + orig_ms = orig_t * 1000 / _BENCHMARK_RUNS + obf_ms = obf_t * 1000 / _BENCHMARK_RUNS + overhead_pct = (obf_ms / orig_ms - 1.0) * 100 if orig_ms > 0 else 0.0 + + print( + f"\n{'─' * 62}\n" + f" {py_file.name}\n" + f" original : {orig_ms:8.3f} ms/run\n" + f" obfuscated : {obf_ms:8.3f} ms/run\n" + f" overhead : {overhead_pct:+8.1f} %\n" + f" runs : {_BENCHMARK_RUNS}\n" + ) + + +# ── per-technique breakdown ─────────────────────────────────────────────────── + + +_TECHNIQUE_CONFIGS: list[tuple[str, ObfuscationConfig]] = [ + ("baseline (none)", ObfuscationConfig.only()), + ("variable_renamer", ObfuscationConfig.only("variable_renamer")), + ("string_hex_encoder", ObfuscationConfig.only("string_hex_encoder")), + ("dead_code_injector", ObfuscationConfig.only("dead_code_injector")), + ("exec_wrapper", ObfuscationConfig.only("exec_wrapper")), + ("all techniques", ObfuscationConfig.all_enabled()), +] + + +@pytest.mark.parametrize("py_file", E2E_FILES, ids=[f.stem for f in E2E_FILES]) +def test_e2e_per_technique_benchmark(py_file: Path) -> None: + """Break down the runtime overhead contributed by each technique. + + Always passes; prints a table to stdout when run with ``-s``. + """ + source = py_file.read_text() + + rows: list[tuple[str, float]] = [] + for label, cfg in _TECHNIQUE_CONFIGS: + code = obfuscate(source, config=cfg) + t = _time_runs(code, _BENCHMARK_RUNS) + rows.append((label, t * 1000 / _BENCHMARK_RUNS)) + + baseline_ms = rows[0][1] + lines = [f"\n{'─' * 62}", f" Per-technique breakdown: {py_file.name}"] + for label, ms in rows: + overhead = (ms / baseline_ms - 1.0) * 100 if baseline_ms > 0 else 0.0 + bar = "█" * max(0, int(overhead / 5)) + lines.append(f" {label:22s} {ms:8.3f} ms {overhead:+7.1f}% {bar}") + print("\n".join(lines)) diff --git a/tests/techniques/__init__.py b/tests/techniques/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/techniques/test_base.py b/tests/techniques/test_base.py new file mode 100644 index 0000000..be95821 --- /dev/null +++ b/tests/techniques/test_base.py @@ -0,0 +1,57 @@ +"""Tests for the ASTTransform base class.""" + +import ast + +import pytest + +from python_obfuscator.techniques.base import ASTTransform, TechniqueMetadata + + +class _IdentityTransform(ASTTransform): + """No-op transform that returns the tree unchanged.""" + + metadata = TechniqueMetadata(name="_identity", description="identity", priority=0) + + +class _BadTransform(ASTTransform): + """Deliberately returns the wrong node type from visit_Module.""" + + metadata = TechniqueMetadata(name="_bad_transform", description="bad", priority=0) + + def visit_Module(self, node: ast.Module) -> ast.Name: # type: ignore[override] + return ast.Name(id="x", ctx=ast.Load()) + + +class TestASTTransformApply: + def test_apply_returns_module(self) -> None: + tree = ast.parse("x = 1") + result = _IdentityTransform().apply(tree) + assert isinstance(result, ast.Module) + + def test_apply_calls_fix_missing_locations(self) -> None: + tree = ast.parse("x = 1") + result = _IdentityTransform().apply(tree) + compile(result, "", "exec") # fails if locations missing + + def test_apply_raises_runtime_error_on_non_module_return(self) -> None: + tree = ast.parse("x = 1") + with pytest.raises(RuntimeError, match="returned a non-Module node"): + _BadTransform().apply(tree) + + def test_apply_error_message_includes_class_name(self) -> None: + tree = ast.parse("x = 1") + with pytest.raises(RuntimeError, match="_BadTransform"): + _BadTransform().apply(tree) + + +class TestTechniqueMetadata: + def test_fields_are_accessible(self) -> None: + m = TechniqueMetadata(name="foo", description="bar", priority=5) + assert m.name == "foo" + assert m.description == "bar" + assert m.priority == 5 + + def test_metadata_is_frozen(self) -> None: + m = TechniqueMetadata(name="foo", description="bar", priority=5) + with pytest.raises((AttributeError, TypeError)): + m.name = "other" # type: ignore[misc] diff --git a/tests/techniques/test_dead_code_injector.py b/tests/techniques/test_dead_code_injector.py new file mode 100644 index 0000000..a9c4f55 --- /dev/null +++ b/tests/techniques/test_dead_code_injector.py @@ -0,0 +1,381 @@ +"""Tests for DeadCodeInjector.""" + +import ast +import random +import textwrap + +import pytest + +from python_obfuscator.techniques.ast_transforms.dead_code_injector import ( + DeadCodeInjector, + InjectionParams, + _interleave, +) +from python_obfuscator.techniques.registry import all_technique_names + +# Small params so tests run quickly and counts are predictable. +_FAST = InjectionParams(stmts_per_scope_min=2, stmts_per_scope_max=4) + + +def _apply( + source: str, + rng: random.Random | None = None, + params: InjectionParams | None = None, +) -> str: + tree = ast.parse(source) + return ast.unparse(DeadCodeInjector(rng=rng, params=params).apply(tree)) + + +def _all_assigns(source: str) -> list[str]: + """Return every assigned name in *source* (module-wide walk).""" + return [ + node.targets[0].id + for node in ast.walk(ast.parse(source)) + if isinstance(node, ast.Assign) and isinstance(node.targets[0], ast.Name) + ] + + +class TestInterleave: + def test_result_is_sum_of_lengths(self) -> None: + orig = [ast.parse("x = 1").body[0], ast.parse("y = 2").body[0]] + inj = [ast.parse("j = 0").body[0]] + assert len(_interleave(orig, inj, random.Random(0))) == 3 + + def test_empty_injected_returns_copy(self) -> None: + orig = [ast.parse("x = 1").body[0]] + result = _interleave(orig, [], random.Random(0)) + assert len(result) == 1 + + def test_empty_original_receives_injected(self) -> None: + inj = [ast.parse("j = 0").body[0]] + assert len(_interleave([], inj, random.Random(0))) == 1 + + def test_does_not_mutate_original(self) -> None: + orig = [ast.parse("x = 1").body[0]] + _interleave(orig, [ast.parse("j = 0").body[0]], random.Random(0)) + assert len(orig) == 1 + + def test_seeded_order_is_reproducible(self) -> None: + orig = [ast.parse("x = 1").body[0], ast.parse("y = 2").body[0]] + inj = [ast.parse(f"j{i} = {i}").body[0] for i in range(5)] + r1 = [ast.unparse(n) for n in _interleave(orig, inj[:], random.Random(7))] + r2 = [ast.unparse(n) for n in _interleave(orig, inj[:], random.Random(7))] + assert r1 == r2 + + +class TestDeadCodeInjector: + def test_is_registered(self) -> None: + assert "dead_code_injector" in all_technique_names() + + def test_output_is_valid_python(self) -> None: + ast.parse(_apply("x = 1", params=_FAST)) + + def test_output_has_more_assignments_than_input(self) -> None: + result = _apply("x = 1\ny = 2\n", rng=random.Random(0), params=_FAST) + assert len(_all_assigns(result)) > 2 + + def test_seeded_rng_produces_identical_output(self) -> None: + r1 = _apply("x = 1", rng=random.Random(42), params=_FAST) + r2 = _apply("x = 1", rng=random.Random(42), params=_FAST) + assert r1 == r2 + + def test_different_seeds_produce_different_output(self) -> None: + r1 = _apply("x = 1", rng=random.Random(1), params=_FAST) + r2 = _apply("x = 1", rng=random.Random(2), params=_FAST) + assert r1 != r2 + + def test_original_variable_not_clobbered(self) -> None: + source = "sentinel_val = 999\n" + ns: dict = {} + exec(_apply(source, rng=random.Random(0), params=_FAST), ns) + assert ns["sentinel_val"] == 999 + + def test_builtin_not_shadowed(self) -> None: + source = "result = len([1, 2, 3])\n" + ns: dict = {} + exec(_apply(source, rng=random.Random(0), params=_FAST), ns) + assert ns["result"] == 3 + + def test_imported_name_not_shadowed(self) -> None: + source = "import os\npath = os.path.sep\n" + ns: dict = {} + exec(_apply(source, rng=random.Random(0), params=_FAST), ns) + assert isinstance(ns["path"], str) + + def test_function_behavior_preserved(self) -> None: + source = textwrap.dedent( + """\ + def add(a, b): + return a + b + result = add(3, 4) + """ + ) + ns: dict = {} + exec(_apply(source, rng=random.Random(7), params=_FAST), ns) + assert ns["result"] == 7 + + def test_junk_names_are_valid_identifiers(self) -> None: + for name in _all_assigns(_apply("x = 1", rng=random.Random(0), params=_FAST)): + assert name.isidentifier(), f"{name!r} is not a valid identifier" + + def test_junk_names_globally_unique(self) -> None: + result = _apply("x = 1", rng=random.Random(0), params=_FAST) + names = _all_assigns(result) + assert len(names) == len(set(names)) + + def test_apply_returns_module(self) -> None: + tree = ast.parse("x = 1") + assert isinstance( + DeadCodeInjector(rng=random.Random(0)).apply(tree), ast.Module + ) + + def test_apply_produces_compilable_tree(self) -> None: + tree = ast.parse("x = 1\ny = 2") + compile(DeadCodeInjector(rng=random.Random(0)).apply(tree), "", "exec") + + def test_max_attempts_guard_with_all_names_colliding( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + from python_obfuscator.helpers.variable_name_generator import ( + VariableNameGenerator, + ) + + monkeypatch.setattr(VariableNameGenerator, "get_random", lambda self, id: "x") + tree = ast.parse("x = 1") + result = DeadCodeInjector(rng=random.Random(0)).apply(tree) + assert isinstance(result, ast.Module) + ns: dict = {} + exec(ast.unparse(result), ns) + assert ns["x"] == 1 + + def test_zero_stmts_per_scope_produces_no_junk(self) -> None: + # Covers the for-loop-exhausted-naturally branch (count * 10 = 0 + # iterations so the loop body never executes). + params = InjectionParams(stmts_per_scope_min=0, stmts_per_scope_max=0) + source = "x = 1\n" + result = _apply(source, rng=random.Random(0), params=params) + # No new Assign nodes should have been injected + assigns = _all_assigns(result) + assert assigns == ["x"] + + +class TestRecursiveInjection: + """Verify that injection happens inside nested scopes.""" + + def test_injects_into_function_body(self) -> None: + source = textwrap.dedent( + """\ + def foo(): + return 1 + """ + ) + result = _apply(source, rng=random.Random(0), params=_FAST) + tree = ast.parse(result) + func = next(n for n in ast.walk(tree) if isinstance(n, ast.FunctionDef)) + assigns_in_func = [ + n + for n in ast.walk(ast.Module(body=func.body, type_ignores=[])) + if isinstance(n, ast.Assign) + ] + assert len(assigns_in_func) > 0 + + def test_injects_into_class_body(self) -> None: + source = textwrap.dedent( + """\ + class Foo: + x = 1 + """ + ) + result = _apply(source, rng=random.Random(0), params=_FAST) + tree = ast.parse(result) + cls = next(n for n in ast.walk(tree) if isinstance(n, ast.ClassDef)) + assigns = [ + n + for n in ast.walk(ast.Module(body=cls.body, type_ignores=[])) + if isinstance(n, ast.Assign) + ] + assert len(assigns) > 1 # original 'x = 1' plus junk + + def test_injects_into_if_body(self) -> None: + source = "if True:\n x = 1\n" + result = _apply(source, rng=random.Random(0), params=_FAST) + tree = ast.parse(result) + if_node = next(n for n in ast.walk(tree) if isinstance(n, ast.If)) + assigns = [ + n + for n in ast.walk(ast.Module(body=if_node.body, type_ignores=[])) + if isinstance(n, ast.Assign) + ] + assert len(assigns) > 1 + + def test_injects_into_for_body(self) -> None: + source = "total = 0\nfor i in range(3):\n total += i\n" + result = _apply(source, rng=random.Random(0), params=_FAST) + tree = ast.parse(result) + for_node = next(n for n in ast.walk(tree) if isinstance(n, ast.For)) + assigns = [ + n + for n in ast.walk(ast.Module(body=for_node.body, type_ignores=[])) + if isinstance(n, ast.Assign) + ] + assert len(assigns) > 0 + + def test_injects_into_for_orelse(self) -> None: + source = textwrap.dedent( + """\ + for i in range(3): + pass + else: + found = True + """ + ) + result = _apply(source, rng=random.Random(0), params=_FAST) + tree = ast.parse(result) + for_node = next(n for n in ast.walk(tree) if isinstance(n, ast.For)) + assert for_node.orelse, "for/else should be preserved" + assigns = [ + n + for n in ast.walk(ast.Module(body=for_node.orelse, type_ignores=[])) + if isinstance(n, ast.Assign) + ] + # orelse has 'found = True' plus injected junk + assert len(assigns) > 1 + + def test_injects_into_while_body(self) -> None: + source = "i = 0\nwhile i < 1:\n i += 1\n" + result = _apply(source, rng=random.Random(0), params=_FAST) + tree = ast.parse(result) + while_node = next(n for n in ast.walk(tree) if isinstance(n, ast.While)) + assigns = [ + n + for n in ast.walk(ast.Module(body=while_node.body, type_ignores=[])) + if isinstance(n, ast.Assign) + ] + assert len(assigns) > 0 + + def test_injects_into_try_body(self) -> None: + source = textwrap.dedent( + """\ + try: + x = 1 + except Exception: + pass + """ + ) + result = _apply(source, rng=random.Random(0), params=_FAST) + tree = ast.parse(result) + try_node = next(n for n in ast.walk(tree) if isinstance(n, ast.Try)) + assigns = [ + n + for n in ast.walk(ast.Module(body=try_node.body, type_ignores=[])) + if isinstance(n, ast.Assign) + ] + assert len(assigns) > 1 + + def test_injects_into_except_handler(self) -> None: + source = textwrap.dedent( + """\ + try: + x = 1 + except Exception: + pass + """ + ) + result = _apply(source, rng=random.Random(0), params=_FAST) + tree = ast.parse(result) + try_node = next(n for n in ast.walk(tree) if isinstance(n, ast.Try)) + handler_assigns = [ + n + for h in try_node.handlers + for n in ast.walk(ast.Module(body=h.body, type_ignores=[])) + if isinstance(n, ast.Assign) + ] + assert len(handler_assigns) > 0 + + def test_injects_into_with_body(self) -> None: + source = textwrap.dedent( + """\ + import contextlib + with contextlib.suppress(Exception): + x = 1 + """ + ) + result = _apply(source, rng=random.Random(0), params=_FAST) + tree = ast.parse(result) + with_node = next(n for n in ast.walk(tree) if isinstance(n, ast.With)) + assigns = [ + n + for n in ast.walk(ast.Module(body=with_node.body, type_ignores=[])) + if isinstance(n, ast.Assign) + ] + assert len(assigns) > 1 + + def test_nested_function_behavior_preserved(self) -> None: + source = textwrap.dedent( + """\ + def outer(x): + def inner(y): + return x + y + return inner + result = outer(3)(4) + """ + ) + ns: dict = {} + exec(_apply(source, rng=random.Random(0), params=_FAST), ns) + assert ns["result"] == 7 + + +class TestCrossReferences: + """Verify that cross-scope junk references work correctly.""" + + def test_cross_ref_code_executes_without_error(self) -> None: + # With cross_ref_probability=1.0 all junk vars reference earlier ones. + # The first injected var has no pool to draw from so is a literal; + # subsequent ones reference earlier junk. + params = InjectionParams( + stmts_per_scope_min=5, + stmts_per_scope_max=5, + cross_ref_probability=1.0, + ) + source = "x = 1\n" + ns: dict = {} + exec(_apply(source, rng=random.Random(0), params=params), ns) + + def test_zero_cross_ref_produces_only_literals(self) -> None: + params = InjectionParams( + stmts_per_scope_min=3, + stmts_per_scope_max=3, + cross_ref_probability=0.0, + ) + result = _apply("x = 1", rng=random.Random(0), params=params) + tree = ast.parse(result) + # All junk assignment values should be constants (no Name references) + junk_values = [ + node.value + for node in ast.walk(tree) + if isinstance(node, ast.Assign) + and isinstance(node.targets[0], ast.Name) + and node.targets[0].id != "x" + ] + for val in junk_values: + assert isinstance( + val, ast.Constant + ), f"Expected Constant, got {ast.dump(val)}" + + def test_inner_scope_intra_cross_ref_executes_without_error(self) -> None: + # Cross-refs within a function body's own local junk must work. + params = InjectionParams( + stmts_per_scope_min=5, + stmts_per_scope_max=5, + cross_ref_probability=1.0, + ) + source = textwrap.dedent( + """\ + def foo(): + return 42 + result = foo() + """ + ) + ns: dict = {} + exec(_apply(source, rng=random.Random(5), params=params), ns) + assert ns["result"] == 42 diff --git a/tests/techniques/test_exec_wrapper.py b/tests/techniques/test_exec_wrapper.py new file mode 100644 index 0000000..7ae2fbc --- /dev/null +++ b/tests/techniques/test_exec_wrapper.py @@ -0,0 +1,101 @@ +"""Tests for ExecWrapper.""" + +import ast +import textwrap + +import python_obfuscator # trigger registration +from python_obfuscator.techniques.ast_transforms.exec_wrapper import ExecWrapper +from python_obfuscator.techniques.registry import all_technique_names + + +def _apply(source: str) -> str: + tree = ast.parse(source) + return ast.unparse(ExecWrapper().apply(tree)) + + +class TestExecWrapper: + def test_is_registered(self) -> None: + assert "exec_wrapper" in all_technique_names() + + def test_output_is_one_statement(self) -> None: + tree = ast.parse(_apply("x = 1\ny = 2")) + assert len(tree.body) == 1 + assert isinstance(tree.body[0], ast.Expr) + + def test_output_is_exec_call(self) -> None: + tree = ast.parse(_apply("x = 1")) + call = tree.body[0].value + assert isinstance(call, ast.Call) + assert isinstance(call.func, ast.Name) + assert call.func.id == "exec" + + def test_simple_source_executes_correctly(self) -> None: + source = "x = 10\ny = 20\nz = x + y\n" + ns: dict = {} + exec(_apply(source), ns) + assert ns["z"] == 30 + + def test_multiline_function_executes_correctly(self) -> None: + source = textwrap.dedent( + """\ + def add(a, b): + return a + b + result = add(3, 4) + """ + ) + ns: dict = {} + exec(_apply(source), ns) + assert ns["result"] == 7 + + def test_class_definition_executes_correctly(self) -> None: + source = textwrap.dedent( + """\ + class Counter: + def __init__(self): + self.value = 0 + def increment(self): + self.value += 1 + c = Counter() + c.increment() + c.increment() + """ + ) + ns: dict = {} + exec(_apply(source), ns) + assert ns["c"].value == 2 + + def test_string_with_quotes_executes_correctly(self) -> None: + source = "x = 'hello'\ny = \"world\"\n" + ns: dict = {} + exec(_apply(source), ns) + assert ns["x"] == "hello" + assert ns["y"] == "world" + + def test_string_with_backslash_executes_correctly(self) -> None: + source = "import re\npattern = re.compile('\\\\d+')\n" + ns: dict = {} + exec(_apply(source), ns) + + def test_empty_source_executes_correctly(self) -> None: + ns: dict = {} + exec(_apply(""), ns) + + def test_apply_returns_module(self) -> None: + tree = ast.parse("x = 1") + result = ExecWrapper().apply(tree) + assert isinstance(result, ast.Module) + + def test_apply_produces_compilable_tree(self) -> None: + tree = ast.parse("x = 1\ny = 2") + result = ExecWrapper().apply(tree) + compile(result, "", "exec") + + def test_inner_source_is_string_constant(self) -> None: + tree = ast.parse("x = 42") + result = ExecWrapper().apply(tree) + call = result.body[0].value + assert isinstance(call, ast.Call) + inner = call.args[0] + assert isinstance(inner, ast.Constant) + assert isinstance(inner.value, str) + assert "42" in inner.value diff --git a/tests/techniques/test_string_hex_encoder.py b/tests/techniques/test_string_hex_encoder.py new file mode 100644 index 0000000..5fe5c9a --- /dev/null +++ b/tests/techniques/test_string_hex_encoder.py @@ -0,0 +1,167 @@ +"""Tests for StringHexEncoder.""" + +import ast +import textwrap + +import pytest + +from python_obfuscator.techniques.ast_transforms.string_hex_encoder import ( + StringHexEncoder, + _str_to_hex_call, +) + + +def _apply(source: str) -> str: + tree = ast.parse(source) + result = StringHexEncoder().apply(tree) + return ast.unparse(result) + + +def _eval(source: str) -> object: + """Execute source and return the value of the last expression.""" + ns: dict = {} + exec(source, ns) + return ns + + +class TestStrToHexCall: + def test_produces_call_node(self) -> None: + node = _str_to_hex_call("hello") + assert isinstance(node, ast.Call) + + def test_result_evaluates_to_original_string(self) -> None: + node = _str_to_hex_call("hello") + source = ast.unparse(ast.fix_missing_locations(node)) + assert eval(source) == "hello" # noqa: S307 + + def test_empty_string(self) -> None: + node = _str_to_hex_call("") + source = ast.unparse(ast.fix_missing_locations(node)) + assert eval(source) == "" # noqa: S307 + + def test_unicode_string(self) -> None: + node = _str_to_hex_call("café") + source = ast.unparse(ast.fix_missing_locations(node)) + assert eval(source) == "café" # noqa: S307 + + def test_string_with_spaces(self) -> None: + node = _str_to_hex_call("hello world") + source = ast.unparse(ast.fix_missing_locations(node)) + assert eval(source) == "hello world" # noqa: S307 + + def test_string_with_quotes(self) -> None: + node = _str_to_hex_call('say "hi"') + source = ast.unparse(ast.fix_missing_locations(node)) + assert eval(source) == 'say "hi"' # noqa: S307 + + def test_hex_representation_is_visible_in_output(self) -> None: + node = _str_to_hex_call("hi") + source = ast.unparse(ast.fix_missing_locations(node)) + # 'hi' -> 68 69 + assert "6869" in source + + def test_uses_fromhex_decode_pattern(self) -> None: + node = _str_to_hex_call("x") + source = ast.unparse(ast.fix_missing_locations(node)) + assert "fromhex" in source + assert "decode" in source + + +class TestStringHexEncoderTransform: + def test_string_literal_is_replaced(self) -> None: + result = _apply('x = "hello"') + assert "hello" not in result + assert "fromhex" in result + + def test_replacement_evaluates_to_original(self) -> None: + source = 'x = "hello"' + result = _apply(source) + ns: dict = {} + exec(result, ns) + assert ns["x"] == "hello" + + def test_integer_constant_is_unchanged(self) -> None: + result = _apply("x = 42") + assert "42" in result + assert "fromhex" not in result + + def test_float_constant_is_unchanged(self) -> None: + result = _apply("x = 3.14") + assert "3.14" in result + assert "fromhex" not in result + + def test_bytes_constant_is_unchanged(self) -> None: + result = _apply("x = b'hello'") + assert "fromhex" not in result + + def test_none_constant_is_unchanged(self) -> None: + result = _apply("x = None") + assert "fromhex" not in result + + def test_bool_constant_is_unchanged(self) -> None: + result = _apply("x = True") + assert "fromhex" not in result + + def test_empty_string_is_replaced(self) -> None: + source = 'x = ""' + result = _apply(source) + ns: dict = {} + exec(result, ns) + assert ns["x"] == "" + + def test_unicode_string_is_replaced(self) -> None: + source = 'x = "café"' + result = _apply(source) + ns: dict = {} + exec(result, ns) + assert ns["x"] == "café" + + def test_multiple_strings_in_file(self) -> None: + source = textwrap.dedent( + """\ + a = "foo" + b = "bar" + """ + ) + result = _apply(source) + ns: dict = {} + exec(result, ns) + assert ns["a"] == "foo" + assert ns["b"] == "bar" + + def test_fstring_is_not_modified(self) -> None: + source = 'name = "world"\ngreeting = f"hello {name}"' + result = _apply(source) + ns: dict = {} + exec(result, ns) + assert ns["greeting"] == "hello world" + + def test_output_is_valid_python(self) -> None: + source = textwrap.dedent( + """\ + x = "test" + y = 42 + z = "another" + """ + ) + result = _apply(source) + ast.parse(result) # raises SyntaxError if invalid + + def test_nested_string_in_function(self) -> None: + source = textwrap.dedent( + """\ + def greet(): + return "hello" + """ + ) + result = _apply(source) + ns: dict = {} + exec(result, ns) + assert ns["greet"]() == "hello" + + def test_apply_calls_fix_missing_locations(self) -> None: + """apply() must produce a tree that compiles without LocationError.""" + source = 'x = "hello"' + tree = ast.parse(source) + new_tree = StringHexEncoder().apply(tree) + compile(new_tree, "", "exec") # should not raise diff --git a/tests/techniques/test_variable_renamer.py b/tests/techniques/test_variable_renamer.py new file mode 100644 index 0000000..f9ed264 --- /dev/null +++ b/tests/techniques/test_variable_renamer.py @@ -0,0 +1,313 @@ +"""Tests for VariableRenamer.""" + +import ast +import textwrap + +import pytest + +from python_obfuscator.techniques.ast_transforms.variable_renamer import ( + VariableRenamer, + _NameCollector, +) + + +def _apply(source: str) -> str: + tree = ast.parse(source) + result = VariableRenamer().apply(tree) + return ast.unparse(result) + + +def _collect(source: str) -> frozenset[str]: + tree = ast.parse(source) + collector = _NameCollector() + collector.visit(tree) + return collector.renameable + + +class TestNameCollector: + def test_collects_simple_assignment(self) -> None: + assert "x" in _collect("x = 1") + + def test_does_not_collect_builtin_names(self) -> None: + renameable = _collect("print('hello')\nx = len([1, 2])") + assert "print" not in renameable + assert "len" not in renameable + + def test_does_not_collect_imported_name(self) -> None: + renameable = _collect("import os\npath = os.getcwd()") + assert "os" not in renameable + + def test_does_not_collect_from_import_name(self) -> None: + renameable = _collect("from pathlib import Path\np = Path('.')") + assert "Path" not in renameable + + def test_collects_alias_of_import(self) -> None: + renameable = _collect("import os as operating_system\nx = 1") + assert "operating_system" not in renameable + assert "x" in renameable + + def test_from_import_with_asname_excluded(self) -> None: + renameable = _collect("from os import path as p\nx = 1") + assert "p" not in renameable + + def test_star_import_does_not_crash(self) -> None: + renameable = _collect("from os.path import *\nx = 1") + assert "x" in renameable + + def test_does_not_collect_dunder_names(self) -> None: + renameable = _collect("__name__ = 'main'\nx = 1") + assert "__name__" not in renameable + assert "x" in renameable + + def test_collects_function_name(self) -> None: + assert "my_func" in _collect("def my_func(): pass") + + def test_collects_async_function_name(self) -> None: + assert "my_coro" in _collect("async def my_coro(): pass") + + def test_collects_class_name(self) -> None: + assert "MyClass" in _collect("class MyClass: pass") + + def test_collects_function_argument(self) -> None: + renameable = _collect("def foo(bar): pass") + assert "bar" in renameable + + def test_collects_for_loop_variable(self) -> None: + assert "item" in _collect("for item in range(10): pass") + + def test_does_not_collect_load_context_name(self) -> None: + # 'y' is only loaded, never stored + renameable = _collect("x = y + 1") + assert "y" not in renameable + + def test_does_not_collect_builtin_even_if_shadowed(self) -> None: + renameable = _collect("list = [1, 2, 3]") + assert "list" not in renameable + + +class TestVariableRenamer: + def test_renames_simple_variable(self) -> None: + result = _apply("my_variable = 42") + assert "my_variable" not in result + + def test_renamed_code_produces_same_value(self) -> None: + source = textwrap.dedent( + """\ + x = 10 + y = 20 + z = x + y + """ + ) + result = _apply(source) + ns: dict = {} + exec(result, ns) + values = {v for k, v in ns.items() if not k.startswith("__")} + assert 30 in values + + def test_builtin_names_not_renamed(self) -> None: + source = "result = len([1, 2, 3])" + result = _apply(source) + assert "len" in result + + def test_import_name_not_renamed(self) -> None: + source = "import os\npath = os.getcwd()" + result = _apply(source) + assert "os" in result + + def test_dunder_not_renamed(self) -> None: + source = "if __name__ == '__main__': pass" + result = _apply(source) + assert "__name__" in result + + def test_function_name_is_renamed(self) -> None: + source = "def compute(): return 42" + result = _apply(source) + assert "compute" not in result + + def test_renamed_function_still_callable(self) -> None: + source = textwrap.dedent( + """\ + def add(a, b): + return a + b + """ + ) + result = _apply(source) + ns: dict = {} + exec(result, ns) + funcs = [v for v in ns.values() if callable(v)] + assert len(funcs) == 1 + assert funcs[0](3, 4) == 7 + + def test_function_argument_is_renamed(self) -> None: + source = "def foo(my_arg): return my_arg" + result = _apply(source) + assert "my_arg" not in result + + def test_renamed_argument_still_works(self) -> None: + source = "def identity(value): return value" + result = _apply(source) + ns: dict = {} + exec(result, ns) + funcs = [v for v in ns.values() if callable(v)] + assert funcs[0](99) == 99 + + def test_class_name_is_renamed(self) -> None: + source = "class MyClass: pass" + result = _apply(source) + assert "MyClass" not in result + + def test_async_function_name_is_renamed(self) -> None: + source = "async def my_coro(): pass" + result = _apply(source) + assert "my_coro" not in result + + def test_same_name_renamed_consistently(self) -> None: + source = textwrap.dedent( + """\ + counter = 0 + counter = counter + 1 + result = counter + """ + ) + result = _apply(source) + ns: dict = {} + exec(result, ns) + values = {v for k, v in ns.items() if not k.startswith("__")} + assert 1 in values + + def test_output_is_valid_python(self) -> None: + source = textwrap.dedent( + """\ + def process(items): + total = 0 + for item in items: + total += item + return total + """ + ) + result = _apply(source) + ast.parse(result) + + def test_for_loop_variable_renamed(self) -> None: + source = textwrap.dedent( + """\ + total = 0 + for num in range(5): + total += num + """ + ) + result = _apply(source) + assert "num" not in result + ns: dict = {} + exec(result, ns) + values = {v for k, v in ns.items() if not k.startswith("__")} + assert 10 in values + + def test_apply_produces_compilable_tree(self) -> None: + source = "x = 1\ndef foo(a): return a" + tree = ast.parse(source) + new_tree = VariableRenamer().apply(tree) + compile(new_tree, "", "exec") + + def test_multiple_calls_independent(self) -> None: + """Two separate VariableRenamer instances do not share state.""" + source = "value = 5" + r1 = _apply(source) + r2 = _apply(source) + # Both are valid Python; they may or may not produce the same name + # (random), but neither should contain the original name. + assert "value" not in r1 + assert "value" not in r2 + + +class TestVariableRenamerFalseBranches: + """Cover the 'name NOT in rename_map' branches in each visit_* method.""" + + def _make_renamer_with_empty_map(self) -> VariableRenamer: + renamer = VariableRenamer() + renamer._rename_map = {} + return renamer + + def test_visit_name_unchanged_when_not_in_map(self) -> None: + renamer = self._make_renamer_with_empty_map() + node = ast.Name(id="foo", ctx=ast.Load()) + result = renamer.visit_Name(node) + assert result.id == "foo" + + def test_visit_function_def_unchanged_when_not_in_map(self) -> None: + renamer = self._make_renamer_with_empty_map() + func_node = ast.parse("def my_func(): pass").body[0] + assert isinstance(func_node, ast.FunctionDef) + result = renamer.visit_FunctionDef(func_node) + assert result.name == "my_func" + + def test_visit_async_function_def_unchanged_when_not_in_map(self) -> None: + renamer = self._make_renamer_with_empty_map() + func_node = ast.parse("async def my_coro(): pass").body[0] + assert isinstance(func_node, ast.AsyncFunctionDef) + result = renamer.visit_AsyncFunctionDef(func_node) + assert result.name == "my_coro" + + def test_visit_class_def_unchanged_when_not_in_map(self) -> None: + renamer = self._make_renamer_with_empty_map() + class_node = ast.parse("class MyClass: pass").body[0] + assert isinstance(class_node, ast.ClassDef) + result = renamer.visit_ClassDef(class_node) + assert result.name == "MyClass" + + def test_visit_arg_unchanged_when_not_in_map(self) -> None: + renamer = self._make_renamer_with_empty_map() + arg_node = ast.arg(arg="my_param") + result = renamer.visit_arg(arg_node) + assert result.arg == "my_param" + + def test_builtin_named_function_not_renamed_during_apply(self) -> None: + # 'list' is a builtin; when a function is defined with that name it + # must be excluded from the rename map so visit_FunctionDef takes the + # false branch in a real apply() call. + source = "def list(x): return x" + result = _apply(source) + assert "list" in result + + def test_visit_nonlocal_renames_known_names(self) -> None: + # If nonlocal is not updated when 'count' is renamed, Python raises + # SyntaxError: no binding for nonlocal 'count' found. + source = textwrap.dedent( + """\ + def outer(): + count = 0 + def inner(): + nonlocal count + count += 1 + return count + return inner + result = outer()() + """ + ) + ns: dict = {} + exec(_apply(source), ns) + # 'result' is renamed; locate the int value in the namespace. + result_val = next( + v for k, v in ns.items() if k != "__builtins__" and isinstance(v, int) + ) + assert result_val == 1 + + def test_visit_global_renames_known_names(self) -> None: + # If global is not updated when 'counter' is renamed, the renamed + # increment() will refer to a non-existent global name. + source = textwrap.dedent( + """\ + counter = 0 + def increment(): + global counter + counter += 1 + increment() + result = counter + """ + ) + ns: dict = {} + exec(_apply(source), ns) + result_val = next( + v for k, v in ns.items() if k != "__builtins__" and isinstance(v, int) + ) + assert result_val == 1 diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..662b47a --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,75 @@ +import pytest + +import python_obfuscator # ensure registration side-effects run +from python_obfuscator.config import ObfuscationConfig +from python_obfuscator.techniques.registry import all_technique_names + + +class TestObfuscationConfig: + def test_all_enabled_contains_all_registered(self) -> None: + cfg = ObfuscationConfig.all_enabled() + assert cfg.enabled_techniques == all_technique_names() + + def test_all_enabled_is_non_empty(self) -> None: + cfg = ObfuscationConfig.all_enabled() + assert len(cfg.enabled_techniques) > 0 + + def test_only_restricts_to_given_names(self) -> None: + cfg = ObfuscationConfig.only("variable_renamer") + assert cfg.enabled_techniques == frozenset({"variable_renamer"}) + + def test_only_multiple_names(self) -> None: + cfg = ObfuscationConfig.only("variable_renamer", "string_hex_encoder") + assert cfg.enabled_techniques == frozenset( + {"variable_renamer", "string_hex_encoder"} + ) + + def test_only_zero_names_produces_empty(self) -> None: + cfg = ObfuscationConfig.only() + assert cfg.enabled_techniques == frozenset() + + def test_without_removes_named_technique(self) -> None: + cfg = ObfuscationConfig.all_enabled().without("variable_renamer") + assert "variable_renamer" not in cfg.enabled_techniques + + def test_without_keeps_other_techniques(self) -> None: + cfg = ObfuscationConfig.all_enabled().without("variable_renamer") + assert "string_hex_encoder" in cfg.enabled_techniques + + def test_without_unknown_name_is_a_noop(self) -> None: + cfg = ObfuscationConfig.all_enabled() + cfg2 = cfg.without("nonexistent_technique") + assert cfg2.enabled_techniques == cfg.enabled_techniques + + def test_without_multiple_names(self) -> None: + cfg = ObfuscationConfig.all_enabled().without( + "variable_renamer", "string_hex_encoder" + ) + assert "variable_renamer" not in cfg.enabled_techniques + assert "string_hex_encoder" not in cfg.enabled_techniques + + def test_with_added_adds_names(self) -> None: + cfg = ObfuscationConfig.only("variable_renamer").with_added( + "string_hex_encoder" + ) + assert "variable_renamer" in cfg.enabled_techniques + assert "string_hex_encoder" in cfg.enabled_techniques + + def test_with_added_duplicate_is_idempotent(self) -> None: + cfg = ObfuscationConfig.only("variable_renamer").with_added("variable_renamer") + assert cfg.enabled_techniques == frozenset({"variable_renamer"}) + + def test_config_is_immutable(self) -> None: + cfg = ObfuscationConfig.all_enabled() + with pytest.raises((AttributeError, TypeError)): + cfg.enabled_techniques = frozenset() # type: ignore[misc] + + def test_configs_are_equal_with_same_techniques(self) -> None: + a = ObfuscationConfig.only("variable_renamer") + b = ObfuscationConfig.only("variable_renamer") + assert a == b + + def test_configs_differ_with_different_techniques(self) -> None: + a = ObfuscationConfig.only("variable_renamer") + b = ObfuscationConfig.only("string_hex_encoder") + assert a != b diff --git a/tests/test_helpers.py b/tests/test_helpers.py new file mode 100644 index 0000000..1a7d20e --- /dev/null +++ b/tests/test_helpers.py @@ -0,0 +1,78 @@ +"""Tests for helper utilities.""" + +import random + +from python_obfuscator.helpers.random_datatype import RandomDataTypeGenerator +from python_obfuscator.helpers.variable_name_generator import VariableNameGenerator + + +class TestRandomDataTypeGenerator: + def test_get_random_returns_str_or_int(self) -> None: + gen = RandomDataTypeGenerator() + for _ in range(20): + assert isinstance(gen.get_random(), (str, int)) + + def test_random_string_default_length(self) -> None: + gen = RandomDataTypeGenerator() + assert len(gen.random_string()) == 79 + + def test_random_string_custom_length(self) -> None: + gen = RandomDataTypeGenerator() + assert len(gen.random_string(length=10)) == 10 + + def test_random_int_in_valid_range(self) -> None: + gen = RandomDataTypeGenerator() + for _ in range(20): + val = gen.random_int() + assert isinstance(val, int) + assert 0 <= val <= 999 + + def test_seeded_rng_is_deterministic(self) -> None: + g1 = RandomDataTypeGenerator(rng=random.Random(42)) + g2 = RandomDataTypeGenerator(rng=random.Random(42)) + results1 = [g1.get_random() for _ in range(10)] + results2 = [g2.get_random() for _ in range(10)] + assert results1 == results2 + + def test_different_seeds_produce_different_sequences(self) -> None: + g1 = RandomDataTypeGenerator(rng=random.Random(1)) + g2 = RandomDataTypeGenerator(rng=random.Random(2)) + s1 = [g1.get_random() for _ in range(20)] + s2 = [g2.get_random() for _ in range(20)] + assert s1 != s2 + + +class TestVariableNameGenerator: + def test_get_random_returns_valid_identifier(self) -> None: + gen = VariableNameGenerator() + for i in range(1, 15): + name = gen.get_random(i) + assert name.isidentifier(), f"{name!r} is not a valid identifier" + + def test_seeded_rng_is_deterministic(self) -> None: + g1 = VariableNameGenerator(rng=random.Random(42)) + g2 = VariableNameGenerator(rng=random.Random(42)) + names1 = [g1.get_random(i) for i in range(1, 15)] + names2 = [g2.get_random(i) for i in range(1, 15)] + assert names1 == names2 + + def test_different_seeds_produce_different_names(self) -> None: + g1 = VariableNameGenerator(rng=random.Random(1)) + g2 = VariableNameGenerator(rng=random.Random(2)) + n1 = [g1.get_random(i) for i in range(1, 15)] + n2 = [g2.get_random(i) for i in range(1, 15)] + assert n1 != n2 + + def test_individual_generators_return_identifiers(self) -> None: + gen = VariableNameGenerator(rng=random.Random(0)) + for i in range(1, 10): + for method in ( + gen.random_string, + gen.l_and_i, + gen.time_based, + gen.just_id, + gen.scream, + gen.single_letter_a_lot, + ): + name = method(i) + assert name.isidentifier(), f"{method.__name__}({i}) = {name!r}" diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..dc95061 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,679 @@ +"""End-to-end integration tests. + +Each test writes a non-trivial Python program, executes it with an empty +config (no transforms) to get the baseline output, then executes the fully +obfuscated version and asserts the output is identical. + +Output is captured by overriding ``print`` in the execution namespace. +This works even when ``exec_wrapper`` is active because the inner ``exec`` +call inherits the outer globals dict (which contains our fake print), and +``variable_renamer`` and ``dead_code_injector`` never touch the ``print`` +name (it is a builtin and therefore excluded from all transforms). +""" + +from __future__ import annotations + +import textwrap + +import pytest + +from python_obfuscator import ObfuscationConfig, obfuscate + + +def run(source: str, config: ObfuscationConfig | None = None) -> list[str]: + """Execute *source* and return every string passed to print(), in order.""" + captured: list[str] = [] + + def fake_print( + *args: object, sep: str = " ", end: str = "\n", **_: object + ) -> None: # noqa: ARG001 + captured.append(sep.join(str(a) for a in args)) + + cfg = config or ObfuscationConfig.all_enabled() + ns: dict = {"print": fake_print} + exec(obfuscate(source, config=cfg), ns) # noqa: S102 + return captured + + +_BASELINE = ObfuscationConfig.only() # no transforms — plain exec + + +def check(source: str) -> None: + """Assert that fully-obfuscated output matches unobfuscated output.""" + expected = run(source, config=_BASELINE) + actual = run(source) + assert ( + actual == expected + ), f"Obfuscated output differs.\nExpected: {expected}\nActual: {actual}" + + +# --------------------------------------------------------------------------- +# Individual technique passes (useful for isolating regressions) +# --------------------------------------------------------------------------- + + +class TestIndividualTechniques: + _SRC = textwrap.dedent( + """\ + x = 10 + y = 20 + print(x + y) + print(x * y) + """ + ) + + def test_string_hex_encoder_only(self) -> None: + cfg = ObfuscationConfig.only("string_hex_encoder") + assert run(self._SRC, config=_BASELINE) == run(self._SRC, config=cfg) + + def test_variable_renamer_only(self) -> None: + cfg = ObfuscationConfig.only("variable_renamer") + assert run(self._SRC, config=_BASELINE) == run(self._SRC, config=cfg) + + def test_dead_code_injector_only(self) -> None: + cfg = ObfuscationConfig.only("dead_code_injector") + assert run(self._SRC, config=_BASELINE) == run(self._SRC, config=cfg) + + def test_exec_wrapper_only(self) -> None: + cfg = ObfuscationConfig.only("exec_wrapper") + assert run(self._SRC, config=_BASELINE) == run(self._SRC, config=cfg) + + +# --------------------------------------------------------------------------- +# Full-pipeline scenarios +# --------------------------------------------------------------------------- + + +class TestArithmetic: + def test_basic_arithmetic(self) -> None: + check( + textwrap.dedent( + """\ + a = 6 + b = 7 + print(a + b) + print(a * b) + print(a - b) + print(100 // a) + print(100 % b) + """ + ) + ) + + def test_float_arithmetic(self) -> None: + check( + textwrap.dedent( + """\ + x = 3.14 + y = 2.0 + print(round(x * y, 4)) + print(round(x / y, 4)) + """ + ) + ) + + def test_chained_operations(self) -> None: + check( + textwrap.dedent( + """\ + result = (1 + 2) * (3 + 4) - (5 * 6) + (7 ** 2) + print(result) + """ + ) + ) + + +class TestFunctions: + def test_simple_function(self) -> None: + check( + textwrap.dedent( + """\ + def add(a, b): + return a + b + print(add(3, 4)) + print(add(10, -3)) + """ + ) + ) + + def test_recursive_fibonacci(self) -> None: + check( + textwrap.dedent( + """\ + def fib(n): + if n <= 1: + return n + return fib(n - 1) + fib(n - 2) + for i in range(8): + print(fib(i)) + """ + ) + ) + + def test_default_arguments(self) -> None: + # Keyword-argument call sites use bare strings (ast.keyword.arg), not + # ast.Name nodes, so variable_renamer cannot update them when a parameter + # is renamed. Use positional arguments to keep the test self-consistent + # after renaming. + check( + textwrap.dedent( + """\ + def greet(name, prefix="Hello"): + return prefix + " " + name + print(greet("world")) + print(greet("Python", "Hi")) + """ + ) + ) + + def test_multiple_return_values(self) -> None: + check( + textwrap.dedent( + """\ + def minmax(seq): + return min(seq), max(seq) + lo, hi = minmax([3, 1, 4, 1, 5, 9, 2, 6]) + print(lo, hi) + """ + ) + ) + + def test_variadic_args(self) -> None: + check( + textwrap.dedent( + """\ + def total(*nums): + return sum(nums) + print(total(1, 2, 3, 4, 5)) + """ + ) + ) + + def test_higher_order_function(self) -> None: + check( + textwrap.dedent( + """\ + def apply(fn, values): + return [fn(v) for v in values] + result = apply(lambda x: x * x, [1, 2, 3, 4]) + for v in result: + print(v) + """ + ) + ) + + +class TestClosures: + def test_simple_closure(self) -> None: + check( + textwrap.dedent( + """\ + def make_adder(n): + def inner(x): + return x + n + return inner + add5 = make_adder(5) + print(add5(10)) + print(add5(0)) + """ + ) + ) + + def test_nonlocal(self) -> None: + check( + textwrap.dedent( + """\ + def counter(): + count = 0 + def inc(): + nonlocal count + count += 1 + return count + return inc + c = counter() + print(c()) + print(c()) + print(c()) + """ + ) + ) + + +class TestClasses: + def test_simple_class(self) -> None: + check( + textwrap.dedent( + """\ + class Point: + def __init__(self, x, y): + self.x = x + self.y = y + def distance(self): + return (self.x ** 2 + self.y ** 2) ** 0.5 + p = Point(3, 4) + print(p.distance()) + """ + ) + ) + + def test_inheritance(self) -> None: + check( + textwrap.dedent( + """\ + class Animal: + def __init__(self, name): + self.name = name + def speak(self): + return "..." + class Dog(Animal): + def speak(self): + return "Woof" + class Cat(Animal): + def speak(self): + return "Meow" + for animal in [Dog("Rex"), Cat("Whiskers")]: + print(animal.speak()) + """ + ) + ) + + def test_class_method_and_static(self) -> None: + check( + textwrap.dedent( + """\ + class MathHelper: + factor = 2 + @classmethod + def double(cls, n): + return n * cls.factor + @staticmethod + def square(n): + return n * n + print(MathHelper.double(5)) + print(MathHelper.square(4)) + """ + ) + ) + + +class TestControlFlow: + def test_for_loop(self) -> None: + check( + textwrap.dedent( + """\ + total = 0 + for i in range(1, 6): + total += i + print(total) + """ + ) + ) + + def test_while_loop(self) -> None: + check( + textwrap.dedent( + """\ + n = 1 + while n < 32: + n *= 2 + print(n) + """ + ) + ) + + def test_nested_for_loops(self) -> None: + check( + textwrap.dedent( + """\ + pairs = [] + for i in range(3): + for j in range(3): + if i != j: + pairs.append((i, j)) + print(len(pairs)) + """ + ) + ) + + def test_if_elif_else(self) -> None: + check( + textwrap.dedent( + """\ + def classify(n): + if n < 0: + return "negative" + elif n == 0: + return "zero" + else: + return "positive" + for v in [-5, 0, 7]: + print(classify(v)) + """ + ) + ) + + def test_break_and_continue(self) -> None: + check( + textwrap.dedent( + """\ + evens = [] + for i in range(20): + if i % 2 != 0: + continue + if i > 10: + break + evens.append(i) + print(evens) + """ + ) + ) + + +class TestDataStructures: + def test_list_comprehension(self) -> None: + check( + textwrap.dedent( + """\ + squares = [x * x for x in range(6)] + print(squares) + """ + ) + ) + + def test_dict_comprehension(self) -> None: + check( + textwrap.dedent( + """\ + word = "hello" + freq = {c: word.count(c) for c in set(word)} + for key in sorted(freq): + print(key, freq[key]) + """ + ) + ) + + def test_set_operations(self) -> None: + check( + textwrap.dedent( + """\ + a = {1, 2, 3, 4} + b = {3, 4, 5, 6} + print(sorted(a & b)) + print(sorted(a | b)) + print(sorted(a - b)) + """ + ) + ) + + def test_nested_list(self) -> None: + check( + textwrap.dedent( + """\ + matrix = [[i * j for j in range(1, 4)] for i in range(1, 4)] + for row in matrix: + print(row) + """ + ) + ) + + +class TestExceptionHandling: + def test_try_except(self) -> None: + check( + textwrap.dedent( + """\ + def safe_div(a, b): + try: + return a // b + except ZeroDivisionError: + return None + print(safe_div(10, 2)) + print(safe_div(10, 0)) + """ + ) + ) + + def test_try_except_finally(self) -> None: + check( + textwrap.dedent( + """\ + log = [] + try: + log.append("try") + x = 1 // 0 + except ZeroDivisionError: + log.append("except") + finally: + log.append("finally") + print(log) + """ + ) + ) + + def test_multiple_except_clauses(self) -> None: + check( + textwrap.dedent( + """\ + def parse_int(s): + try: + return int(s) + except (ValueError, TypeError): + return -1 + print(parse_int("42")) + print(parse_int("abc")) + print(parse_int(None)) + """ + ) + ) + + def test_try_else(self) -> None: + check( + textwrap.dedent( + """\ + results = [] + for val in ["10", "x", "5"]: + try: + n = int(val) + except ValueError: + results.append("bad") + else: + results.append(n * 2) + print(results) + """ + ) + ) + + +class TestGenerators: + def test_generator_function(self) -> None: + check( + textwrap.dedent( + """\ + def countdown(n): + while n > 0: + yield n + n -= 1 + print(list(countdown(5))) + """ + ) + ) + + def test_generator_expression(self) -> None: + check( + textwrap.dedent( + """\ + gen = (x * x for x in range(5)) + print(list(gen)) + """ + ) + ) + + +class TestStringOperations: + def test_string_formatting(self) -> None: + check( + textwrap.dedent( + """\ + name = "world" + n = 42 + print(f"Hello, {name}!") + print(f"The answer is {n}.") + """ + ) + ) + + def test_string_methods(self) -> None: + check( + textwrap.dedent( + """\ + s = "Hello, World!" + print(s.lower()) + print(s.upper()) + print(s.replace("World", "Python")) + print(s.split(", ")) + """ + ) + ) + + def test_multiline_string(self) -> None: + check( + textwrap.dedent( + """\ + text = "line one\\nline two\\nline three" + lines = text.split("\\n") + for line in lines: + print(line) + """ + ) + ) + + def test_string_with_quotes(self) -> None: + check( + textwrap.dedent( + """\ + s1 = "it's a test" + s2 = 'say "hello"' + print(s1) + print(s2) + """ + ) + ) + + +class TestComplexPrograms: + def test_bubble_sort(self) -> None: + check( + textwrap.dedent( + """\ + def bubble_sort(lst): + n = len(lst) + for i in range(n): + for j in range(n - i - 1): + if lst[j] > lst[j + 1]: + lst[j], lst[j + 1] = lst[j + 1], lst[j] + return lst + data = [64, 34, 25, 12, 22, 11, 90] + print(bubble_sort(data)) + """ + ) + ) + + def test_binary_search(self) -> None: + check( + textwrap.dedent( + """\ + def binary_search(lst, target): + lo, hi = 0, len(lst) - 1 + while lo <= hi: + mid = (lo + hi) // 2 + if lst[mid] == target: + return mid + elif lst[mid] < target: + lo = mid + 1 + else: + hi = mid - 1 + return -1 + arr = list(range(0, 20, 2)) + print(binary_search(arr, 8)) + print(binary_search(arr, 7)) + """ + ) + ) + + def test_class_with_dunder_methods(self) -> None: + check( + textwrap.dedent( + """\ + class Stack: + def __init__(self): + self._data = [] + def push(self, item): + self._data.append(item) + def pop(self): + return self._data.pop() + def __len__(self): + return len(self._data) + def __repr__(self): + return repr(self._data) + s = Stack() + s.push(1) + s.push(2) + s.push(3) + print(len(s)) + print(s.pop()) + print(len(s)) + """ + ) + ) + + def test_functional_pipeline(self) -> None: + check( + textwrap.dedent( + """\ + from functools import reduce + numbers = list(range(1, 11)) + evens = list(filter(lambda x: x % 2 == 0, numbers)) + doubled = list(map(lambda x: x * 2, evens)) + total = reduce(lambda a, b: a + b, doubled) + print(evens) + print(doubled) + print(total) + """ + ) + ) + + def test_deeply_nested_closures(self) -> None: + check( + textwrap.dedent( + """\ + def level1(a): + def level2(b): + def level3(c): + return a + b + c + return level3 + return level2 + fn = level1(1)(2) + print(fn(3)) + print(fn(10)) + """ + ) + ) + + def test_memoisation_with_dict(self) -> None: + check( + textwrap.dedent( + """\ + cache = {} + def fib(n): + if n in cache: + return cache[n] + if n <= 1: + cache[n] = n + else: + cache[n] = fib(n - 1) + fib(n - 2) + return cache[n] + for i in [0, 1, 5, 10, 15]: + print(fib(i)) + """ + ) + ) diff --git a/tests/test_module.py b/tests/test_module.py index aef2b8e..28e23e9 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -1,17 +1,35 @@ +"""Smoke test: obfuscated output must execute and preserve semantics.""" + +import textwrap + import python_obfuscator +from python_obfuscator import ObfuscationConfig, Obfuscator, obfuscate + -# TODO: Improve tests -def test_module_methods(): - obfuscate = python_obfuscator.obfuscator() - - code = """ - v1 = 0 - v2 = 0 - v4 = 10 - assert v4 + v4 == 20 - assert v1 + v2 == 0 - """.replace( - " ", "" +def test_default_obfuscation_preserves_computation() -> None: + source = textwrap.dedent( + """\ + v1 = 0 + v2 = 0 + v4 = 10 + assert v4 + v4 == 20 + assert v1 + v2 == 0 + """ ) + result = obfuscate(source) + exec(result) # must not raise + + +def test_obfuscator_class_smoke() -> None: + o = Obfuscator() + result = o.obfuscate("x = 1 + 2") + assert isinstance(result, str) + - exec(obfuscate.obfuscate(code)) +def test_obfuscator_with_subset_config() -> None: + cfg = ObfuscationConfig.only("string_hex_encoder") + result = obfuscate('greeting = "hello"', config=cfg) + assert "fromhex" in result + ns: dict = {} + exec(result, ns) + assert ns["greeting"] == "hello" diff --git a/tests/test_obfuscator.py b/tests/test_obfuscator.py new file mode 100644 index 0000000..e7b7a61 --- /dev/null +++ b/tests/test_obfuscator.py @@ -0,0 +1,78 @@ +import ast +import textwrap + +import pytest + +import python_obfuscator +from python_obfuscator.config import ObfuscationConfig +from python_obfuscator.obfuscator import Obfuscator, _validate_config, obfuscate + +SIMPLE_SOURCE = textwrap.dedent( + """\ + x = 1 + y = 2 + result = x + y + """ +) + + +class TestValidateConfig: + def test_valid_config_does_not_raise(self) -> None: + _validate_config(ObfuscationConfig.all_enabled()) + + def test_unknown_technique_raises_value_error(self) -> None: + with pytest.raises(ValueError, match="Unknown technique"): + _validate_config(ObfuscationConfig.only("does_not_exist")) + + def test_error_message_lists_available_techniques(self) -> None: + with pytest.raises(ValueError, match="Available:"): + _validate_config(ObfuscationConfig.only("bad_name")) + + +class TestObfuscateFunction: + def test_returns_string(self) -> None: + assert isinstance(obfuscate(SIMPLE_SOURCE), str) + + def test_none_config_uses_all_enabled(self) -> None: + assert isinstance(obfuscate(SIMPLE_SOURCE, config=None), str) + + def test_explicit_config_is_respected(self) -> None: + cfg = ObfuscationConfig.only("string_hex_encoder") + result = obfuscate('x = "hello"', config=cfg) + assert "fromhex" in result + + def test_empty_config_returns_unparsed_source(self) -> None: + result = obfuscate("x = 1 + 2\n", config=ObfuscationConfig.only()) + assert "x = 1 + 2" in result + + def test_unknown_technique_raises(self) -> None: + with pytest.raises(ValueError): + obfuscate(SIMPLE_SOURCE, config=ObfuscationConfig.only("bad")) + + def test_output_is_valid_python(self) -> None: + ast.parse(obfuscate(SIMPLE_SOURCE)) + + +class TestObfuscatorClass: + def test_instantiation_with_no_config(self) -> None: + o = Obfuscator() + assert o.config == ObfuscationConfig.all_enabled() + + def test_instantiation_with_explicit_config(self) -> None: + cfg = ObfuscationConfig.only("variable_renamer") + assert Obfuscator(cfg).config == cfg + + def test_instantiation_with_unknown_technique_raises(self) -> None: + with pytest.raises(ValueError): + Obfuscator(ObfuscationConfig.only("nonexistent")) + + def test_obfuscate_returns_string(self) -> None: + assert isinstance(Obfuscator().obfuscate(SIMPLE_SOURCE), str) + + def test_obfuscate_output_is_valid_python(self) -> None: + ast.parse(Obfuscator().obfuscate(SIMPLE_SOURCE)) + + def test_obfuscate_called_multiple_times_is_safe(self) -> None: + o = Obfuscator() + assert isinstance(o.obfuscate("x = 1"), str) + assert isinstance(o.obfuscate("y = 2"), str) diff --git a/tests/test_registry.py b/tests/test_registry.py new file mode 100644 index 0000000..4a11b4f --- /dev/null +++ b/tests/test_registry.py @@ -0,0 +1,72 @@ +import pytest + +import python_obfuscator # trigger registration +from python_obfuscator.techniques.base import ASTTransform, TechniqueMetadata +from python_obfuscator.techniques.registry import ( + _REGISTRY, + all_technique_names, + get_transforms, + register, +) + + +class TestRegister: + def test_known_transforms_are_registered(self) -> None: + assert "variable_renamer" in _REGISTRY + assert "string_hex_encoder" in _REGISTRY + assert "dead_code_injector" in _REGISTRY + assert "exec_wrapper" in _REGISTRY + + def test_register_invalid_type_raises_type_error(self) -> None: + class NotATransform: + metadata = TechniqueMetadata(name="bad", description="bad", priority=0) + + with pytest.raises(TypeError, match="must be a subclass"): + register(NotATransform) # type: ignore[arg-type] + + def test_register_returns_class_unchanged(self) -> None: + class _Temp(ASTTransform): + metadata = TechniqueMetadata( + name="_temp_register_test", description="temp", priority=999 + ) + + result = register(_Temp) + assert result is _Temp + _REGISTRY.pop("_temp_register_test", None) + + +class TestAllTechniqueNames: + def test_returns_frozenset(self) -> None: + assert isinstance(all_technique_names(), frozenset) + + def test_includes_all_known_techniques(self) -> None: + names = all_technique_names() + for expected in ( + "variable_renamer", + "string_hex_encoder", + "dead_code_injector", + "exec_wrapper", + ): + assert expected in names + + def test_equals_registry_keys(self) -> None: + assert all_technique_names() == frozenset(_REGISTRY) + + +class TestGetTransforms: + def test_returns_only_enabled_techniques(self) -> None: + transforms = get_transforms(frozenset({"variable_renamer"})) + assert [cls.metadata.name for cls in transforms] == ["variable_renamer"] + + def test_empty_enabled_returns_empty(self) -> None: + assert get_transforms(frozenset()) == [] + + def test_unknown_name_is_silently_ignored(self) -> None: + assert get_transforms(frozenset({"nonexistent"})) == [] + + def test_sorted_by_priority_ascending(self) -> None: + transforms = get_transforms( + frozenset({"variable_renamer", "string_hex_encoder", "dead_code_injector"}) + ) + priorities = [cls.metadata.priority for cls in transforms] + assert priorities == sorted(priorities) From 59e636506eaed6cc8937f7e0bd6357b127e00993 Mon Sep 17 00:00:00 2001 From: davidteather <34144122+davidteather@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:06:13 -0400 Subject: [PATCH 07/11] lint improvements --- .github/workflows/package-test.yml | 42 +++- CHANGELOG.md | 72 +++++++ README.md | 183 ++++++++++++++---- pyproject.toml | 27 ++- python_obfuscator/cli/__init__.py | 1 - python_obfuscator/helpers/__init__.py | 4 +- python_obfuscator/helpers/random_datatype.py | 2 +- .../helpers/variable_name_generator.py | 2 +- python_obfuscator/version.py | 2 +- tests/techniques/test_exec_wrapper.py | 1 - tests/techniques/test_string_hex_encoder.py | 2 - tests/techniques/test_variable_renamer.py | 2 - tests/test_config.py | 1 - tests/test_integration.py | 2 - tests/test_module.py | 1 - tests/test_obfuscator.py | 1 - tests/test_registry.py | 1 - 17 files changed, 272 insertions(+), 74 deletions(-) create mode 100644 CHANGELOG.md diff --git a/.github/workflows/package-test.yml b/.github/workflows/package-test.yml index bcd7ed0..caec03f 100644 --- a/.github/workflows/package-test.yml +++ b/.github/workflows/package-test.yml @@ -1,4 +1,5 @@ -name: CI Test +name: CI + on: push: branches: @@ -13,8 +14,34 @@ permissions: contents: read jobs: - Unit-Tests: - timeout-minutes: 10 + lint: + name: Lint & format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install lint tools + run: pip install ruff black isort pyright + + - name: Ruff (lint) + run: ruff check python_obfuscator tests + + - name: Black (format check) + run: black --check python_obfuscator tests + + - name: isort (import order check) + run: isort --check-only python_obfuscator tests + + - name: Pyright (type check) + run: pyright python_obfuscator + + test: + name: Test (Python ${{ matrix.python-version }}) runs-on: ubuntu-latest strategy: fail-fast: false @@ -22,14 +49,15 @@ jobs: python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 + - name: Install Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - name: Install Poetry - run: | - curl -sSL https://install.python-poetry.org | python - -y + run: curl -sSL https://install.python-poetry.org | python - -y - name: Update PATH run: echo "$HOME/.local/bin" >> $GITHUB_PATH @@ -40,13 +68,13 @@ jobs: - name: Type check (mypy) run: poetry run mypy python_obfuscator - - name: Run unit tests with coverage + - name: Run tests with coverage run: | poetry run coverage run -m pytest poetry run coverage report poetry run coverage xml - - name: Upload coverage reports to Codecov + - name: Upload coverage to Codecov if: matrix.python-version == '3.12' uses: codecov/codecov-action@v5 with: diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..15c67dd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,72 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +--- + +## [0.1.0] — AST-Based Rewrite + +Complete rewrite from a regex-based implementation to a fully AST-based pipeline. + +### Added + +- **`ObfuscationConfig`** — immutable frozen dataclass for selecting techniques. + Factory methods: `all_enabled()`, `only(*names)`, `.without(*names)`, `.with_added(*names)`. +- **Technique registry** — `@register` class decorator; techniques self-register by name. + `all_technique_names()` and `get_transforms(enabled)` for pipeline construction. +- **`variable_renamer`** — two-pass AST renamer. First pass collects renameable names + (excludes builtins, imports, dunders, and attribute-accessed names); second pass applies + the mapping. Also renames `nonlocal` and `global` statement name lists. +- **`string_hex_encoder`** — converts string literals to `bytes.fromhex(…).decode('utf-8')` + call nodes. Skips f-strings where replacing a `Constant` with a `Call` is invalid. +- **`dead_code_injector`** — recursively injects dead variable assignments at every scope + level: module body, function bodies, class bodies, if/for/while/try/with branches. + Some assignments reference earlier dead variables (intra-scope cross-references) to + simulate computation. Accepts `InjectionParams` and a seeded `random.Random` for + reproducible output. +- **`exec_wrapper`** — wraps the entire module in `exec(ast.unparse(tree))`, reducing the + top-level AST to one statement. +- **`Obfuscator` class** — caches the transform pipeline across multiple `obfuscate()` calls. +- **`obfuscate()` module-level helper** — convenience wrapper for one-shot use. +- **CLI** (`pyobfuscate`) — `--disable/-d` flag accepts technique names; `--stdout`; `--version/-V`. +- **E2E test suite** with correctness and benchmark tests across six complex programs. +- **Per-technique runtime benchmarks** showing individual overhead contribution. +- 100 % branch coverage enforced in CI. +- Python 3.10–3.14 test matrix. + +### Changed + +- Priority ordering now encoded in `TechniqueMetadata.priority` rather than list position. +- `VariableNameGenerator` and `RandomDataTypeGenerator` accept an optional `rng` argument + for deterministic testing. +- `_NameCollector` exposes `all_bound_names` (assigned + imported + builtins) for use by + the dead-code injector's exclusion set. + +### Removed + +- All regex-based technique implementations (`techniques.py`). +- `regex` runtime dependency (was used only for the old hex-encoding approach). +- `SourceTransform` base class and `source_transforms` package. + +### Fixed + +- `VariableRenamer` now updates `nonlocal` and `global` statement name lists, preventing + `SyntaxError: no binding for nonlocal '…' found` after renaming. +- `VariableRenamer` excludes attribute-accessed names from renaming to prevent + `AttributeError` when calling methods by the original name after their definition is renamed. +- `_interleave` preserves relative ordering of injected statements (sorted random slots) + so intra-scope cross-references never appear before their definitions. + +--- + +## [0.0.2] — prior release + +Initial public release with regex-based obfuscation techniques. + +- `one_liner` — collapsed newlines into semicolons (superceded by `exec_wrapper`). +- `variable_renamer` — regex-based name replacement. +- `add_random_variables` — prepended/appended random assignments at module level. +- `str_to_hex_bytes` — regex-based string-to-hex conversion. diff --git a/README.md b/README.md index 6cee937..3a1d203 100644 --- a/README.md +++ b/README.md @@ -1,83 +1,184 @@ -# Python-Obfuscator +# python-obfuscator -One night I got bored of writing good code, so I made good code to make bad code. +[![CI](https://github.com/davidteather/python-obfuscator/actions/workflows/package-test.yml/badge.svg)](https://github.com/davidteather/python-obfuscator/actions/workflows/package-test.yml) +[![GitHub release (latest by date)](https://img.shields.io/github/v/release/davidteather/python-obfuscator?style=flat-square)](https://github.com/davidteather/python-obfuscator/releases) +[![Downloads](https://static.pepy.tech/personalized-badge/python-obfuscator?period=total&units=international_system&left_color=grey&right_color=orange&left_text=Downloads)](https://pypi.org/project/python-obfuscator/) +[![Codecov](https://codecov.io/gh/davidteather/python-obfuscator/branch/main/graph/badge.svg)](https://codecov.io/gh/davidteather/python-obfuscator) -[![GitHub release (latest by date)](https://img.shields.io/github/v/release/davidteather/python-obfuscator?style=flat-square)](https://github.com/davidteather/python-obfuscator/releases) [![Downloads](https://static.pepy.tech/personalized-badge/python-obfuscator?period=total&units=international_system&left_color=grey&right_color=orange&left_text=Downloads)](https://pypi.org/project/python-obfuscator/) ![](https://visitor-badge.laobi.icu/badge?page_id=davidteather.python-obfuscator) [![Linkedin](https://img.shields.io/badge/LinkedIn-0077B5?style=flat-square&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/david-teather-4400a37a/) +A Python source-code obfuscator built on the standard-library `ast` module. It applies multiple independent techniques — each individually togglable — to make code harder to read while keeping it fully executable. See [Known limitations](#known-limitations) before use. -### **DONT USE IN PRODUCTION** +If this project is useful to you, consider [sponsoring development](https://github.com/sponsors/davidteather). -**I just made this because it was interesting to me. I do plan on making this more official in the future, but currently don't have the time!** - -Consider sponsoring me [here](https://github.com/sponsors/davidteather) +--- ## Installing -``` +```bash pip install python-obfuscator ``` -## Quickstart +Requires Python ≥ 3.10. -By default, obfuscated code is written under an **`obfuscated/`** directory (created next to your current working directory). Paths under the current directory are preserved (e.g. `src/app.py` → `obfuscated/src/app.py`). +--- -``` -pyobfuscate -i your_file.py -``` +## Quick start — CLI -Print to stdout instead (e.g. for piping): +```bash +# Writes obfuscated/your_file.py (path structure is preserved) +pyobfuscate -i your_file.py -``` +# Print to stdout pyobfuscate -i your_file.py --stdout -``` -Print the installed version (from `python_obfuscator.version`): +# Disable specific techniques +pyobfuscate -i your_file.py --disable dead_code_injector --disable exec_wrapper -``` +# Show version pyobfuscate --version ``` -## More Detailed Documentation +--- + +## Python API + +### One-shot helper + +```python +from python_obfuscator import obfuscate + +source = "x = 1\nprint(x + 2)" +result = obfuscate(source) +print(result) +``` + +### Selective techniques + +```python +from python_obfuscator import obfuscate, ObfuscationConfig + +# All techniques except dead-code injection +config = ObfuscationConfig.all_enabled().without("dead_code_injector") +result = obfuscate(source, config=config) -You can use this as a module if you want +# Only string encoding +config = ObfuscationConfig.only("string_hex_encoder") +result = obfuscate(source, config=config) ``` -import python_obfuscator -obfuscator = python_obfuscator.obfuscator() -code_to_obfuscate = "print('hello world')" +### Reusing across multiple files (caches the pipeline) + +```python +from python_obfuscator import Obfuscator, ObfuscationConfig + +obf = Obfuscator(ObfuscationConfig.all_enabled()) +for path in my_files: + path.write_text(obf.obfuscate(path.read_text())) ``` -You can also exclude certain techniques applied for obfuscation +### Config combinators + +```python +cfg = ObfuscationConfig.all_enabled() # every registered technique +cfg = ObfuscationConfig.only("variable_renamer", "exec_wrapper") +cfg = cfg.without("exec_wrapper") # returns a new frozen config +cfg = cfg.with_added("dead_code_injector") ``` -import python_obfuscator -from python_obfuscator.techniques import add_random_variables -obfuscator = python_obfuscator.obfuscator() -code_to_obfuscate = "print('hello world')" -obfuscated_code = obfuscator.obfuscate(code_to_obfuscate, remove_techniques=[add_random_variables]) +--- + +## Techniques + +| Name | Priority | What it does | +|------|----------|--------------| +| `variable_renamer` | 10 | Renames local variables, function names, parameters, and class names to visually ambiguous identifiers (`lIIllIlI…`). Excludes builtins, imports, dunders, and attribute-accessed names. | +| `string_hex_encoder` | 20 | Replaces every string literal `"hi"` with `bytes.fromhex('6869').decode('utf-8')`. Skips f-strings. | +| `dead_code_injector` | 30 | Injects dead variable assignments at **every scope level** — module body, function bodies, class bodies, if/for/while/try/with branches. Some assignments reference other dead variables to simulate computation. | +| `exec_wrapper` | 100 | Wraps the entire module in a single `exec("…")` call, reducing the top-level AST to one statement. Runs last. | + +Techniques are applied in priority order (lowest first). + +--- + +## Example + +**Input** + +```python +def greet(name): + msg = "Hello, " + name + print(msg) + +greet("world") ``` -Find a list of all techniques [here](https://github.com/davidteather/python-obfuscator/blob/210da2d3dfb96ab7653fad869a43cb67aeb0fe67/python_obfuscator/techniques.py#L87) -## Example Obfuscated Code +**Output** (all techniques enabled — abridged) -Input +```python +exec('def lIlIllI(IIlIlII):\n lIllIlI = bytes.fromhex(\'48656c6c6f2c20\').decode(\'utf-8\') + IIlIlII\n ...\nlIlIllI(bytes.fromhex(\'776f726c64\').decode(\'utf-8\'))') ``` -y = input("what's your favorite number") -user_value = int(y) -print("{} that's a great number!".format(user_value)) +--- + +## Performance overhead + +Benchmarks run on an Apple M-series machine, 20 iterations each. The test programs cover OOP, algorithms, functional patterns, number theory, and string processing. + +### Total overhead (all techniques) + +| Program | Original | Obfuscated | Overhead | +|---------|----------|------------|---------| +| `algorithms.py` | 0.94 ms | 1.98 ms | +112% | +| `cipher.py` | 1.37 ms | 2.67 ms | +95% | +| `data_structures.py` | 0.72 ms | 2.20 ms | +207% | +| `functional.py` | 0.67 ms | 1.71 ms | +155% | +| `number_theory.py` | 1.84 ms | 3.16 ms | +72% | +| `oop_simulation.py` | 0.68 ms | 1.66 ms | +144% | + +### Per-technique contribution (average across all programs) + +| Technique | Avg overhead | Notes | +|-----------|-------------|-------| +| `variable_renamer` | ~5% | Pure rename — negligible at runtime | +| `string_hex_encoder` | ~12% | `bytes.fromhex` call per string literal | +| `dead_code_injector` | ~85% | Dominant cost — dead assignments execute every iteration | +| `exec_wrapper` | ~2% | Single extra `exec` layer | + +The dead-code injector's overhead scales with the number of scopes and loop iterations in the original program. Programs with tight inner loops see the most overhead. + +--- + +## Known limitations + +- **Class method names are not renamed.** Attribute-accessed names (`obj.method`) cannot be safely renamed without full type-inference, so the renamer conservatively excludes them. +- **Keyword argument names are not renamed.** `fn(key=val)` call-site keyword strings are bare AST strings, not `Name` nodes, and are not updated when a parameter is renamed. +- **No scope-aware renaming.** The same identifier used in two independent function scopes maps to the same obfuscated name (which is semantically correct but less obfuscated than it could be). +- **No control-flow obfuscation.** Opaque predicates, bogus branches, and integer encoding are not implemented. + +--- + +## Running the test suite + +```bash +pip install -e ".[dev]" +pytest ``` -[With `pyobfuscate -i file.py`](https://gist.github.com/davidteather/b6ff932140d8c174b9c6f50c9b42fdaf) +Coverage is enforced at ≥ 95% on every CI run. +```bash +# With coverage report +coverage run -m pytest && coverage report -[With `--one-liner`](https://gist.github.com/davidteather/75e48c04bf74f0262fe2919239a74295) +# E2E tests with benchmark output +pytest tests/e2e/ -v -s +``` -## Authors +--- -* **David Teather** - *Initial work* - [davidteather](https://github.com/davidteather) +## Authors -See also the list of [contributors](https://github.com/davidteather/python-obfuscator) who participated in this project. +**David Teather** — [davidteather](https://github.com/davidteather) ## License -This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details +MIT — see [LICENSE](LICENSE). diff --git a/pyproject.toml b/pyproject.toml index 9dde95c..aa2193a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,26 +7,25 @@ packages = [{ include = "python_obfuscator" }] [tool.poetry.dependencies] python = ">=3.10" -regex = ">=2026.3.32" typer = "^0.24.1" [tool.poetry.group.dev.dependencies] mypy = "^1.15.0" black = "^25.1.0" isort = "^6.0.0" +ruff = ">=0.9.0" pytest = "^8.3.4" coverage = "^7.6.12" pytest-cov = "^7.1.0" -types-regex = "^2026.3.32.20260329" [project] name = "python_obfuscator" -version = "0.0.2" -description = "It's a python obfuscator." +version = "0.1.0" +description = "A Python source-code obfuscator built on the ast module." readme = "README.md" authors = [{ name = "David Teather", email = "contact.davidteather@gmail.com" }] license = { file = "LICENSE" } -dependencies = ["regex>=2026.3.32", "typer>=0.24.1,<0.25.0"] +dependencies = ["typer>=0.24.1,<0.25.0"] requires-python = ">=3.10" classifiers = [ "Development Status :: 3 - Alpha", @@ -42,7 +41,7 @@ dev = [ "pytest-cov", "black", "mypy", - "types-regex", + "ruff", "coverage", "isort", ] @@ -54,6 +53,17 @@ pyobfuscate = "python_obfuscator.cli:cli" profile = "black" line_length = 88 +[tool.ruff] +line-length = 88 +target-version = "py310" + +[tool.ruff.lint] +select = ["E", "F", "I", "UP", "W"] +ignore = [ + "E501", # line-too-long — black handles this + "UP007", # use X | Y for union types — conflicts with from __future__ import annotations +] + [tool.pytest.ini_options] testpaths = ["tests"] @@ -65,9 +75,8 @@ warn_unused_ignores = true warn_return_any = true show_error_codes = true strict = false -disallow_untyped_defs = false -# TODO: Enable when more of the codebase is annotated: -# check_untyped_defs = true +disallow_untyped_defs = true +check_untyped_defs = true ignore_missing_imports = false [tool.pyright] diff --git a/python_obfuscator/cli/__init__.py b/python_obfuscator/cli/__init__.py index 905f787..741683d 100644 --- a/python_obfuscator/cli/__init__.py +++ b/python_obfuscator/cli/__init__.py @@ -4,7 +4,6 @@ import typer -import python_obfuscator from python_obfuscator.config import ObfuscationConfig from python_obfuscator.obfuscator import Obfuscator from python_obfuscator.techniques import all_technique_names diff --git a/python_obfuscator/helpers/__init__.py b/python_obfuscator/helpers/__init__.py index a5cbba9..6130fea 100644 --- a/python_obfuscator/helpers/__init__.py +++ b/python_obfuscator/helpers/__init__.py @@ -1,2 +1,2 @@ -from .variable_name_generator import VariableNameGenerator -from .random_datatype import RandomDataTypeGenerator +from .random_datatype import RandomDataTypeGenerator as RandomDataTypeGenerator +from .variable_name_generator import VariableNameGenerator as VariableNameGenerator diff --git a/python_obfuscator/helpers/random_datatype.py b/python_obfuscator/helpers/random_datatype.py index fd2fa67..ba0c7a5 100644 --- a/python_obfuscator/helpers/random_datatype.py +++ b/python_obfuscator/helpers/random_datatype.py @@ -2,7 +2,7 @@ import random import string -from typing import Callable +from collections.abc import Callable class RandomDataTypeGenerator: diff --git a/python_obfuscator/helpers/variable_name_generator.py b/python_obfuscator/helpers/variable_name_generator.py index ef3d70c..e1019cb 100644 --- a/python_obfuscator/helpers/variable_name_generator.py +++ b/python_obfuscator/helpers/variable_name_generator.py @@ -2,7 +2,7 @@ import random import string -from typing import Callable +from collections.abc import Callable class VariableNameGenerator: diff --git a/python_obfuscator/version.py b/python_obfuscator/version.py index 3b93d0b..3dc1f76 100644 --- a/python_obfuscator/version.py +++ b/python_obfuscator/version.py @@ -1 +1 @@ -__version__ = "0.0.2" +__version__ = "0.1.0" diff --git a/tests/techniques/test_exec_wrapper.py b/tests/techniques/test_exec_wrapper.py index 7ae2fbc..5ef92e3 100644 --- a/tests/techniques/test_exec_wrapper.py +++ b/tests/techniques/test_exec_wrapper.py @@ -3,7 +3,6 @@ import ast import textwrap -import python_obfuscator # trigger registration from python_obfuscator.techniques.ast_transforms.exec_wrapper import ExecWrapper from python_obfuscator.techniques.registry import all_technique_names diff --git a/tests/techniques/test_string_hex_encoder.py b/tests/techniques/test_string_hex_encoder.py index 5fe5c9a..4e34adf 100644 --- a/tests/techniques/test_string_hex_encoder.py +++ b/tests/techniques/test_string_hex_encoder.py @@ -3,8 +3,6 @@ import ast import textwrap -import pytest - from python_obfuscator.techniques.ast_transforms.string_hex_encoder import ( StringHexEncoder, _str_to_hex_call, diff --git a/tests/techniques/test_variable_renamer.py b/tests/techniques/test_variable_renamer.py index f9ed264..66f84fa 100644 --- a/tests/techniques/test_variable_renamer.py +++ b/tests/techniques/test_variable_renamer.py @@ -3,8 +3,6 @@ import ast import textwrap -import pytest - from python_obfuscator.techniques.ast_transforms.variable_renamer import ( VariableRenamer, _NameCollector, diff --git a/tests/test_config.py b/tests/test_config.py index 662b47a..1827efe 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,6 +1,5 @@ import pytest -import python_obfuscator # ensure registration side-effects run from python_obfuscator.config import ObfuscationConfig from python_obfuscator.techniques.registry import all_technique_names diff --git a/tests/test_integration.py b/tests/test_integration.py index dc95061..54df503 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -15,8 +15,6 @@ import textwrap -import pytest - from python_obfuscator import ObfuscationConfig, obfuscate diff --git a/tests/test_module.py b/tests/test_module.py index 28e23e9..0768e9f 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -2,7 +2,6 @@ import textwrap -import python_obfuscator from python_obfuscator import ObfuscationConfig, Obfuscator, obfuscate diff --git a/tests/test_obfuscator.py b/tests/test_obfuscator.py index e7b7a61..3a4b6d1 100644 --- a/tests/test_obfuscator.py +++ b/tests/test_obfuscator.py @@ -3,7 +3,6 @@ import pytest -import python_obfuscator from python_obfuscator.config import ObfuscationConfig from python_obfuscator.obfuscator import Obfuscator, _validate_config, obfuscate diff --git a/tests/test_registry.py b/tests/test_registry.py index 4a11b4f..aafe6f2 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -1,6 +1,5 @@ import pytest -import python_obfuscator # trigger registration from python_obfuscator.techniques.base import ASTTransform, TechniqueMetadata from python_obfuscator.techniques.registry import ( _REGISTRY, From 3a55caa13f710fc2dfd9b115eae39b1c8b0e5bf4 Mon Sep 17 00:00:00 2001 From: davidteather <34144122+davidteather@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:08:29 -0400 Subject: [PATCH 08/11] fix bumpversion --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 7ac5da9..8765be9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.0.2 +current_version = 0.1.0 [bumpversion:file:pyproject.toml] search = version = "{current_version}" From d352d206f714debdafd3546c1c89fc1a7959bec0 Mon Sep 17 00:00:00 2001 From: davidteather <34144122+davidteather@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:13:22 -0400 Subject: [PATCH 09/11] lint fixes and poetry --- .github/workflows/package-test.yml | 4 +- poetry.lock | 168 ++++++----------------------- pyproject.toml | 4 + 3 files changed, 37 insertions(+), 139 deletions(-) diff --git a/.github/workflows/package-test.yml b/.github/workflows/package-test.yml index caec03f..aa540d6 100644 --- a/.github/workflows/package-test.yml +++ b/.github/workflows/package-test.yml @@ -26,7 +26,9 @@ jobs: python-version: "3.12" - name: Install lint tools - run: pip install ruff black isort pyright + run: pip install ruff isort pyright + - name: Install Black (match pyproject / poetry.lock) + run: pip install "black==25.11.0" - name: Ruff (lint) run: ruff check python_obfuscator tests diff --git a/poetry.lock b/poetry.lock index a2d6556..a47591b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -647,130 +647,6 @@ files = [ [package.extras] dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"] -[[package]] -name = "regex" -version = "2026.3.32" -description = "Alternative regular expression module, to replace re." -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "regex-2026.3.32-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:462a041d2160090553572f6bb0be417ab9bb912a08de54cb692829c871ee88c1"}, - {file = "regex-2026.3.32-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c3c6f6b027d10f84bfe65049028892b5740878edd9eae5fea0d1710b09b1d257"}, - {file = "regex-2026.3.32-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:879ae91f2928a13f01a55cfa168acedd2b02b11b4cd8b5bb9223e8cde777ca52"}, - {file = "regex-2026.3.32-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:887a9fa74418d74d645281ee0edcf60694053bd1bc2ebc49eb5e66bfffc6d107"}, - {file = "regex-2026.3.32-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d571f0b2eec3513734ea31a16ce0f7840c0b85a98e7edfa0e328ed144f9ef78f"}, - {file = "regex-2026.3.32-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ada7bd5bb6511d12177a7b00416ce55caee49fbf8c268f26b909497b534cacb"}, - {file = "regex-2026.3.32-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:918db4e34a7ef3d0beee913fa54b34231cc3424676f1c19bdb85f01828d3cd37"}, - {file = "regex-2026.3.32-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:69a847a6ffaa86e8af7b9e7037606e05a6f663deec516ad851e8e05d9908d16a"}, - {file = "regex-2026.3.32-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2c8d402ea3dfe674288fe3962016affd33b5b27213d2b5db1823ffa4de524c57"}, - {file = "regex-2026.3.32-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d6b39a2cc5625bbc4fda18919a891eab9aab934eecf83660a90ce20c53621a9a"}, - {file = "regex-2026.3.32-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f7cc00089b4c21847852c0ad76fb3680f9833b855a0d30bcec94211c435bff6b"}, - {file = "regex-2026.3.32-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:fd03e38068faeef937cc6761a250a4aaa015564bd0d61481fefcf15586d31825"}, - {file = "regex-2026.3.32-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e006ea703d5c0f3d112b51ba18af73b58209b954acfe3d8da42eacc9a00e4be6"}, - {file = "regex-2026.3.32-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6980ceb5c1049d4878632f08ba0bf7234c30e741b0dc9081da0f86eca13189d3"}, - {file = "regex-2026.3.32-cp310-cp310-win32.whl", hash = "sha256:6128dd0793a87287ea1d8bf16b4250dd96316c464ee15953d5b98875a284d41e"}, - {file = "regex-2026.3.32-cp310-cp310-win_amd64.whl", hash = "sha256:5aa78c857c1731bdd9863923ffadc816d823edf475c7db6d230c28b53b7bdb5e"}, - {file = "regex-2026.3.32-cp310-cp310-win_arm64.whl", hash = "sha256:34c905a721ddee0f84c99e3e3b59dd4a5564a6fe338222bc89dd4d4df166115c"}, - {file = "regex-2026.3.32-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d7855f5e59fcf91d0c9f4a51dc5d8847813832a2230c3e8e35912ccf20baaa2"}, - {file = "regex-2026.3.32-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:18eb45f711e942c27dbed4109830bd070d8d618e008d0db39705f3f57070a4c6"}, - {file = "regex-2026.3.32-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed3b8281c5d0944d939c82db4ec2300409dd69ee087f7a75a94f2e301e855fb4"}, - {file = "regex-2026.3.32-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad5c53f2e8fcae9144009435ebe3d9832003508cf8935c04542a1b3b8deefa15"}, - {file = "regex-2026.3.32-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70c634e39c5cda0da05c93d6747fdc957599f7743543662b6dbabdd8d3ba8a96"}, - {file = "regex-2026.3.32-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e0f6648fd48f4c73d801c55ab976cd602e2da87de99c07bff005b131f269c6a"}, - {file = "regex-2026.3.32-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5e0fdb5744caf1036dec5510f543164f2144cb64932251f6dfd42fa872b7f9c"}, - {file = "regex-2026.3.32-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dab4178a0bc1ef13178832b12db7bc7f562e8f028b2b5be186e370090dc50652"}, - {file = "regex-2026.3.32-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f95bd07f301135771559101c060f558e2cf896c7df00bec050ca7f93bf11585a"}, - {file = "regex-2026.3.32-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2dcca2bceb823c9cc610e57b86a265d7ffc30e9fe98548c609eba8bd3c0c2488"}, - {file = "regex-2026.3.32-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:567b57eb987547a23306444e4f6f85d4314f83e65c71d320d898aa7550550443"}, - {file = "regex-2026.3.32-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:b6acb765e7c1f2fa08ac9057a33595e26104d7d67046becae184a8f100932dd9"}, - {file = "regex-2026.3.32-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1ed17104d1be7f807fdec35ec99777168dd793a09510d753f8710590ba54cdd"}, - {file = "regex-2026.3.32-cp311-cp311-win32.whl", hash = "sha256:c60f1de066eb5a0fd8ee5974de4194bb1c2e7692941458807162ffbc39887303"}, - {file = "regex-2026.3.32-cp311-cp311-win_amd64.whl", hash = "sha256:8fe14e24124ef41220e5992a0f09432f890037df6f93fd3d6b7a0feff2db16b2"}, - {file = "regex-2026.3.32-cp311-cp311-win_arm64.whl", hash = "sha256:ded4fc0edf3de792850cb8b04bbf3c5bd725eeaf9df4c27aad510f6eed9c4e19"}, - {file = "regex-2026.3.32-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ad8d372587e659940568afd009afeb72be939c769c552c9b28773d0337251391"}, - {file = "regex-2026.3.32-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3f5747501b69299c6b0b047853771e4ed390510bada68cb16da9c9c2078343f7"}, - {file = "regex-2026.3.32-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:db976be51375bca900e008941639448d148c655c9545071965d0571ecc04f5d0"}, - {file = "regex-2026.3.32-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66a5083c3ffe5a5a95f8281ea47a88072d4f24001d562d1d9d28d4cdc005fec5"}, - {file = "regex-2026.3.32-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e83ce8008b48762be296f1401f19afd9ea29f3d035d1974e0cecb74e9afbd1df"}, - {file = "regex-2026.3.32-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3aa21bad31db904e0b9055e12c8282df62d43169c4a9d2929407060066ebc74"}, - {file = "regex-2026.3.32-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f54840bea73541652f1170dc63402a5b776fc851ad36a842da9e5163c1f504a0"}, - {file = "regex-2026.3.32-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2ffbadc647325dd4e3118269bda93ded1eb5f5b0c3b7ba79a3da9fbd04f248e9"}, - {file = "regex-2026.3.32-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:66d3126afe7eac41759cd5f0b3b246598086e88e70527c0d68c9e615b81771c4"}, - {file = "regex-2026.3.32-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f785f44a44702dea89b28bce5bc82552490694ce4e144e21a4f0545e364d2150"}, - {file = "regex-2026.3.32-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b7836aa13721dbdef658aebd11f60d00de633a95726521860fe1f6be75fa225a"}, - {file = "regex-2026.3.32-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5336b1506142eb0f23c96fb4a34b37c4fefd4fed2a7042069f3c8058efe17855"}, - {file = "regex-2026.3.32-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b56993a7aeb4140c4770f4f7965c9e5af4f024457d06e23c01b0d47501cb18ed"}, - {file = "regex-2026.3.32-cp312-cp312-win32.whl", hash = "sha256:d363660f9ef8c734495598d2f3e527fb41f745c73159dc0d743402f049fb6836"}, - {file = "regex-2026.3.32-cp312-cp312-win_amd64.whl", hash = "sha256:c9f261ad3cd97257dc1d9355bfbaa7dd703e06574bffa0fa8fe1e31da915ee38"}, - {file = "regex-2026.3.32-cp312-cp312-win_arm64.whl", hash = "sha256:89e50667e7e8c0e7903e4d644a2764fffe9a3a5d6578f72ab7a7b4205bf204b7"}, - {file = "regex-2026.3.32-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c6d9c6e783b348f719b6118bb3f187b2e138e3112576c9679eb458cc8b2e164b"}, - {file = "regex-2026.3.32-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f21ae18dfd15752cdd98d03cbd7a3640be826bfd58482a93f730dbd24d7b9fb"}, - {file = "regex-2026.3.32-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:844d88509c968dd44b30daeefac72b038b1bf31ac372d5106358ab01d393c48b"}, - {file = "regex-2026.3.32-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8fc918cd003ba0d066bf0003deb05a259baaaab4dc9bd4f1207bbbe64224857a"}, - {file = "regex-2026.3.32-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bbc458a292aee57d572075f22c035fa32969cdb7987d454e3e34d45a40a0a8b4"}, - {file = "regex-2026.3.32-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:987cdfcfb97a249abc3601ad53c7de5c370529f1981e4c8c46793e4a1e1bfe8e"}, - {file = "regex-2026.3.32-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5d88fa37ba5e8a80ca8d956b9ea03805cfa460223ac94b7d4854ee5e30f3173"}, - {file = "regex-2026.3.32-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d082be64e51671dd5ee1c208c92da2ddda0f2f20d8ef387e57634f7e97b6aae"}, - {file = "regex-2026.3.32-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c1d7fa44aece1fa02b8927441614c96520253a5cad6a96994e3a81e060feed55"}, - {file = "regex-2026.3.32-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d478a2ca902b6ef28ffc9521e5f0f728d036abe35c0b250ee8ae78cfe7c5e44e"}, - {file = "regex-2026.3.32-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2820d2231885e97aff0fcf230a19ebd5d2b5b8a1ba338c20deb34f16db1c7897"}, - {file = "regex-2026.3.32-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc8ced733d6cd9af5e412f256a32f7c61cd2d7371280a65c689939ac4572499f"}, - {file = "regex-2026.3.32-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:847087abe98b3c1ebf1eb49d6ef320dbba75a83ee4f83c94704580f1df007dd4"}, - {file = "regex-2026.3.32-cp313-cp313-win32.whl", hash = "sha256:d21a07edddb3e0ca12a8b8712abc8452481c3d3db19ae87fc94e9842d005964b"}, - {file = "regex-2026.3.32-cp313-cp313-win_amd64.whl", hash = "sha256:3c054e39a9f85a3d76c62a1d50c626c5e9306964eaa675c53f61ff7ec1204bbb"}, - {file = "regex-2026.3.32-cp313-cp313-win_arm64.whl", hash = "sha256:b2e9c2ea2e93223579308263f359eab8837dc340530b860cb59b713651889f14"}, - {file = "regex-2026.3.32-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5d86e3fb08c94f084a625c8dc2132a79a3a111c8bf6e2bc59351fa61753c2f6e"}, - {file = "regex-2026.3.32-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b6f366a5ef66a2df4d9e68035cfe9f0eb8473cdfb922c37fac1d169b468607b0"}, - {file = "regex-2026.3.32-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b8fca73e16c49dd972ce3a88278dfa5b93bf91ddef332a46e9443abe21ca2f7c"}, - {file = "regex-2026.3.32-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b953d9d496d19786f4d46e6ba4b386c6e493e81e40f9c5392332458183b0599d"}, - {file = "regex-2026.3.32-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b565f25171e04d4fad950d1fa837133e3af6ea6f509d96166eed745eb0cf63bc"}, - {file = "regex-2026.3.32-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f28eac18a8733a124444643a66ac96fef2c0ad65f50034e0a043b90333dc677f"}, - {file = "regex-2026.3.32-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cdd508664430dd51b8888deb6c5b416d8de046b2e11837254378d31febe4a98"}, - {file = "regex-2026.3.32-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5c35d097f509cf7e40d20d5bee548d35d6049b36eb9965e8d43e4659923405b9"}, - {file = "regex-2026.3.32-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:85c9b0c131427470a6423baa0a9330be6fd8c3630cc3ee6fdee03360724cbec5"}, - {file = "regex-2026.3.32-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:e50af656c15e2723eeb7279c0837e07accc594b95ec18b86821a4d44b51b24bf"}, - {file = "regex-2026.3.32-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4bc32b4dbdb4f9f300cf9f38f8ea2ce9511a068ffaa45ac1373ee7a943f1d810"}, - {file = "regex-2026.3.32-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e3e5d1802cba785210a4a800e63fcee7a228649a880f3bf7f2aadccb151a834b"}, - {file = "regex-2026.3.32-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ef250a3f5e93182193f5c927c5e9575b2cb14b80d03e258bc0b89cc5de076b60"}, - {file = "regex-2026.3.32-cp313-cp313t-win32.whl", hash = "sha256:9cf7036dfa2370ccc8651521fcbb40391974841119e9982fa312b552929e6c85"}, - {file = "regex-2026.3.32-cp313-cp313t-win_amd64.whl", hash = "sha256:c940e00e8d3d10932c929d4b8657c2ea47d2560f31874c3e174c0d3488e8b865"}, - {file = "regex-2026.3.32-cp313-cp313t-win_arm64.whl", hash = "sha256:ace48c5e157c1e58b7de633c5e257285ce85e567ac500c833349c363b3df69d4"}, - {file = "regex-2026.3.32-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a416ee898ecbc5d8b283223b4cf4d560f93244f6f7615c1bd67359744b00c166"}, - {file = "regex-2026.3.32-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d76d62909bfb14521c3f7cfd5b94c0c75ec94b0a11f647d2f604998962ec7b6c"}, - {file = "regex-2026.3.32-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:631f7d95c83f42bccfe18946a38ad27ff6b6717fb4807e60cf24860b5eb277fc"}, - {file = "regex-2026.3.32-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12917c6c6813ffcdfb11680a04e4d63c5532b88cf089f844721c5f41f41a63ad"}, - {file = "regex-2026.3.32-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e221b615f83b15887636fcb90ed21f1a19541366f8b7ba14ba1ad8304f4ded4"}, - {file = "regex-2026.3.32-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f9ae4755fa90f1dc2d0d393d572ebc134c0fe30fcfc0ab7e67c1db15f192041"}, - {file = "regex-2026.3.32-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a094e9dcafedfb9d333db5cf880304946683f43a6582bb86688f123335122929"}, - {file = "regex-2026.3.32-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c1cecea3e477af105f32ef2119b8d895f297492e41d317e60d474bc4bffd62ff"}, - {file = "regex-2026.3.32-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f26262900edd16272b6360014495e8d68379c6c6e95983f9b7b322dc928a1194"}, - {file = "regex-2026.3.32-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1cb22fa9ee6a0acb22fc9aecce5f9995fe4d2426ed849357d499d62608fbd7f9"}, - {file = "regex-2026.3.32-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:9b9118a78e031a2e4709cd2fcc3028432e89b718db70073a8da574c249b5b249"}, - {file = "regex-2026.3.32-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b193ed199848aa96618cd5959c1582a0bf23cd698b0b900cb0ffe81b02c8659c"}, - {file = "regex-2026.3.32-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:10fb2aaae1aaadf7d43c9f3c2450404253697bf8b9ce360bd5418d1d16292298"}, - {file = "regex-2026.3.32-cp314-cp314-win32.whl", hash = "sha256:110ba4920721374d16c4c8ea7ce27b09546d43e16aea1d7f43681b5b8f80ba61"}, - {file = "regex-2026.3.32-cp314-cp314-win_amd64.whl", hash = "sha256:245667ad430745bae6a1e41081872d25819d86fbd9e0eec485ba00d9f78ad43d"}, - {file = "regex-2026.3.32-cp314-cp314-win_arm64.whl", hash = "sha256:1ca02ff0ef33e9d8276a1fcd6d90ff6ea055a32c9149c0050b5b67e26c6d2c51"}, - {file = "regex-2026.3.32-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:51fb7e26f91f9091fd8ec6a946f99b15d3bc3667cb5ddc73dd6cb2222dd4a1cc"}, - {file = "regex-2026.3.32-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:51a93452034d671b0e21b883d48ea66c5d6a05620ee16a9d3f229e828568f3f0"}, - {file = "regex-2026.3.32-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:03c2ebd15ff51e7b13bb3dc28dd5ac18cd39e59ebb40430b14ae1a19e833cff1"}, - {file = "regex-2026.3.32-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5bf2f3c2c5bd8360d335c7dcd4a9006cf1dabae063ee2558ee1b07bbc8a20d88"}, - {file = "regex-2026.3.32-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a4a3189a99ecdd1c13f42513ab3fc7fa8311b38ba7596dd98537acb8cd9acc3"}, - {file = "regex-2026.3.32-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3c0bbfbd38506e1ea96a85da6782577f06239cb9fcf9696f1ea537c980c0680b"}, - {file = "regex-2026.3.32-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8aaf8ee8f34b677f90742ca089b9c83d64bdc410528767273c816a863ed57327"}, - {file = "regex-2026.3.32-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ea568832eca219c2be1721afa073c1c9eb8f98a9733fdedd0a9747639fc22a5"}, - {file = "regex-2026.3.32-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e4c8fa46aad1a11ae2f8fcd1c90b9d55e18925829ac0d98c5bb107f93351745"}, - {file = "regex-2026.3.32-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cec365d44835b043d7b3266487797639d07d621bec9dc0ea224b00775797cc1"}, - {file = "regex-2026.3.32-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:09e26cad1544d856da85881ad292797289e4406338afe98163f3db9f7fac816c"}, - {file = "regex-2026.3.32-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:6062c4ef581a3e9e503dccf4e1b7f2d33fdc1c13ad510b287741ac73bc4c6b27"}, - {file = "regex-2026.3.32-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88ebc0783907468f17fca3d7821b30f9c21865a721144eb498cb0ff99a67bcac"}, - {file = "regex-2026.3.32-cp314-cp314t-win32.whl", hash = "sha256:e480d3dac06c89bc2e0fd87524cc38c546ac8b4a38177650745e64acbbcfdeba"}, - {file = "regex-2026.3.32-cp314-cp314t-win_amd64.whl", hash = "sha256:67015a8162d413af9e3309d9a24e385816666fbf09e48e3ec43342c8536f7df6"}, - {file = "regex-2026.3.32-cp314-cp314t-win_arm64.whl", hash = "sha256:1a6ac1ed758902e664e0d95c1ee5991aa6fb355423f378ed184c6ec47a1ec0e9"}, - {file = "regex-2026.3.32.tar.gz", hash = "sha256:f1574566457161678297a116fa5d1556c5a4159d64c5ff7c760e7c564bf66f16"}, -] - [[package]] name = "rich" version = "14.3.3" @@ -790,6 +666,34 @@ pygments = ">=2.13.0,<3.0.0" [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] +[[package]] +name = "ruff" +version = "0.15.9" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "ruff-0.15.9-py3-none-linux_armv6l.whl", hash = "sha256:6efbe303983441c51975c243e26dff328aca11f94b70992f35b093c2e71801e1"}, + {file = "ruff-0.15.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4965bac6ac9ea86772f4e23587746f0b7a395eccabb823eb8bfacc3fa06069f7"}, + {file = "ruff-0.15.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf05aad70ca5b5a0a4b0e080df3a6b699803916d88f006efd1f5b46302daab8"}, + {file = "ruff-0.15.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9439a342adb8725f32f92732e2bafb6d5246bd7a5021101166b223d312e8fc59"}, + {file = "ruff-0.15.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5e6faf9d97c8edc43877c3f406f47446fc48c40e1442d58cfcdaba2acea745"}, + {file = "ruff-0.15.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b34a9766aeec27a222373d0b055722900fbc0582b24f39661aa96f3fe6ad901"}, + {file = "ruff-0.15.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89dd695bc72ae76ff484ae54b7e8b0f6b50f49046e198355e44ea656e521fef9"}, + {file = "ruff-0.15.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce187224ef1de1bd225bc9a152ac7102a6171107f026e81f317e4257052916d5"}, + {file = "ruff-0.15.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0c7c341f68adb01c488c3b7d4b49aa8ea97409eae6462d860a79cf55f431b6"}, + {file = "ruff-0.15.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:55cc15eee27dc0eebdfcb0d185a6153420efbedc15eb1d38fe5e685657b0f840"}, + {file = "ruff-0.15.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6537f6eed5cda688c81073d46ffdfb962a5f29ecb6f7e770b2dc920598997ed"}, + {file = "ruff-0.15.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6d3fcbca7388b066139c523bda744c822258ebdcfbba7d24410c3f454cc9af71"}, + {file = "ruff-0.15.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:058d8e99e1bfe79d8a0def0b481c56059ee6716214f7e425d8e737e412d69677"}, + {file = "ruff-0.15.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8e1ddb11dbd61d5983fa2d7d6370ef3eb210951e443cace19594c01c72abab4c"}, + {file = "ruff-0.15.9-py3-none-win32.whl", hash = "sha256:bde6ff36eaf72b700f32b7196088970bf8fdb2b917b7accd8c371bfc0fd573ec"}, + {file = "ruff-0.15.9-py3-none-win_amd64.whl", hash = "sha256:45a70921b80e1c10cf0b734ef09421f71b5aa11d27404edc89d7e8a69505e43d"}, + {file = "ruff-0.15.9-py3-none-win_arm64.whl", hash = "sha256:0694e601c028fd97dc5c6ee244675bc241aeefced7ef80cd9c6935a871078f53"}, + {file = "ruff-0.15.9.tar.gz", hash = "sha256:29cbb1255a9797903f6dde5ba0188c707907ff44a9006eb273b5a17bfa0739a2"}, +] + [[package]] name = "shellingham" version = "1.5.4" @@ -878,18 +782,6 @@ click = ">=8.2.1" rich = ">=12.3.0" shellingham = ">=1.3.0" -[[package]] -name = "types-regex" -version = "2026.3.32.20260329" -description = "Typing stubs for regex" -optional = false -python-versions = ">=3.10" -groups = ["main", "dev"] -files = [ - {file = "types_regex-2026.3.32.20260329-py3-none-any.whl", hash = "sha256:861d0893bcfe08a57eb7486a502014e29dc2721d46dd5130798fbccafdb31cc0"}, - {file = "types_regex-2026.3.32.20260329.tar.gz", hash = "sha256:12653e44694cb3e3ccdc39bab3d433d2a83fec1c01220e6871fd6f3cf434675c"}, -] - [[package]] name = "typing-extensions" version = "4.15.0" @@ -903,9 +795,9 @@ files = [ ] [extras] -dev = ["black", "coverage", "isort", "mypy", "pytest", "pytest-cov", "types-regex"] +dev = ["black", "coverage", "isort", "mypy", "pytest", "pytest-cov", "ruff"] [metadata] lock-version = "2.1" python-versions = ">=3.10" -content-hash = "b7f8f0f24b8bf0b821951e3c916c0e6e3dd77ef5c98823995f17a28ff4d946f9" +content-hash = "dcbfa1b9b88bd14395db96d67473ee51b742a3fff1fd2a9646cc53af9e8ac093" diff --git a/pyproject.toml b/pyproject.toml index aa2193a..f96376b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,10 @@ dev = [ [project.scripts] pyobfuscate = "python_obfuscator.cli:cli" +[tool.black] +line-length = 88 +target-version = ["py310"] + [tool.isort] profile = "black" line_length = 88 From cd33e2973c7cdf4ae3b8f21070d6dd85f8b46eed Mon Sep 17 00:00:00 2001 From: davidteather <34144122+davidteather@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:15:56 -0400 Subject: [PATCH 10/11] lint fixes and poetry --- .github/workflows/package-test.yml | 20 +++++++++------- poetry.lock | 37 ++++++++++++++++++++++++++++-- pyproject.toml | 2 ++ 3 files changed, 49 insertions(+), 10 deletions(-) diff --git a/.github/workflows/package-test.yml b/.github/workflows/package-test.yml index aa540d6..9bdf2f5 100644 --- a/.github/workflows/package-test.yml +++ b/.github/workflows/package-test.yml @@ -25,22 +25,26 @@ jobs: with: python-version: "3.12" - - name: Install lint tools - run: pip install ruff isort pyright - - name: Install Black (match pyproject / poetry.lock) - run: pip install "black==25.11.0" + - name: Install Poetry + run: curl -sSL https://install.python-poetry.org | python - -y + + - name: Update PATH + run: echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: Install dependencies + run: poetry install --no-interaction - name: Ruff (lint) - run: ruff check python_obfuscator tests + run: poetry run ruff check python_obfuscator tests - name: Black (format check) - run: black --check python_obfuscator tests + run: poetry run black --check python_obfuscator tests - name: isort (import order check) - run: isort --check-only python_obfuscator tests + run: poetry run isort --check-only python_obfuscator tests - name: Pyright (type check) - run: pyright python_obfuscator + run: poetry run pyright python_obfuscator test: name: Test (Python ${{ matrix.python-version }}) diff --git a/poetry.lock b/poetry.lock index a47591b..63b6e66 100644 --- a/poetry.lock +++ b/poetry.lock @@ -470,6 +470,18 @@ files = [ {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] +[[package]] +name = "nodeenv" +version = "1.10.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] +files = [ + {file = "nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827"}, + {file = "nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb"}, +] + [[package]] name = "packaging" version = "26.0" @@ -548,6 +560,27 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pyright" +version = "1.1.408" +description = "Command line wrapper for pyright" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1"}, + {file = "pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684"}, +] + +[package.dependencies] +nodeenv = ">=1.6.0" +typing-extensions = ">=4.1" + +[package.extras] +all = ["nodejs-wheel-binaries", "twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] +nodejs = ["nodejs-wheel-binaries"] + [[package]] name = "pytest" version = "8.4.2" @@ -795,9 +828,9 @@ files = [ ] [extras] -dev = ["black", "coverage", "isort", "mypy", "pytest", "pytest-cov", "ruff"] +dev = ["black", "coverage", "isort", "mypy", "pyright", "pytest", "pytest-cov", "ruff"] [metadata] lock-version = "2.1" python-versions = ">=3.10" -content-hash = "dcbfa1b9b88bd14395db96d67473ee51b742a3fff1fd2a9646cc53af9e8ac093" +content-hash = "c32f6a7026a11e7d4fad3be092ab5623fcfa67db20afe99e4221506d78853c32" diff --git a/pyproject.toml b/pyproject.toml index f96376b..6787932 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ ruff = ">=0.9.0" pytest = "^8.3.4" coverage = "^7.6.12" pytest-cov = "^7.1.0" +pyright = "^1.1.408" [project] name = "python_obfuscator" @@ -44,6 +45,7 @@ dev = [ "ruff", "coverage", "isort", + "pyright", ] [project.scripts] From 4ece184b89a82f7184d332728f8b589a09184ba6 Mon Sep 17 00:00:00 2001 From: davidteather <34144122+davidteather@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:20:28 -0400 Subject: [PATCH 11/11] show poetry in readme --- README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3a1d203..d9cb99d 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,20 @@ pip install python-obfuscator Requires Python ≥ 3.10. +### From source (contributors) + +```bash +pip install -e ".[dev]" +``` + +Or with [Poetry](https://python-poetry.org/): + +```bash +poetry install +``` + +(`poetry install` pulls dev dependencies from the lockfile; use `poetry run ` to run tools inside that environment.) + --- ## Quick start — CLI @@ -158,8 +172,9 @@ The dead-code injector's overhead scales with the number of scopes and loop iter ## Running the test suite +After a dev install ([from source](#from-source-contributors)): + ```bash -pip install -e ".[dev]" pytest ``` @@ -173,6 +188,8 @@ coverage run -m pytest && coverage report pytest tests/e2e/ -v -s ``` +With Poetry, run the same commands through the project environment, for example `poetry run pytest`, `poetry run coverage run -m pytest`, and `poetry run pytest tests/e2e/ -v -s`. + --- ## Authors