Skip to content

Add pyuppsala as an alternative lxml-compatible etree backend#529

Open
cleder wants to merge 1 commit into
developfrom
527-pyuppsala-080
Open

Add pyuppsala as an alternative lxml-compatible etree backend#529
cleder wants to merge 1 commit into
developfrom
527-pyuppsala-080

Conversation

@cleder

@cleder cleder commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Extends fastkml's swappable etree backend (stdlib / lxml) to also support pyuppsala>=0.8.0, a Rust-based lxml-compatible XML library. Fixes a handful of places that special-cased lxml specifically instead of "any non-stdlib backend" (default-namespace element construction, recover-mode parsing fallback), and makes schema validation backend-agnostic:

  • get_schema_parser() now builds XMLSchema(file=schema) instead of from an already-parsed tree, so relative xsd:import/xsd:include schemaLocations resolve correctly, and its cache is keyed on the active etree module too (fixes a latent bug where switching backends mid-process could return a schema object built for the wrong backend).
  • validate() works uniformly via a new assert_valid() helper, since pyuppsala's XMLSchema has no assert_() alias.
  • handle_validation_error() tolerates error-log entries without a .path attribute (pyuppsala's ValidationError doesn't have one).
  • parse_and_validate (renamed from lxml_parse_and_validate) falls back gracefully when a backend doesn't support recover-mode parsing.

Mirrors lxml test coverage for pyuppsala across the whole test suite, including hypothesis property-based tests (via a PyuppsalaNoSchemaValidation mixin that stubs out schema validation, working around a real pyuppsala 0.8 bug where xs:double range validation rejects in-range scientific-notation values), plus CI, packaging, and doc updates.

Summary by Sourcery

Add pyuppsala as an optional lxml-compatible etree backend for fastkml and make validation, parsing, and namespace handling backend-agnostic.

New Features:

  • Introduce pyuppsala as a third etree implementation alongside stdlib and lxml, configurable via fastkml.config.
  • Add a backend-neutral parse_and_validate helper that supports rich etree backends without depending on lxml-specific behavior.

Bug Fixes:

  • Ensure schema parsers are built from schema file paths and cached per active etree backend so relative imports resolve correctly and backend switches are safe.
  • Make validation error handling robust to backends whose error-log entries lack a path attribute.
  • Provide a backend-independent assert_valid wrapper so schema validation consistently raises AssertionError across different etree implementations.

Enhancements:

  • Treat any non-stdlib etree implementation as supporting default-namespace nsmap construction instead of special-casing lxml.
  • Adjust recover-mode parsing to gracefully fall back when the active backend does not support recover=True.
  • Extend validator and configuration tests to cover pyuppsala behavior, including differences in XML and XSD error reporting.
  • Stub out XSD schema validation in hypothesis-based tests when running under pyuppsala to work around its current xs:double range-validation bug.
  • Augment test base mixins to support switching to pyuppsala and to patch schema parsing for specific test variants.
  • Document pyuppsala integration details and compatibility notes in a new markdown report.

Build:

  • Add a pyuppsala optional dependency extra in pyproject.toml and include it in the dev extra for local development.

CI:

  • Introduce a dedicated GitHub Actions job to run the full test suite against pyuppsala on multiple CPython versions and require it for release and publish workflows.

Tests:

  • Mirror lxml-backed tests for pyuppsala across unit and property-based suites, including geometry, styles, features, overlays, views, links, containers, atom, times, gx data/track, and network link control.
  • Add pyuppsala-specific tests for KML default-namespace serialization, strict/relaxed parsing behavior without recover-mode, and schema validation semantics.
  • Update config tests to cover registering namespaces and default namespace setup when using pyuppsala.
  • Add hypothesis health-check suppression for reused hypothesis-wrapped test functions across Lxml and Pyuppsala test classes.

Summary by CodeRabbit

  • New Features

    • Added support for an additional optional XML backend, expanding installation and configuration choices.
    • Broadened test coverage to run more KML, geometry, style, and related features with the new backend.
  • Bug Fixes

    • Improved parsing and validation behavior across backends.
    • Fixed namespace and serialization handling in edge cases, including round-trips and undeclared prefixes.
  • Documentation

    • Updated setup and configuration docs to show how to use the new optional backend.

Extends fastkml's swappable etree backend (stdlib / lxml) to also support
pyuppsala>=0.8.0, a Rust-based lxml-compatible XML library. Fixes a handful
of places that special-cased lxml specifically instead of "any non-stdlib
backend" (default-namespace element construction, recover-mode parsing
fallback), and makes schema validation backend-agnostic:

- get_schema_parser() now builds XMLSchema(file=schema) instead of from an
  already-parsed tree, so relative xsd:import/xsd:include schemaLocations
  resolve correctly, and its cache is keyed on the active etree module too
  (fixes a latent bug where switching backends mid-process could return a
  schema object built for the wrong backend).
- validate() works uniformly via a new assert_valid() helper, since
  pyuppsala's XMLSchema has no assert_() alias.
- handle_validation_error() tolerates error-log entries without a `.path`
  attribute (pyuppsala's ValidationError doesn't have one).
- parse_and_validate (renamed from lxml_parse_and_validate) falls back
  gracefully when a backend doesn't support recover-mode parsing.

Mirrors lxml test coverage for pyuppsala across the whole test suite,
including hypothesis property-based tests (via a PyuppsalaNoSchemaValidation
mixin that stubs out schema validation, working around a real pyuppsala 0.8
bug where xs:double range validation rejects in-range scientific-notation
values), plus CI, packaging, and doc updates.

Detailed pyuppsala compatibility findings (good/bad/ugly) are written up in
pyuppsala-integration-notes.md for upstream feedback.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 5, 2026 20:15
@cleder cleder linked an issue Jul 5, 2026 that may be closed by this pull request
@semanticdiff-com

semanticdiff-com Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review changes with  SemanticDiff

Changed Files
File Status
  fastkml/kml.py  27% smaller
  fastkml/validator.py  12% smaller
  .github/workflows/run-all-tests.yml  1% smaller
  README.rst Unsupported file format
  docs/configuration.rst Unsupported file format
  pyproject.toml Unsupported file format
  pyuppsala-integration-notes.md Unsupported file format
  tests/atom_test.py  0% smaller
  tests/base.py  0% smaller
  tests/base_test.py  0% smaller
  tests/config_test.py  0% smaller
  tests/containers_test.py  0% smaller
  tests/data_test.py  0% smaller
  tests/features_test.py  0% smaller
  tests/geometries/boundaries_test.py  0% smaller
  tests/geometries/coordinates_test.py  0% smaller
  tests/geometries/geometry_test.py  0% smaller
  tests/geometries/linearring_test.py  0% smaller
  tests/geometries/linestring_test.py  0% smaller
  tests/geometries/multigeometry_test.py  0% smaller
  tests/geometries/point_test.py  0% smaller
  tests/geometries/polygon_test.py  0% smaller
  tests/gx/data_test.py  0% smaller
  tests/gx/track_test.py  0% smaller
  tests/hypothesis/atom_test.py  0% smaller
  tests/hypothesis/container_test.py  0% smaller
  tests/hypothesis/data_test.py  0% smaller
  tests/hypothesis/feature_test.py  0% smaller
  tests/hypothesis/geometry_test.py  0% smaller
  tests/hypothesis/gx/data_test.py  0% smaller
  tests/hypothesis/gx/track_test.py  0% smaller
  tests/hypothesis/kml_test.py  0% smaller
  tests/hypothesis/links_test.py  0% smaller
  tests/hypothesis/model_test.py  0% smaller
  tests/hypothesis/multi_geometry_test.py  0% smaller
  tests/hypothesis/network_link_control_test.py  0% smaller
  tests/hypothesis/overlay_test.py  0% smaller
  tests/hypothesis/style_test.py  0% smaller
  tests/hypothesis/times_test.py  0% smaller
  tests/hypothesis/views_test.py  0% smaller
  tests/kml_test.py  0% smaller
  tests/links_test.py  0% smaller
  tests/model_test.py  0% smaller
  tests/network_link_control_test.py  0% smaller
  tests/overlays_test.py  0% smaller
  tests/repr_eq_test.py  0% smaller
  tests/styles_test.py  0% smaller
  tests/times_test.py  0% smaller
  tests/utils_test.py  0% smaller
  tests/validator_test.py  0% smaller
  tests/views_test.py  0% smaller

@sourcery-ai

sourcery-ai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds pyuppsala>=0.8.0 as a third, lxml-compatible etree backend alongside stdlib and lxml, makes KML parsing/validation and default-namespace handling backend-agnostic, and mirrors lxml’s test/CI coverage for pyuppsala, including workarounds for current pyuppsala XSD validation limitations.

Sequence diagram for backend-agnostic KML parsing and validation

sequenceDiagram
    actor User
    participant KML as KML
    participant config as config
    participant etree as config.etree
    participant validator as validator

    User->>KML: parse(file, strict, validate)
    KML->>KML: parse_and_validate(file, strict, validate)
    KML->>etree: XMLParser(huge_tree=True, recover=True)
    alt [backend supports recover]
        etree-->>KML: parser
    else [backend raises NotImplementedError]
        etree-->>KML: NotImplementedError
        KML->>etree: XMLParser(huge_tree=True)
        etree-->>KML: parser
    end
    KML->>etree: parse(file, parser)
    etree-->>KML: tree
    KML->>KML: getroot()
    KML-->>User: root (if validate is False)

    opt [validate is True]
        KML->>validator: validate(element=root, schema=None)
        validator->>validator: get_schema_parser(schema=None)
        validator->>validator: _build_schema_parser(config.etree, schema_path)
        validator-->>validator: schema_parser
        validator->>validator: assert_valid(schema_parser, root)
        alt [validation passes]
            validator-->>User: OK
        else [validation fails]
            validator->>validator: handle_validation_error(schema_parser, root)
            validator-->>User: AssertionError
        end
    end
Loading

File-Level Changes

Change Details Files
Make schema loading and validation backend-agnostic and compatible with pyuppsala’s XMLSchema API and error logging.
  • Introduce _build_schema_parser(etree_module, schema) to construct XMLSchema from a file path, preserving relative xsd:import/xsd:include resolution.
  • Change get_schema_parser() to use the active config.etree module and delegate to _build_schema_parser, ensuring cache entries are keyed by backend.
  • Redirect get_schema_parser.cache_clear to build_schema_parser.cache_clear so tests can invalidate schemas per-backend.
  • Relax the LogEntry Protocol so error log entries may have a missing/None path attribute.
  • Update handle_validation_error() to tolerate missing path by using xpath() only when a path is available and falling back to the parent element otherwise.
  • Add assert_valid(schema_parser, element) helper that abstracts over lxml’s assert() and pyuppsala’s assertValid(), re-raising backend-specific exceptions as AssertionError.
  • Update validate() to call assert_valid() instead of schema_parser.assert().
fastkml/validator.py
Generalize KML parsing and element construction to work with any rich etree backend (lxml or pyuppsala) instead of hard-coding lxml behavior.
  • Rename lxml_parse_and_validate() to parse_and_validate() and adjust docstring to describe backend-agnostic usage.
  • Wrap XMLParser(huge_tree=True, recover=True) construction in a try/except NotImplementedError and fall back to XMLParser(huge_tree=True) for backends without recover-mode parsing (e.g. pyuppsala).
  • Update KML.parse() to call parse_and_validate() instead of lxml_parse_and_validate().
  • Relax default-namespace element construction: detect stdlib vs non-stdlib by module name instead of LXML_VERSION, and use nsmap={None: uri} for lxml/pyuppsala while keeping stdlib’s Element without nsmap.
