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]]