pax_global_header00006660000000000000000000000064150362052530014513gustar00rootroot0000000000000052 comment=d5baf8205e5dff17d3bbd615d260c3e99ccb526a python-nh3-0.3.0/000077500000000000000000000000001503620525300135225ustar00rootroot00000000000000python-nh3-0.3.0/.github/000077500000000000000000000000001503620525300150625ustar00rootroot00000000000000python-nh3-0.3.0/.github/FUNDING.yml000066400000000000000000000000211503620525300166700ustar00rootroot00000000000000github: messense python-nh3-0.3.0/.github/dependabot.yml000066400000000000000000000007701503620525300177160ustar00rootroot00000000000000# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "cargo" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" python-nh3-0.3.0/.github/workflows/000077500000000000000000000000001503620525300171175ustar00rootroot00000000000000python-nh3-0.3.0/.github/workflows/CI.yml000066400000000000000000000235771503620525300201530ustar00rootroot00000000000000name: CI on: push: branches: - main tags: ["v*"] pull_request: jobs: macos: runs-on: macos-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: 3.12 architecture: x64 - uses: dtolnay/rust-toolchain@nightly - name: Build wheels uses: pyo3/maturin-action@v1 with: target: universal2-apple-darwin args: --release --out dist sccache: true - name: Build free-threaded wheels uses: pyo3/maturin-action@v1 with: target: universal2-apple-darwin args: --release --out dist -i python3.13t sccache: true - name: Install built wheel run: | pip install nh3 --no-index --find-links dist --force-reinstall pip install pytest cd tests && pytest - name: Upload wheels uses: actions/upload-artifact@v4 with: name: wheels-macos path: dist windows: runs-on: windows-latest strategy: matrix: target: [x64, x86] steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: 3.12 architecture: ${{ matrix.target }} - uses: dtolnay/rust-toolchain@nightly - name: Build wheels uses: pyo3/maturin-action@v1 with: target: ${{ matrix.target }} args: --release --out dist - name: Build free-threaded wheels uses: pyo3/maturin-action@v1 with: target: ${{ matrix.target }} args: --release --out dist -i python3.13t - name: Install built wheel run: | pip install nh3 --no-index --find-links dist --force-reinstall pip install pytest cd tests && pytest - name: Upload wheels uses: actions/upload-artifact@v4 with: name: wheels-windows-${{ matrix.target }} path: dist windows-arm: runs-on: windows-11-arm strategy: matrix: target: [aarch64-pc-windows-msvc] steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: 3.12 architecture: arm64 # rust toolchain is not currently installed on windopws arm64 images https://github.com/actions/partner-runner-images/issues/77 - name: Setup rust id: setup-rust run: | Invoke-WebRequest https://static.rust-lang.org/rustup/dist/aarch64-pc-windows-msvc/rustup-init.exe -OutFile .\rustup-init.exe .\rustup-init.exe -y Add-Content $env:GITHUB_PATH "$env:USERPROFILE\.cargo\bin" - name: Build wheels uses: pyo3/maturin-action@v1 with: target: ${{ matrix.target }} args: --release --out dist -i - name: Build free-threaded wheels uses: pyo3/maturin-action@v1 with: target: ${{ matrix.target }} args: --release --out dist -i python3.13t - name: Install built wheel run: | pip install nh3 --no-index --find-links dist --force-reinstall pip install pytest cd tests && pytest - name: Upload wheels uses: actions/upload-artifact@v4 with: name: wheels-windows-arm-${{ matrix.target }} path: dist linux: runs-on: ubuntu-latest strategy: matrix: target: [x86_64, i686] steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: 3.12 architecture: x64 - name: Build wheels uses: pyo3/maturin-action@v1 with: rust-toolchain: nightly target: ${{ matrix.target }} manylinux: auto args: --release --out dist sccache: true before-script-linux: yum install -y libatomic - name: Build free-threaded wheels uses: pyo3/maturin-action@v1 with: rust-toolchain: nightly target: ${{ matrix.target }} manylinux: auto args: --release --out dist -i python3.13t sccache: true before-script-linux: yum install -y libatomic - name: Install built wheel if: matrix.target == 'x86_64' run: | pip install nh3 --no-index --find-links dist --force-reinstall pip install pytest cd tests && pytest - name: Upload wheels uses: actions/upload-artifact@v4 with: name: wheels-linux-${{ matrix.target }} path: dist linux-cross: runs-on: ubuntu-latest strategy: matrix: target: [aarch64, armv7, s390x, ppc64le, ppc64] steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: 3.12 - name: Build wheels uses: pyo3/maturin-action@v1 with: rust-toolchain: nightly target: ${{ matrix.target }} manylinux: auto args: --release --out dist sccache: true - name: Build free-threaded wheels uses: pyo3/maturin-action@v1 with: rust-toolchain: nightly target: ${{ matrix.target }} manylinux: auto args: --release --out dist -i python3.13 sccache: true - uses: uraimo/run-on-arch-action@v2.8.1 if: matrix.target != 'ppc64' name: Install built wheel with: arch: ${{ matrix.target }} distro: ubuntu22.04 githubToken: ${{ github.token }} install: | apt-get update apt-get install -y --no-install-recommends python3 python3-pip pip3 install -U pip pytest run: | pip3 install nh3 --no-index --find-links dist/ --force-reinstall cd tests && pytest - name: Upload wheels uses: actions/upload-artifact@v4 with: name: wheels-linux-${{ matrix.target }} path: dist musllinux: runs-on: ubuntu-latest strategy: matrix: target: - x86_64-unknown-linux-musl - i686-unknown-linux-musl steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: 3.12 architecture: x64 - name: Build wheels uses: pyo3/maturin-action@v1 with: target: ${{ matrix.target }} manylinux: musllinux_1_2 args: --release --out dist sccache: true - name: Build free-threaded wheels uses: pyo3/maturin-action@v1 with: target: ${{ matrix.target }} manylinux: musllinux_1_2 args: --release --out dist -i python3.13t sccache: true - name: Install built wheel if: matrix.target == 'x86_64-unknown-linux-musl' uses: addnab/docker-run-action@v3 with: image: alpine:latest options: -v ${{ github.workspace }}:/io -w /io run: | apk add py3-virtualenv python3 -m virtualenv .venv source .venv/bin/activate pip install -U pip pytest pip install nh3 --no-index --find-links /io/dist/ --force-reinstall cd tests && python3 -m pytest - name: Upload wheels uses: actions/upload-artifact@v4 with: name: wheels-${{ matrix.target }} path: dist musllinux-cross: runs-on: ubuntu-latest strategy: matrix: platform: - target: aarch64-unknown-linux-musl arch: aarch64 - target: armv7-unknown-linux-musleabihf arch: armv7 steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: 3.12 - name: Build wheels uses: pyo3/maturin-action@v1 with: target: ${{ matrix.platform.target }} manylinux: musllinux_1_2 args: --release --out dist sccache: true - name: Build free-threaded wheels uses: pyo3/maturin-action@v1 with: target: ${{ matrix.platform.target }} manylinux: musllinux_1_2 args: --release --out dist -i python3.13t sccache: true - uses: uraimo/run-on-arch-action@v2.8.1 name: Install built wheel with: arch: ${{ matrix.platform.arch }} distro: alpine_latest githubToken: ${{ github.token }} install: | apk add py3-virtualenv run: | python3 -m virtualenv .venv source .venv/bin/activate pip install pytest pip install nh3 --no-index --find-links dist/ --force-reinstall cd tests && python3 -m pytest - name: Upload wheels uses: actions/upload-artifact@v4 with: name: wheels-${{ matrix.platform.target }} path: dist sdist: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: 3.12 architecture: x64 - name: Build wheels uses: pyo3/maturin-action@v1 with: command: sdist args: --out dist - name: Upload sdist uses: actions/upload-artifact@v4 with: name: wheels-sdist path: dist release: name: Release runs-on: ubuntu-latest if: "startsWith(github.ref, 'refs/tags/')" needs: [macos, windows, windows-arm, linux, linux-cross, musllinux, musllinux-cross, sdist] steps: - uses: actions/download-artifact@v4 with: pattern: wheels-* merge-multiple: true - uses: actions/setup-python@v4 with: python-version: 3.12 - name: Publish to PyPI env: MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_PASSWORD }} run: | pip install --upgrade maturin maturin upload --skip-existing * python-nh3-0.3.0/.gitignore000066400000000000000000000012141503620525300155100ustar00rootroot00000000000000/target # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ venv/ bin/ build/ develop-eggs/ dist/ eggs/ lib/ lib64/ parts/ sdist/ var/ include/ man/ *.egg-info/ .installed.cfg *.egg # Installer logs pip-log.txt pip-delete-this-directory.txt pip-selfcheck.json # Unit test / coverage reports htmlcov/ .tox/ .coverage .cache nosetests.xml coverage.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject # Rope .ropeproject # Django stuff: *.log *.pot # Sphinx documentation docs/_build/ # PyCharm .idea/ .python-version .pytest_cache/ .DS_Store python-nh3-0.3.0/.readthedocs.yml000066400000000000000000000004721503620525300166130ustar00rootroot00000000000000# https://docs.readthedocs.io/en/stable/config-file/v2.html#supported-settings version: 2 sphinx: builder: html configuration: docs/conf.py build: os: "ubuntu-22.04" tools: python: "3.12" rust: "1.82" python: install: - requirements: docs/requirements.txt - method: pip path: . python-nh3-0.3.0/Cargo.lock000066400000000000000000000573161503620525300154430ustar00rootroot00000000000000# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "aliasable" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" [[package]] name = "ammonia" version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6b346764dd0814805de8abf899fe03065bcee69bb1a4771c785817e39f3978f" dependencies = [ "cssparser", "html5ever", "maplit", "tendril", "url", ] [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "cc" version = "1.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" dependencies = [ "shlex", ] [[package]] name = "cfg-if" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "cssparser" version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e901edd733a1472f944a45116df3f846f54d37e67e68640ac8bb69689aca2aa" dependencies = [ "cssparser-macros", "dtoa-short", "itoa", "phf", "smallvec", ] [[package]] name = "cssparser-macros" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", "syn", ] [[package]] name = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "dtoa" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" [[package]] name = "dtoa-short" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" dependencies = [ "dtoa", ] [[package]] name = "form_urlencoded" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "futf" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" dependencies = [ "mac", "new_debug_unreachable", ] [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "html5ever" version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" dependencies = [ "log", "markup5ever", "match_token", ] [[package]] name = "icu_collections" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_locale_core" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", "tinystr", "writeable", "zerovec", ] [[package]] name = "icu_normalizer" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", "zerovec", ] [[package]] name = "icu_normalizer_data" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", "zerotrie", "zerovec", ] [[package]] name = "idna" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ "idna_adapter", "smallvec", "utf8_iter", ] [[package]] name = "idna_adapter" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", ] [[package]] name = "indoc" version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "libc" version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "litemap" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "mac" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "maplit" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "markup5ever" version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" dependencies = [ "log", "tendril", "web_atoms", ] [[package]] name = "match_token" version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "memoffset" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] [[package]] name = "new_debug_unreachable" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "nh3" version = "0.3.0" dependencies = [ "ammonia", "ouroboros", "pyo3", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "ouroboros" version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" dependencies = [ "aliasable", "ouroboros_macro", "static_assertions", ] [[package]] name = "ouroboros_macro" version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" dependencies = [ "heck 0.4.1", "proc-macro2", "proc-macro2-diagnostics", "quote", "syn", ] [[package]] name = "parking_lot" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets", ] [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "phf" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ "phf_macros", "phf_shared", ] [[package]] name = "phf_codegen" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ "phf_generator", "phf_shared", ] [[package]] name = "phf_generator" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", "rand", ] [[package]] name = "phf_macros" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ "phf_generator", "phf_shared", "proc-macro2", "quote", "syn", ] [[package]] name = "phf_shared" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ "siphasher", ] [[package]] name = "portable-atomic" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "potential_utf" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" dependencies = [ "zerovec", ] [[package]] name = "precomputed-hash" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "proc-macro2" version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "proc-macro2-diagnostics" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", "syn", "version_check", "yansi", ] [[package]] name = "pyo3" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8970a78afe0628a3e3430376fc5fd76b6b45c4d43360ffd6cdd40bdde72b682a" dependencies = [ "indoc", "libc", "memoffset", "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", "unindent", ] [[package]] name = "pyo3-build-config" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "458eb0c55e7ece017adeba38f2248ff3ac615e53660d7c71a238d7d2a01c7598" dependencies = [ "once_cell", "python3-dll-a", "target-lexicon", ] [[package]] name = "pyo3-ffi" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7114fe5457c61b276ab77c5055f206295b812608083644a5c5b2640c3102565c" dependencies = [ "libc", "pyo3-build-config", ] [[package]] name = "pyo3-macros" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8725c0a622b374d6cb051d11a0983786448f7785336139c3c94f5aa6bef7e50" dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", "syn", ] [[package]] name = "pyo3-macros-backend" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4109984c22491085343c05b0dbc54ddc405c3cf7b4374fc533f5c3313a572ccc" dependencies = [ "heck 0.5.0", "proc-macro2", "pyo3-build-config", "quote", "syn", ] [[package]] name = "python3-dll-a" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d381ef313ae70b4da5f95f8a4de773c6aa5cd28f73adec4b4a31df70b66780d8" dependencies = [ "cc", ] [[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" [[package]] name = "redox_syscall" version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" dependencies = [ "bitflags", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "siphasher" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "string_cache" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" dependencies = [ "new_debug_unreachable", "parking_lot", "phf_shared", "precomputed-hash", "serde", ] [[package]] name = "string_cache_codegen" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" dependencies = [ "phf_generator", "phf_shared", "proc-macro2", "quote", ] [[package]] name = "syn" version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "synstructure" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "target-lexicon" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" [[package]] name = "tendril" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" dependencies = [ "futf", "mac", "utf-8", ] [[package]] name = "tinystr" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", ] [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unindent" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" [[package]] name = "url" version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] [[package]] name = "utf-8" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "web_atoms" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" dependencies = [ "phf", "phf_codegen", "string_cache", "string_cache_codegen", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "writeable" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yoke" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", "yoke-derive", "zerofrom", ] [[package]] name = "yoke-derive" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "zerofrom" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "zerotrie" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" dependencies = [ "displaydoc", "yoke", "zerofrom", ] [[package]] name = "zerovec" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" dependencies = [ "yoke", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", "syn", ] python-nh3-0.3.0/Cargo.toml000066400000000000000000000010121503620525300154440ustar00rootroot00000000000000[package] name = "nh3" version = "0.3.0" authors = ["messense "] edition = "2021" description = "Python bindings to the ammonia HTML sanitization library." license = "MIT" repository = "https://github.com/messense/nh3" readme = "README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] crate-type = ["cdylib"] [dependencies] ammonia = "4.1.1" pyo3 = { version = "0.25.1", features = ["abi3-py38", "generate-import-lib"] } ouroboros = "0.18" python-nh3-0.3.0/LICENSE000066400000000000000000000020761503620525300145340ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2021-present Messense Lv Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. python-nh3-0.3.0/README.md000066400000000000000000000025651503620525300150110ustar00rootroot00000000000000# nh3 ![CI](https://github.com/messense/nh3/workflows/CI/badge.svg) [![PyPI](https://img.shields.io/pypi/v/nh3.svg)](https://pypi.org/project/nh3) [![Documentation Status](https://readthedocs.org/projects/nh3/badge/?version=latest)](https://nh3.readthedocs.io/en/latest/?badge=latest) Python bindings to the [ammonia](https://github.com/rust-ammonia/ammonia) HTML sanitization library. ## Installation ```bash pip install nh3 ``` ## Usage See [the documentation](https://nh3.readthedocs.io/en/latest/). ## Performance A quick benchmark showing that nh3 is about 20 times faster than the deprecated [bleach](https://pypi.org/project/bleach/) package. Measured on a MacBook Air (M2, 2022). ```ipython Python 3.11.0 (main, Oct 25 2022, 16:25:24) [Clang 14.0.0 (clang-1400.0.29.102)] Type 'copyright', 'credits' or 'license' for more information IPython 8.9.0 -- An enhanced Interactive Python. Type '?' for help. In [1]: import requests In [2]: import bleach In [3]: import nh3 In [4]: html = requests.get("https://www.google.com").text In [5]: %timeit bleach.clean(html) 2.85 ms ± 22.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) In [6]: %timeit nh3.clean(html) 138 µs ± 860 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each) ``` ## License This work is released under the MIT license. A copy of the license is provided in the [LICENSE](./LICENSE) file. python-nh3-0.3.0/docs/000077500000000000000000000000001503620525300144525ustar00rootroot00000000000000python-nh3-0.3.0/docs/Makefile000066400000000000000000000011721503620525300161130ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) python-nh3-0.3.0/docs/conf.py000066400000000000000000000034551503620525300157600ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('..')) # -- Project information ----------------------------------------------------- project = 'nh3' copyright = '2021-present, messense' author = 'messense' # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.coverage', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [ '.DS_Store', '.venv', '_build', 'Thumbs.db', ] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'shibuya' html_theme_options = { 'page_layout': 'compact', 'github_url': 'https://github.com/messense/nh3', } pygments_style = "xcode" pygments_dark_style = "monokai" python-nh3-0.3.0/docs/index.rst000066400000000000000000000037451503620525300163240ustar00rootroot00000000000000nh3 === Python bindings to the `ammonia `__ HTML sanitization library. Installation ------------ .. code-block:: bash pip install nh3 Usage ----- Use ``clean()`` to sanitize HTML fragments: .. code-block:: pycon >>> import nh3 >>> nh3.clean("hi") 'hi' >>> nh3.clean("XSS?") 'XSS?' It has many options to customize the sanitization, as documented below. For example, to only allow ```` tags: .. code-block:: python >>> nh3.clean("Hello", tags={"b"}) 'Hello' API reference ------------- .. automodule:: nh3 :members: .. attribute:: ALLOWED_TAGS The default set of tags allowed by ``clean()``. Useful for customizing the default to add or remove some tags: .. code-block:: pycon >>> tags = nh3.ALLOWED_TAGS - {"b"} >>> nh3.clean("yeah", tags=tags) 'yeah' .. attribute:: ALLOWED_ATTRIBUTES The default mapping of tags to allowed attributes for ``clean()``. Useful for customizing the default to add or remove some attributes: .. code-block:: pycon >>> from copy import deepcopy >>> attributes = deepcopy(nh3.ALLOWED_ATTRIBUTES) >>> attributes["img"].add("data-invert") >>> nh3.clean("", attributes=attributes) '' .. attribute:: ALLOWED_URL_SCHEMES The default set of URL schemes permitted on ``href`` and ``src`` attributes. Useful for customizing the default to add or remove some URL schemes: .. code-block:: pycon >>> url_schemes = nh3.ALLOWED_URL_SCHEMES - {'tel'} >>> nh3.clean('Call or email me.', url_schemes=url_schemes) 'Call or email me.' python-nh3-0.3.0/docs/make.bat000066400000000000000000000014401503620525300160560ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.https://www.sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd python-nh3-0.3.0/docs/requirements.txt000066400000000000000000000000171503620525300177340ustar00rootroot00000000000000shibuya sphinx python-nh3-0.3.0/nh3.pyi000066400000000000000000000033501503620525300147360ustar00rootroot00000000000000from typing import Callable, Dict, Optional, Set ALLOWED_TAGS: Set[str] ALLOWED_ATTRIBUTES: Dict[str, Set[str]] ALLOWED_URL_SCHEMES: Set[str] class Cleaner: def __init__( self, tags: Optional[Set[str]] = None, clean_content_tags: Optional[Set[str]] = None, attributes: Optional[Dict[str, Set[str]]] = None, attribute_filter: Optional[Callable[[str, str, str], Optional[str]]] = None, strip_comments: bool = True, link_rel: Optional[str] = "noopener noreferrer", generic_attribute_prefixes: Optional[Set[str]] = None, tag_attribute_values: Optional[Dict[str, Dict[str, Set[str]]]] = None, set_tag_attribute_values: Optional[Dict[str, Dict[str, str]]] = None, url_schemes: Optional[Set[str]] = None, allowed_classes: Optional[Dict[str, Set[str]]] = None, filter_style_properties: Optional[Set[str]] = None, ) -> None: ... def clean(self, html: str) -> str: ... def clean( html: str, tags: Optional[Set[str]] = None, clean_content_tags: Optional[Set[str]] = None, attributes: Optional[Dict[str, Set[str]]] = None, attribute_filter: Optional[Callable[[str, str, str], Optional[str]]] = None, strip_comments: bool = True, link_rel: Optional[str] = "noopener noreferrer", generic_attribute_prefixes: Optional[Set[str]] = None, tag_attribute_values: Optional[Dict[str, Dict[str, Set[str]]]] = None, set_tag_attribute_values: Optional[Dict[str, Dict[str, str]]] = None, url_schemes: Optional[Set[str]] = None, allowed_classes: Optional[Dict[str, Set[str]]] = None, filter_style_properties: Optional[Set[str]] = None, ) -> str: ... def clean_text(html: str) -> str: ... def is_html(html: str) -> bool: ... python-nh3-0.3.0/pyproject.toml000066400000000000000000000011421503620525300164340ustar00rootroot00000000000000[build-system] requires = ["maturin>=1.0,<2.0"] build-backend = "maturin" [project] name = "nh3" description = "Python binding to Ammonia HTML sanitizer Rust crate" authors = [{ name = "messense", email = "messense@icloud.com" }] requires-python = ">=3.8" license = {text = "MIT"} classifiers = [ "Programming Language :: Rust", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Python Modules", ] dynamic = ["version"] [tool.maturin] features = ["pyo3/extension-module"] python-nh3-0.3.0/src/000077500000000000000000000000001503620525300143115ustar00rootroot00000000000000python-nh3-0.3.0/src/lib.rs000066400000000000000000000440111503620525300154250ustar00rootroot00000000000000use std::borrow::Cow; use std::collections::{HashMap, HashSet}; use ouroboros::self_referencing; use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; use pyo3::types::{PyString, PyTuple}; struct Config { tags: Option>, clean_content_tags: Option>, attributes: Option>>, attribute_filter: Option, strip_comments: bool, link_rel: Option, generic_attribute_prefixes: Option>, tag_attribute_values: Option>>>, set_tag_attribute_values: Option>>, url_schemes: Option>, allowed_classes: Option>>, filter_style_properties: Option>, } impl Default for Config { fn default() -> Self { Self { tags: None, clean_content_tags: None, attributes: None, attribute_filter: None, strip_comments: true, link_rel: Some("noopener noreferrer".to_string()), generic_attribute_prefixes: None, tag_attribute_values: None, set_tag_attribute_values: None, url_schemes: None, allowed_classes: None, filter_style_properties: None, } } } #[self_referencing] struct Inner { config: Config, #[borrows(config)] #[not_covariant] builder: ammonia::Builder<'this>, } #[pyclass] pub struct Cleaner { inner: Inner, } impl Cleaner { fn new(config: Config) -> Self { let inner = InnerBuilder { config, builder_builder: |config| Self::build_ammonia_from_config(config), } .build(); Self { inner } } fn build_ammonia_from_config(config: &Config) -> ammonia::Builder<'_> { let mut builder = ammonia::Builder::default(); if let Some(tags) = config.tags.as_ref() { let tags: HashSet<&str> = tags.iter().map(|s| s.as_str()).collect(); builder.tags(tags); } if let Some(tags) = config.clean_content_tags.as_ref() { let tags: HashSet<&str> = tags.iter().map(|s| s.as_str()).collect(); builder.clean_content_tags(tags); } if let Some(attrs) = config.attributes.as_ref() { let attrs: HashMap<&str, HashSet<&str>> = attrs .iter() .filter(|(k, _)| k.as_str() != "*") .map(|(k, v)| (k.as_str(), v.iter().map(|s| s.as_str()).collect())) .collect(); builder.tag_attributes(attrs); if let Some(generic_attrs) = config.attributes.as_ref().and_then(|a| a.get("*")) { let generic_attrs: HashSet<&str> = generic_attrs.iter().map(|s| s.as_str()).collect(); builder.generic_attributes(generic_attrs); } } if let Some(prefixes) = config.generic_attribute_prefixes.as_ref() { let prefixes: HashSet<&str> = prefixes.iter().map(|s| s.as_str()).collect(); builder.generic_attribute_prefixes(prefixes); } if let Some(values) = config.tag_attribute_values.as_ref() { let values: HashMap<&str, HashMap<&str, HashSet<&str>>> = values .iter() .map(|(tag, attrs)| { let inner: HashMap<&str, HashSet<&str>> = attrs .iter() .map(|(attr, vals)| { (attr.as_str(), vals.iter().map(|v| v.as_str()).collect()) }) .collect(); (tag.as_str(), inner) }) .collect(); builder.tag_attribute_values(values); } if let Some(values) = config.set_tag_attribute_values.as_ref() { let values: HashMap<&str, HashMap<&str, &str>> = values .iter() .map(|(tag, attrs)| { let inner: HashMap<&str, &str> = attrs .iter() .map(|(attr, val)| (attr.as_str(), val.as_str())) .collect(); (tag.as_str(), inner) }) .collect(); builder.set_tag_attribute_values(values); } let attribute_filter = config .attribute_filter .as_ref() .map(|f| Python::with_gil(|py| f.clone_ref(py))); if let Some(callback) = attribute_filter { builder.attribute_filter(move |element, attribute, value| { Python::with_gil(|py| { let res = callback.call( py, PyTuple::new( py, [ PyString::new(py, element), PyString::new(py, attribute), PyString::new(py, value), ], ) .unwrap(), None, ); let err = match res { Ok(val) => { if val.is_none(py) { return None; } else if let Ok(s) = val.extract::(py) { return Some(Cow::::Owned(s)); } else { PyTypeError::new_err( "expected attribute_filter to return str or None", ) } } Err(err) => err, }; err.write_unraisable( py, Some( &PyTuple::new( py, [ PyString::new(py, element), PyString::new(py, attribute), PyString::new(py, value), ], ) .unwrap(), ), ); Some(value.into()) }) }); } builder.strip_comments(config.strip_comments); builder.link_rel(config.link_rel.as_deref()); if let Some(url_schemes) = config.url_schemes.as_ref() { let url_schemes: HashSet<_> = url_schemes.iter().map(|s| s.as_str()).collect(); builder.url_schemes(url_schemes); } if let Some(allowed_classes) = config.allowed_classes.as_ref() { builder.allowed_classes( allowed_classes .iter() .map(|(tag, class_set)| { (tag.as_str(), class_set.iter().map(|c| c.as_str()).collect()) }) .collect(), ); } if let Some(filter_style_properties) = config.filter_style_properties.as_ref() { builder.filter_style_properties( filter_style_properties .iter() .map(|prop| prop.as_str()) .collect(), ); } builder } pub fn clean(&self, html: &str) -> String { self.inner .with_builder(|builder| builder.clean(html).to_string()) } } #[pymethods] impl Cleaner { /// Create a reusable sanitizer according to the given options. /// /// :param tags: Sets the tags that are allowed. /// :type tags: ``set[str]``, optional /// :param clean_content_tags: Sets the tags whose contents will be completely removed from the output. /// :type clean_content_tags: ``set[str]``, optional /// :param attributes: Sets the HTML attributes that are allowed on specific tags, /// ``*`` key means the attributes are allowed on any tag. /// :type attributes: ``dict[str, set[str]]``, optional /// :param attribute_filter: Allows rewriting of all attributes using a callback. /// The callback takes name of the element, attribute and its value. /// Returns ``None`` to remove the attribute, or a value to use. /// :type attribute_filter: ``Callable[[str, str, str], str | None]``, optional /// :param strip_comments: Configures the handling of HTML comments, defaults to ``True``. /// :type strip_comments: ``bool`` /// :param link_rel: Configures a ``rel`` attribute that will be added on links, defaults to ``noopener noreferrer``. /// To turn on rel-insertion, pass a space-separated list. /// If ``rel`` is in the generic or tag attributes, this must be set to ``None``. Common ``rel`` values to include: /// /// - ``noopener``: This prevents a particular type of XSS attack, and should usually be turned on for untrusted HTML. /// - ``noreferrer``: This prevents the browser from sending the source URL to the website that is linked to. /// - ``nofollow``: This prevents search engines from using this link for ranking, which disincentivizes spammers. /// :type link_rel: ``str`` /// :param generic_attribute_prefixes: Sets the prefix of attributes that are allowed on any tag. /// :type generic_attribute_prefixes: ``set[str]``, optional /// :param tag_attribute_values: Sets the values of HTML attributes that are allowed on specific tags. /// The value is structured as a map from tag names to a map from attribute names to a set of attribute values. /// If a tag is not itself whitelisted, adding entries to this map will do nothing. /// :type tag_attribute_values: ``dict[str, dict[str, set[str]]]``, optional /// :param set_tag_attribute_values: Sets the values of HTML attributes that are to be set on specific tags. /// The value is structured as a map from tag names to a map from attribute names to an attribute value. /// If a tag is not itself whitelisted, adding entries to this map will do nothing. /// :type set_tag_attribute_values: ``dict[str, dict[str, str]]``, optional /// :param url_schemes: Sets the URL schemes permitted on ``href`` and ``src`` attributes. /// :type url_schemes: ``set[str]``, optional /// :param allowed_classes: Sets the CSS classes that are allowed on specific tags. /// The values is structured as a map from tag names to a set of class names. /// The `class` attribute itself should not be whitelisted if this parameter is used. /// :type allowed_classes: ``dict[str, set[str]]``, optional /// :param filter_style_properties: Only allows the specified properties in `style` attributes. /// Irrelevant if `style` is not an allowed attribute. /// Note that if style filtering is enabled style properties will be normalised e.g. /// invalid declarations and @rules will be removed, with only syntactically valid /// declarations kept. /// :type filter_style_properties: ``set[str]``, optional #[new] #[pyo3(signature = ( tags = None, clean_content_tags = None, attributes = None, attribute_filter = None, strip_comments = true, link_rel = "noopener noreferrer", generic_attribute_prefixes = None, tag_attribute_values = None, set_tag_attribute_values = None, url_schemes = None, allowed_classes = None, filter_style_properties = None ))] fn py_new( py: Python, tags: Option>, clean_content_tags: Option>, attributes: Option>>, attribute_filter: Option, strip_comments: bool, link_rel: Option<&str>, generic_attribute_prefixes: Option>, tag_attribute_values: Option>>>, set_tag_attribute_values: Option>>, url_schemes: Option>, allowed_classes: Option>>, filter_style_properties: Option>, ) -> PyResult { if let Some(callback) = attribute_filter.as_ref() { if !callback.bind(py).is_callable() { return Err(PyTypeError::new_err("attribute_filter must be callable")); } } let config = Config { tags, clean_content_tags, attributes, attribute_filter, strip_comments, link_rel: link_rel.map(|s| s.to_string()), generic_attribute_prefixes, tag_attribute_values, set_tag_attribute_values, url_schemes, allowed_classes, filter_style_properties, }; Ok(Self::new(config)) } /// Sanitize an HTML fragment #[pyo3(name = "clean")] fn py_clean(&self, py: Python, html: &str) -> PyResult { Ok(py.allow_threads(|| self.clean(html))) } } /// Sanitize an HTML fragment according to the given options. /// See ``Cleaner()`` for detailed sanitizer options. /// /// :param html: Input HTML fragment /// :type html: ``str`` /// :return: Sanitized HTML fragment /// :rtype: ``str`` /// /// For example: /// /// .. code-block:: pycon /// /// >>> import nh3 /// >>> nh3.clean("hi") /// 'hi' /// >>> nh3.clean("XSS?") /// 'XSS?' /// /// Example of using ``attribute_filter``: /// /// .. code-block:: pycon /// /// >>> from copy import deepcopy /// >>> attributes = deepcopy(nh3.ALLOWED_ATTRIBUTES) /// >>> attributes["a"].add("class") /// >>> def attribute_filter(tag, attr, value): /// ... if tag == "a" and attr == "class": /// ... if "mention" in value.split(" "): /// ... return "mention" /// ... return None /// ... return value /// >>> nh3.clean("@foo", /// ... attributes=attributes, /// ... attribute_filter=attribute_filter) /// '@foo' /// /// Example of maintaining the ``rel`` attribute: /// /// .. code-block:: pycon /// /// >>> from copy import deepcopy /// >>> attributes = deepcopy(nh3.ALLOWED_ATTRIBUTES) /// >>> attributes["a"].add("rel") /// >>> nh3.clean("", /// ... link_rel=None, attributes=attributes) /// '' #[pyfunction(signature = ( html, tags = None, clean_content_tags = None, attributes = None, attribute_filter = None, strip_comments = true, link_rel = "noopener noreferrer", generic_attribute_prefixes = None, tag_attribute_values = None, set_tag_attribute_values = None, url_schemes = None, allowed_classes = None, filter_style_properties = None ))] #[allow(clippy::too_many_arguments)] fn clean( py: Python, html: &str, tags: Option>, clean_content_tags: Option>, attributes: Option>>, attribute_filter: Option, strip_comments: bool, link_rel: Option<&str>, generic_attribute_prefixes: Option>, tag_attribute_values: Option>>>, set_tag_attribute_values: Option>>, url_schemes: Option>, allowed_classes: Option>>, filter_style_properties: Option>, ) -> PyResult { let cleaner = Cleaner::py_new( py, tags, clean_content_tags, attributes, attribute_filter, strip_comments, link_rel, generic_attribute_prefixes, tag_attribute_values, set_tag_attribute_values, url_schemes, allowed_classes, filter_style_properties, )?; Ok(py.allow_threads(|| cleaner.clean(html))) } /// Turn an arbitrary string into unformatted HTML. /// /// Roughly equivalent to Python’s html.escape() or PHP’s htmlspecialchars and /// htmlentities. Escaping is as strict as possible, encoding every character /// that has special meaning to the HTML parser. /// /// :param html: Input HTML fragment /// :type html: ``str`` /// :return: Cleaned text /// :rtype: ``str`` /// /// For example: /// /// .. code-block:: pycon /// /// >>> import nh3 /// >>> nh3.clean_text('Robert"); abuse();//') /// 'Robert"); abuse();//' #[pyfunction] fn clean_text(py: Python, html: &str) -> String { py.allow_threads(|| ammonia::clean_text(html)) } /// Determine if a given string contains HTML. /// /// This function parses the full string and checks for any HTML syntax. /// /// Note: This function will return True for strings that contain invalid HTML syntax /// like ```` and even ``Vec::::new()``. /// /// :param html: Input string /// :type html: ``str`` /// :rtype: ``bool`` /// /// For example: /// /// .. code-block:: pycon /// /// >>> nh3.is_html("plain text") /// False /// >>> nh3.is_html("

