Error handling¶
Every error the codec raises by intent inherits from
oaknut.basic.BASICError, which is itself a
oaknut.exception.DataError — part of the categorised hierarchy
every oaknut package shares. A tokeniser or de-tokeniser failure is
always a problem with the program (bad source, or a malformed token
stream), never a bug, so these print without a traceback at a CLI
boundary and carry ExitCode.DATA_ERR.
The hierarchy¶
OaknutException # root (oaknut.exception)
└── DataError # ExitCode.DATA_ERR (65)
└── BASICError # any tokeniser / de-tokeniser error
├── TokeniseError # source text in
│ ├── UnnumberedLineError
│ ├── AlreadyNumberedError
│ ├── LineNumberRangeError
│ ├── LineNumberOrderError
│ └── LineTooLongError
└── DetokeniseError # token stream in
├── MissingLineMarkerError
├── TruncatedProgramError
└── InvalidLineLengthError
The two families mirror the two directions. A
TokeniseError is raised while turning source into
a program; a DetokeniseError while turning a
program back into source.
Structured attributes¶
Each concrete class carries the specifics of the fault as attributes, so a caller can build its own message or point an editor at the problem rather than scraping the string form.
Every TokeniseError carries the offending source
line:
from oaknut.basic import tokenise, UnnumberedLineError
try:
tokenise("PRINT") # no line number
except UnnumberedLineError as exc:
exc.line_index # 1 (1-based line in the source)
exc.line_text # 'PRINT'
Some add more — LineTooLongError reports the
line_number and the over-long length;
AlreadyNumberedError the line_number it found.
Every DetokeniseError carries the byte offset
into the program where the fault was found:
from oaknut.basic import detokenise, MissingLineMarkerError
try:
detokenise(b"\x99")
except MissingLineMarkerError as exc:
exc.offset # 0
exc.found # 0x99 (the byte seen instead of &0D)
Actionable notes¶
Where a fix is obvious, the exception attaches it as a PEP 678 note, so
oaknut.exception.handled_errors() renders it under the message at a
CLI boundary. Asking to auto-number already-numbered source, for
instance, suggests dropping --start / --step.
Catching errors¶
Catch at whatever breadth suits the call site:
from oaknut.basic import BASICError, TokeniseError
from oaknut.exception import DataError
# A specific failure you can act on.
try:
program = tokenise(source)
except TokeniseError as exc:
report_to_editor(exc.line_index, str(exc))
# Any BASIC codec error, either direction.
try:
run_conversion()
except BASICError as exc:
log.warning("conversion failed: %s", exc)
# Anywhere DataError is already handled uniformly, BASICError is
# caught too — it is one.
assert issubclass(BASICError, DataError)
Because BASICError is a DataError, code that already wraps work
in oaknut.exception.handled_errors() — as the oaknut-basic CLI
does — needs no per-class mapping: the boundary reads exc.exit_code
and prints the message and its notes. See
Exit codes and errors.