Skip to content

adjust parsing to raise nicer exceptions - #213

Closed
snoyer wants to merge 2 commits into
mathandy:masterfrom
snoyer:use-syntax-errors
Closed

adjust parsing to raise nicer exceptions#213
snoyer wants to merge 2 commits into
mathandy:masterfrom
snoyer:use-syntax-errors

Conversation

@snoyer

@snoyer snoyer commented Aug 30, 2023

Copy link
Copy Markdown

The current parsing works great on well formed path but could be improved when it comes to handling invalid cases:

  1. the "Unallowed implicit command" error message mentions an internal token index
  2. invalid tokens are silently dropped
  3. the parser can run out of tokens and let an IndexError: pop from empty list through
Path('1,2')
ValueError: Unallowed implicit command in 1,2, position -1

Path('M 100 100 L 200 200 Z 100 200')
ValueError: Unallowed implicit command in M 100 100 L 200 200 Z 100 200, position 7

Path('M 0 1 L 0 0 L 0 0z1')
ValueError: Unallowed implicit command in M 0 1 L 0 0 L 0 0z1, position 8

Path('M 1 2 3')
IndexError: pop from empty list

Path('M 0 1 N 2 3')
Path(Line(start=1j, end=(2+3j)))

Path('M 0 1 C 1 2 3 4\n      5 foo')
IndexError: pop from empty list

This PR catches cases 2. and 3. and raises a standard SyntaxError highlighting the error location in the d string in all 3 cases:

Path('1,2')
  File "<svg-d-string>", line 1
    1,2
    ^
SyntaxError: missing command

Path('M 100 100 L 200 200 Z 100 200')
  File "<svg-d-string>", line 1
    M 100 100 L 200 200 Z 100 200
                          ^^^
SyntaxError: missing command

Path('M 0 1 L 0 0 L 0 0z1')
  File "<svg-d-string>", line 1
    M 0 1 L 0 0 L 0 0z1
                      ^
SyntaxError: missing command

Path('M 1 2 3')
  File "<svg-d-string>", line 1
    M 1 2 3
          ^
SyntaxError: not enough arguments

Path('M 0 1 N 2 3')
  File "<svg-d-string>", line 1
    M 0 1 N 2 3
         ^^^
SyntaxError: invalid token ' N '

Path('M 0 1 C 1 2 3 4\n      5 foo')
  File "<svg-d-string>", line 2
    5 foo
     ^^^^
SyntaxError: invalid token ' foo'

@mathandy

mathandy commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Closing due to lack of activity.

@mathandy mathandy closed this Aug 26, 2026
@snoyer

snoyer commented Aug 27, 2026

Copy link
Copy Markdown
Author

Closing due to lack of activity.

Should I rebase for some review activity or is this PR not of interest?

@mathandy mathandy reopened this Aug 28, 2026
@mathandy

Copy link
Copy Markdown
Owner

Hey @snoyer thanks for the PR and for the followup after I closed it.

I like the feature/direction, looking at it, I think it's going to require some work. If you want to rebase and fix up the below issues, I'll take another look and I think we can merge something in along these lines.

I had claude write up a review, it summarized a lot of my quick impressions pretty well. If you have any questions, I can add more explanation.

# Review: adjust parsing to raise nicer exceptions

*Reviewed on the PR's own base (all 10 `test_parsing` tests pass there). Note the base now predates two merged PRs (#245, #246) that touch the same code — see the conflict notes below.*

## Overview

This PR rewrites `Path._parse_path`/`_tokenize_path` error handling so malformed `d`-strings raise a `SyntaxError` that pinpoints the offending token with line/column info and a caret, instead of the current mix of a confusing `ValueError` (with an internal token index), silently dropped garbage tokens, and bare `IndexError: pop from empty list`. Mechanically: the tokenizer emits `(token, lineno, offset)` tuples including *invalid* tokens (nothing silently dropped), all `float(elements.pop())` calls become a `pop_float()` helper that converts failures into located errors, and a `_syntax_error` helper builds the `SyntaxError` with a py<3.10 fallback. Three targeted error tests replace the single old one.

The goal is genuinely good — error messages with carets into the `d`-string are a real usability win, and no longer silently ignoring `M 0 1 N 2 3` fixes silent-corruption behavior. But there is one design problem and a few bugs to resolve.

## Issues

### Blocking / design

- **Breaking exception-type change.** `SyntaxError` is not a subclass of `ValueError`, so any caller doing `except ValueError` around `parse_path` breaks (verified on the branch). It also disagrees with master, where #246 now raises `ValueError` for the truncated case. A `class PathSyntaxError(SyntaxError, ValueError)` would keep the caret rendering *and* backward compatibility; as written this needs at least a changelog entry.
- **Conflicts with merged #245 (packed arc flags) — the rebase is not mechanical.** Master's tokenizer now special-cases arc arguments so `a1.6,1.6 0 01-1.1 2` parses (`_tokenize_arc_args`, `ARC_FLAG_RE`). This PR replaces the tokenizer wholesale with a single `TOKEN_RE`, which lexes `01` as one float — on this branch that input fails with "not enough arguments". A rebase must port flag-aware arc tokenizing into the new position-tracking tokenizer; #246's `COMMAND_NUM_ARGS` check then becomes redundant and can be removed in favor of `pop_float`.

### Bugs

- **Noisy chained tracebacks.** `raise self._syntax_error(...)` inside `except ValueError:`/`except IndexError:` produces "During handling of the above exception, another exception occurred" with the internal `float()`/`pop()` traceback shown first (verified). Needs `raise ... from None`.
- **`end_lineno` off-by-one in `_syntax_error`.** The 6-tuple is `(filename, lineno+1, offset+1, line, lineno, end_offset+1)` — the 5th element is `end_lineno`, which must be 1-based like the 2nd; passing the 0-based `lineno` makes `end_lineno < lineno`. Should be `lineno + 1`.
- **Empty-last-line edge case.** In the `IndexError` branch, `end = len(line) - 1` gives `-1` when the last line is empty (e.g. `parse_path('M 1\n\n')`), producing offset 0 — and the caret points at the wrong line anyway. Better: walk back to the last non-empty line, or reuse the last-popped token's position.

### Minor / style

- Error spans include raw separators — `invalid token ' N '` with the caret span covering the spaces. Trimming to the stripped token would be cleaner.
- `_tokenize_path(*pathdef_lines)` varargs is odd for a private helper; just take the list.
- `def _syntax_error(cls, msg, lineno, line, offset,end_offset)` — missing space (PEP 8); the `except IndexError` fallback for py<3.10 deserves a comment explaining *why* the 6-tuple raises there.
- Tests use bare `assert` inside `unittest` classes; the file's convention is `self.assert*` (e.g. `self.assertIn("missing command", e.exception.msg)`). Also missing: a test that a valid multi-line path still parses (offset bookkeeping), and — post-rebase — the packed-arc-flag cases from #245's tests must keep passing.

## Verdict

Worth reviving — located syntax errors are strictly better than what master has, and #246 only addressed one of the three failure modes this covers. But it can't merge as-is: it needs a rebase that reworks the tokenizer around #245's arc-flag handling, a decision on exception type (recommend a `SyntaxError`+`ValueError` hybrid for compatibility), and the bug fixes above. The PR's tests and design give a clear spec for that work.

@snoyer

snoyer commented Aug 28, 2026

Copy link
Copy Markdown
Author

If we're going to be engaging in this kind of proxying you might as well have claude implement the fix directly.

@snoyer snoyer closed this Aug 28, 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.

2 participants