html!

") /// True #[pyfunction] fn is_html(py: Python, html: &str) -> bool { py.allow_threads(|| ammonia::is_html(html)) } /// Python bindings to the ammonia HTML sanitization library ( https://github.com/rust-ammonia/ammonia ). #[pymodule(gil_used = false)] fn nh3(_py: Python, m: &Bound) -> PyResult<()> { m.add("__version__", env!("CARGO_PKG_VERSION"))?; m.add_function(wrap_pyfunction!(clean, m)?)?; m.add_function(wrap_pyfunction!(clean_text, m)?)?; m.add_function(wrap_pyfunction!(is_html, m)?)?; m.add_class::()?; let a = ammonia::Builder::default(); m.add("ALLOWED_TAGS", a.clone_tags())?; m.add("ALLOWED_ATTRIBUTES", a.clone_tag_attributes())?; m.add("ALLOWED_URL_SCHEMES", a.clone_url_schemes())?; Ok(()) } python-nh3-0.3.0/tests/000077500000000000000000000000001503620525300146645ustar00rootroot00000000000000python-nh3-0.3.0/tests/test_nh3.py000066400000000000000000000062061503620525300167710ustar00rootroot00000000000000import nh3 import pytest def test_clean(): html = "I'm not trying to XSS you" assert nh3.clean(html) == 'I\'m not trying to XSS you' assert nh3.clean(html, tags={"img"}) == 'I\'m not trying to XSS you' assert ( nh3.clean(html, tags={"img"}, attributes={}) == "I'm not trying to XSS you" ) assert nh3.clean(html, attributes={}) == "I'm not trying to XSS you" assert ( nh3.clean('baidu') == 'baidu' ) assert ( nh3.clean('baidu', link_rel=None) == 'baidu' ) assert ( nh3.clean( "", clean_content_tags={"script", "style"}, ) == "" ) assert ( nh3.clean('
', generic_attribute_prefixes={"data-"}) == '
' ) assert ( nh3.clean( "", tags={"my-tag"}, tag_attribute_values={"my-tag": {"my-attr": {"val"}}}, ) == '' ) assert ( nh3.clean( "", tags={"my-tag"}, set_tag_attribute_values={"my-tag": {"my-attr": "val"}}, ) == '' ) assert ( nh3.clean( "T
U
", allowed_classes={ 'a': {'b', 'c'}, 'span': {'a'} } ) == 'T
U
' ) assert ( nh3.clean( "T
", filter_style_properties={'color', 'font-size'}, attributes={'span': {'style'}} ) == 'T
' ) def test_clean_with_attribute_filter(): html = "Home" def attribute_filter(element, attribute, value): if element == "img" and attribute == "src": return None return value assert ( nh3.clean(html, attribute_filter=attribute_filter, link_rel=None) == 'Home' ) with pytest.raises(TypeError): nh3.clean(html, attribute_filter="not a callable") # attribute_filter may raise exception, but it's an infallible API # which writes a unraisable exception nh3.clean(html, attribute_filter=lambda _element, _attribute, _value: True) def test_clean_text(): res = nh3.clean_text('Robert"); abuse();//') assert res == "Robert"); abuse();//" def test_is_html(): assert not nh3.is_html("plain text") assert nh3.is_html("

html!

")