feat(arxjit): add clear errors when source extraction is unavailable#98
Conversation
Complete the extraction stage's error contract: every failure now raises SourceExtractionError with a located diagnostic. Unretrievable source (exec- or REPL-defined functions, C builtins, self-referential wrappers) and unparsable source blocks are translated at the inspect/ast boundary. SyntaxError locations are mapped to real file lines, including through the synthetic wrapper used for indented definitions, and the offset is used as the one-based character column the Diagnostic contract requires (a zero offset is normalized to None). A __wrapped__ cycle during filename attribution falls back to the wrapper's own file instead of letting ValueError escape.
|
thanks for working on that @Jaskirat-s7 I found two compatibility gaps in the new error-translation contract.
The parse wrapper catches only SyntaxError. On Python 3.10, 3.11, and 3.12, ast.parse() is documented to raise ValueError when the retrieved source contains a null character. That means a corrupted or subsequently modified source file can still bypass SourceExtractionError, contradicting the new “every failure” guarantee. A regression test could make getsourcelines() return: [ On those Python versions, the raw ValueError escapes. Suggested fix: catch ValueError around ast.parse() and translate it to SourceExtractionError, generally without line or column information because ValueError does not provide structured syntax-location attributes. Location: packages/arxjit/src/arxjit/source.py:178–193
The implementation stores exc.offset directly under the assumption that it always points into the original source line. Python explicitly documents an exception for malformed f-string replacement fields: their offsets may refer to a synthetic string constructed from the replacement expression rather than the original file line. For example, an error in f"prefix {a b}" can carry an offset into text such as (a b)\n. Storing that offset directly produces a misleading diagnostic column. A cross-version regression test should cover a malformed f-string, especially on Python 3.10 and 3.11. A safe fallback is to set column=None when exc.text does not match the corresponding retrieved source line; alternatively, implement explicit f-string location recovery. Location: packages/arxjit/src/arxjit/source.py:185–189 Summary: The general design is good: retrieval exceptions are consistently chained, the synthetic-wrapper line adjustment is correct for ordinary syntax errors, and the added tests are focused. All three workflows currently pass. I would address these two cases before merging because the PR’s primary promise is that all extraction failures become correctly located SourceExtractionError instances. |
…ation Address both review findings on arxlang#98: 1. ast.parse raises ValueError instead of SyntaxError for source containing null bytes on older Python versions (observed on 3.10; documented for 3.10-3.12). The parse wrapper now translates ValueError to SourceExtractionError as well, without location information since ValueError carries no syntax-location attributes. 2. SyntaxError.offset is not always a real-file column: for malformed f-string replacement fields, Python 3.10/3.11 report the offset into a synthetic string built from the replacement expression. The diagnostic column is now produced by _column_of, which trusts the offset only when SyntaxError.text matches the parsed line it claims to come from and falls back to None otherwise; newer versions report real file locations and keep their column. The parse-failure handling moved into a _parse_block helper, keeping extract_source below the mccabe threshold. Regression tests: the null-byte block, a deterministic ValueError-translation test (stubbed parser, since only 3.10 triggers it naturally), a cross-version malformed-f-string test (strict expectations on locally verified versions, either-way on the unverified 3.12 transition), and direct branch tests of _column_of with synthetic SyntaxError instances.
|
Thanks for catching these @xmnlab! Both gaps are closed in new commit:
56 tests, coverage still 100%. Ready for another look! |
|
thanks for addressing the previous issues @Jaskirat-s7 Just one comment from my side: Medium — source retrieval can still leak tokenizer exceptions The retrieval wrapper catches only: except (OSError, TypeError, ValueError) However, inspect.getsourcelines() tokenizes the source block internally. CPython’s inspect.getblock() calls tokenize.generate_tokens() but does not catch tokenize.TokenError; it can also re-raise some SyntaxError instances. A concrete case:
inspect.getsourcelines() raises a raw tokenize.TokenError("EOF in multi-line string", ...), bypassing the new SourceExtractionError contract. I recommend translating tokenize.TokenError and retrieval-time SyntaxError in the same block. A TokenError includes a (line, column) tuple, but returning no location would also be acceptable unless its offset is carefully mapped to the original file. Summary Changes requested. The two original compatibility gaps are fixed well, but the retrieval-time tokenizer leak still contradicts this PR’s central guarantee that every extraction failure becomes a SourceExtractionError. |
inspect.getsourcelines tokenizes the source file, so a corrupted or since-modified source (for example an unterminated triple-quoted string) can surface a raw tokenize.TokenError, which is not a SyntaxError subclass and escaped the retrieval except clause, bypassing the PR's guarantee that every extraction failure becomes a SourceExtractionError. Add tokenize.TokenError and SyntaxError to the retrieval except clause and translate both like every other retrieval failure. No file location is attached because retrieval fails before the block's start line is known, so mapping the error's position to the original file is not possible. A _retrieval_reason helper keeps only the human-readable message from each exception type (TokenError's (message, position) tuple reduces to its message; SyntaxError drops its embedded line-number suffix), so no misleading position leaks into the diagnostic text. Tests: a real end-to-end reproduction (import a module, corrupt its file, clear linecache, extract) plus deterministic stubs for the TokenError and retrieval-time SyntaxError branches and a direct test of _retrieval_reason.
|
Thanks @xmnlab , good catch, this is a real leak. Fixed in new commit: inspect.getsourcelines tokenizes the file, and tokenize.TokenError isn't a SyntaxError subclass, so it slipped past the retrieval except. I reproduced your exact scenario (import a module, corrupt its file with an unterminated triple-quote, clear linecache, extract) on 3.10/3.11/3.13/3.14 — all four leak TokenError("EOF in multi-line string", ...). Now both tokenize.TokenError and retrieval-time SyntaxError are translated in the same except block, exactly as you suggested. I went with no location: the failure happens before getsourcelines returns, so block_start doesn't exist yet and the error's (line, col) genuinely can't be mapped to the file , your "returning no location would also be acceptable" applies directly. One extra thing I tightened while in there: I made sure the message text never embeds a location either. str(TokenError) would leak the raw (message, position) tuple and str(SyntaxError) appends its own block-relative line number, so a small helper now keeps only the clean human message from each. Tests: your real corrupted-file reproduction, plus deterministic stubs for each branch, all verified to fail without the fix. 60 tests, coverage still 100%. |
|
thanks for working on that @Jaskirat-s7 ! appreciate it |
Summary
Sprint 2, source extraction part 2 — the follow-up promised in #96, completing the last bullet of wiki "Proposed Issue 3":
With this PR, every extraction failure raises
SourceExtractionErrorcarrying one located, structuredDiagnostic(contract from #95) instead of leaking a raw stdlib exception:OSError)OSErrorSourceExtractionError, filename fromco_filename(e.g."<string>")TypeError)TypeErrorSourceExtractionError, filename"<unknown>"__wrapped__cycle (ValueError)ValueErrorSourceExtractionError; filename attribution falls back to the wrapper's own fileast.parse(SyntaxError)SyntaxErrorSourceExtractionErrorwith the error mapped to the real file line and columnSourceExtractionError(#96)Details worth reviewing
"if True:\n" + block, which shiftsSyntaxError.linenoby one relative to the block; the translation uses the sameline_deltaas the success path, so the diagnostic points at the real file line either way. There's a dedicated test for the indented case (test_indented_unparsable_source_maps_to_file_lines).SyntaxError.offsetis already a one-based character column (probed on CPython 3.10/3.11/3.13/3.14), and since feat(arxjit): add source extraction for decorated functions #96 parses the text with its original indentation it points into the real file line — so it's stored directly, satisfying theDiagnostic.columncontract from feat(arxjit): add structured diagnostics and the public error hierarchy #95. A zero offset (no usable column) is normalized toNone.__wrapped__cycles:inspect.unwrapinside_filename_ofcan raiseValueErroron a self-referential wrapper; it now falls back to attributing the wrapper's own file and lets the source retrieval report the cycle, which is then translated like the other retrieval failures — so the documented "failures raiseSourceExtractionError" contract has no gaps.__cause__(raise ... from exc) for debugging.Verification
__wrapped__cycle, unparsable block, unparsable indented block), arxjit coverage 100%.douki syncidempotent.Refs: #96 (extraction pipeline), #95 (diagnostics/error layer). The normalized-source follow-up remains tracked in #97.