Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ The **third number** is for emergencies when we need to start branches for older
- New `gpx validate` CLI options:
- `--strict`: Treat warnings as failures (non-zero exit code).
- `--json`: Output a machine-readable validation report.
- New `--strict` option on the `gpx info`, `gpx edit`, `gpx merge` and `gpx convert` CLI commands. When given, each input is validated against the GPX 1.1 schema first: warnings are printed to stderr and the command aborts (non-zero exit code) if any schema errors are found. For `gpx convert`, validation only applies to GPX input. The default keeps the existing lenient behavior.

### Changed

Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,12 @@ gpx merge file1.gpx file2.gpx file3.gpx -o merged.gpx
gpx convert input.gpx -o output.geojson
gpx convert input.gpx -o output.kml
gpx convert input.geojson -o output.gpx

# Validate the input first with --strict (info, edit, merge and convert):
# prints warnings and aborts on schema errors before doing any work
gpx info --strict path/to/file.gpx
gpx edit --strict input.gpx -o output.gpx --simplify 10
gpx merge --strict file1.gpx file2.gpx -o merged.gpx
```

<!-- end docs-include-usage -->
25 changes: 17 additions & 8 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ options:

## gpx info

Display information and statistics about a GPX file.
Display information and statistics about a GPX file. Use `--strict` to validate the input against the GPX 1.1 schema first, printing warnings and aborting on schema errors.

<!--[[[cog
help_output(["info", "--help"])
Expand All @@ -91,7 +91,7 @@ help_output(["info", "--help"])
Running `gpx info --help` or `python -m gpx info --help` shows a list of all of the available options and arguments:

```shell
usage: gpx info [-h] [--json] <INPUT_FILE>
usage: gpx info [-h] [--json] [--strict] <INPUT_FILE>

Display detailed information and statistics about a GPX file.

Expand All @@ -101,13 +101,15 @@ positional arguments:
options:
-h, --help show this help message and exit
--json Output information in JSON format
--strict Validate the input against the GPX 1.1 schema first; print
warnings and abort on schema errors
```

<!--[[[end]]]-->

## gpx edit

Edit a GPX file with various transformations.
Edit a GPX file with various transformations. Use `--strict` to validate the input against the GPX 1.1 schema first, printing warnings and aborting on schema errors.

<!--[[[cog
help_output(["edit", "--help"])
Expand All @@ -126,7 +128,7 @@ usage: gpx edit [-h] -o <OUTPUT_FILE> [--min-lat LATITUDE]
[--strip-author] [--strip-copyright] [--strip-time]
[--strip-keywords] [--strip-links] [--strip-all-metadata]
[--strip-extensions] [--precision DIGITS]
[--elevation-precision DIGITS]
[--elevation-precision DIGITS] [--strict]
<INPUT_FILE>

Edit a GPX file with various transformations like cropping, trimming,
Expand All @@ -139,6 +141,8 @@ options:
-h, --help show this help message and exit
-o, --output-file <OUTPUT_FILE>
Path to the output file
--strict Validate the input against the GPX 1.1 schema first;
print warnings and abort on schema errors

crop options:
Crop to a geographic bounding box
Expand Down Expand Up @@ -218,7 +222,7 @@ precision options:

## gpx merge

Merge multiple GPX files into a single file.
Merge multiple GPX files into a single file. Use `--strict` to validate every input against the GPX 1.1 schema first, printing warnings and aborting if any input has schema errors.

<!--[[[cog
help_output(["merge", "--help"])
Expand All @@ -227,7 +231,8 @@ help_output(["merge", "--help"])
Running `gpx merge --help` or `python -m gpx merge --help` shows a list of all of the available options and arguments:

```shell
usage: gpx merge [-h] -o <OUTPUT_FILE> <INPUT_FILE> [<INPUT_FILE> ...]
usage: gpx merge [-h] -o <OUTPUT_FILE> [--strict]
<INPUT_FILE> [<INPUT_FILE> ...]

Merge multiple GPX files into a single GPX file.

Expand All @@ -238,13 +243,15 @@ options:
-h, --help show this help message and exit
-o, --output-file <OUTPUT_FILE>
Path to the output file
--strict Validate the input against the GPX 1.1 schema first;
print warnings and abort on schema errors
```

<!--[[[end]]]-->

## gpx convert

Convert between GPX, GeoJSON, and KML formats.
Convert between GPX, GeoJSON, and KML formats. Use `--strict` to validate GPX input against the GPX 1.1 schema first, printing warnings and aborting on schema errors (ignored for non-GPX input).

<!--[[[cog
help_output(["convert", "--help"])
Expand All @@ -254,7 +261,7 @@ Running `gpx convert --help` or `python -m gpx convert --help` shows a list of a

```shell
usage: gpx convert [-h] -o <OUTPUT_FILE> [-f {gpx,geojson,kml}]
[-t {gpx,geojson,kml}]
[-t {gpx,geojson,kml}] [--strict]
<INPUT_FILE>

