diff --git a/.github/instructions/python-bindings.instructions.md b/.github/instructions/python-bindings.instructions.md index c0bba73..df2fb0e 100644 --- a/.github/instructions/python-bindings.instructions.md +++ b/.github/instructions/python-bindings.instructions.md @@ -477,7 +477,7 @@ name = "feedparser-rs" version = "0.1.8" description = "High-performance RSS/Atom feed parser (drop-in feedparser replacement)" readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.10" classifiers = [ "Programming Language :: Rust", "Programming Language :: Python :: 3", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37925e6..c460559 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -133,15 +133,54 @@ jobs: - name: Check formatting run: cargo +nightly fmt --all -- --check + # Python lint (ruff) + lint-python: + name: Lint Python (ruff) + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: + - uses: actions/checkout@v6 + + - name: Run ruff lint + uses: astral-sh/ruff-action@v3 + with: + src: crates/feedparser-rs-py + + - name: Run ruff format + uses: astral-sh/ruff-action@v3 + with: + src: crates/feedparser-rs-py + args: format --check + + # Node.js lint (biome) + lint-node: + name: Lint Node.js (biome) + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: + - uses: actions/checkout@v6 + + - name: Setup Biome + uses: biomejs/setup-biome@v2 + + - name: Run Biome + working-directory: crates/feedparser-rs-node + run: biome check . + # Security audit security: name: Security Audit needs: [changes] runs-on: ubuntu-latest timeout-minutes: 10 - # Run on: Rust core changes, CI config changes, or full CI mode + # Run on: Rust core changes, Node changes, CI config changes, or full CI mode if: | needs.changes.outputs.rust-core == 'true' || + needs.changes.outputs.node == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.full-ci == 'true' permissions: @@ -163,6 +202,15 @@ jobs: - name: Run cargo-deny run: cargo deny check + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '22' + + - name: Run npm audit + working-directory: crates/feedparser-rs-node + run: npm audit --audit-level=high + # Cross-platform Rust tests test-rust: name: Test Rust (${{ matrix.os }}) @@ -211,7 +259,7 @@ jobs: # Python bindings tests test-python: name: Test Python (${{ matrix.os }} - Py${{ matrix.python }}) - needs: [changes, lint-stable, lint-nightly] + needs: [changes, lint-stable, lint-nightly, lint-python] runs-on: ${{ matrix.os }} timeout-minutes: 20 # Run on: Python changes, CI config changes, or full CI mode @@ -225,21 +273,25 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python: ['3.9', '3.10', '3.11', '3.12', '3.13'] + python: ['3.10', '3.11', '3.12', '3.13', '3.14'] exclude: # Reduce matrix size - test all versions on Linux only - - os: macos-latest - python: '3.9' - os: macos-latest python: '3.10' - os: macos-latest python: '3.11' - - os: windows-latest - python: '3.9' + - os: macos-latest + python: '3.12' + - os: macos-latest + python: '3.13' - os: windows-latest python: '3.10' - os: windows-latest python: '3.11' + - os: windows-latest + python: '3.12' + - os: windows-latest + python: '3.13' steps: - uses: actions/checkout@v6 @@ -278,7 +330,7 @@ jobs: # Node.js bindings tests test-node: name: Test Node.js (${{ matrix.os }} - Node ${{ matrix.node }}) - needs: [changes, lint-stable, lint-nightly] + needs: [changes, lint-stable, lint-nightly, lint-node] runs-on: ${{ matrix.os }} timeout-minutes: 20 # Run on: Node changes, CI config changes, or full CI mode @@ -383,7 +435,7 @@ jobs: # Python code coverage coverage-python: name: Python Code Coverage - needs: [changes, lint-stable, lint-nightly] + needs: [changes, lint-stable, lint-nightly, lint-python] runs-on: ubuntu-latest timeout-minutes: 15 # Run on: Python changes, CI config changes, or full CI mode @@ -408,7 +460,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v6 with: - python-version: '3.12' + python-version: '3.14' - name: Install uv uses: astral-sh/setup-uv@v7 @@ -432,7 +484,7 @@ jobs: # Node.js code coverage coverage-node: name: Node.js Code Coverage - needs: [changes, lint-stable, lint-nightly] + needs: [changes, lint-stable, lint-nightly, lint-node] runs-on: ubuntu-latest timeout-minutes: 15 # Run on: Node changes, CI config changes, or full CI mode @@ -516,7 +568,7 @@ jobs: # All checks passed gate ci-success: name: CI Success - needs: [changes, lint-stable, lint-nightly, security, test-rust, test-python, test-node, coverage-rust, coverage-python, coverage-node, msrv] + needs: [changes, lint-stable, lint-nightly, lint-python, lint-node, security, test-rust, test-python, test-node, coverage-rust, coverage-python, coverage-node, msrv] runs-on: ubuntu-latest if: always() permissions: @@ -556,6 +608,20 @@ jobs: echo "lint-nightly: ${{ needs.lint-nightly.result }} ✓" fi + if [[ "${{ needs.lint-python.result }}" != "success" ]]; then + echo "lint-python: ${{ needs.lint-python.result }} ✗ (must succeed)" + FAILED=1 + else + echo "lint-python: ${{ needs.lint-python.result }} ✓" + fi + + if [[ "${{ needs.lint-node.result }}" != "success" ]]; then + echo "lint-node: ${{ needs.lint-node.result }} ✗ (must succeed)" + FAILED=1 + else + echo "lint-node: ${{ needs.lint-node.result }} ✓" + fi + # Check all other jobs (success or skipped is OK) check_job "security" "${{ needs.security.result }}" || FAILED=1 check_job "test-rust" "${{ needs.test-rust.result }}" || FAILED=1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 107e0f7..b61576f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,13 +67,10 @@ jobs: target: aarch64 steps: - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 - with: - python-version: '3.12' - uses: PyO3/maturin-action@v1 with: target: ${{ matrix.target }} - args: --release --out dist + args: --release --out dist -i python3.10 -i python3.11 -i python3.12 -i python3.13 -i python3.14 manylinux: auto working-directory: crates/feedparser-rs-py - uses: actions/upload-artifact@v6 @@ -94,11 +91,17 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: - python-version: '3.12' + python-version: | + 3.10 + 3.11 + 3.12 + 3.13 + 3.14 + allow-prereleases: true - uses: PyO3/maturin-action@v1 with: target: ${{ matrix.target }} - args: --release --out dist + args: --release --out dist --find-interpreter working-directory: crates/feedparser-rs-py - uses: actions/upload-artifact@v6 with: @@ -114,11 +117,17 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: - python-version: '3.12' + python-version: | + 3.10 + 3.11 + 3.12 + 3.13 + 3.14 + allow-prereleases: true - uses: PyO3/maturin-action@v1 with: target: x64 - args: --release --out dist + args: --release --out dist --find-interpreter working-directory: crates/feedparser-rs-py - uses: actions/upload-artifact@v6 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index bdc60ac..c4b27b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.4.3] - 2026-01-15 + +### Added +- Add `ruff` linter and formatter for Python bindings +- Add `biome` linter and formatter for Node.js bindings +- Add `npm audit` to CI security checks (alongside `cargo deny`) + +### Changed +- Drop Python 3.9 support (dependencies require 3.10+) +- Add Python 3.14 support +- Build wheels for all supported Python versions (3.10-3.14) + ## [0.4.2] - 2026-01-14 ### Fixed @@ -175,7 +187,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Comprehensive test coverage - Documentation with examples -[Unreleased]: https://github.com/bug-ops/feedparser-rs/compare/v0.4.2...HEAD +[Unreleased]: https://github.com/bug-ops/feedparser-rs/compare/v0.4.3...HEAD +[0.4.3]: https://github.com/bug-ops/feedparser-rs/compare/v0.4.2...v0.4.3 [0.4.2]: https://github.com/bug-ops/feedparser-rs/compare/v0.4.1...v0.4.2 [0.4.1]: https://github.com/bug-ops/feedparser-rs/compare/v0.4.0...v0.4.1 [0.4.0]: https://github.com/bug-ops/feedparser-rs/compare/v0.3.0...v0.4.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6a6662a..e23ee5b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,7 @@ This project follows the [Rust Code of Conduct](https://www.rust-lang.org/polici - Rust 1.88.0 or later (edition 2024) - [cargo-make](https://github.com/sagiegurari/cargo-make) for task automation - Node.js 18+ (for Node.js bindings development) -- Python 3.9+ (for Python bindings development) +- Python 3.10+ (for Python bindings development) ### Setup diff --git a/Cargo.lock b/Cargo.lock index 371fd59..bb94d99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,9 +126,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.15.2" +version = "1.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88aab2464f1f25453baa7a07c84c5b7684e274054ba06817f382357f77a288" +checksum = "e84ce723ab67259cfeb9877c6a639ee9eb7a27b28123abd71db7f0d5d0cc9d86" dependencies = [ "aws-lc-sys", "zeroize", @@ -136,9 +136,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.35.0" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45afffdee1e7c9126814751f88dddc747f41d91da16c9551a0f1e8a11e788a1" +checksum = "43a442ece363113bd4bd4c8b18977a7798dd4d3c3383f34fb61936960e8f4ad8" dependencies = [ "cc", "cmake", @@ -253,9 +253,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", "num-traits", @@ -600,7 +600,7 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "feedparser-rs" -version = "0.4.2" +version = "0.4.3" dependencies = [ "ammonia", "chrono", @@ -623,7 +623,7 @@ dependencies = [ [[package]] name = "feedparser-rs-node" -version = "0.4.2" +version = "0.4.3" dependencies = [ "feedparser-rs", "napi", @@ -633,7 +633,7 @@ dependencies = [ [[package]] name = "feedparser-rs-py" -version = "0.4.2" +version = "0.4.3" dependencies = [ "chrono", "feedparser-rs", diff --git a/Cargo.toml b/Cargo.toml index cfefec4..f0c0d93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.4.2" +version = "0.4.3" edition = "2024" rust-version = "1.88.0" authors = ["bug-ops"] @@ -26,13 +26,13 @@ encoding_rs = "0.8" flate2 = "1.1" html-escape = "0.2" memchr = "2.7" -mockito = "1.6" +mockito = "1.7" napi = "3.8" napi-derive = "3.5" -once_cell = "1.20" +once_cell = "1.21" pyo3 = "0.27" quick-xml = "0.39" -regex = "1.11" +regex = "1.12" reqwest = { version = "0.13", default-features = false } serde = "1.0" serde_json = "1.0" diff --git a/crates/feedparser-rs-node/__test__/index.spec.mjs b/crates/feedparser-rs-node/__test__/index.spec.mjs index 57134b2..7e72285 100644 --- a/crates/feedparser-rs-node/__test__/index.spec.mjs +++ b/crates/feedparser-rs-node/__test__/index.spec.mjs @@ -1,6 +1,6 @@ -import { describe, it } from 'node:test'; import assert from 'node:assert'; -import { parse, parseWithOptions, detectFormat } from '../index.js'; +import { describe, it } from 'node:test'; +import { detectFormat, parse, parseWithOptions } from '../index.js'; describe('feedparser-rs', () => { describe('parse()', () => { @@ -251,7 +251,8 @@ describe('feedparser-rs', () => { }); it('should detect RSS 1.0', () => { - const xml = ''; + const xml = + ''; assert.strictEqual(detectFormat(xml), 'rss10'); }); @@ -384,7 +385,7 @@ describe('feedparser-rs', () => { }); it('should handle binary garbage gracefully', () => { - const garbage = Buffer.from([0xFF, 0xFE, 0x00, 0x01, 0x02, 0x03]); + const garbage = Buffer.from([0xff, 0xfe, 0x00, 0x01, 0x02, 0x03]); // Parser should handle binary garbage without crashing try { const feed = parse(garbage); @@ -635,8 +636,8 @@ describe('feedparser-rs', () => { const feed = parse(xml); - const altLink = feed.feed.links.find(l => l.rel === 'alternate'); - const selfLink = feed.feed.links.find(l => l.rel === 'self'); + const altLink = feed.feed.links.find((l) => l.rel === 'alternate'); + const selfLink = feed.feed.links.find((l) => l.rel === 'self'); assert(altLink); assert.strictEqual(altLink.href, 'https://example.com'); @@ -730,7 +731,11 @@ describe('feedparser-rs', () => { assert.strictEqual(enclosure.url, 'https://example.com/file.mp3'); // length and type may be null - assert(enclosure.length === null || enclosure.length === undefined || typeof enclosure.length === 'number'); + assert( + enclosure.length === null || + enclosure.length === undefined || + typeof enclosure.length === 'number', + ); }); }); @@ -748,8 +753,8 @@ describe('feedparser-rs', () => { authors: [ { name: 'Author Name', - url: 'https://example.com/author' - } + url: 'https://example.com/author', + }, ], items: [ { @@ -766,11 +771,11 @@ describe('feedparser-rs', () => { { url: 'https://example.com/audio.mp3', mime_type: 'audio/mpeg', - size_in_bytes: 12345 - } - ] - } - ] + size_in_bytes: 12345, + }, + ], + }, + ], }); const feed = parse(json); @@ -792,7 +797,7 @@ describe('feedparser-rs', () => { const json = JSON.stringify({ version: 'https://jsonfeed.org/version/1', title: 'JSON Feed 1.0', - items: [] + items: [], }); const feed = parse(json); diff --git a/crates/feedparser-rs-node/__test__/phase3-fields.spec.mjs b/crates/feedparser-rs-node/__test__/phase3-fields.spec.mjs index e7fb9b5..d502cf8 100644 --- a/crates/feedparser-rs-node/__test__/phase3-fields.spec.mjs +++ b/crates/feedparser-rs-node/__test__/phase3-fields.spec.mjs @@ -1,5 +1,5 @@ -import { describe, it } from 'node:test'; import assert from 'node:assert'; +import { describe, it } from 'node:test'; import { parse } from '../index.js'; describe('Field Bindings', () => { @@ -183,7 +183,7 @@ describe('Field Bindings', () => { assert.ok(feed.entries[0].geo); assert.strictEqual(feed.entries[0].geo.geoType, 'point'); assert.strictEqual(feed.entries[0].geo.coordinates[0][0], 40.7128); - assert.strictEqual(feed.entries[0].geo.coordinates[0][1], -74.0060); + assert.strictEqual(feed.entries[0].geo.coordinates[0][1], -74.006); }); it('should parse GeoRSS polygon in entry', () => { diff --git a/crates/feedparser-rs-node/__test__/syndication.spec.mjs b/crates/feedparser-rs-node/__test__/syndication.spec.mjs index 3661da7..bba359f 100644 --- a/crates/feedparser-rs-node/__test__/syndication.spec.mjs +++ b/crates/feedparser-rs-node/__test__/syndication.spec.mjs @@ -1,5 +1,5 @@ -import { describe, it } from 'node:test'; import assert from 'node:assert'; +import { describe, it } from 'node:test'; import { parse } from '../index.js'; describe('syndication', () => { diff --git a/crates/feedparser-rs-node/biome.json b/crates/feedparser-rs-node/biome.json new file mode 100644 index 0000000..57ec103 --- /dev/null +++ b/crates/feedparser-rs-node/biome.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100 + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "semicolons": "always" + } + }, + "files": { + "includes": ["__test__/**/*.mjs", "biome.json", "package.json"] + }, + "assist": { + "actions": { + "source": { + "organizeImports": { + "level": "on" + } + } + } + } +} diff --git a/crates/feedparser-rs-node/package-lock.json b/crates/feedparser-rs-node/package-lock.json index 4d0367c..2b3ecda 100644 --- a/crates/feedparser-rs-node/package-lock.json +++ b/crates/feedparser-rs-node/package-lock.json @@ -1,14 +1,15 @@ { "name": "feedparser-rs", - "version": "0.4.0", + "version": "0.4.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "feedparser-rs", - "version": "0.4.0", + "version": "0.4.3", "license": "MIT OR Apache-2.0", "devDependencies": { + "@biomejs/biome": "^2.3", "@napi-rs/cli": "^3.5", "c8": "^10.1.3" }, @@ -26,6 +27,169 @@ "node": ">=18" } }, + "node_modules/@biomejs/biome": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.11.tgz", + "integrity": "sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.3.11", + "@biomejs/cli-darwin-x64": "2.3.11", + "@biomejs/cli-linux-arm64": "2.3.11", + "@biomejs/cli-linux-arm64-musl": "2.3.11", + "@biomejs/cli-linux-x64": "2.3.11", + "@biomejs/cli-linux-x64-musl": "2.3.11", + "@biomejs/cli-win32-arm64": "2.3.11", + "@biomejs/cli-win32-x64": "2.3.11" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.11.tgz", + "integrity": "sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.11.tgz", + "integrity": "sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.11.tgz", + "integrity": "sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.11.tgz", + "integrity": "sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.11.tgz", + "integrity": "sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.11.tgz", + "integrity": "sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.11.tgz", + "integrity": "sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.11.tgz", + "integrity": "sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@emnapi/core": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", diff --git a/crates/feedparser-rs-node/package.json b/crates/feedparser-rs-node/package.json index 13e8345..f6dd0ea 100644 --- a/crates/feedparser-rs-node/package.json +++ b/crates/feedparser-rs-node/package.json @@ -1,6 +1,6 @@ { "name": "feedparser-rs", - "version": "0.4.2", + "version": "0.4.3", "description": "High-performance RSS/Atom/JSON Feed parser for Node.js", "main": "index.js", "types": "index.d.ts", @@ -41,9 +41,13 @@ "build:debug": "napi build --platform", "test": "node --test __test__/index.spec.mjs", "test:coverage": "c8 --reporter=lcov --reporter=text --reports-dir=coverage node --test __test__/index.spec.mjs", + "lint": "biome check .", + "lint:fix": "biome check --write .", + "format": "biome format --write .", "prepublishOnly": "napi prepublish -t npm" }, "devDependencies": { + "@biomejs/biome": "^2.3", "@napi-rs/cli": "^3.5", "c8": "^10.1.3" } diff --git a/crates/feedparser-rs-node/pnpm-lock.yaml b/crates/feedparser-rs-node/pnpm-lock.yaml index cf5c446..dded465 100644 --- a/crates/feedparser-rs-node/pnpm-lock.yaml +++ b/crates/feedparser-rs-node/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + '@biomejs/biome': + specifier: ^2.3 + version: 2.3.11 '@napi-rs/cli': specifier: ^3.5 version: 3.5.1(@emnapi/runtime@1.8.1) @@ -21,6 +24,59 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@biomejs/biome@2.3.11': + resolution: {integrity: sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.3.11': + resolution: {integrity: sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.3.11': + resolution: {integrity: sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.3.11': + resolution: {integrity: sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@2.3.11': + resolution: {integrity: sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@2.3.11': + resolution: {integrity: sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@2.3.11': + resolution: {integrity: sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@2.3.11': + resolution: {integrity: sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.3.11': + resolution: {integrity: sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + '@emnapi/core@1.8.1': resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} @@ -898,6 +954,41 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@biomejs/biome@2.3.11': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.3.11 + '@biomejs/cli-darwin-x64': 2.3.11 + '@biomejs/cli-linux-arm64': 2.3.11 + '@biomejs/cli-linux-arm64-musl': 2.3.11 + '@biomejs/cli-linux-x64': 2.3.11 + '@biomejs/cli-linux-x64-musl': 2.3.11 + '@biomejs/cli-win32-arm64': 2.3.11 + '@biomejs/cli-win32-x64': 2.3.11 + + '@biomejs/cli-darwin-arm64@2.3.11': + optional: true + + '@biomejs/cli-darwin-x64@2.3.11': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.3.11': + optional: true + + '@biomejs/cli-linux-arm64@2.3.11': + optional: true + + '@biomejs/cli-linux-x64-musl@2.3.11': + optional: true + + '@biomejs/cli-linux-x64@2.3.11': + optional: true + + '@biomejs/cli-win32-arm64@2.3.11': + optional: true + + '@biomejs/cli-win32-x64@2.3.11': + optional: true + '@emnapi/core@1.8.1': dependencies: '@emnapi/wasi-threads': 1.1.0 diff --git a/crates/feedparser-rs-py/README.md b/crates/feedparser-rs-py/README.md index d0383f0..cd901ab 100644 --- a/crates/feedparser-rs-py/README.md +++ b/crates/feedparser-rs-py/README.md @@ -24,7 +24,7 @@ pip install feedparser-rs ``` > [!IMPORTANT] -> Requires Python 3.9 or later. +> Requires Python 3.10 or later. ## Usage @@ -208,7 +208,7 @@ Pre-built wheels available for: | Linux | x64, arm64 | | Windows | x64 | -Supported Python versions: 3.9, 3.10, 3.11, 3.12, 3.13 +Supported Python versions: 3.10, 3.11, 3.12, 3.13, 3.14 ## Development diff --git a/crates/feedparser-rs-py/pyproject.toml b/crates/feedparser-rs-py/pyproject.toml index b259ff7..7e31e1b 100644 --- a/crates/feedparser-rs-py/pyproject.toml +++ b/crates/feedparser-rs-py/pyproject.toml @@ -4,11 +4,11 @@ build-backend = "maturin" [project] name = "feedparser-rs" -version = "0.4.2" +version = "0.4.3" description = "High-performance RSS/Atom/JSON Feed parser with feedparser-compatible API" readme = "README.md" license = { text = "MIT OR Apache-2.0" } -requires-python = ">=3.9" +requires-python = ">=3.10" keywords = ["rss", "atom", "feed", "parser", "feedparser", "rust"] classifiers = [ "Development Status :: 4 - Beta", @@ -16,7 +16,6 @@ classifiers = [ "License :: OSI Approved :: MIT License", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -38,4 +37,28 @@ module-name = "feedparser_rs._feedparser_rs" [dependency-groups] dev = [ "pytest>=9.0,<10", + "ruff>=0.9,<1", ] + +[tool.ruff] +target-version = "py310" +line-length = 100 + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "UP", # pyupgrade + "B", # flake8-bugbear + "SIM", # flake8-simplify + "RUF", # ruff-specific +] +ignore = [ + "E501", # line too long (handled by formatter) +] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" diff --git a/crates/feedparser-rs-py/python/feedparser_rs/__init__.py b/crates/feedparser-rs-py/python/feedparser_rs/__init__.py index 405864f..0f713a1 100644 --- a/crates/feedparser-rs-py/python/feedparser_rs/__init__.py +++ b/crates/feedparser-rs-py/python/feedparser_rs/__init__.py @@ -25,14 +25,14 @@ ) __all__ = [ + "FeedParserDict", + "ParserLimits", + "__version__", + "detect_format", "parse", "parse_url", "parse_url_with_limits", "parse_with_limits", - "detect_format", - "FeedParserDict", - "ParserLimits", - "__version__", ] # Type alias for better IDE support diff --git a/crates/feedparser-rs-py/tests/test_basic.py b/crates/feedparser-rs-py/tests/test_basic.py index bb7ba11..3626236 100644 --- a/crates/feedparser-rs-py/tests/test_basic.py +++ b/crates/feedparser-rs-py/tests/test_basic.py @@ -214,7 +214,7 @@ def test_podcast_itunes_metadata(): # Feed-level iTunes metadata assert d.feed.itunes is not None assert d.feed.itunes.author == "John Doe" - assert d.feed.itunes.explicit == False + assert d.feed.itunes.explicit is False # Entry-level iTunes metadata assert d.entries[0].itunes is not None diff --git a/crates/feedparser-rs-py/tests/test_bindings.py b/crates/feedparser-rs-py/tests/test_bindings.py index a167c61..479be71 100644 --- a/crates/feedparser-rs-py/tests/test_bindings.py +++ b/crates/feedparser-rs-py/tests/test_bindings.py @@ -348,7 +348,7 @@ def test_podcast_soundbite(): """ result = feedparser_rs.parse(xml) - entry = result.entries[0] + _ = result.entries[0] # Note: entry.podcast may not be populated until parser implements it # For now just verify the direct soundbite access works if implemented diff --git a/crates/feedparser-rs-py/tests/test_compat.py b/crates/feedparser-rs-py/tests/test_compat.py index 7f4bf99..f6d1319 100644 --- a/crates/feedparser-rs-py/tests/test_compat.py +++ b/crates/feedparser-rs-py/tests/test_compat.py @@ -7,8 +7,8 @@ - Container-level: channel, items """ -import pytest import feedparser_rs +import pytest def test_feed_description_alias(): @@ -389,13 +389,13 @@ def test_dict_access_feed_fields(): feed = feedparser_rs.parse(xml) # Dict-style access should work - assert feed['feed']['title'] == "Test Feed" - assert feed['feed']['link'] == "https://example.com" - assert feed['feed']['subtitle'] == "Feed description" + assert feed["feed"]["title"] == "Test Feed" + assert feed["feed"]["link"] == "https://example.com" + assert feed["feed"]["subtitle"] == "Feed description" # Mixed access should work - assert feed['feed'].title == "Test Feed" - assert feed.feed['title'] == "Test Feed" + assert feed["feed"].title == "Test Feed" + assert feed.feed["title"] == "Test Feed" def test_dict_access_entry_fields(): @@ -412,17 +412,17 @@ def test_dict_access_entry_fields(): """ feed = feedparser_rs.parse(xml) - entry = feed['entries'][0] + entry = feed["entries"][0] # Dict-style access should work - assert entry['title'] == "Entry Title" - assert entry['link'] == "https://example.com/entry" - assert entry['id'] == "entry-1" - assert entry['summary'] == "Entry summary" + assert entry["title"] == "Entry Title" + assert entry["link"] == "https://example.com/entry" + assert entry["id"] == "entry-1" + assert entry["summary"] == "Entry summary" # Mixed access should work - assert feed['entries'][0].title == "Entry Title" - assert feed.entries[0]['title'] == "Entry Title" + assert feed["entries"][0].title == "Entry Title" + assert feed.entries[0]["title"] == "Entry Title" def test_dict_access_with_deprecated_aliases(): @@ -443,16 +443,16 @@ def test_dict_access_with_deprecated_aliases(): feed = feedparser_rs.parse(xml) # Feed-level deprecated aliases should work with dict access - assert feed['feed']['description'] == "Feed description" - assert feed['feed']['tagline'] == "Feed description" - assert feed['feed']['copyright'] == "Copyright 2024" - assert feed['feed']['modified'] is not None + assert feed["feed"]["description"] == "Feed description" + assert feed["feed"]["tagline"] == "Feed description" + assert feed["feed"]["copyright"] == "Copyright 2024" + assert feed["feed"]["modified"] is not None # Entry-level deprecated aliases should work with dict access - entry = feed['entries'][0] - assert entry['guid'] == "entry-1" - assert entry['description'] == "Entry summary" - assert entry['issued'] is not None + entry = feed["entries"][0] + assert entry["guid"] == "entry-1" + assert entry["description"] == "Entry summary" + assert entry["issued"] is not None def test_dict_access_container_aliases(): @@ -468,14 +468,14 @@ def test_dict_access_container_aliases(): d = feedparser_rs.parse(xml) # channel → feed alias should work with dict access - assert d['channel']['title'] == "RSS Feed" - assert d['feed']['title'] == "RSS Feed" + assert d["channel"]["title"] == "RSS Feed" + assert d["feed"]["title"] == "RSS Feed" # items → entries alias should work with dict access - assert len(d['items']) == 2 - assert len(d['entries']) == 2 - assert d['items'][0]['title'] == "Item 1" - assert d['entries'][0]['title'] == "Item 1" + assert len(d["items"]) == 2 + assert len(d["entries"]) == 2 + assert d["items"][0]["title"] == "Item 1" + assert d["entries"][0]["title"] == "Item 1" def test_dict_access_top_level_fields(): @@ -489,9 +489,9 @@ def test_dict_access_top_level_fields(): feed = feedparser_rs.parse(xml) # Top-level fields should be accessible via dict-style - assert feed['version'] == 'rss20' - assert feed['bozo'] is False - assert feed['encoding'] is not None + assert feed["version"] == "rss20" + assert feed["bozo"] is False + assert feed["encoding"] is not None def test_dict_access_unknown_key_raises_keyerror(): @@ -509,13 +509,13 @@ def test_dict_access_unknown_key_raises_keyerror(): # Unknown keys should raise KeyError for dict access with pytest.raises(KeyError): - _ = feed['nonexistent_field'] + _ = feed["nonexistent_field"] with pytest.raises(KeyError): - _ = feed['feed']['fake_field'] + _ = feed["feed"]["fake_field"] with pytest.raises(KeyError): - _ = feed['entries'][0]['unknown_key'] + _ = feed["entries"][0]["unknown_key"] # But AttributeError should still be raised for attribute access with pytest.raises(AttributeError, match="has no attribute"): @@ -541,22 +541,22 @@ def test_dict_and_attribute_access_equivalence(): feed = feedparser_rs.parse(xml) # Feed-level fields should be identical via both access methods - assert feed.feed.title == feed['feed']['title'] - assert feed.feed.subtitle == feed['feed']['subtitle'] - assert feed.feed.link == feed['feed']['link'] - assert feed.feed.updated == feed['feed']['updated'] + assert feed.feed.title == feed["feed"]["title"] + assert feed.feed.subtitle == feed["feed"]["subtitle"] + assert feed.feed.link == feed["feed"]["link"] + assert feed.feed.updated == feed["feed"]["updated"] # Entry-level fields should be identical via both access methods entry = feed.entries[0] - assert entry.id == entry['id'] - assert entry.title == entry['title'] - assert entry.summary == entry['summary'] - assert entry.link == entry['link'] - assert entry.updated == entry['updated'] + assert entry.id == entry["id"] + assert entry.title == entry["title"] + assert entry.summary == entry["summary"] + assert entry.link == entry["link"] + assert entry.updated == entry["updated"] # Top-level fields should be identical - assert feed.version == feed['version'] - assert feed.bozo == feed['bozo'] + assert feed.version == feed["version"] + assert feed.bozo == feed["bozo"] def test_dict_access_with_none_values(): @@ -570,10 +570,10 @@ def test_dict_access_with_none_values(): feed = feedparser_rs.parse(xml) # Missing optional fields should return None via dict access - assert feed['feed']['subtitle'] is None - assert feed['feed']['updated'] is None - assert feed['feed']['author'] is None - assert feed['feed']['image'] is None + assert feed["feed"]["subtitle"] is None + assert feed["feed"]["updated"] is None + assert feed["feed"]["author"] is None + assert feed["feed"]["image"] is None def test_dict_access_detail_fields(): @@ -589,16 +589,16 @@ def test_dict_access_detail_fields(): feed = feedparser_rs.parse(xml) # _detail fields should work with dict access - assert feed['feed']['subtitle_detail'] is not None - assert feed['feed']['subtitle_detail'].type == 'html' + assert feed["feed"]["subtitle_detail"] is not None + assert feed["feed"]["subtitle_detail"].type == "html" - assert feed['feed']['rights_detail'] is not None - assert feed['feed']['copyright_detail'] is not None - assert feed['feed']['copyright_detail'].type == 'text' + assert feed["feed"]["rights_detail"] is not None + assert feed["feed"]["copyright_detail"] is not None + assert feed["feed"]["copyright_detail"].type == "text" - entry = feed['entries'][0] - assert entry['summary_detail'] is not None - assert entry['description_detail'] is not None + entry = feed["entries"][0] + assert entry["summary_detail"] is not None + assert entry["description_detail"] is not None def test_dict_access_list_fields(): @@ -617,16 +617,16 @@ def test_dict_access_list_fields(): feed = feedparser_rs.parse(xml) # List fields should work with dict access - assert len(feed['feed']['links']) == 2 - assert feed['feed']['links'][0].href == "https://example.com/feed" + assert len(feed["feed"]["links"]) == 2 + assert feed["feed"]["links"][0].href == "https://example.com/feed" - assert len(feed['feed']['tags']) == 2 - assert feed['feed']['tags'][0].term == "technology" + assert len(feed["feed"]["tags"]) == 2 + assert feed["feed"]["tags"][0].term == "technology" - entry = feed['entries'][0] - assert len(entry['links']) >= 1 - assert len(entry['tags']) == 1 - assert entry['tags'][0].term == "rust" + entry = feed["entries"][0] + assert len(entry["links"]) >= 1 + assert len(entry["tags"]) == 1 + assert entry["tags"][0].term == "rust" # ============================================================================= @@ -646,7 +646,7 @@ def test_parse_with_optional_http_params(): # Should work with optional params (they're just ignored for content) feed = feedparser_rs.parse(xml, etag="some-etag", modified="some-date") assert feed.feed.title == "Test Feed" - assert feed.version == 'rss20' + assert feed.version == "rss20" def test_parse_with_user_agent_param(): @@ -730,10 +730,6 @@ def test_parse_with_limits_accepts_http_params(): # Should work with all optional params feed = feedparser_rs.parse_with_limits( - xml, - etag="etag", - modified="modified", - user_agent="TestBot/1.0", - limits=limits + xml, etag="etag", modified="modified", user_agent="TestBot/1.0", limits=limits ) assert feed.feed.title == "Test Feed" diff --git a/crates/feedparser-rs-py/uv.lock b/crates/feedparser-rs-py/uv.lock index b311d84..07d30d1 100644 --- a/crates/feedparser-rs-py/uv.lock +++ b/crates/feedparser-rs-py/uv.lock @@ -1,10 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.9" -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version < '3.10'", -] +requires-python = ">=3.10" [[package]] name = "colorama" @@ -29,38 +25,27 @@ wheels = [ [[package]] name = "feedparser-rs" -version = "0.4.2" +version = "0.4.3" source = { editable = "." } [package.dev-dependencies] dev = [ { name = "pytest" }, + { name = "ruff" }, ] [package.metadata] [package.metadata.requires-dev] -dev = [{ name = "pytest", specifier = "<9" }] - -[[package]] -name = "iniconfig" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +dev = [ + { name = "pytest", specifier = ">=9.0,<10" }, + { name = "ruff", specifier = ">=0.9,<1" }, ] [[package]] name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", -] sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, @@ -95,21 +80,46 @@ wheels = [ [[package]] name = "pytest" -version = "8.4.2" +version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/77/9a7fe084d268f8855d493e5031ea03fa0af8cc05887f638bf1c4e3363eb8/ruff-0.14.11.tar.gz", hash = "sha256:f6dc463bfa5c07a59b1ff2c3b9767373e541346ea105503b4c0369c520a66958", size = 5993417, upload-time = "2026-01-08T19:11:58.322Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a6/a4c40a5aaa7e331f245d2dc1ac8ece306681f52b636b40ef87c88b9f7afd/ruff-0.14.11-py3-none-linux_armv6l.whl", hash = "sha256:f6ff2d95cbd335841a7217bdfd9c1d2e44eac2c584197ab1385579d55ff8830e", size = 12951208, upload-time = "2026-01-08T19:12:09.218Z" }, + { url = "https://files.pythonhosted.org/packages/5c/5c/360a35cb7204b328b685d3129c08aca24765ff92b5a7efedbdd6c150d555/ruff-0.14.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f6eb5c1c8033680f4172ea9c8d3706c156223010b8b97b05e82c59bdc774ee6", size = 13330075, upload-time = "2026-01-08T19:12:02.549Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9e/0cc2f1be7a7d33cae541824cf3f95b4ff40d03557b575912b5b70273c9ec/ruff-0.14.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2fc34cc896f90080fca01259f96c566f74069a04b25b6205d55379d12a6855e", size = 12257809, upload-time = "2026-01-08T19:12:00.366Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e5/5faab97c15bb75228d9f74637e775d26ac703cc2b4898564c01ab3637c02/ruff-0.14.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53386375001773ae812b43205d6064dae49ff0968774e6befe16a994fc233caa", size = 12678447, upload-time = "2026-01-08T19:12:13.899Z" }, + { url = "https://files.pythonhosted.org/packages/1b/33/e9767f60a2bef779fb5855cab0af76c488e0ce90f7bb7b8a45c8a2ba4178/ruff-0.14.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a697737dce1ca97a0a55b5ff0434ee7205943d4874d638fe3ae66166ff46edbe", size = 12758560, upload-time = "2026-01-08T19:11:42.55Z" }, + { url = "https://files.pythonhosted.org/packages/eb/84/4c6cf627a21462bb5102f7be2a320b084228ff26e105510cd2255ea868e5/ruff-0.14.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6845ca1da8ab81ab1dce755a32ad13f1db72e7fba27c486d5d90d65e04d17b8f", size = 13599296, upload-time = "2026-01-08T19:11:30.371Z" }, + { url = "https://files.pythonhosted.org/packages/88/e1/92b5ed7ea66d849f6157e695dc23d5d6d982bd6aa8d077895652c38a7cae/ruff-0.14.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e36ce2fd31b54065ec6f76cb08d60159e1b32bdf08507862e32f47e6dde8bcbf", size = 15048981, upload-time = "2026-01-08T19:12:04.742Z" }, + { url = "https://files.pythonhosted.org/packages/61/df/c1bd30992615ac17c2fb64b8a7376ca22c04a70555b5d05b8f717163cf9f/ruff-0.14.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:590bcc0e2097ecf74e62a5c10a6b71f008ad82eb97b0a0079e85defe19fe74d9", size = 14633183, upload-time = "2026-01-08T19:11:40.069Z" }, + { url = "https://files.pythonhosted.org/packages/04/e9/fe552902f25013dd28a5428a42347d9ad20c4b534834a325a28305747d64/ruff-0.14.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53fe71125fc158210d57fe4da26e622c9c294022988d08d9347ec1cf782adafe", size = 14050453, upload-time = "2026-01-08T19:11:37.555Z" }, + { url = "https://files.pythonhosted.org/packages/ae/93/f36d89fa021543187f98991609ce6e47e24f35f008dfe1af01379d248a41/ruff-0.14.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a35c9da08562f1598ded8470fcfef2afb5cf881996e6c0a502ceb61f4bc9c8a3", size = 13757889, upload-time = "2026-01-08T19:12:07.094Z" }, + { url = "https://files.pythonhosted.org/packages/b7/9f/c7fb6ecf554f28709a6a1f2a7f74750d400979e8cd47ed29feeaa1bd4db8/ruff-0.14.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0f3727189a52179393ecf92ec7057c2210203e6af2676f08d92140d3e1ee72c1", size = 13955832, upload-time = "2026-01-08T19:11:55.064Z" }, + { url = "https://files.pythonhosted.org/packages/db/a0/153315310f250f76900a98278cf878c64dfb6d044e184491dd3289796734/ruff-0.14.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:eb09f849bd37147a789b85995ff734a6c4a095bed5fd1608c4f56afc3634cde2", size = 12586522, upload-time = "2026-01-08T19:11:35.356Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2b/a73a2b6e6d2df1d74bf2b78098be1572191e54bec0e59e29382d13c3adc5/ruff-0.14.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c61782543c1231bf71041461c1f28c64b961d457d0f238ac388e2ab173d7ecb7", size = 12724637, upload-time = "2026-01-08T19:11:47.796Z" }, + { url = "https://files.pythonhosted.org/packages/f0/41/09100590320394401cd3c48fc718a8ba71c7ddb1ffd07e0ad6576b3a3df2/ruff-0.14.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82ff352ea68fb6766140381748e1f67f83c39860b6446966cff48a315c3e2491", size = 13145837, upload-time = "2026-01-08T19:11:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d8/e035db859d1d3edf909381eb8ff3e89a672d6572e9454093538fe6f164b0/ruff-0.14.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:728e56879df4ca5b62a9dde2dd0eb0edda2a55160c0ea28c4025f18c03f86984", size = 13850469, upload-time = "2026-01-08T19:12:11.694Z" }, + { url = "https://files.pythonhosted.org/packages/4e/02/bb3ff8b6e6d02ce9e3740f4c17dfbbfb55f34c789c139e9cd91985f356c7/ruff-0.14.11-py3-none-win32.whl", hash = "sha256:337c5dd11f16ee52ae217757d9b82a26400be7efac883e9e852646f1557ed841", size = 12851094, upload-time = "2026-01-08T19:11:45.163Z" }, + { url = "https://files.pythonhosted.org/packages/58/f1/90ddc533918d3a2ad628bc3044cdfc094949e6d4b929220c3f0eb8a1c998/ruff-0.14.11-py3-none-win_amd64.whl", hash = "sha256:f981cea63d08456b2c070e64b79cb62f951aa1305282974d4d5216e6e0178ae6", size = 14001379, upload-time = "2026-01-08T19:11:52.591Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1c/1dbe51782c0e1e9cfce1d1004752672d2d4629ea46945d19d731ad772b3b/ruff-0.14.11-py3-none-win_arm64.whl", hash = "sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0", size = 12938644, upload-time = "2026-01-08T19:11:50.027Z" }, ] [[package]]