Skip to content

Register namespaces used only inside extensions before serializing - #297

Closed
afonsojanu wants to merge 1 commit into
tkrajina:devfrom
afonsojanu:fix/extension-nested-default-namespace
Closed

afonsojanu wants to merge 1 commit into
tkrajina:devfrom
afonsojanu:fix/extension-nested-default-namespace

Conversation

@afonsojanu

Copy link
Copy Markdown

Fixes #234.

An extension element can declare its own default namespace that never shows up anywhere else in the document. The classic real-world example, reported independently by three different users on this issue over the years, is a Garmin gpx_style block:

<extensions>
  <line xmlns="http://www.topografix.com/GPX/gpx_style/0/2">
    <color>000000</color>
  </line>
</extensions>

to_xml() can only write a prefix for a namespace that's already in nsmap, and nothing currently puts this one there, since it only appears nested inside an extensions element rather than on the gpx root or one of the other attributes the parser's regex scans for. When _resolve_prefix in gpxfield.py hits a Clark-notation tag whose namespace it can't find, it falls back to gluing the raw URI onto the local name as if it were a prefix:

<http://www.topografix.com/GPX/gpx_style/0/2:line>

which isn't well formed XML and can't be parsed back at all. I confirmed this against current dev with the file from the issue: the round trip through to_xml() and back raises ParseError: not well-formed (invalid token).

@tkrajina had already dug into the root cause and committed a failing test for this back in 2021 (test_extension_with_xmlns, referenced in the issue thread) but never got to a fix.

This adds GPX._register_extension_namespaces, called from to_xml right before the root element is written. It walks every extensions list in the document, including the root, metadata, waypoints, routes and their points, tracks and their segments and points, and recurses into nested children such as the line element above. Any namespace it finds that isn't already in nsmap gets a fresh nsN prefix assigned to it, which is then declared normally on the gpx root alongside the rest, so _resolve_prefix can resolve it like any other namespace instead of falling back to the broken path.

I've added a regression test with the same shape as the maintainer's 2021 one (adapted since ElementTree.getchildren() no longer exists on current Python), checking that the serialized output stays parseable and the extension data survives the round trip. Full test suite (146 tests) and mypy --strict both pass with no new findings compared to dev.

An extension element can declare its own default namespace that is
never mentioned anywhere else in the document, the classic example
being a Garmin gpx_style block:

    <extensions>
      <line xmlns="http://www.topografix.com/GPX/gpx_style/0/2">
        <color>000000</color>
      </line>
    </extensions>

to_xml() only knows how to write prefixes for namespaces already
present in nsmap, and nothing ever adds this one there, since it only
shows up nested inside an extensions element rather than on the gpx
root or one of the other namespaced attributes the parser scans for.
When _resolve_prefix in gpxfield.py hits a Clark-notation tag whose
namespace it cannot find, it falls back to gluing the raw URI onto
the local name as if it were a prefix, producing something like

    <http://www.topografix.com/GPX/gpx_style/0/2:line>

which is not well formed XML and cannot be parsed back at all. Three
separate users ran into this over the years on issue #234, each with
real files produced by different GPS hardware or software, and the
maintainer confirmed it and even committed a failing test for it back
in 2021 without getting the chance to fix it.

This adds GPX._register_extension_namespaces, called from to_xml
before the root element is written, which walks every extensions list
in the document (root, metadata, waypoints, routes and their points,
tracks and their segments and points), including nested children such
as the line element above, and assigns a fresh nsN prefix to any
namespace it finds that isn't already in nsmap. That prefix then gets
declared normally on the gpx root alongside the others, and
_resolve_prefix can find it like any other namespace.

Added a regression test with the same shape as the maintainer's 2021
one (adapted since ElementTree.getchildren() no longer exists) that
checks the output stays parseable and the extension data survives a
round trip.
@afonsojanu

Copy link
Copy Markdown
Author

Closing this one too, same reason: starting fresh and clearing out everything I had open.

@afonsojanu afonsojanu closed this Oct 5, 2026
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.

malformed xml

2 participants