Convert GPX files to other file formats or vice versa.
Expand All @@ -272,6 +279,8 @@ options:
-t, --to-format {gpx,geojson,kml}
Output format (default: auto-detect from file
extension)
--strict Validate the input against the GPX 1.1 schema first;
print warnings and abort on schema errors
```

<!--[[[end]]]-->
55 changes: 53 additions & 2 deletions src/gpx/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from pathlib import Path
from typing import TYPE_CHECKING

from .io import convert_file, read_gpx
from .io import convert_file, detect_format, read_gpx
from .operations import (
crop,
reduce_precision,
Expand All @@ -29,7 +29,7 @@
trim,
)
from .operations import merge as merge_op
from .validation import ValidationResult
from .validation import InvalidGPXError, ValidationResult
from .validation import validate as validate_gpx

if TYPE_CHECKING:
Expand Down Expand Up @@ -215,6 +215,38 @@ def _validate_text_output(
return 1 if failed else 0


def _add_strict_argument(parser: argparse.ArgumentParser) -> None:
"""Add the shared ``--strict`` option to a file-reading subcommand."""
parser.add_argument(
"--strict",
action="store_true",
help="Validate the input against the GPX 1.1 schema first; print "
"warnings and abort on schema errors",
)


def _validate_or_raise(file_path: Path) -> None:
"""Validate a GPX file, printing warnings and raising on schema errors.

Any validation warnings are printed to stderr; any schema errors raise
:class:`~gpx.validation.InvalidGPXError`, which the CLI reports as an error
and exits non-zero. This backs the ``--strict`` option of the file-reading
subcommands, mirroring ``read_gpx(..., strict=True)`` while also surfacing
warnings.
"""
result = validate_gpx(file_path)

if result.warnings:
n_warnings = len(result.warnings)
noun = "warning" if n_warnings == 1 else "warnings"
print(f"⚠ {file_path}: {n_warnings} {noun}", file=sys.stderr)
for issue in result.warnings:
print(f" {issue}", file=sys.stderr)

if not result.is_valid:
raise InvalidGPXError(result)


def _add_info_parser(
subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
) -> None:
Expand All @@ -235,6 +267,7 @@ def _add_info_parser(
action="store_true",
help="Output information in JSON format",
)
_add_strict_argument(info_parser)
info_parser.set_defaults(func=info)


Expand All @@ -246,6 +279,9 @@ def info(args: argparse.Namespace) -> int:
print(f"Error: File not found: {file_path}", file=sys.stderr)
return 1

if args.strict:
_validate_or_raise(file_path)

gpx = read_gpx(file_path)

info = _gather_gpx_info(gpx)
Expand Down Expand Up @@ -696,6 +732,7 @@ def _add_edit_parser(
help="Number of decimal places for elevation (e.g., 1)",
)

_add_strict_argument(edit_parser)
edit_parser.set_defaults(func=edit)


Expand All @@ -708,6 +745,9 @@ def edit(args: argparse.Namespace) -> int: # noqa: C901, PLR0912
print(f"Error: File not found: {input_path}", file=sys.stderr)
return 1

if args.strict:
_validate_or_raise(input_path)

gpx = read_gpx(input_path)

# Apply crop
Expand Down Expand Up @@ -852,6 +892,7 @@ def _add_merge_parser(
metavar="<OUTPUT_FILE>",
dest="output_file",
)
_add_strict_argument(parser)
parser.set_defaults(func=merge)


Expand All @@ -866,6 +907,11 @@ def merge(args: argparse.Namespace) -> int:
print(f"Error: File not found: {file_path}", file=sys.stderr)
return 1

# In strict mode, validate every input before merging any of them.
if args.strict:
for file_path in files:
_validate_or_raise(file_path)

# Read and merge all GPX files
merged_gpx = merge_op(read_gpx(file_path) for file_path in files)

Expand Down Expand Up @@ -916,6 +962,7 @@ def _add_convert_parser(
choices=["gpx", "geojson", "kml"],
help="Output format (default: auto-detect from file extension)",
)
_add_strict_argument(parser)
parser.set_defaults(func=convert)


Expand All @@ -928,6 +975,10 @@ def convert(args: argparse.Namespace) -> int:
print(f"Error: File not found: {input_path}", file=sys.stderr)
return 1

# Schema validation only applies to GPX input.
if args.strict and (args.from_format or detect_format(input_path)) == "gpx":
_validate_or_raise(input_path)

try:
input_format, output_format = convert_file(
input_path,
Expand Down
Loading