fastkml/kml.py
Introduce pyuppsala as a first-class optional backend and expand tests to exercise pyuppsala across the suite, including hypothesis-based property tests and backend configuration helpers.
  • Extend tests.base to detect pyuppsala.etree availability and define PYUPPSALA flag.
  • Add Pyuppsala mixin that sets config.etree to pyuppsala.etree, registers default namespaces via config.set_default_namespaces(), and primes the schema parser.
  • Add NoOpSchema stub with assert/assertValid no-op methods and empty error_log for tests that need to bypass real XSD validation.
  • Add PyuppsalaNoSchemaValidation mixin that uses unittest.mock.patch to override fastkml.validator.get_schema_parser with _NoOpSchema during a test’s lifetime, restoring it in teardown.
  • Introduce numerous Pyuppsala*-prefixed test classes across existing test modules (geometry, overlays, features, containers, styles, times, atom, network_link_control, gx, repr_eq, utils, hypothesis/*) that reuse existing StdLibrary/Lxml test bodies via mixins but run under the Pyuppsala/PyuppsalaNoSchemaValidation backend.
  • In hypothesis-based tests, wrap reused TestLxml test methods with hypothesis.settings(suppress_health_check=[HealthCheck.differing_executors]) to silence differing_executors warnings caused by the same test function being executed by multiple classes, and in a few cases mutate existing _hypothesis_internal_use_settings in place when a function already has settings.
  • Add pyuppsala-specific behavioral tests in tests/kml_test.py and tests/validator_test.py to verify default-namespace serialization quirks, parse-time failures for unbound prefixes under strict/relaxed/validate=False modes, backend-specific schema-parser caching behavior, and differences in validation error messages.
  • Add pyuppsala variants for all core functional tests (e.g., TestPyuppsala classes mirroring TestLxml/TestStdLibrary) to ensure broad coverage of parsing, serialization, round-tripping, and type-specific behavior under the new backend.
tests/base.py
tests/kml_test.py
tests/validator_test.py
tests/base_test.py
tests/config_test.py
tests/geometries/multigeometry_test.py
tests/geometries/geometry_test.py
tests/geometries/boundaries_test.py
tests/geometries/coordinates_test.py
tests/geometries/linearring_test.py
tests/geometries/linestring_test.py
tests/geometries/point_test.py
tests/geometries/polygon_test.py
tests/gx/track_test.py
tests/gx/data_test.py
tests/data_test.py
tests/atom_test.py
tests/containers_test.py
tests/features_test.py
tests/links_test.py
tests/model_test.py
tests/network_link_control_test.py
tests/overlays_test.py
tests/repr_eq_test.py
tests/styles_test.py
tests/times_test.py
tests/utils_test.py
tests/views_test.py
tests/hypothesis/atom_test.py
tests/hypothesis/container_test.py
tests/hypothesis/data_test.py
tests/hypothesis/feature_test.py
tests/hypothesis/geometry_test.py
tests/hypothesis/gx/data_test.py
tests/hypothesis/gx/track_test.py
tests/hypothesis/kml_test.py
tests/hypothesis/links_test.py
tests/hypothesis/model_test.py
tests/hypothesis/multi_geometry_test.py
tests/hypothesis/network_link_control_test.py
tests/hypothesis/overlay_test.py
tests/hypothesis/style_test.py
tests/hypothesis/times_test.py
tests/hypothesis/views_test.py
Update packaging extras, CI workflows, and documentation to include pyuppsala as an optional dependency and test target, and add a standalone integration notes document.
  • Add a new pyuppsala extra in pyproject.toml with dependency pyuppsala>=0.8.0 and update the dev extra to include pyuppsala.
  • Add a new cpython-pyuppsala GitHub Actions job that installs fastkml with the [tests, pyuppsala] extras and runs the test suite across multiple CPython versions.
  • Make release/publish CI jobs depend on the new cpython-pyuppsala job in addition to existing cpython, pypy, lxml, doctest, build-package, and graalpy jobs.
  • Add pyuppsala-integration-notes.md documenting the integration experience, compatibility behavior, and identified pyuppsala bugs/limitations, including numeric XSD validation and namespace-handling differences.
  • Update configuration docs and README (diff not shown) to mention pyuppsala as a supported backend and explain configuration of etree implementations and namespaces.
pyproject.toml
.github/workflows/run-all-tests.yml
pyuppsala-integration-notes.md
docs/configuration.rst
README.rst

Assessment against linked issues

Issue Objective Addressed Explanation
#527 Add support for pyuppsala >= 0.8.0 as an etree backend alternative to the stdlib ElementTree, treated like lxml.
#527 Add tests that exercise the pyuppsala backend similarly to the existing lxml test coverage.
#527 Adjust Hypothesis-based property tests so they also run against the pyuppsala backend.

Possibly linked issues

  • #N/A: The PR implements pyuppsala as an lxml-compatible backend, updates hypothesis tests, and wiring, fulfilling the issue.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@kody-ai

kody-ai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Kody Review Complete

Great news! 🎉
No issues were found that match your current review configurations.

Keep up the excellent work! 🚀

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the @kody start-review command at the root of your PR.

  • Validate Business Logic: Ask Kody to validate your code against business rules by adding a comment with the @kody -v business-logic command.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug
Performance
Security
Business Logic

Access your configuration settings here.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds pyuppsala as an alternative optional XML etree backend to lxml. Updates fastkml's parsing (parse_and_validate) and validator (backend-aware schema caching, assert_valid) logic, packaging/CI/docs, and extends the entire test suite (standard and Hypothesis-based) with pyuppsala-backed test classes. Includes an integration notes report on compatibility.

Changes

Pyuppsala Backend Integration

Layer / File(s) Summary
Core parsing and validation backend support
fastkml/kml.py, fastkml/validator.py
Replaces lxml_parse_and_validate with backend-agnostic parse_and_validate, updates default-namespace detection in etree_element, and adds assert_valid() plus backend-aware schema caching and optional .path handling in the validator.
Packaging, CI, and documentation for pyuppsala extra
pyproject.toml, README.rst, docs/configuration.rst, .github/workflows/run-all-tests.yml, pyuppsala-integration-notes.md
Adds pyuppsala optional dependency, a cpython-pyuppsala CI matrix job gating publish, README/docs mentions of the new backend, and a detailed compatibility report documenting known gaps and bugs.
Test infrastructure and config tests
tests/base.py, tests/config_test.py
Adds Pyuppsala/PyuppsalaNoSchemaValidation mixins, a _NoOpSchema stand-in, and pyuppsala-gated tests for etree implementation/namespace configuration.
KML parsing and validator-specific pyuppsala tests
tests/base_test.py, tests/kml_test.py, tests/validator_test.py
Adds pyuppsala-specific assertions for namespace serialization, unbound-prefix parsing errors, and differing validation error messages.
Standard test suite mixin extensions
tests/atom_test.py, tests/containers_test.py, tests/data_test.py, tests/features_test.py, tests/geometries/*, tests/gx/*, tests/links_test.py, tests/model_test.py, tests/network_link_control_test.py, tests/overlays_test.py, tests/repr_eq_test.py, tests/styles_test.py, tests/times_test.py, tests/utils_test.py, tests/views_test.py
Adds TestPyuppsala-style subclasses mixing the Pyuppsala backend into existing shared test suites.
Hypothesis property-based test extensions
tests/hypothesis/*
Adds TestPyuppsala classes reusing TestLxml fuzz tests wrapped with Hypothesis settings that suppress the differing_executors health check.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • cleder/fastkml#360: Both PRs modify fastkml/kml.py's KML.etree_element namespace handling logic.
  • cleder/fastkml#375: Both PRs modify fastkml/validator.py's get_schema_parser()/validate() logic.
  • cleder/fastkml#397: Both PRs touch KML parsing and namespace handling in fastkml/kml.py.

Suggested labels: enhancement, Tests, Review effort [1-5]: 4

Poem

A rabbit hops through XML trees so wide,
lxml and pyuppsala now stand side by side. 🐇
Parsers recover, schemas still bind,
Tests multiply gently, of every kind.
Hop, hop, hooray — one more backend to find!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding pyuppsala as an alternative etree backend.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 527-pyuppsala-080

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mergify

mergify Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@what-the-diff

what-the-diff Bot commented Jul 5, 2026

Copy link
Copy Markdown

PR Summary

  • Enhanced Workflow and Dependency Management
    A new job has been added to our GitHub actions workflow to perform tests with Python versions 3.10-3.14 using pyuppsala, an XML library, replacing lxml. The updated workflow file uses pyuppsala to install dependencies. Changes have also been made to the project's dependency management file to include pyuppsala as a new optional dependency.

  • Implemented pyuppsala Back-end
    We have updated our function naming and logic, improving the parse_and_validate function to support both lxml and pyuppsala. An independent validation contract is provided through a new function, assert_valid, enhancing our backend-independent schema validation.

  • Additional Test Classes
    To ensure compatibility with pyuppsala, we introduced test classes in various test files. These additional test classes ensure continued harmonistic operation as with TestLxml.

  • Documentation Refinement
    Additional information on using pyuppsala as an alternative XML library and its installation instructions are added to the README documentation. The configuration documentation has been updated to keep users informed about the new backend option.

  • Improved Health Checks Management
    We have incorporated HealthCheck imports in multiple test files. Test methods have been adjusted to suppress specific health checks, reducing dependency warnings and allowing shared method usage across different test classes.

  • In-depth Validation Tests
    New validation tests have been implemented in the TestPyuppsala class that checks the validity of KML files and elements. These tests cover a variety of scenarios, including error handling when it comes to schema management.

  • Comprehensive KML Tests
    Additional KML handling methods in test classes demonstrate the namespace handling differences between lxml and pyuppsala, ensuring comprehensive testing of various KML elements and behaviors using pyuppsala.

@codacy-production

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 1 high

Alerts:
⚠ 1 issue (≤ 0 issues of at least minor severity)

Results:
1 new issue

Category Results
Security 1 high

View in Codacy

🟢 Metrics 25 complexity · 30 duplication

Metric Results
Complexity 25
Duplication 30

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Comment thread fastkml/kml.py
Comment on lines 97 to 98
if validate:
validator.validate(element=root)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If validator.validate(element=root) raises an exception due to invalid KML, the exception will propagate without context or user-friendly error reporting. This can make debugging difficult for users.

Recommendation: Catch validation exceptions and raise a more informative error, or log the validation failure with details about the file and error cause. For example:

try:
    validator.validate(element=root)
except Exception as e:
    raise ValueError(f"KML validation failed: {e}") from e

Comment thread fastkml/kml.py
Comment on lines 176 to 177
f"{self.ns}{self.get_tag_name()}",
nsmap={None: self.ns[1:-1]}, # ty: ignore[invalid-argument-type]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code uses self.ns[1:-1] to extract the namespace URI, assuming self.ns is always in the format '{namespace}'. If self.ns is not in this format, this will result in incorrect namespace mapping or a runtime error.

Recommendation: Add validation for the namespace format before slicing, or use a more robust method to extract the namespace URI. For example:

if self.ns.startswith('{') and self.ns.endswith('}'):
    ns_uri = self.ns[1:-1]
else:
    raise ValueError(f"Invalid namespace format: {self.ns}")

Comment thread fastkml/validator.py
Comment on lines 117 to 125
# attributes, even though the real lxml class supports both.
log = cast("Iterable[_LogEntry]", schema_parser.error_log)
for error_entry in log:
path = getattr(error_entry, "path", None)
try:
matches = cast("list[Element]", element.xpath(error_entry.path))
matches = cast("list[Element]", element.xpath(path) if path else [])
parent = matches[0].getparent()
except (config.etree.XPathEvalError, IndexError):
parent = element

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential backend incompatibility and error context loss in handle_validation_error

The use of element.xpath(path) assumes that the backend supports XPath and that path is a valid expression. For etree-compatible backends that do not support XPath, this may raise an exception or fail silently, leading to loss of error context. Additionally, the fallback to element as the parent may obscure the actual location of the validation error.

Recommendation:

  • Check for XPath support in the backend before attempting to use it.
  • Provide a more informative fallback or log when XPath is not supported or fails, to aid debugging.
  • Consider logging the original error context and path for traceability.

Comment thread fastkml/validator.py
Comment on lines 193 to 199
element = config.etree.parse(file_to_validate).getroot()
assert element is not None # noqa: S101
try:
schema_parser.assert_(element) # noqa: PT009
assert_valid(schema_parser, element)
except AssertionError:
handle_validation_error(schema_parser, element)
raise

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uncaught parsing errors in validate function

When file_to_validate is provided, the code parses the file using config.etree.parse(file_to_validate).getroot(). If the file is malformed or unreadable, this will raise an exception that is not caught, potentially causing the application to crash or obscure the source of the error.

Recommendation:

  • Wrap the parsing operation in a try-except block to catch parsing-related exceptions (e.g., XMLSyntaxError or OSError).
  • Log the error and raise a more informative exception or return None to signal failure gracefully.

Comment thread tests/base.py
Comment on lines +115 to +117
def teardown_method(self) -> None:
"""Restore the real schema parser."""
self._get_schema_parser_patcher.stop()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In teardown_method, if setup_method fails before self._get_schema_parser_patcher is assigned, calling self._get_schema_parser_patcher.stop() will raise an AttributeError. To make teardown more robust, check for the existence of the patcher before stopping it:

if hasattr(self, '_get_schema_parser_patcher'):
    self._get_schema_parser_patcher.stop()

This ensures that teardown does not fail if setup was interrupted.

Comment thread tests/config_test.py
@@ -57,6 +71,21 @@ def test_register_namespaces() -> None:
assert config.etree._namespace_map[v] == k

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test directly accesses and asserts on the private attribute _namespace_map of the etree implementation. This approach tightly couples the test to the internal implementation, making it fragile to refactoring and breaking encapsulation. It is recommended to verify the effect of namespace registration through the public API or observable behavior, rather than relying on internal state. If necessary, consider exposing a public method or property for testability.

Comment thread tests/config_test.py
Comment on lines +113 to +119
empty_namespace_map: dict[str, str] = {}
config.etree._namespace_map = empty_namespace_map

config.set_default_namespaces()

for k, v in config.DEFAULT_NAME_SPACES.items():
assert config.etree._namespace_map[v] == k

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct manipulation and assertion of the private attribute _namespace_map in the test introduces maintainability risks. Tests should avoid depending on internal implementation details. Instead, validate the outcome of set_default_namespaces() through public interfaces or observable effects. If such verification is not possible, consider refactoring the code to provide a testable public API.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request integrates pyuppsala (a pure-Rust, memory-safe XML library) as an optional alternative XML backend alongside lxml and the standard library's xml.etree.ElementTree. The changes include updating core parsing, validation, and element generation logic to accommodate pyuppsala's API differences, adding detailed integration notes, and expanding the test suite to run standard and property-based tests against the new backend. There are no review comments, so no feedback is provided.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add pyuppsala as an lxml-compatible etree backend (plus CI/docs/tests)

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add pyuppsala (>=0.8.0) as a third swappable etree backend.
• Make parsing and XSD validation backend-agnostic (cache, assert/paths, recover-mode).
• Mirror lxml coverage for pyuppsala across unit/property tests and CI.
Diagram

graph TD
  U["Library user"] --> C["fastkml.config"] --> K["fastkml.kml parse"] --> V["fastkml.validator"] --> X[("KML XSD")]
  C --> S{{"stdlib etree"}}
  C --> L{{"lxml.etree"}}
  C --> P{{"pyuppsala.etree"}}

  subgraph Legend
    direction LR
    _m["Module"] ~~~ _e{{"External backend"}} ~~~ _f[("Schema file")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Backend capability flags (no exceptions / no __name__ checks)
  • ➕ Makes backend feature support explicit (recover-mode, nsmap support, schema API surface).
  • ➕ Avoids try/except control flow and string-based module-name comparisons.
  • ➕ Easier to extend to future etree backends without more special-casing.
  • ➖ Requires designing and maintaining a capability surface (and testing it) across backends.
  • ➖ Slightly more code/indirection than the current pragmatic checks.
2. Provide a thin adapter/shim layer per backend
  • ➕ Centralizes API normalization (XMLParser options, XMLSchema.assert_, error log shape) in one place.
  • ➕ Keeps core KML/validator logic simple and backend-agnostic.
  • ➖ More files/classes and higher upfront structure than needed for just one additional backend.
  • ➖ Risk of drifting from true backend behavior if the shim becomes too opinionated.

Recommendation: The PR’s approach (minimal, targeted normalization: schema built from file path, cache keyed by etree module, assert_valid wrapper, and recover-mode fallback) is appropriate and low-risk. If additional backends are expected soon, consider a small capability/adapter layer to prevent incremental special-casing from accumulating further.

Files changed (51) +1296 / -22

Enhancement (1) +15 / -14
kml.pyGeneralize rich-backend parsing/validation beyond lxml +15/-14

Generalize rich-backend parsing/validation beyond lxml

• Renames 'lxml_parse_and_validate' to 'parse_and_validate' and makes it work with lxml-compatible backends (including pyuppsala). Adds a recover-mode parser fallback when 'recover=True' is unsupported and broadens the default-namespace 'nsmap' logic to any non-stdlib backend.

fastkml/kml.py

Bug fix (1) +51 / -5
validator.pyMake schema parsing/validation backend-agnostic and cache-safe +51/-5

Make schema parsing/validation backend-agnostic and cache-safe

• Builds XMLSchema from a schema file path (improving relative import/include resolution) and keys the schema cache by the active etree module. Adds 'assert_valid()' to normalize lxml/pyuppsala schema assertion behavior and tolerates validation log entries lacking '.path'.

fastkml/validator.py

Tests (44) +905 / -0
atom_test.pyRun Atom tests against pyuppsala backend +5/-0

Run Atom tests against pyuppsala backend

• Adds a 'TestPyuppsala' class to execute the existing stdlib-based Atom tests with the pyuppsala etree implementation.

tests/atom_test.py

base.pyAdd Pyuppsala test mixins and schema-validation stub helper +62/-0

Add Pyuppsala test mixins and schema-validation stub helper

• Introduces 'Pyuppsala' mixin to configure the test suite to use 'pyuppsala.etree'. Adds 'PyuppsalaNoSchemaValidation' which patches 'get_schema_parser()' with a no-op schema to work around a pyuppsala 0.8 XSD numeric validation bug in hypothesis-generated cases.

tests/base.py

base_test.pyAdd pyuppsala variants for base-object parsing/serialization +23/-0

Add pyuppsala variants for base-object parsing/serialization

• Adds pyuppsala-backed test class and asserts expected string output under pyuppsala. Ensures '_BaseObject' round-trips correctly with the alternative backend.

tests/base_test.py

config_test.pyTest config switching and namespace registration under pyuppsala +42/-0

Test config switching and namespace registration under pyuppsala

• Adds skip-guarded tests to confirm 'set_etree_implementation(pyuppsala.etree)' and namespace registration/defaults behave as expected. Mirrors existing lxml/stdlib coverage for the new backend.

tests/config_test.py

containers_test.pyRun container tests against pyuppsala backend +5/-0

Run container tests against pyuppsala backend

• Adds a 'TestPyuppsala' class to execute existing container tests with pyuppsala configured as the etree implementation.

tests/containers_test.py

data_test.pyRun data model tests against pyuppsala backend +5/-0

Run data model tests against pyuppsala backend

• Adds a 'TestPyuppsala' class mirroring the lxml test variant so the same data tests run with pyuppsala.

tests/data_test.py

features_test.pyRun feature tests against pyuppsala backend +5/-0

Run feature tests against pyuppsala backend

• Adds a 'TestPyuppsala' class to run feature tests under pyuppsala, mirroring the existing lxml variant.

tests/features_test.py

boundaries_test.pyRun boundary geometry tests against pyuppsala backend +5/-0

Run boundary geometry tests against pyuppsala backend

• Adds a pyuppsala-backed test class so boundary-related geometry tests execute with the new backend.

tests/geometries/boundaries_test.py

coordinates_test.pyRun coordinate parsing tests against pyuppsala backend +5/-0

Run coordinate parsing tests against pyuppsala backend

• Adds a pyuppsala-backed test class to run coordinate tests under the new etree implementation.

tests/geometries/coordinates_test.py

geometry_test.pyRun geometry tests against pyuppsala backend +13/-0

Run geometry tests against pyuppsala backend

• Adds pyuppsala-backed variants for geometry, get-geometry, and geometry creation tests to ensure API compatibility across complex geometry behaviors.

tests/geometries/geometry_test.py

linearring_test.pyRun LinearRing tests against pyuppsala backend +5/-0

Run LinearRing tests against pyuppsala backend

• Adds a pyuppsala-backed test class for LinearRing parsing/serialization behavior.

tests/geometries/linearring_test.py

linestring_test.pyRun LineString tests against pyuppsala backend +5/-0

Run LineString tests against pyuppsala backend

• Adds a pyuppsala-backed test class to execute LineString tests under the alternative backend.

tests/geometries/linestring_test.py

multigeometry_test.pyRun MultiGeometry tests against pyuppsala backend +17/-0

Run MultiGeometry tests against pyuppsala backend

• Adds pyuppsala-backed variants for multi-point, multi-line, multi-polygon, and geometry-collection tests to mirror lxml coverage.

tests/geometries/multigeometry_test.py

point_test.pyRun Point tests against pyuppsala backend +5/-0

Run Point tests against pyuppsala backend

• Adds a pyuppsala-backed test class so Point tests run under the new etree implementation.

tests/geometries/point_test.py

polygon_test.pyRun Polygon tests against pyuppsala backend +5/-0

Run Polygon tests against pyuppsala backend

• Adds a 'TestPyuppsala' class mirroring the lxml variant to execute polygon tests with pyuppsala.

tests/geometries/polygon_test.py

data_test.pyRun gx data tests against pyuppsala backend +5/-0

Run gx data tests against pyuppsala backend

• Adds a 'TestPyuppsala' class so gx data model tests run with pyuppsala configured.

tests/gx/data_test.py

track_test.pyRun gx track tests against pyuppsala backend +13/-0

Run gx track tests against pyuppsala backend

• Adds pyuppsala-backed variants for gx geometry and track tests to mirror existing lxml coverage.

tests/gx/track_test.py

atom_test.pyAdd pyuppsala hypothesis coverage (schema validation stubbed) +17/-0

Add pyuppsala hypothesis coverage (schema validation stubbed)

• Adds 'TestPyuppsala' reusing lxml hypothesis tests while suppressing Hypothesis' differing-executors health check. Uses 'PyuppsalaNoSchemaValidation' to avoid pyuppsala’s known XSD numeric validation bug for generated values.

tests/hypothesis/atom_test.py

container_test.pyRun container hypothesis tests under pyuppsala without schema validation +17/-0

Run container hypothesis tests under pyuppsala without schema validation

• Adds a pyuppsala hypothesis test class that reuses existing lxml property-based tests and suppresses differing-executors warnings. Schema validation is stubbed to focus on round-trip fidelity.

tests/hypothesis/container_test.py

data_test.pyRun data hypothesis tests under pyuppsala without schema validation +29/-0

Run data hypothesis tests under pyuppsala without schema validation

• Adds a pyuppsala hypothesis variant reusing lxml property-based tests with suppressed executor health checks. Avoids pyuppsala XSD numeric edge cases by stubbing schema validation.

tests/hypothesis/data_test.py

feature_test.pyRun feature hypothesis tests under pyuppsala without schema validation +41/-0

Run feature hypothesis tests under pyuppsala without schema validation

• Adds a pyuppsala hypothesis class that reuses many lxml property-based tests via settings-wrapped method aliases. Suppresses 'HealthCheck.differing_executors' due to deliberate test reuse across backends.

tests/hypothesis/feature_test.py

geometry_test.pyRun geometry hypothesis tests under pyuppsala with health-check suppression +61/-0

Run geometry hypothesis tests under pyuppsala with health-check suppression

• Adds a pyuppsala hypothesis variant that reuses lxml test methods and merges Hypothesis settings in-place when methods are already decorated. Stubs schema validation to avoid known pyuppsala numeric validation failures.

tests/hypothesis/geometry_test.py

data_test.pyRun gx data hypothesis tests under pyuppsala without schema validation +17/-0

Run gx data hypothesis tests under pyuppsala without schema validation

• Adds a pyuppsala hypothesis class reusing lxml property tests and suppressing differing-executors warnings. Uses the schema-validation stub mixin for generated XML.

tests/hypothesis/gx/data_test.py

track_test.pyRun gx track hypothesis tests under pyuppsala without schema validation +17/-0

Run gx track hypothesis tests under pyuppsala without schema validation

• Adds a pyuppsala hypothesis class that reuses lxml property-based tests and suppresses differing-executors checks. Schema validation is stubbed for generated cases.

tests/hypothesis/gx/track_test.py

kml_test.pyRun KML hypothesis tests under pyuppsala without schema validation +14/-0

Run KML hypothesis tests under pyuppsala without schema validation

• Adds a pyuppsala hypothesis class reusing lxml property-based tests with suppressed differing-executors warnings. Focuses on parse/serialize round-trip behavior.

tests/hypothesis/kml_test.py

links_test.pyRun link hypothesis tests under pyuppsala without schema validation +14/-0

Run link hypothesis tests under pyuppsala without schema validation

• Adds a pyuppsala hypothesis class reusing the existing lxml test function while suppressing executor health checks. Avoids schema validation for generated cases.

tests/hypothesis/links_test.py

model_test.pyRun model hypothesis tests under pyuppsala without schema validation +29/-0

Run model hypothesis tests under pyuppsala without schema validation

• Adds a pyuppsala hypothesis class reusing lxml property-based tests through settings-wrapped aliases. Suppresses differing-executors warnings caused by deliberate test reuse.

tests/hypothesis/model_test.py

multi_geometry_test.pyRun multi-geometry hypothesis tests under pyuppsala with merged settings +71/-0

Run multi-geometry hypothesis tests under pyuppsala with merged settings

• Adds a pyuppsala hypothesis class reusing lxml property tests and merges settings for already-decorated methods to avoid Hypothesis InvalidArgument errors. Schema validation remains stubbed for generated values.

tests/hypothesis/multi_geometry_test.py

network_link_control_test.pyRun network-link-control hypothesis tests under pyuppsala without schema validation +23/-0

Run network-link-control hypothesis tests under pyuppsala without schema validation

• Adds a pyuppsala hypothesis variant that reuses lxml property-based tests and suppresses differing-executors warnings. Keeps focus on serialization/parsing behavior.

tests/hypothesis/network_link_control_test.py

overlay_test.pyRun overlay hypothesis tests under pyuppsala without schema validation +32/-0

Run overlay hypothesis tests under pyuppsala without schema validation

• Adds a pyuppsala hypothesis class reusing lxml tests through settings-wrapped aliases. Suppresses differing-executors warnings due to shared test functions across backends.

tests/hypothesis/overlay_test.py

style_test.pyRun style hypothesis tests under pyuppsala without schema validation +47/-0

Run style hypothesis tests under pyuppsala without schema validation

• Adds a pyuppsala hypothesis class that reuses lxml property-based tests via settings-wrapped methods. Suppresses differing-executors warnings and avoids schema validation issues for generated values.

tests/hypothesis/style_test.py

times_test.pyRun time hypothesis tests under pyuppsala without schema validation +17/-0

Run time hypothesis tests under pyuppsala without schema validation

• Adds a pyuppsala hypothesis class reusing existing property-based tests with suppressed differing-executors checks. Uses schema-validation stubbing for generated documents.

tests/hypothesis/times_test.py

views_test.pyRun view hypothesis tests under pyuppsala without schema validation +26/-0

Run view hypothesis tests under pyuppsala without schema validation

• Adds a pyuppsala hypothesis class reusing lxml property-based tests via settings-wrapped aliases. Suppresses differing-executors warnings caused by backend-matrix reuse.

tests/hypothesis/views_test.py

kml_test.pyAdd pyuppsala backend coverage for KML parsing edge cases +94/-0

Add pyuppsala backend coverage for KML parsing edge cases

• Adds pyuppsala variants for KML tests, including backend-specific expectations around 'Element.set("xmlns", ...)' serialization and strict parsing behavior without recover-mode support. Adds explicit tests asserting unbound-prefix failures raise parse-time XMLSyntaxError under pyuppsala.

tests/kml_test.py

links_test.pyRun link tests against pyuppsala backend +5/-0

Run link tests against pyuppsala backend

• Adds a 'TestPyuppsala' class to execute link/icon tests with pyuppsala configured as the etree backend.

tests/links_test.py

model_test.pyRun model tests against pyuppsala backend +5/-0

Run model tests against pyuppsala backend

• Adds a pyuppsala-backed model test class to mirror existing lxml coverage.

tests/model_test.py

network_link_control_test.pyRun network link control tests against pyuppsala backend +5/-0

Run network link control tests against pyuppsala backend

• Adds a 'TestPyuppsala' class so network link control tests run under pyuppsala.

tests/network_link_control_test.py

overlays_test.pyRun overlay tests against pyuppsala backend +13/-0

Run overlay tests against pyuppsala backend

• Adds pyuppsala-backed variants for screen/ground/photo overlay tests to mirror lxml coverage.

tests/overlays_test.py

repr_eq_test.pyRun repr/eq round-trip tests against pyuppsala backend +5/-0

Run repr/eq round-trip tests against pyuppsala backend

• Adds a 'TestReprPyuppsala' class to validate '__repr__'/'__str__' and equality round-trips using pyuppsala.

tests/repr_eq_test.py

styles_test.pyRun style tests against pyuppsala backend +5/-0

Run style tests against pyuppsala backend

• Adds a pyuppsala-backed test class to execute style tests under the alternative etree implementation.

tests/styles_test.py

times_test.pyRun time tests against pyuppsala backend +5/-0

Run time tests against pyuppsala backend

• Adds a 'TestPyuppsala' class to execute time parsing/serialization tests under pyuppsala.

tests/times_test.py

utils_test.pyRun utils/find tests against pyuppsala backend +5/-0

Run utils/find tests against pyuppsala backend

• Adds a pyuppsala-backed test class for find/find_all behavior to ensure utility XPath helpers work across backends.

tests/utils_test.py

validator_test.pyAdd validator tests for pyuppsala schema behavior and cache clearing +61/-0

Add validator tests for pyuppsala schema behavior and cache clearing

• Adds pyuppsala validator coverage, explicitly clearing the schema cache between tests to ensure backend-keyed caching is correct. Verifies valid/invalid validation paths and matches pyuppsala-specific error message wording where it differs from lxml.

tests/validator_test.py

views_test.pyRun view tests against pyuppsala backend +5/-0

Run view tests against pyuppsala backend

• Adds a 'TestPyuppsala' class to run view-related tests (e.g., LOD) under pyuppsala.

tests/views_test.py

Documentation (3) +300 / -0
README.rstDocument pyuppsala as an optional lxml alternative +9/-0

Document pyuppsala as an optional lxml alternative

• Adds installation instructions for 'fastkml[pyuppsala]' and lists pyuppsala in Optional Dependencies. Adds the missing pyuppsala reference link.

README.rst

configuration.rstAdd configuration example for pyuppsala etree backend +11/-0

Add configuration example for pyuppsala etree backend

• Documents how to set 'fastkml.config.etree' to 'pyuppsala.etree' via 'set_etree_implementation()'. Includes a short REPL example mirroring the existing lxml/stdlib guidance.

docs/configuration.rst

pyuppsala-integration-notes.mdAdd integration report for pyuppsala compatibility and gaps +280/-0

Add integration report for pyuppsala compatibility and gaps

• Adds a detailed compatibility scorecard and migration notes from running fastkml’s suite against pyuppsala 0.8.0. Documents key limitations (recover-mode parsing) and known issues (scientific-notation XSD double validation).

pyuppsala-integration-notes.md

Other (2) +25 / -3
run-all-tests.ymlAdd CI job to run full test suite with pyuppsala extra +21/-2

Add CI job to run full test suite with pyuppsala extra

• Introduces a new GitHub Actions matrix job (3.10–3.14) installing the 'pyuppsala' extra and running pytest. Updates release/publish workflows to depend on this new job.

.github/workflows/run-all-tests.yml

pyproject.tomlAdd pyuppsala optional extra and include it in dev extra +4/-1

Add pyuppsala optional extra and include it in dev extra

• Defines a new 'pyuppsala' extra pinned to 'pyuppsala>=0.8.0'. Extends the 'dev' extra to install the pyuppsala extra alongside existing development dependencies.

pyproject.toml

ⓘ You are approaching your monthly quota for Qodo. Upgrade your plan

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The manual aliasing of get_schema_parser.cache_clear to _build_schema_parser.cache_clear with multiple type ignores is a bit obscure; consider exposing a small explicit clear_schema_cache() helper instead of rebinding internals to make cache invalidation clearer and easier to maintain.
  • The Pyuppsala Hypothesis classes repeatedly re-wrap TestLxml methods with settings(suppress_health_check=[HealthCheck.differing_executors], ...); factoring this into a shared helper or mixin would reduce duplication and make it easier to adjust Hypothesis configuration in one place.
  • In _NoOpSchema, error_log is typed as list[str] while real implementations expose objects with message/path; aligning the stand‑in’s shape with the real protocol (e.g., using a lightweight object or namedtuple) would avoid surprises if tests start relying on error_log contents.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The manual aliasing of `get_schema_parser.cache_clear` to `_build_schema_parser.cache_clear` with multiple type ignores is a bit obscure; consider exposing a small explicit `clear_schema_cache()` helper instead of rebinding internals to make cache invalidation clearer and easier to maintain.
- The Pyuppsala Hypothesis classes repeatedly re-wrap `TestLxml` methods with `settings(suppress_health_check=[HealthCheck.differing_executors], ...)`; factoring this into a shared helper or mixin would reduce duplication and make it easier to adjust Hypothesis configuration in one place.
- In `_NoOpSchema`, `error_log` is typed as `list[str]` while real implementations expose objects with `message`/`path`; aligning the stand‑in’s shape with the real protocol (e.g., using a lightweight object or namedtuple) would avoid surprises if tests start relying on `error_log` contents.

## Individual Comments

### Comment 1
<location path="docs/configuration.rst" line_range="13-15" />
<code_context>
 implementation.

+``pyuppsala.etree`` (a pure-Rust, memory-safe implementation of the
+``lxml.etree`` API, installable via the ``pyuppsala`` extra) can be used the
+same way, as an alternative to ``lxml.etree``:
+
</code_context>
<issue_to_address>
**nitpick (typo):** Minor grammar tweak: consider "can be used in the same way".

For smoother reading, consider "can be used in the same way, as an alternative to ``lxml.etree``" (adding "in" before "the same way").

```suggestion
``pyuppsala.etree`` (a pure-Rust, memory-safe implementation of the
``lxml.etree`` API, installable via the ``pyuppsala`` extra) can be used in the
same way, as an alternative to ``lxml.etree``:
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread docs/configuration.rst
Comment on lines +13 to +15
``pyuppsala.etree`` (a pure-Rust, memory-safe implementation of the
``lxml.etree`` API, installable via the ``pyuppsala`` extra) can be used the
same way, as an alternative to ``lxml.etree``:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (typo): Minor grammar tweak: consider "can be used in the same way".

For smoother reading, consider "can be used in the same way, as an alternative to lxml.etree" (adding "in" before "the same way").

Suggested change
``pyuppsala.etree`` (a pure-Rust, memory-safe implementation of the
``lxml.etree`` API, installable via the ``pyuppsala`` extra) can be used the
same way, as an alternative to ``lxml.etree``:
``pyuppsala.etree`` (a pure-Rust, memory-safe implementation of the
``lxml.etree`` API, installable via the ``pyuppsala`` extra) can be used in the
same way, as an alternative to ``lxml.etree``:

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown

Failed to generate code suggestions for PR

@codescene-access codescene-access Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gates Failed
Enforce advisory code health rules (2 files with Code Duplication, Lines of Code in a Single File)

Our agent can fix these. Install it.

Gates Passed
5 Quality Gates Passed

Reason for failure
Enforce advisory code health rules Violations Code Health Impact
config_test.py 1 advisory rule 10.00 → 9.39 Suppress
kml_test.py 1 advisory rule 7.07 → 6.66 Suppress

See analysis details in CodeScene

Quality Gate Profile: Customizable Safeguards
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.

Comment thread tests/config_test.py
Comment on lines +75 to +86
def test_register_namespaces_pyuppsala() -> None:
"""Register namespaces for use in etree."""
config.set_etree_implementation(pyuppsala.etree)
ns = {
"real_person": "http://people.example.com",
"role": "http://characters.example.com",
}

config.register_namespaces(**ns)

for k, v in ns.items():
assert config.etree._namespace_map[v] == k

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Code Duplication
The module contains 2 functions with similar structure: test_register_namespaces,test_register_namespaces_pyuppsala

Suppress

Comment thread tests/kml_test.py
import pytest
from pygeoif.geometry import Polygon

from fastkml import config

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Lines of Code in a Single File
This module has 628 lines of code, improve code health by reducing it to 600

Suppress

@codecov

codecov Bot commented Jul 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.54430% with 52 lines in your changes missing coverage. Please review.
✅ Project coverage is 99.22%. Comparing base (196a12f) to head (fa8c4e8).

Files with missing lines Patch % Lines
tests/config_test.py 36.36% 14 Missing ⚠️
tests/kml_test.py 41.66% 14 Missing ⚠️
tests/validator_test.py 38.09% 13 Missing ⚠️
fastkml/validator.py 68.75% 4 Missing and 1 partial ⚠️
tests/base_test.py 44.44% 5 Missing ⚠️
fastkml/kml.py 87.50% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop     #529      +/-   ##
===========================================
- Coverage    99.98%   99.22%   -0.76%     
===========================================
  Files           80       80              
  Lines         6544     6853     +309     
  Branches       166      169       +3     
===========================================
+ Hits          6543     6800     +257     
- Misses           1       52      +51     
- Partials         0        1       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (0)

Context used

Grey Divider


Action required

1. Recover fallback misses TypeError 🐞 Bug ≡ Correctness
Description
parse_and_validate() only falls back when XMLParser(..., recover=True) raises
NotImplementedError; if a backend rejects recover (or other kwargs) with TypeError,
KML.parse() catches that TypeError and silently falls back to a non-validating parse path. This
can unintentionally disable schema validation for backends that otherwise support validation but not
recover-mode parsing.
Code

fastkml/kml.py[R90-95]

+    try:
+        parser = config.etree.XMLParser(huge_tree=True, recover=True)
+    except NotImplementedError:
+        # Some backends (e.g. pyuppsala) don't support recover-mode parsing.
+        parser = config.etree.XMLParser(huge_tree=True)
+    tree = config.etree.parse(file, parser=parser)
Evidence
parse_and_validate() only handles NotImplementedError for the recover-mode parser, but KML.parse()
treats any TypeError from parse_and_validate() as a signal to bypass the rich-backend path
entirely and parse without schema validation. Therefore a backend that raises TypeError for
unsupported recover will silently skip validation.

fastkml/kml.py[88-99]
fastkml/kml.py[229-233]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`parse_and_validate()` only catches `NotImplementedError` when constructing `XMLParser(huge_tree=True, recover=True)`. If the backend signals unsupported kwargs via `TypeError` (common for unexpected keyword args), that `TypeError` bubbles up to `KML.parse()`, which catches it and falls back to `config.etree.parse(file).getroot()`—skipping schema validation entirely.

## Issue Context
This undermines the intent of the new “recover-mode parsing fallback” and can make `strict/validate` behave inconsistently depending on how a backend reports unsupported options.

## Fix Focus Areas
- fastkml/kml.py[88-99]
- fastkml/kml.py[229-233]

### Suggested approach
In `parse_and_validate()`, broaden the fallback to also catch `TypeError` (and possibly `ValueError`) when `recover=True` is unsupported:
- First try `XMLParser(huge_tree=True, recover=True)`
- On `(NotImplementedError, TypeError)`, retry `XMLParser(huge_tree=True)`
- (Optional hardening) If that still raises `TypeError` due to `huge_tree`, retry `XMLParser()`
This keeps validation behavior consistent by ensuring parser-option incompatibility doesn’t trigger the outer `TypeError` fallback that skips validation.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Unsafe nsmap backend check 🐞 Bug ≡ Correctness
Description
KML.etree_element decides whether to pass nsmap based on `config.etree.__name__ !=
"xml.etree.ElementTree"`, which is not a capability check. Since fastkml explicitly allows swapping
in any ElementTree-compatible module, this can raise TypeError at runtime for backends that don’t
accept nsmap but also aren’t named exactly like the stdlib module.
Code

fastkml/kml.py[R170-177]

+        elif config.etree.__name__ != "xml.etree.ElementTree":
+            # lxml and lxml-compatible backends (e.g. pyuppsala) support a
+            # `None` key in `nsmap` to declare a default namespace; the
+            # stdlib's `Element` doesn't accept `nsmap` at all. lxml-stubs'
+            # `_NSMapArg` doesn't model this.
            root = config.etree.Element(
                f"{self.ns}{self.get_tag_name()}",
                nsmap={None: self.ns[1:-1]},  # ty: ignore[invalid-argument-type]
Evidence
The code uses a name-based heuristic to decide whether to pass nsmap, but the project
documentation explicitly states any ElementTree-compatible module may be supplied, which is not
guaranteed to support nsmap. This mismatch can cause runtime failures when using non-stdlib,
non-lxml-compatible backends.

fastkml/kml.py[162-183]
docs/configuration.rst[7-36]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`KML.etree_element()` uses a module-name comparison (`config.etree.__name__ != "xml.etree.ElementTree"`) to decide whether to call `Element(..., nsmap=...)`. This is brittle because `fastkml.config.set_etree_implementation()` explicitly allows *any* ElementTree-compatible module, and many such modules won’t accept an `nsmap` kwarg.

## Issue Context
This can break callers who plug in an alternative ElementTree implementation: root element construction fails with `TypeError` when `nsmap` is passed.

## Fix Focus Areas
- fastkml/kml.py[165-183]

### Suggested approach
Replace the name-based check with a capability check:
- Prefer `try: Element(tag, nsmap=...)` and `except TypeError: Element(tag)`.
- Or gate on a known capability attribute (less ideal than try/except).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ You are approaching your monthly quota for Qodo. Upgrade your plan

Qodo Logo

Comment thread fastkml/kml.py
Comment on lines +170 to 177
elif config.etree.__name__ != "xml.etree.ElementTree":
# lxml and lxml-compatible backends (e.g. pyuppsala) support a
# `None` key in `nsmap` to declare a default namespace; the
# stdlib's `Element` doesn't accept `nsmap` at all. lxml-stubs'
# `_NSMapArg` doesn't model this.
root = config.etree.Element(
f"{self.ns}{self.get_tag_name()}",
nsmap={None: self.ns[1:-1]}, # ty: ignore[invalid-argument-type]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Unsafe nsmap backend check 🐞 Bug ≡ Correctness

KML.etree_element decides whether to pass nsmap based on `config.etree.__name__ !=
"xml.etree.ElementTree"`, which is not a capability check. Since fastkml explicitly allows swapping
in any ElementTree-compatible module, this can raise TypeError at runtime for backends that don’t
accept nsmap but also aren’t named exactly like the stdlib module.
Agent Prompt
## Issue description
`KML.etree_element()` uses a module-name comparison (`config.etree.__name__ != "xml.etree.ElementTree"`) to decide whether to call `Element(..., nsmap=...)`. This is brittle because `fastkml.config.set_etree_implementation()` explicitly allows *any* ElementTree-compatible module, and many such modules won’t accept an `nsmap` kwarg.

## Issue Context
This can break callers who plug in an alternative ElementTree implementation: root element construction fails with `TypeError` when `nsmap` is passed.

## Fix Focus Areas
- fastkml/kml.py[165-183]

### Suggested approach
Replace the name-based check with a capability check:
- Prefer `try: Element(tag, nsmap=...)` and `except TypeError: Element(tag)`.
- Or gate on a known capability attribute (less ideal than try/except).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread fastkml/kml.py
Comment on lines +90 to +95
try:
parser = config.etree.XMLParser(huge_tree=True, recover=True)
except NotImplementedError:
# Some backends (e.g. pyuppsala) don't support recover-mode parsing.
parser = config.etree.XMLParser(huge_tree=True)
tree = config.etree.parse(file, parser=parser)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Recover fallback misses typeerror 🐞 Bug ≡ Correctness

parse_and_validate() only falls back when XMLParser(..., recover=True) raises
NotImplementedError; if a backend rejects recover (or other kwargs) with TypeError,
KML.parse() catches that TypeError and silently falls back to a non-validating parse path. This
can unintentionally disable schema validation for backends that otherwise support validation but not
recover-mode parsing.
Agent Prompt
## Issue description
`parse_and_validate()` only catches `NotImplementedError` when constructing `XMLParser(huge_tree=True, recover=True)`. If the backend signals unsupported kwargs via `TypeError` (common for unexpected keyword args), that `TypeError` bubbles up to `KML.parse()`, which catches it and falls back to `config.etree.parse(file).getroot()`—skipping schema validation entirely.

## Issue Context
This undermines the intent of the new “recover-mode parsing fallback” and can make `strict/validate` behave inconsistently depending on how a backend reports unsupported options.

## Fix Focus Areas
- fastkml/kml.py[88-99]
- fastkml/kml.py[229-233]

### Suggested approach
In `parse_and_validate()`, broaden the fallback to also catch `TypeError` (and possibly `ValueError`) when `recover=True` is unsupported:
- First try `XMLParser(huge_tree=True, recover=True)`
- On `(NotImplementedError, TypeError)`, retry `XMLParser(huge_tree=True)`
- (Optional hardening) If that still raises `TypeError` due to `huge_tree`, retry `XMLParser()`
This keeps validation behavior consistent by ensuring parser-option incompatibility doesn’t trigger the outer `TypeError` fallback that skips validation.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds pyuppsala.etree as a third, optional “rich etree” backend (alongside stdlib xml.etree.ElementTree and lxml.etree) and updates validation/parsing/namespace-handling to be backend-agnostic, with extensive mirrored test coverage.

Changes:

  • Add pyuppsala>=0.8.0 as an optional extra and introduce a CI job to run the full test suite against it.
  • Make schema parsing/validation backend-neutral (schema cache keyed by active etree module; uniform AssertionError contract; tolerant error-log handling).
  • Mirror lxml-backed tests for pyuppsala across unit + Hypothesis suites (with a pyuppsala-only schema-validation stub for a known xs:double validator bug).

Reviewed changes

Copilot reviewed 51 out of 51 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/views_test.py Adds a Pyuppsala backend test variant for views.
tests/validator_test.py Adds pyuppsala-specific validator tests + cache invalidation in setup.
tests/utils_test.py Adds a Pyuppsala backend test variant for utils find/find_all.
tests/times_test.py Adds a Pyuppsala backend test variant for times.
tests/styles_test.py Adds a Pyuppsala backend test variant for styles.
tests/repr_eq_test.py Adds a Pyuppsala backend variant for repr/eq/str round-trips.
tests/overlays_test.py Adds Pyuppsala backend variants for overlay tests.
tests/network_link_control_test.py Adds a Pyuppsala backend test variant for network link control.
tests/model_test.py Adds a Pyuppsala backend test variant for model.
tests/links_test.py Adds a Pyuppsala backend test variant for links.
tests/kml_test.py Adds pyuppsala-specific expectations for KML parse/serialize edge cases.
tests/hypothesis/views_test.py Adds pyuppsala Hypothesis variant with health-check suppression.
tests/hypothesis/times_test.py Adds pyuppsala Hypothesis variant with health-check suppression.
tests/hypothesis/style_test.py Adds pyuppsala Hypothesis variant with health-check suppression.
tests/hypothesis/overlay_test.py Adds pyuppsala Hypothesis variant with health-check suppression.
tests/hypothesis/network_link_control_test.py Adds pyuppsala Hypothesis variant with health-check suppression.
tests/hypothesis/multi_geometry_test.py Adds pyuppsala Hypothesis variant; merges Hypothesis settings for reused tests.
tests/hypothesis/model_test.py Adds pyuppsala Hypothesis variant with health-check suppression.
tests/hypothesis/links_test.py Adds pyuppsala Hypothesis variant with health-check suppression.
tests/hypothesis/kml_test.py Adds pyuppsala Hypothesis variant with health-check suppression.
tests/hypothesis/gx/track_test.py Adds pyuppsala Hypothesis variant for gx track with health-check suppression.
tests/hypothesis/gx/data_test.py Adds pyuppsala Hypothesis variant for gx data with health-check suppression.
tests/hypothesis/geometry_test.py Adds pyuppsala Hypothesis variant; merges settings for a pre-settings-decorated test.
tests/hypothesis/feature_test.py Adds pyuppsala Hypothesis variant with health-check suppression.
tests/hypothesis/data_test.py Adds pyuppsala Hypothesis variant with health-check suppression.
tests/hypothesis/container_test.py Adds pyuppsala Hypothesis variant with health-check suppression.
tests/hypothesis/atom_test.py Adds pyuppsala Hypothesis variant with health-check suppression.
tests/gx/track_test.py Adds Pyuppsala backend variants for gx track unit tests.
tests/gx/data_test.py Adds Pyuppsala backend variants for gx data unit tests.
tests/geometries/polygon_test.py Adds Pyuppsala backend variant for polygon geometry tests.
tests/geometries/point_test.py Adds Pyuppsala backend variant for point geometry tests.
tests/geometries/multigeometry_test.py Adds Pyuppsala backend variants for multigeometry tests.
tests/geometries/linestring_test.py Adds Pyuppsala backend variant for linestring tests.
tests/geometries/linearring_test.py Adds Pyuppsala backend variant for linearring tests.
tests/geometries/geometry_test.py Adds Pyuppsala backend variants for geometry factory/round-trip tests.
tests/geometries/coordinates_test.py Adds Pyuppsala backend variant for coordinates tests.
tests/geometries/boundaries_test.py Adds Pyuppsala backend variant for boundaries tests.
tests/features_test.py Adds Pyuppsala backend variant for features tests.
tests/data_test.py Adds Pyuppsala backend variant for data tests.
tests/containers_test.py Adds Pyuppsala backend variant for containers tests.
tests/config_test.py Adds config tests for selecting pyuppsala + namespace registration/defaults.
tests/base.py Introduces Pyuppsala / PyuppsalaNoSchemaValidation test mixins.
tests/base_test.py Adds pyuppsala-specific base object serialization/parsing tests.
tests/atom_test.py Adds Pyuppsala backend variant for atom tests.
README.rst Documents installing/using the pyuppsala extra.
pyuppsala-integration-notes.md Adds an integration report/compatibility notes for pyuppsala.
pyproject.toml Adds pyuppsala optional extra; includes it in dev extras.
fastkml/validator.py Makes schema parsing/validation backend-agnostic + caches by active etree module.
fastkml/kml.py Generalizes parse+validate helper and expands default-namespace handling beyond lxml-only.
docs/configuration.rst Documents configuring fastkml to use pyuppsala.etree.
.github/workflows/run-all-tests.yml Adds a dedicated pyuppsala test job and makes publishing depend on it.

Comment thread fastkml/kml.py
Comment on lines 165 to +170
if not self.ns:
root = config.etree.Element(
f"{self.ns}{self.get_tag_name()}",
)
root.set("xmlns", config.KMLNS[1:-1])
elif hasattr(config.etree, "LXML_VERSION"):
# lxml supports a `None` key in `nsmap` to declare a default
# namespace; lxml-stubs' `_NSMapArg` doesn't model this.
elif config.etree.__name__ != "xml.etree.ElementTree":
Comment on lines +501 to +505
test_coordinates_str_roundtrip = TestLxml.test_coordinates_str_roundtrip
test_coordinates_str_roundtrip._hypothesis_internal_use_settings = settings(
parent=test_coordinates_str_roundtrip._hypothesis_internal_use_settings,
suppress_health_check=[HealthCheck.differing_executors],
)
Comment on lines +732 to +736
_multipoint_repr_roundtrip = TestLxml.test_multipoint_repr_roundtrip
_multipoint_repr_roundtrip._hypothesis_internal_use_settings = settings(
_multipoint_repr_roundtrip._hypothesis_internal_use_settings,
suppress_health_check=[HealthCheck.differing_executors],
)
@greptile-apps

greptile-apps Bot commented Jul 5, 2026

Copy link
Copy Markdown

Greptile Summary

This PR extends fastkml's swappable etree backend to support pyuppsala>=0.8.0, a Rust-based, lxml-compatible XML library, alongside the existing stdlib and lxml backends. The changes are focused and surgical — four production files touched — with comprehensive mirrored test coverage including hypothesis property-based tests and a new CI matrix job.

  • fastkml/validator.py: get_schema_parser now delegates caching to _build_schema_parser(etree_module, schema), keyed on both the active backend and the schema path; fixes a latent bug where switching backends mid-process could return a schema built for the wrong backend. A new assert_valid() helper normalises assert_()/assertValid() across backends, and handle_validation_error gracefully tolerates error-log entries without a .path attribute (a pyuppsala 0.8 difference).
  • fastkml/kml.py: The recover=True parser fallback is now guarded with NotImplementedError (pyuppsala rejects that flag at construction time), and the nsmap branch is generalised from hasattr(etree, "LXML_VERSION") to a module-name check that covers any non-stdlib backend.
  • tests/base.py: Adds Pyuppsala and PyuppsalaNoSchemaValidation mixins; the latter patches out schema validation to work around pyuppsala 0.8's xs:double scientific-notation range bug, with the trade-off clearly documented.

Confidence Score: 4/5

Safe to merge; all production changes are defensive guards that leave the lxml and stdlib paths unaffected.

The core logic changes in validator.py and kml.py are small, well-targeted, and the existing lxml and stdlib paths are preserved exactly. Test coverage mirrors the lxml suite across the whole test tree. The findings are confined to test infrastructure: a teardown guard that could mask errors on unexpected failures, reliance on a private Hypothesis internal that could break with a Hypothesis update, and a missing coverage gate in CI for the new backend.

tests/base.py (teardown fragility) and tests/hypothesis/geometry_test.py (private Hypothesis API mutation) deserve a second look before the next Hypothesis version bump.

Important Files Changed

Filename Overview
fastkml/validator.py Refactors schema parsing to use file= keyword (fixes relative xsd:import resolution), adds _build_schema_parser as the actual cached function, delegates cache_clear, adds assert_valid helper for backend-agnostic validation, and tolerates missing .path on error-log entries.
fastkml/kml.py Renames lxml_parse_and_validate to parse_and_validate, adds a NotImplementedError guard for recover-mode parsing, and generalises the nsmap branch from hasattr(etree, "LXML_VERSION") to a module-name check to cover pyuppsala.
tests/base.py Adds Pyuppsala and PyuppsalaNoSchemaValidation test mixins; the latter patches get_schema_parser to work around pyuppsala 0.8's xs:double scientific-notation bug. teardown_method has a minor fragility if setup_method raises before _get_schema_parser_patcher is assigned.
tests/hypothesis/geometry_test.py Adds TestPyuppsala inheriting from TestLxml; suppresses HealthCheck.differing_executors by mutating _hypothesis_internal_use_settings — a private Hypothesis attribute that may break with future Hypothesis upgrades.
.github/workflows/run-all-tests.yml Adds cpython-pyuppsala CI job across Python 3.10–3.14, wired into release gates; job runs pytest tests without a coverage threshold unlike the cpython-lxml job.
tests/validator_test.py Adds TestPyuppsala with cache-clearing in setup_method and pyuppsala-specific test_validate_empty_element matching pyuppsala's different error message wording.
tests/kml_test.py Adds pyuppsala variants of all KML test classes; explicitly tests the xmlns_= serialisation quirk and the fact that pyuppsala raises XMLSyntaxError rather than a later AssertionError for undeclared namespace prefixes.
pyproject.toml Adds pyuppsala>=0.8.0 as an optional extra and includes it in the dev group alongside lxml.
pyuppsala-integration-notes.md Detailed integration report documenting compatibility, known gaps (recover-mode, xs:double, namespace prefix inheritance), and suggested priorities for the pyuppsala project; placed in the repo root rather than docs/.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Caller
    participant KML
    participant parse_and_validate
    participant config_etree
    participant validator
    participant _build_schema_parser

    Caller->>KML: parse(file, strict, validate)
    KML->>parse_and_validate: file, strict, validate
    parse_and_validate->>config_etree: "XMLParser(huge_tree=True, recover=True)"
    alt NotImplementedError (pyuppsala)
        config_etree-->>parse_and_validate: raise NotImplementedError
        parse_and_validate->>config_etree: "XMLParser(huge_tree=True)"
    else TypeError (stdlib)
        config_etree-->>parse_and_validate: raise TypeError
        Note over KML: outer try/except catches TypeError, falls back to plain parse
    end
    config_etree-->>parse_and_validate: parser
    parse_and_validate->>config_etree: parse(file, parser)
    config_etree-->>parse_and_validate: tree
    parse_and_validate->>validator: "validate(element=root)"
    validator->>_build_schema_parser: (etree_module, schema_path)
    _build_schema_parser-->>validator: XMLSchema
    validator->>validator: assert_valid(schema, element)
    alt has assert_() lxml
        validator->>validator: schema.assert_(element)
    else has assertValid() pyuppsala
        validator->>validator: schema.assertValid(element) re-raise as AssertionError
    end
    validator-->>parse_and_validate: True / AssertionError
    parse_and_validate-->>KML: root Element
    KML-->>Caller: KML object
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Caller
    participant KML
    participant parse_and_validate
    participant config_etree
    participant validator
    participant _build_schema_parser

    Caller->>KML: parse(file, strict, validate)
    KML->>parse_and_validate: file, strict, validate
    parse_and_validate->>config_etree: "XMLParser(huge_tree=True, recover=True)"
    alt NotImplementedError (pyuppsala)
        config_etree-->>parse_and_validate: raise NotImplementedError
        parse_and_validate->>config_etree: "XMLParser(huge_tree=True)"
    else TypeError (stdlib)
        config_etree-->>parse_and_validate: raise TypeError
        Note over KML: outer try/except catches TypeError, falls back to plain parse
    end
    config_etree-->>parse_and_validate: parser
    parse_and_validate->>config_etree: parse(file, parser)
    config_etree-->>parse_and_validate: tree
    parse_and_validate->>validator: "validate(element=root)"
    validator->>_build_schema_parser: (etree_module, schema_path)
    _build_schema_parser-->>validator: XMLSchema
    validator->>validator: assert_valid(schema, element)
    alt has assert_() lxml
        validator->>validator: schema.assert_(element)
    else has assertValid() pyuppsala
        validator->>validator: schema.assertValid(element) re-raise as AssertionError
    end
    validator-->>parse_and_validate: True / AssertionError
    parse_and_validate-->>KML: root Element
    KML-->>Caller: KML object
Loading

Reviews (1): Last reviewed commit: "Add pyuppsala as an alternative lxml-com..." | Re-trigger Greptile

Comment thread tests/base.py
Comment on lines +115 to +117
def teardown_method(self) -> None:
"""Restore the real schema parser."""
self._get_schema_parser_patcher.stop()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 teardown_method unconditionally calls self._get_schema_parser_patcher.stop(), but _get_schema_parser_patcher is only assigned after super().setup_method() succeeds. If super().setup_method() raises (e.g. a pyuppsala import error slips through, or get_schema_parser() fails), pytest will still invoke teardown_method, which will then fail with AttributeError: 'PyuppsalaNoSchemaValidation' object has no attribute '_get_schema_parser_patcher', masking the original error. A getattr guard makes this safe.

Suggested change
def teardown_method(self) -> None:
"""Restore the real schema parser."""
self._get_schema_parser_patcher.stop()
def teardown_method(self) -> None:
"""Restore the real schema parser."""
patcher = getattr(self, "_get_schema_parser_patcher", None)
if patcher is not None:
patcher.stop()

Comment on lines +493 to +499
# happening here, deliberately, so it's suppressed rather than avoided.
#
# `test_coordinates_str_roundtrip` already carries an explicit
# `@settings(deadline=None)`. `settings.__call__` refuses to decorate a
# test that has already been settings-decorated, so instead of
# rewrapping it with `settings(...)(...)` like the others below, merge
# the extra `suppress_health_check` into its existing settings object

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Private Hypothesis API mutation

_hypothesis_internal_use_settings is an undocumented internal attribute. Hypothesis has no stability contract on it — a minor release could rename or restructure it, silently converting the test into one that runs without suppress_health_check (and therefore fails with HealthCheck.differing_executors) or fails at collection time. The comment explains why settings()(already_decorated_fn) isn't usable here, but it's worth tracking the upstream issue or pinning hypothesis tightly if this pattern is relied upon across all the new TestPyuppsala hypothesis classes.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +69 to +71
- name: Test with pytest
run: |
pytest tests

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The cpython-pyuppsala job runs pytest tests without a coverage threshold, unlike the cpython-lxml job which enforces --cov-fail-under=95. This means coverage regressions introduced specifically through the pyuppsala path won't be caught in CI.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@llamapreview llamapreview Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Code Review by LlamaPReview

🎯 TL;DR & Recommendation

Recommendation: Approve with suggestions

This PR successfully adds pyuppsala as a third etree backend with comprehensive test coverage and backend-agnostic refactoring. However, a few defensive-coding and maintainability concerns in the new validation and parsing code should be addressed before merging to avoid subtle runtime failures.

📄 Documentation Diagram

This diagram illustrates the backend-agnostic parse, validate, and schema caching flow introduced in the PR.

sequenceDiagram
    participant Client
    participant PAV as parse_and_validate
    participant Backend as etree Backend
    participant Svc as Schema Parser

    Client->>PAV: parse_and_validate(file, strict, validate)
    PAV->>Backend: try XMLParser(huge_tree=True, recover=True)
    alt recover not supported
        Backend-->>PAV: NotImplementedError or TypeError
        PAV->>Backend: XMLParser(huge_tree=True) or default
    end
    PAV->>Backend: parse(file, parser)
    Backend-->>PAV: element tree root
    alt validate
        PAV->>Svc: assert_valid(parser, element)
        Svc->>Svc: get_schema_parser() with cache keyed on (module, path)
        Svc-->>PAV: raises AssertionError if invalid
    end
    PAV-->>Client: root element
    note over PAV,Backend: PR #35;529: Added recover fallback and<br/>backend-agnostic assert_valid
    note over Svc: Build XMLSchema from file path<br/>to preserve relative imports
Loading

🌟 Strengths

  • Excellent test mirroring across lxml and pyuppsala, including property-based tests with hypothesis.
  • Thoughtful workarounds for pyuppsala limitations (e.g., recover mode, assert_valid helper, schema file path resolution).

Summary of Findings

Priority File Category Impact Summary Anchors
P2 fastkml/validator.py Maintainability Overly broad exception catch in assert_valid symbol:assert_valid
P2 fastkml/validator.py Maintainability Fragile cache_clear delegation pattern symbol:get_schema_parser
P2 fastkml/kml.py Bug Incomplete exception handling in parser fallback symbol:parse_and_validate

🔍 Notable Themes

  • All three findings relate to defensive error handling in the new backend-agnostic code: overly broad exception catches, fragile delegation, and missing exception types. These could lead to silent failures or confusion during maintenance.

💡 Have feedback? We'd love to hear it in our GitHub Discussions.
✨ This review was generated by LlamaPReview Advanced, which is free for all open-source projects. Learn more.

Comment thread fastkml/validator.py
Comment on lines +154 to +157
try:
schema_parser.assertValid(element)
except Exception as error:
raise AssertionError(str(error)) from error

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 | Confidence: High

assert_valid catches a bare Exception in the fallback path for backends that don't provide assert_(). This is overly broad: any unexpected runtime error (e.g., MemoryError, KeyboardInterrupt, or an unrelated programming error inside assertValid) will be silently converted into an AssertionError, obscuring the real problem and potentially halting cleanup during a shutdown. The code should catch the specific exception type that DocumentInvalid or similar validation exceptions raise (e.g., lxml.etree.DocumentInvalid or its pyuppsala equivalent). Since the point of the helper is to normalize the exception for backends that lack assert_(), a more precise catch protects debugging and avoids swallowing unrelated failures.

Code Suggestion:

try:
        schema_parser.assertValid(element)
    except (schema_parser.DocumentInvalid, ...) as error:
        raise AssertionError(str(error)) from error
(Note: The exact exception class depends on the active backend; you could use `type(schema_parser).__module__` to derive the correct one, or catch a common base like `ValueError` if the validation API guarantees that. Alternatively, use `lxml.etree.DocumentInvalid` as a standardpyuppsala should expose the same hierarchy per its documentation.)

Evidence: symbol:assert_valid

Comment thread fastkml/validator.py
Comment on lines +98 to +100
get_schema_parser.cache_clear = ( # type: ignore[attr-defined] # ty: ignore[unresolved-attribute]
_build_schema_parser.cache_clear
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 | Confidence: Medium

The cache-clearing delegation reassigns get_schema_parser.cache_clear to the inner _build_schema_parser.cache_clear. This works but introduces fragility: if a future developer mistakenly adds @lru_cache on get_schema_parser itself (e.g., thinking it is still cached), the reassignment will silently break, and the type: ignore comments suppress type-checking warnings that would normally flag this. The pattern also loses the original cache_clear attribute on get_schema_parser (which was never decorated, so it didn't have one). While functionally correct, it is an unusual indirection that may confuse maintenance. A simpler, more readable approach would be to keep @lru_cache on get_schema_parser but include config.etree in its arguments (already done via the helper) and then use get_schema_parser.cache_clear() directly—the cache would already be keyed on (config.etree, schema). The current design adds an unnecessary extra level of indirection.

Code Suggestion:

@lru_cache(maxsize=16)
def get_schema_parser(
    schema: pathlib.Path | None = None,
) -> "etree.XMLSchema":
    if schema is None:
        schema = pathlib.Path(__file__).parent / "schema" / "ogckml22.xsd"
    # Build from file path to preserve relative import resolution
    return config.etree.XMLSchema(file=schema)
(Note: The cache then automatically depends on `config.etree` because `config.etree` is a module object that is hashable and will key correctly as long as the module reference doesn't change identity. If it does change, the cache will naturally create a new entry. The `cache_clear` method is available automatically.)

Evidence: symbol:get_schema_parser

Comment thread fastkml/kml.py
Comment on lines +90 to +94
try:
parser = config.etree.XMLParser(huge_tree=True, recover=True)
except NotImplementedError:
# Some backends (e.g. pyuppsala) don't support recover-mode parsing.
parser = config.etree.XMLParser(huge_tree=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 | Confidence: Medium

The try block assumes that a NotImplementedError is raised only for the recover=True argument, and that huge_tree=True is universally supported. However, the standard library's xml.etree.ElementTree.XMLParser does not accept huge_tree either—it will raise TypeError (because the keyword argument is not defined). This TypeError is not caught here, so when the active backend is stdlib, the exception propagates out of parse_and_validate and is caught by KML.parse's broader except TypeError fallback. While that fallback exists (and the overall behavior is correct), the intent of the PR is to make parse_and_validate backend-agnostic; the current design inadvertently relies on the caller to handle stdlib-specific failures. This is a design inconsistency: the function's docstring states it is for "rich etree backends", but it is called by KML.parse which also handles stdlib. A cleaner approach would be to detect support for huge_tree and recover upfront (e.g., via a capability check or by catching both NotImplementedError and TypeError here) so that the function works uniformly across all three backends without forcing the caller to know about internal limitations.

Code Suggestion:

try:
        parser = config.etree.XMLParser(huge_tree=True, recover=True)
    except (NotImplementedError, TypeError):
        # Backends may not support recover or huge_tree (e.g., stdlib
        # doesn't accept huge_tree, pyuppsala doesn't accept recover).
        try:
            parser = config.etree.XMLParser(huge_tree=True)
        except (NotImplementedError, TypeError):
            parser = config.etree.XMLParser()  # fallback to standard parser
(Adapt the fallback chain as needed; the key point is to handle both exceptions at both levels.)

Evidence: symbol:parse_and_validate

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
tests/hypothesis/atom_test.py (1)

92-105: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Correct, but this boilerplate is repeated near-identically across ~16 test files.

Every TestPyuppsala class in this cohort follows the same shape: rebind each inherited TestLxml method through settings(suppress_health_check=[HealthCheck.differing_executors])(...). A small helper in tests/base.py (e.g. a class decorator that iterates over a class's test_* attributes carrying _hypothesis_internal_use_settings and rewraps them) would collapse this to one line per file, and would also auto-cover any new fuzz test added to TestLxml in the future without requiring every TestPyuppsala subclass to be updated by hand.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/hypothesis/atom_test.py` around lines 92 - 105, The repeated hypothesis
rebindings in TestPyuppsala should be centralized instead of duplicated across
each test module. Add a helper in tests/base.py, ideally a class decorator or
similar utility, that walks inherited test_* attributes from TestLxml, detects
those carrying _hypothesis_internal_use_settings, and reapplies
settings(suppress_health_check=[HealthCheck.differing_executors]) automatically.
Then replace the per-method rewraps in TestPyuppsala subclasses with the helper
so new fuzz tests added to TestLxml are covered without manual updates.
tests/hypothesis/gx/data_test.py (1)

86-99: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Correctly reuses TestLxml's fuzz tests; both methods are wrapped.

The reasoning for suppressing HealthCheck.differing_executors is well documented. As an optional improvement, since this exact same boilerplate (import HealthCheck/settings, define TestPyuppsala(PyuppsalaNoSchemaValidation, TestLxml), manually re-wrap every test_fuzz_* method) is duplicated across ~16 hypothesis test files in this cohort, consider extracting a small helper (e.g. in tests/base.py) that automatically discovers and wraps all test_fuzz_* methods from a base class, reducing copy-paste and the risk of forgetting to wrap a newly added fuzz test.

♻️ Example helper to reduce duplication
def reuse_fuzz_tests(source_cls: type) -> dict[str, object]:
    """Reuse all `test_fuzz_*` methods from `source_cls`, suppressing
    the `differing_executors` health check triggered by invoking the
    same underlying hypothesis-wrapped test from multiple classes."""
    return {
        name: settings(suppress_health_check=[HealthCheck.differing_executors])(method)
        for name, method in vars(source_cls).items()
        if name.startswith("test_fuzz")
    }

Usage:

class TestPyuppsala(PyuppsalaNoSchemaValidation, TestLxml):
    locals().update(reuse_fuzz_tests(TestLxml))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/hypothesis/gx/data_test.py` around lines 86 - 99, The current
`TestPyuppsala` setup correctly re-wraps the `TestLxml` fuzz tests, but the same
`HealthCheck.differing_executors` boilerplate is duplicated across many
hypothesis test modules. Add a small shared helper (for example in
`tests/base.py`) that discovers all `test_fuzz_*` methods from a source class
and applies the `settings(...)(...)` wrapper, then use it from `TestPyuppsala`
and the other cohort classes to reduce copy-paste and prevent missing a new fuzz
test.
fastkml/kml.py (1)

170-182: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider centralizing backend-capability detection.

The config.etree.__name__ != "xml.etree.ElementTree" string check works, but hardcoding the stdlib module's dotted name here duplicates backend-detection logic that likely belongs in fastkml/config.py (e.g. a config.is_stdlib_etree() helper). This would keep the "which backends support X" knowledge in one place as more backends are added.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@fastkml/kml.py` around lines 170 - 182, Centralize the backend check used in
KML root creation: the string comparison in Kml-like element construction
duplicates knowledge about which etree backends support nsmap. Add or reuse a
helper in config.py such as config.is_stdlib_etree(), then update the root
element creation logic in fastkml/kml.py to call that helper instead of
comparing config.etree.__name__ directly. Keep the backend-capability decision
in one place so future backends only need config.py changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/run-all-tests.yml:
- Around line 54-72: The new cpython-pyuppsala GitHub Actions job should be
hardened by explicitly restricting the job’s GITHUB_TOKEN permissions and
disabling checkout credential persistence. Update the workflow job definition
for cpython-pyuppsala to set only the minimal permissions it needs, and adjust
the actions/checkout usage in that job to avoid persisting credentials after
checkout.

In `@tests/model_test.py`:
- Around line 161-164: The backend mixin order is wrong in the test classes, so
`setup_method` is resolving from `StdLibrary` through `TestModel` before the
backend-specific mixin can install its etree behavior. Update
`TestModelPyuppsala` to put `Pyuppsala` before `TestModel`, and make the same
base-class order fix in `TestModelLxml` so the backend mixin is applied first
during test setup.

In `@tests/utils_test.py`:
- Around line 213-218: The backend test classes are missing the shared
TestFindAll mixin, so they only inherit backend-specific behavior and skip the
common test_* cases. Update the class definitions for TestFindAllLxml and
TestFindAllPyuppsala to include TestFindAll in their base classes, alongside
Lxml and Pyuppsala, so both backend variants run the shared find-all tests.

---

Nitpick comments:
In `@fastkml/kml.py`:
- Around line 170-182: Centralize the backend check used in KML root creation:
the string comparison in Kml-like element construction duplicates knowledge
about which etree backends support nsmap. Add or reuse a helper in config.py
such as config.is_stdlib_etree(), then update the root element creation logic in
fastkml/kml.py to call that helper instead of comparing config.etree.__name__
directly. Keep the backend-capability decision in one place so future backends
only need config.py changes.

In `@tests/hypothesis/atom_test.py`:
- Around line 92-105: The repeated hypothesis rebindings in TestPyuppsala should
be centralized instead of duplicated across each test module. Add a helper in
tests/base.py, ideally a class decorator or similar utility, that walks
inherited test_* attributes from TestLxml, detects those carrying
_hypothesis_internal_use_settings, and reapplies
settings(suppress_health_check=[HealthCheck.differing_executors]) automatically.
Then replace the per-method rewraps in TestPyuppsala subclasses with the helper
so new fuzz tests added to TestLxml are covered without manual updates.

In `@tests/hypothesis/gx/data_test.py`:
- Around line 86-99: The current `TestPyuppsala` setup correctly re-wraps the
`TestLxml` fuzz tests, but the same `HealthCheck.differing_executors`
boilerplate is duplicated across many hypothesis test modules. Add a small
shared helper (for example in `tests/base.py`) that discovers all `test_fuzz_*`
methods from a source class and applies the `settings(...)(...)` wrapper, then
use it from `TestPyuppsala` and the other cohort classes to reduce copy-paste
and prevent missing a new fuzz test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b043d015-fc24-4719-b2bf-99b689210252

📥 Commits

Reviewing files that changed from the base of the PR and between 196a12f and fa8c4e8.

📒 Files selected for processing (51)
  • .github/workflows/run-all-tests.yml
  • README.rst
  • docs/configuration.rst
  • fastkml/kml.py
  • fastkml/validator.py
  • pyproject.toml
  • pyuppsala-integration-notes.md
  • tests/atom_test.py
  • tests/base.py
  • tests/base_test.py
  • tests/config_test.py
  • tests/containers_test.py
  • tests/data_test.py
  • tests/features_test.py
  • tests/geometries/boundaries_test.py
  • tests/geometries/coordinates_test.py
  • tests/geometries/geometry_test.py
  • tests/geometries/linearring_test.py
  • tests/geometries/linestring_test.py
  • tests/geometries/multigeometry_test.py
  • tests/geometries/point_test.py
  • tests/geometries/polygon_test.py
  • tests/gx/data_test.py
  • tests/gx/track_test.py
  • tests/hypothesis/atom_test.py
  • tests/hypothesis/container_test.py
  • tests/hypothesis/data_test.py
  • tests/hypothesis/feature_test.py
  • tests/hypothesis/geometry_test.py
  • tests/hypothesis/gx/data_test.py
  • tests/hypothesis/gx/track_test.py
  • tests/hypothesis/kml_test.py
  • tests/hypothesis/links_test.py
  • tests/hypothesis/model_test.py
  • tests/hypothesis/multi_geometry_test.py
  • tests/hypothesis/network_link_control_test.py
  • tests/hypothesis/overlay_test.py
  • tests/hypothesis/style_test.py
  • tests/hypothesis/times_test.py
  • tests/hypothesis/views_test.py
  • tests/kml_test.py
  • tests/links_test.py
  • tests/model_test.py
  • tests/network_link_control_test.py
  • tests/overlays_test.py
  • tests/repr_eq_test.py
  • tests/styles_test.py
  • tests/times_test.py
  • tests/utils_test.py
  • tests/validator_test.py
  • tests/views_test.py

Comment on lines +54 to +72
cpython-pyuppsala:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
steps:
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip wheel
pip install -e ".[tests, pyuppsala]"
- name: Test with pytest
run: |
pytest tests

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Harden the new CI job: pin token permissions and disable credential persistence.

Static analysis flags this new job for default (overly broad) GITHUB_TOKEN permissions and for not disabling credential persistence on checkout, both of which are easy to close off.

🔒 Suggested hardening
   cpython-pyuppsala:
     runs-on: ubuntu-latest
+    permissions:
+      contents: read
     strategy:
       matrix:
         python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
     steps:
       - uses: actions/checkout@v7
+        with:
+          persist-credentials: false
       - name: Set up Python ${{ matrix.python-version }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cpython-pyuppsala:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
steps:
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip wheel
pip install -e ".[tests, pyuppsala]"
- name: Test with pytest
run: |
pytest tests
cpython-pyuppsala:
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip wheel
pip install -e ".[tests, pyuppsala]"
- name: Test with pytest
run: |
pytest tests
🧰 Tools
🪛 zizmor (1.26.1)

[warning] 60-60: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[warning] 54-71: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block

(excessive-permissions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/run-all-tests.yml around lines 54 - 72, The new
cpython-pyuppsala GitHub Actions job should be hardened by explicitly
restricting the job’s GITHUB_TOKEN permissions and disabling checkout credential
persistence. Update the workflow job definition for cpython-pyuppsala to set
only the minimal permissions it needs, and adjust the actions/checkout usage in
that job to avoid persisting credentials after checkout.

Source: Linters/SAST tools

Comment thread tests/model_test.py
Comment on lines +161 to +164


class TestModelPyuppsala(TestModel, Pyuppsala):
pass

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
ast-grep run --pattern 'class StdLibrary {
  $$$
}' --lang python tests/base.py

Repository: cleder/fastkml

Length of output: 470


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== tests/base.py outline ==\n'
ast-grep outline tests/base.py --view expanded || true

printf '\n== relevant class definitions ==\n'
rg -n "class (StdLibrary|Pyuppsala|Lxml|TestModelPyuppsala|TestModelLxml|TestGetGeometryPyuppsala|TestGetGeometryLxml)" tests -S

printf '\n== tests/base.py around StdLibrary and mixins ==\n'
python3 - <<'PY'
from pathlib import Path
p = Path("tests/base.py")
lines = p.read_text().splitlines()
for i, line in enumerate(lines, 1):
    if any(k in line for k in ["class StdLibrary", "class Pyuppsala", "class Lxml", "setup_method", "teardown_method", "def __init__"]):
        start = max(1, i-8)
        end = min(len(lines), i+30)
        print(f"\n--- lines {start}-{end} ---")
        for j in range(start, end+1):
            print(f"{j:4}: {lines[j-1]}")
PY

printf '\n== tests/model_test.py around target classes ==\n'
python3 - <<'PY'
from pathlib import Path
p = Path("tests/model_test.py")
lines = p.read_text().splitlines()
for start in [130, 150, 155]:
    end = min(len(lines), start+30)
    print(f"\n--- lines {start}-{end} ---")
    for j in range(start, end+1):
        print(f"{j:4}: {lines[j-1]}")
PY

printf '\n== tests/geometries/geometry_test.py relevant classes ==\n'
python3 - <<'PY'
from pathlib import Path
p = Path("tests/geometries/geometry_test.py")
lines = p.read_text().splitlines()
for i, line in enumerate(lines, 1):
    if "TestGetGeometryPyuppsala" in line or "TestGetGeometryLxml" in line:
        start = max(1, i-5)
        end = min(len(lines), i+8)
        print(f"\n--- lines {start}-{end} ---")
        for j in range(start, end+1):
            print(f"{j:4}: {lines[j-1]}")
PY

Repository: cleder/fastkml

Length of output: 17196


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,220p' tests/base.py | cat -n

Repository: cleder/fastkml

Length of output: 4880


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== tests/base.py outline =='
ast-grep outline tests/base.py --view expanded || true

echo
echo '== relevant definitions across tests/ =='
rg -n "class (StdLibrary|Pyuppsala|Lxml|TestModelPyuppsala|TestModelLxml|TestGetGeometryPyuppsala|TestGetGeometryLxml)" tests -S || true

echo
echo '== tests/base.py excerpts =='
python3 - <<'PY'
from pathlib import Path
p = Path("tests/base.py")
lines = p.read_text().splitlines()
keys = ["class StdLibrary", "class Pyuppsala", "class Lxml", "setup_method", "teardown_method"]
seen = set()
for i, line in enumerate(lines, 1):
    if any(k in line for k in keys):
        start = max(1, i-8)
        end = min(len(lines), i+25)
        if (start, end) in seen:
            continue
        seen.add((start, end))
        print(f"\n--- lines {start}-{end} ---")
        for j in range(start, end+1):
            print(f"{j:4}: {lines[j-1]}")
PY

echo
echo '== tests/model_test.py excerpts =='
python3 - <<'PY'
from pathlib import Path
p = Path("tests/model_test.py")
lines = p.read_text().splitlines()
for start in [130, 150, 155]:
    end = min(len(lines), start+30)
    print(f"\n--- lines {start}-{end} ---")
    for j in range(start, end+1):
        print(f"{j:4}: {lines[j-1]}")
PY

echo
echo '== tests/geometries/geometry_test.py excerpts =='
python3 - <<'PY'
from pathlib import Path
p = Path("tests/geometries/geometry_test.py")
lines = p.read_text().splitlines()
for i, line in enumerate(lines, 1):
    if "TestGetGeometryPyuppsala" in line or "TestGetGeometryLxml" in line:
        start = max(1, i-5)
        end = min(len(lines), i+8)
        print(f"\n--- lines {start}-{end} ---")
        for j in range(start, end+1):
            print(f"{j:4}: {lines[j-1]}")
PY

Repository: cleder/fastkml

Length of output: 15868


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

for path in ["tests/base.py", "tests/model_test.py", "tests/geometries/geometry_test.py"]:
    p = Path(path)
    print(f"\n### {path} ({sum(1 for _ in p.open())} lines)")
    lines = p.read_text().splitlines()
    for needle in ["class StdLibrary", "class Pyuppsala", "class Lxml",
                   "class TestModelPyuppsala", "class TestModelLxml",
                   "class TestGetGeometryPyuppsala", "class TestGetGeometryLxml",
                   "setup_method", "teardown_method"]:
        for i, line in enumerate(lines, 1):
            if needle in line:
                start = max(1, i-6)
                end = min(len(lines), i+20)
                print(f"\n-- {needle} at line {i} --")
                for j in range(start, end+1):
                    print(f"{j:4}: {lines[j-1]}")
                break
PY

Repository: cleder/fastkml

Length of output: 6806


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

for path in ["tests/base.py", "tests/model_test.py", "tests/geometries/geometry_test.py"]:
    p = Path(path)
    lines = p.read_text().splitlines()
    print(f"\n### {path} ({len(lines)} lines)")
    for needle in ["class StdLibrary", "class Pyuppsala", "class Lxml",
                   "class TestModelPyuppsala", "class TestModelLxml",
                   "class TestGetGeometryPyuppsala", "class TestGetGeometryLxml",
                   "setup_method", "teardown_method"]:
        for i, line in enumerate(lines, 1):
            if needle in line:
                start = max(1, i-6)
                end = min(len(lines), i+20)
                print(f"\n-- {needle} at line {i} --")
                for j in range(start, end+1):
                    print(f"{j:4}: {lines[j-1]}")
                break
PY

Repository: cleder/fastkml

Length of output: 6806


Put the backend mixin first here (and in TestModelLxml). TestModel inherits StdLibrary, so class TestModelPyuppsala(TestModel, Pyuppsala) resolves setup_method from StdLibrary before Pyuppsala, and the pyuppsala etree never gets installed. Swap the bases to class TestModelPyuppsala(Pyuppsala, TestModel); TestModelLxml has the same issue.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/model_test.py` around lines 161 - 164, The backend mixin order is wrong
in the test classes, so `setup_method` is resolving from `StdLibrary` through
`TestModel` before the backend-specific mixin can install its etree behavior.
Update `TestModelPyuppsala` to put `Pyuppsala` before `TestModel`, and make the
same base-class order fix in `TestModelLxml` so the backend mixin is applied
first during test setup.

Comment thread tests/utils_test.py
Comment on lines 213 to +218
class TestFindAllLxml(Lxml):
"""Run the tests using lxml."""


class TestFindAllPyuppsala(Pyuppsala):
"""Run the tests using pyuppsala."""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -A2 'class TestFindAll' tests/utils_test.py

Repository: cleder/fastkml

Length of output: 403


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline tests/utils_test.py --view expanded

Repository: cleder/fastkml

Length of output: 572


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the test class hierarchy and any pytest-relevant markers.
sed -n '1,260p' tests/utils_test.py

Repository: cleder/fastkml

Length of output: 7422


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check how pytest treats inherited test methods in classes with multiple inheritance.
python3 - <<'PY'
import sys, textwrap, tempfile, subprocess, os, json, pathlib

print("Python:", sys.version)
PY

Repository: cleder/fastkml

Length of output: 210


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,260p' tests/utils_test.py | cat -n

Repository: cleder/fastkml

Length of output: 8948


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant class definitions and nearby context in tests/utils_test.py.
sed -n '1,240p' tests/utils_test.py | cat -n | sed -n '1,240p'

Repository: cleder/fastkml

Length of output: 8948


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -A6 -B6 'class (StdLibrary|Lxml|Pyuppsala|TestFindAll)' tests/utils_test.py

Repository: cleder/fastkml

Length of output: 897


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check pytest collection semantics with a minimal in-memory example, if pytest is available.
python3 - <<'PY'
import importlib.util, sys, tempfile, textwrap, subprocess, pathlib, os

spec = importlib.util.find_spec("pytest")
print("pytest_available=", bool(spec))
if not spec:
    sys.exit(0)

tmp = tempfile.mkdtemp()
path = pathlib.Path(tmp) / "test_sample.py"
path.write_text(textwrap.dedent("""
class Base:
    def test_base(self):
        pass

class Child(Base):
    pass

class TestBase:
    def test_base(self):
        pass

class TestChild(TestBase):
    pass
"""))
r = subprocess.run([sys.executable, "-m", "pytest", "--collect-only", "-q", str(path)], capture_output=True, text=True)
print("returncode=", r.returncode)
print(r.stdout)
print(r.stderr)
PY

Repository: cleder/fastkml

Length of output: 292


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -A8 -B4 'class (StdLibrary|Lxml|Pyuppsala)\b' tests/utils_test.py

Repository: cleder/fastkml

Length of output: 152


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' tests/utils_test.py | cat -n | sed -n '1,220p'

Repository: cleder/fastkml

Length of output: 8948


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -A6 -B4 'class (Lxml|Pyuppsala)\b|from .* import (Lxml|Pyuppsala)\b' tests

Repository: cleder/fastkml

Length of output: 31792


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,180p' tests/base.py | cat -n

Repository: cleder/fastkml

Length of output: 4880


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,140p' tests/base.py | cat -n

Repository: cleder/fastkml

Length of output: 4880


Add TestFindAll to the backend test classes.
TestFindAllLxml and TestFindAllPyuppsala only inherit the backend mixins, so they don’t pick up the shared test_* methods. Add TestFindAll to each base list.

🐛 Proposed fix
-class TestFindAllLxml(Lxml):
+class TestFindAllLxml(Lxml, TestFindAll):
     """Run the tests using lxml."""


-class TestFindAllPyuppsala(Pyuppsala):
+class TestFindAllPyuppsala(Pyuppsala, TestFindAll):
     """Run the tests using pyuppsala."""
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class TestFindAllLxml(Lxml):
"""Run the tests using lxml."""
class TestFindAllPyuppsala(Pyuppsala):
"""Run the tests using pyuppsala."""
class TestFindAllLxml(Lxml, TestFindAll):
"""Run the tests using lxml."""
class TestFindAllPyuppsala(Pyuppsala, TestFindAll):
"""Run the tests using pyuppsala."""
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/utils_test.py` around lines 213 - 218, The backend test classes are
missing the shared TestFindAll mixin, so they only inherit backend-specific
behavior and skip the common test_* cases. Update the class definitions for
TestFindAllLxml and TestFindAllPyuppsala to include TestFindAll in their base
classes, alongside Lxml and Pyuppsala, so both backend variants run the shared
find-all tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pyuppsala 0.8.0

2 participants