oaknut-basic 12.17.0__tar.gz → 12.18.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/PKG-INFO +15 -14
  2. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/README.md +11 -11
  3. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/pyproject.toml +16 -10
  4. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/src/oaknut/basic/__init__.py +1 -1
  5. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/src/oaknut/basic/cli.py +2 -2
  6. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/src/oaknut/basic/detect.py +2 -6
  7. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/src/oaknut/basic/dialect.py +1 -3
  8. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/src/oaknut/basic/exceptions.py +2 -6
  9. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/src/oaknut/basic/float5.py +1 -3
  10. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/src/oaknut/basic/tokeniser.py +1 -0
  11. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/src/oaknut_basic.egg-info/PKG-INFO +15 -14
  12. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/src/oaknut_basic.egg-info/SOURCES.txt +1 -0
  13. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/src/oaknut_basic.egg-info/requires.txt +2 -2
  14. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/tests/test_data_cli.py +1 -3
  15. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/tests/test_detect.py +1 -1
  16. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/tests/test_greedy_crunch.py +1 -3
  17. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/tests/test_keyword_coverage.py +3 -1
  18. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/tests/test_numbering.py +2 -2
  19. oaknut_basic-12.18.0/tests/test_packaging.py +56 -0
  20. oaknut_basic-12.18.0/tests/test_rom_golden.py +100 -0
  21. oaknut_basic-12.18.0/tests/test_rom_golden_detokenise.py +54 -0
  22. oaknut_basic-12.17.0/tests/test_rom_golden.py +0 -100
  23. oaknut_basic-12.17.0/tests/test_rom_golden_detokenise.py +0 -51
  24. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/LICENSE +0 -0
  25. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/setup.cfg +0 -0
  26. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/src/oaknut/basic/datafile.py +0 -0
  27. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/src/oaknut/basic/detokeniser.py +0 -0
  28. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/src/oaknut/basic/linenumber.py +0 -0
  29. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/src/oaknut/basic/numbering.py +0 -0
  30. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/src/oaknut/basic/scanner.py +0 -0
  31. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/src/oaknut/basic/tokens.py +0 -0
  32. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/src/oaknut_basic.egg-info/dependency_links.txt +0 -0
  33. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/src/oaknut_basic.egg-info/entry_points.txt +0 -0
  34. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/src/oaknut_basic.egg-info/top_level.txt +0 -0
  35. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/tests/test_basic.py +0 -0
  36. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/tests/test_basic_v.py +0 -0
  37. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/tests/test_cli.py +0 -0
  38. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/tests/test_crunch_rules.py +0 -0
  39. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/tests/test_datafile.py +0 -0
  40. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/tests/test_detokeniser.py +0 -0
  41. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/tests/test_float5.py +0 -0
  42. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/tests/test_linenumber.py +0 -0
  43. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/tests/test_scanner.py +0 -0
  44. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/tests/test_tokeniser.py +0 -0
  45. {oaknut_basic-12.17.0 → oaknut_basic-12.18.0}/tests/test_tokens.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oaknut-basic
3
- Version: 12.17.0
3
+ Version: 12.18.0
4
4
  Summary: BBC BASIC tools: program tokeniser/de-tokeniser and PRINT#/INPUT# data-file reader/writer
5
5
  Author-email: Robert Smallshire <robert@smallshire.org.uk>
6
6
  License-Expression: MIT
@@ -17,8 +17,9 @@ Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
18
  Requires-Dist: oaknut-exception>=10.0
19
19
  Requires-Dist: oaknut-codecs>=12.7
20
- Provides-Extra: cli
21
- Requires-Dist: oaknut-cli>=10.0; extra == "cli"
20
+ Requires-Dist: click>=8.1.7
21
+ Requires-Dist: asyoulikeit>=1.5.1
22
+ Requires-Dist: oaknut-cli>=10.0
22
23
  Dynamic: license-file
23
24
 
24
25
  <p align="center">
@@ -67,12 +68,12 @@ and a data file round-trips through Python values **byte-for-byte**.
67
68
 
68
69
  ## Installation
69
70
 
70
- Install with the `[cli]` extra for the `oaknut-basic` command, or bare for the
71
- library only:
71
+ Install as a tool for the `oaknut-basic` command, or add it to a project for
72
+ the library:
72
73
 
73
74
  ```
74
- uv tool install "oaknut-basic[cli]" # the command-line tool
75
- uv add oaknut-basic # the importable library
75
+ uv tool install oaknut-basic # the command-line tool
76
+ uv add oaknut-basic # the importable library
76
77
  ```
77
78
 
78
79
  `pip` works identically with the same names. `oaknut-basic` requires Python
@@ -130,9 +131,9 @@ Programs are handled by the functions `tokenise`, `detokenise`, and
130
131
  ```python
131
132
  from oaknut.basic import tokenise, detokenise
132
133
 
133
- program = tokenise('10 PRINT "HELLO"\n20 GOTO 10\n') # str -> bytes
134
- listing = detokenise(program) # bytes -> str
135
- assert tokenise(detokenise(program)) == program # byte-exact
134
+ program = tokenise('10 PRINT "HELLO"\n20 GOTO 10\n') # str -> bytes
135
+ listing = detokenise(program) # bytes -> str
136
+ assert tokenise(detokenise(program)) == program # byte-exact
136
137
  ```
137
138
 
138
139
  When the program lives in a disc image, prefer the path-object wrappers
@@ -149,12 +150,12 @@ polymorphic `write` picks the record type from the Python value; typed
149
150
  from oaknut.basic import datafile
150
151
 
151
152
  with datafile.open("scores.dat", "w") as f:
152
- f.write("ALICE") # str -> string record
153
- f.write(42) # int -> integer record
154
- f.write(3.5) # float -> real record
153
+ f.write("ALICE") # str -> string record
154
+ f.write(42) # int -> integer record
155
+ f.write(3.5) # float -> real record
155
156
 
156
157
  with datafile.open("scores.dat", "r") as f:
157
- for value in f: # yields "ALICE", 42, 3.5
158
+ for value in f: # yields "ALICE", 42, 3.5
158
159
  print(value)
159
160
  ```
160
161
 
@@ -44,12 +44,12 @@ and a data file round-trips through Python values **byte-for-byte**.
44
44
 
45
45
  ## Installation
46
46
 
47
- Install with the `[cli]` extra for the `oaknut-basic` command, or bare for the
48
- library only:
47
+ Install as a tool for the `oaknut-basic` command, or add it to a project for
48
+ the library:
49
49
 
50
50
  ```
51
- uv tool install "oaknut-basic[cli]" # the command-line tool
52
- uv add oaknut-basic # the importable library
51
+ uv tool install oaknut-basic # the command-line tool
52
+ uv add oaknut-basic # the importable library
53
53
  ```
54
54
 
55
55
  `pip` works identically with the same names. `oaknut-basic` requires Python
@@ -107,9 +107,9 @@ Programs are handled by the functions `tokenise`, `detokenise`, and
107
107
  ```python
108
108
  from oaknut.basic import tokenise, detokenise
109
109
 
110
- program = tokenise('10 PRINT "HELLO"\n20 GOTO 10\n') # str -> bytes
111
- listing = detokenise(program) # bytes -> str
112
- assert tokenise(detokenise(program)) == program # byte-exact
110
+ program = tokenise('10 PRINT "HELLO"\n20 GOTO 10\n') # str -> bytes
111
+ listing = detokenise(program) # bytes -> str
112
+ assert tokenise(detokenise(program)) == program # byte-exact
113
113
  ```
114
114
 
115
115
  When the program lives in a disc image, prefer the path-object wrappers
@@ -126,12 +126,12 @@ polymorphic `write` picks the record type from the Python value; typed
126
126
  from oaknut.basic import datafile
127
127
 
128
128
  with datafile.open("scores.dat", "w") as f:
129
- f.write("ALICE") # str -> string record
130
- f.write(42) # int -> integer record
131
- f.write(3.5) # float -> real record
129
+ f.write("ALICE") # str -> string record
130
+ f.write(42) # int -> integer record
131
+ f.write(3.5) # float -> real record
132
132
 
133
133
  with datafile.open("scores.dat", "r") as f:
134
- for value in f: # yields "ALICE", 42, 3.5
134
+ for value in f: # yields "ALICE", 42, 3.5
135
135
  print(value)
136
136
  ```
137
137
 
@@ -20,16 +20,22 @@ classifiers = [
20
20
  "Programming Language :: Python :: 3.12",
21
21
  "Programming Language :: Python :: 3.13",
22
22
  ]
23
- # The tokeniser and de-tokeniser raise categorised errors from the shared
24
- # oaknut-exception hierarchy, so the CLI boundary can render them without a
25
- # traceback. That is the package's only runtime dependency.
26
- dependencies = ["oaknut-exception>=10.0", "oaknut-codecs>=12.7"]
27
-
28
- # The `cli` extra pulls the shared CLI toolkit (Click, asyoulikeit, the
29
- # Acorn text codec and the categorised-error boundary) so this package
30
- # can offer the standalone `oaknut-basic` command.
31
- [project.optional-dependencies]
32
- cli = ["oaknut-cli>=10.0"]
23
+ # This package ships the standalone `oaknut-basic` console script (see
24
+ # [project.scripts] below), and that script's module imports Click,
25
+ # asyoulikeit and oaknut.cli at load time. A console script is installed
26
+ # unconditionally, so those must be unconditional dependencies too
27
+ # otherwise `pip install oaknut-basic` / `uvx oaknut-basic` yields an
28
+ # executable that dies with ModuleNotFoundError before it runs. The
29
+ # tokeniser and de-tokeniser also raise categorised errors from the
30
+ # shared oaknut-exception hierarchy so the CLI boundary can render them
31
+ # without a traceback.
32
+ dependencies = [
33
+ "oaknut-exception>=10.0",
34
+ "oaknut-codecs>=12.7",
35
+ "click>=8.1.7",
36
+ "asyoulikeit>=1.5.1",
37
+ "oaknut-cli>=10.0",
38
+ ]
33
39
 
34
40
  [project.scripts]
35
41
  oaknut-basic = "oaknut.basic.cli:cli"
@@ -81,7 +81,7 @@ from oaknut.basic.tokens import (
81
81
  TOKEN_TO_KEYWORD,
82
82
  )
83
83
 
84
- __version__ = "12.17.0"
84
+ __version__ = "12.18.0"
85
85
 
86
86
  # Canonical load addresses for BBC BASIC programs on each host.
87
87
  # Programs saved by *SAVE on a real machine use these by default.
@@ -217,8 +217,8 @@ def _listing_to_bytes(listing: str, encoding: str) -> bytes:
217
217
  type=click.Choice(["rom", "greedy"]),
218
218
  default="rom",
219
219
  show_default=True,
220
- help="Which tokeniser to emulate. \"rom\" is byte-exact to the BBC BASIC "
221
- "ROM. Use \"greedy\" to reproduce the greedier third-party tokeniser "
220
+ help='Which tokeniser to emulate. "rom" is byte-exact to the BBC BASIC '
221
+ 'ROM. Use "greedy" to reproduce the greedier third-party tokeniser '
222
222
  "behind some early-1980s commercial programs, so their de-tokenised "
223
223
  "source re-tokenises byte-identically.",
224
224
  )
@@ -162,8 +162,7 @@ def detect(data: bytes | bytearray) -> Detection:
162
162
  if line_count == 0:
163
163
  return Detection(
164
164
  Verdict.NOT_BASIC,
165
- f"&0D &{marker:02X} at offset 0 — terminator with no lines, "
166
- "not a program",
165
+ f"&0D &{marker:02X} at offset 0 — terminator with no lines, not a program",
167
166
  )
168
167
  program_length = pos + 2
169
168
  trailing = n - program_length
@@ -195,10 +194,7 @@ def detect(data: bytes | bytearray) -> Detection:
195
194
  line_no = (marker << 8) | data[pos + 2]
196
195
  length = data[pos + 3]
197
196
  if length < HEADER_LENGTH:
198
- return broke(
199
- f"line {line_no} at offset {pos} has length {length} "
200
- f"(< {HEADER_LENGTH})"
201
- )
197
+ return broke(f"line {line_no} at offset {pos} has length {length} (< {HEADER_LENGTH})")
202
198
  if pos + length > n:
203
199
  return broke(f"line {line_no} at offset {pos} runs {length} bytes past end of data")
204
200
 
@@ -142,9 +142,7 @@ _BASIC_V_SINGLE_BYTE_OVERRIDES = {
142
142
  # (so a bare prefix never resolves to AUTO/DELETE/LOAD), then apply the
143
143
  # re-purposed single-byte slots.
144
144
  _basic_v_single_byte = {
145
- token: keyword
146
- for token, keyword in TOKEN_TO_KEYWORD.items()
147
- if token not in (0xC6, 0xC7, 0xC8)
145
+ token: keyword for token, keyword in TOKEN_TO_KEYWORD.items() if token not in (0xC6, 0xC7, 0xC8)
148
146
  }
149
147
  _basic_v_single_byte.update(_BASIC_V_SINGLE_BYTE_OVERRIDES)
150
148
 
@@ -96,8 +96,7 @@ class LineNumberRangeError(TokeniseError):
96
96
 
97
97
  def __init__(self, line_index: int, line_number: int, line_text: str) -> None:
98
98
  super().__init__(
99
- f"line {line_index}: line number {line_number} is out of range "
100
- f"(0-{MAX_LINE_NUMBER})",
99
+ f"line {line_index}: line number {line_number} is out of range (0-{MAX_LINE_NUMBER})",
101
100
  line_index=line_index,
102
101
  line_text=line_text,
103
102
  )
@@ -300,10 +299,7 @@ class IntegerRangeError(DataFileError):
300
299
  """
301
300
 
302
301
  def __init__(self, value: int) -> None:
303
- super().__init__(
304
- f"{value} is out of range for a BBC integer "
305
- f"(must be -2**31 .. 2**31 - 1)"
306
- )
302
+ super().__init__(f"{value} is out of range for a BBC integer (must be -2**31 .. 2**31 - 1)")
307
303
  self.value = value
308
304
 
309
305
 
@@ -58,9 +58,7 @@ def unpack_float5(packed: bytes) -> float:
58
58
  return 0.0
59
59
  sign_and_msb = packed[1]
60
60
  negative = bool(sign_and_msb & 0x80)
61
- significand = (
62
- ((sign_and_msb | 0x80) << 24) | (packed[2] << 16) | (packed[3] << 8) | packed[4]
63
- )
61
+ significand = ((sign_and_msb | 0x80) << 24) | (packed[2] << 16) | (packed[3] << 8) | packed[4]
64
62
  value = math.ldexp(significand, exponent - _EXPONENT_FOLD)
65
63
  return -value if negative else value
66
64
 
@@ -72,6 +72,7 @@ Crunch = Literal["rom", "greedy"]
72
72
  def _split_source_lines(source: str) -> list[str]:
73
73
  return LINE_SEPARATOR_RE.split(source)
74
74
 
75
+
75
76
  # Keyword entries grouped by first character, preserving ROM order within
76
77
  # each group, so the crunch only scans the relevant group.
77
78
  _KEYWORDS_BY_FIRST: dict[str, list[tuple[str, int, int]]] = {}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oaknut-basic
3
- Version: 12.17.0
3
+ Version: 12.18.0
4
4
  Summary: BBC BASIC tools: program tokeniser/de-tokeniser and PRINT#/INPUT# data-file reader/writer
5
5
  Author-email: Robert Smallshire <robert@smallshire.org.uk>
6
6
  License-Expression: MIT
@@ -17,8 +17,9 @@ Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
18
  Requires-Dist: oaknut-exception>=10.0
19
19
  Requires-Dist: oaknut-codecs>=12.7
20
- Provides-Extra: cli
21
- Requires-Dist: oaknut-cli>=10.0; extra == "cli"
20
+ Requires-Dist: click>=8.1.7
21
+ Requires-Dist: asyoulikeit>=1.5.1
22
+ Requires-Dist: oaknut-cli>=10.0
22
23
  Dynamic: license-file
23
24
 
24
25
  <p align="center">
@@ -67,12 +68,12 @@ and a data file round-trips through Python values **byte-for-byte**.
67
68
 
68
69
  ## Installation
69
70
 
70
- Install with the `[cli]` extra for the `oaknut-basic` command, or bare for the
71
- library only:
71
+ Install as a tool for the `oaknut-basic` command, or add it to a project for
72
+ the library:
72
73
 
73
74
  ```
74
- uv tool install "oaknut-basic[cli]" # the command-line tool
75
- uv add oaknut-basic # the importable library
75
+ uv tool install oaknut-basic # the command-line tool
76
+ uv add oaknut-basic # the importable library
76
77
  ```
77
78
 
78
79
  `pip` works identically with the same names. `oaknut-basic` requires Python
@@ -130,9 +131,9 @@ Programs are handled by the functions `tokenise`, `detokenise`, and
130
131
  ```python
131
132
  from oaknut.basic import tokenise, detokenise
132
133
 
133
- program = tokenise('10 PRINT "HELLO"\n20 GOTO 10\n') # str -> bytes
134
- listing = detokenise(program) # bytes -> str
135
- assert tokenise(detokenise(program)) == program # byte-exact
134
+ program = tokenise('10 PRINT "HELLO"\n20 GOTO 10\n') # str -> bytes
135
+ listing = detokenise(program) # bytes -> str
136
+ assert tokenise(detokenise(program)) == program # byte-exact
136
137
  ```
137
138
 
138
139
  When the program lives in a disc image, prefer the path-object wrappers
@@ -149,12 +150,12 @@ polymorphic `write` picks the record type from the Python value; typed
149
150
  from oaknut.basic import datafile
150
151
 
151
152
  with datafile.open("scores.dat", "w") as f:
152
- f.write("ALICE") # str -> string record
153
- f.write(42) # int -> integer record
154
- f.write(3.5) # float -> real record
153
+ f.write("ALICE") # str -> string record
154
+ f.write(42) # int -> integer record
155
+ f.write(3.5) # float -> real record
155
156
 
156
157
  with datafile.open("scores.dat", "r") as f:
157
- for value in f: # yields "ALICE", 42, 3.5
158
+ for value in f: # yields "ALICE", 42, 3.5
158
159
  print(value)
159
160
  ```
160
161
 
@@ -33,6 +33,7 @@ tests/test_greedy_crunch.py
33
33
  tests/test_keyword_coverage.py
34
34
  tests/test_linenumber.py
35
35
  tests/test_numbering.py
36
+ tests/test_packaging.py
36
37
  tests/test_rom_golden.py
37
38
  tests/test_rom_golden_detokenise.py
38
39
  tests/test_scanner.py
@@ -1,5 +1,5 @@
1
1
  oaknut-exception>=10.0
2
2
  oaknut-codecs>=12.7
3
-
4
- [cli]
3
+ click>=8.1.7
4
+ asyoulikeit>=1.5.1
5
5
  oaknut-cli>=10.0
@@ -58,9 +58,7 @@ class TestEncode:
58
58
 
59
59
  def test_encode_pound_sign_uses_acorn(self, tmp_path):
60
60
  out = tmp_path / "out.dat"
61
- result = CliRunner().invoke(
62
- cli, ["data", "encode", "-", str(out)], input=json.dumps(["£"])
63
- )
61
+ result = CliRunner().invoke(cli, ["data", "encode", "-", str(out)], input=json.dumps(["£"]))
64
62
  assert result.exit_code == 0, result.output
65
63
  # 00 (string tag) 01 (length) 60 (pound) — single char, reversal a no-op.
66
64
  assert out.read_bytes() == bytes.fromhex("000160")
@@ -23,7 +23,7 @@ def _program(*lines: tuple[int, bytes]) -> bytes:
23
23
 
24
24
  class TestClean:
25
25
  def test_tokenised_program_is_basic(self):
26
- program = tokenise("10 PRINT \"HELLO\"\n20 GOTO 10")
26
+ program = tokenise('10 PRINT "HELLO"\n20 GOTO 10')
27
27
  result = detect(program)
28
28
  assert result.verdict is Verdict.BASIC
29
29
  assert result.is_basic
@@ -61,9 +61,7 @@ _VECTORS = [
61
61
  # Rule 2 — an FN/PROC name breaks at a FLAG_START keyword (THEN/ELSE).
62
62
  (
63
63
  "IFA THENPROCWTKEYELSEPROCBKKEY",
64
- bytes.fromhex(
65
- "e7 41 20 8c f2 57 54 4b 45 59 45 4c 53 45 50 52 4f 43 42 4b 4b 45 59"
66
- ),
64
+ bytes.fromhex("e7 41 20 8c f2 57 54 4b 45 59 45 4c 53 45 50 52 4f 43 42 4b 4b 45 59"),
67
65
  bytes.fromhex("e7 41 20 8c f2 57 54 4b 45 59 8b f2 42 4b 4b 45 59"),
68
66
  ),
69
67
  # Rule 2 — a function keyword embedded in a name is not a break point.
@@ -66,7 +66,9 @@ def test_every_keyword_tokenises_to_its_token(keyword, token, flags):
66
66
  assert _body(prefix + keyword) == prefix.encode("latin-1") + bytes([token])
67
67
 
68
68
 
69
- @pytest.mark.parametrize(("keyword", "token"), [(kw, t) for kw, t, _f in KEYWORDS], ids=_ids(KEYWORDS))
69
+ @pytest.mark.parametrize(
70
+ ("keyword", "token"), [(kw, t) for kw, t, _f in KEYWORDS], ids=_ids(KEYWORDS)
71
+ )
70
72
  def test_every_token_detokenises_to_its_keyword(keyword, token):
71
73
  program = b"\x0d\x00\x0a\x05" + bytes([token]) + b"\x0d\xff"
72
74
  assert detokenise(program) == f"10{keyword}\n"
@@ -16,8 +16,8 @@ _DATA_DIRPATH = Path(__file__).parent / "data"
16
16
 
17
17
  class TestNumberLines:
18
18
  def test_numbers_each_line_from_ten_in_tens(self):
19
- source = "PRINT \"Hello\"\nEND"
20
- assert basic.number_lines(source) == "10 PRINT \"Hello\"\n20 END"
19
+ source = 'PRINT "Hello"\nEND'
20
+ assert basic.number_lines(source) == '10 PRINT "Hello"\n20 END'
21
21
 
22
22
  def test_custom_step(self):
23
23
  source = "A\nB\nC"
@@ -0,0 +1,56 @@
1
+ """Packaging-metadata guards for the standalone ``oaknut-basic`` command.
2
+
3
+ Unlike the filesystem plugin packages (``oaknut-dfs``/``oaknut-adfs`` …),
4
+ which only contribute ``disc`` subcommands via entry points and ship no
5
+ console script of their own, ``oaknut-basic`` declares its own
6
+ ``[project.scripts]`` entry point. A console script that is always
7
+ installed must have everything it imports as an *unconditional* runtime
8
+ dependency, otherwise ``pip install oaknut-basic`` / ``uvx oaknut-basic``
9
+ produces an executable that dies with ``ModuleNotFoundError`` at import
10
+ (see issue #49).
11
+ """
12
+
13
+ from importlib.metadata import entry_points, requires
14
+
15
+ _DISTRIBUTION = "oaknut-basic"
16
+
17
+ # Everything oaknut/basic/cli.py imports at module load, mapped to the
18
+ # distribution that supplies it.
19
+ _CLI_REQUIREMENTS = ("oaknut-cli", "click", "asyoulikeit")
20
+
21
+
22
+ def _unconditional_requirements():
23
+ """Distribution names required without an ``extra ==`` marker."""
24
+ names = set()
25
+ for requirement in requires(_DISTRIBUTION) or []:
26
+ # "oaknut-cli>=10.0; extra == 'cli'" -> skip; extras are optional.
27
+ spec, _, marker = requirement.partition(";")
28
+ if "extra" in marker:
29
+ continue
30
+ name = (
31
+ spec.strip()
32
+ .split("[")[0]
33
+ .split("<")[0]
34
+ .split(">")[0]
35
+ .split("=")[0]
36
+ .split("!")[0]
37
+ .split("~")[0]
38
+ .strip()
39
+ )
40
+ names.add(name.lower())
41
+ return names
42
+
43
+
44
+ def test_console_script_is_declared():
45
+ scripts = entry_points(group="console_scripts")
46
+ assert any(ep.name == "oaknut-basic" for ep in scripts)
47
+
48
+
49
+ def test_cli_dependencies_are_unconditional():
50
+ unconditional = _unconditional_requirements()
51
+ missing = [name for name in _CLI_REQUIREMENTS if name.lower() not in unconditional]
52
+ assert not missing, (
53
+ f"{_DISTRIBUTION} ships a console script but these imports of "
54
+ f"oaknut.basic.cli are not unconditional dependencies: {missing}. "
55
+ "Promote them out of the [cli] extra into [project.dependencies]."
56
+ )
@@ -0,0 +1,100 @@
1
+ """Golden tokeniser vectors from the genuine BBC BASIC II ROM.
2
+
3
+ Generated by scripts (basictool's 6502 emulation of the BASIC II ROM)
4
+ and committed as static data: the ROM is the authority for what the
5
+ tokeniser must produce, and this test holds it to that. basictool is
6
+ NOT a test dependency — regenerate with scripts/gen_rom_golden.py if the
7
+ vector set changes.
8
+ """
9
+
10
+ import pytest
11
+ from oaknut.basic import tokenise
12
+
13
+ # (source after the line number, tokenised body the ROM produces).
14
+ _GOLDEN = [
15
+ ('PRINT "Hi"', b' \xf1 "Hi"'),
16
+ ("END", b" \xe0"),
17
+ ("GOTO 100", b" \xe5 \x8dDd@"),
18
+ ("FOR I=1 TO 10 STEP 2", b" \xe3 I=1 \xb8 10 \x88 2"),
19
+ ("NEXT I", b" \xed I"),
20
+ ("REPEAT", b" \xf5"),
21
+ ("UNTIL X>5", b" \xfd X>5"),
22
+ ("IF A=1 THEN PRINT A", b" \xe7 A=1 \x8c \xf1 A"),
23
+ ("X=INT(RND(1)*6)+1", b" X=\xa8(\xb3(1)*6)+1"),
24
+ ("PROCdraw(x%,y%)", b" \xf2draw(x%,y%)"),
25
+ ("DEF PROCgbpb(A$,T%)", b" \xdd \xf2gbpb(A$,T%)"),
26
+ ("DEF FNsq(x)=x*x", b" \xdd \xa4sq(x)=x*x"),
27
+ ("=FNsq(3)", b" =\xa4sq(3)"),
28
+ ("P.", b" \xf1"),
29
+ ("PR.", b" \xf1"),
30
+ ("PRO.", b" \xf2"),
31
+ ("F.", b" \xe3"),
32
+ ("N.", b" \xed"),
33
+ ("G.100", b" \xe5\x8dDd@"),
34
+ ("REP.", b" \xf5"),
35
+ ("AND0", b" \x80\x8dT@@"),
36
+ ("TO1", b" \xb8\x8dTA@"),
37
+ ("PRINT1", b" \xf11"),
38
+ ("DIV9", b" \x81\x8dTI@"),
39
+ ("SIN1", b" \xb5\x8dTA@"),
40
+ ("TAB(1", b" \x8a\x8dTA@"),
41
+ ("X=2", b" X=2"),
42
+ ("5", b" \x8dTE@"),
43
+ ("GOTO0", b" \xe5\x8dT@@"),
44
+ ("GOTO1", b" \xe5\x8dTA@"),
45
+ ("GOTO 10", b" \xe5 \x8dTJ@"),
46
+ ("GOTO 32767", b" \xe5 \x8d`\x7f\x7f"),
47
+ ("GOSUB 100", b" \xe4 \x8dDd@"),
48
+ ("RESTORE 100,200", b" \xf7 \x8dDd@,\x8ddH@"),
49
+ ("ON X GOTO 10,20,30", b" \xee X \xe5 \x8dTJ@,\x8dTT@,\x8dT^@"),
50
+ ("IF A THEN 100 ELSE 200", b" \xe7 A \x8c \x8dDd@ \x8b \x8ddH@"),
51
+ ("GOTO X", b" \xe5 X"),
52
+ ("GOTO -5", b" \xe5 -5"),
53
+ ("RESTORE", b" \xf7"),
54
+ ("TRACE 500", b" \xfc \x8ddtA"),
55
+ ("RENUMBER 100,10", b" \xcc \x8dDd@,\x8dTJ@"),
56
+ ("PAGE=&2000", b" \xd0=&2000"),
57
+ ("X=PAGE", b" X=\x90"),
58
+ ("PRINT PAGE", b" \xf1 \x90"),
59
+ ("TIME=0", b" \xd1=0"),
60
+ ("X=TIME", b" X=\x91"),
61
+ ("PTR#3=0", b" \xcf#3=0"),
62
+ ("X=PTR#3", b" X=\x8f#3"),
63
+ ("HIMEM=&7C00", b" \xd3=&7C00"),
64
+ ("LOMEM=PAGE", b" \xd2=\x90"),
65
+ ("? ERR PAGE", b" ? \x9f \x90"),
66
+ ("GET$;,PTR", b" \xbe;,\x8f"),
67
+ ("INSTR(A$,B$) PTR", b" \xa7A$,B$) \x8f"),
68
+ ("OPENOUT LOMEM", b" \xae \xd2"),
69
+ ("SQR RAD PTR", b" \xb6 \xb2 \xcf"),
70
+ ("RND * SQR ENDPROC", b" \xb3 * SQR ENDPROC"),
71
+ ('A$="he""llo"', b' A$="he""llo"'),
72
+ ('PRINT "GOTO 10"', b' \xf1 "GOTO 10"'),
73
+ ("X=&FF+&1A2B", b" X=&FF+&1A2B"),
74
+ ("Y=1.5E3+.25", b" Y=1.5E3+.25"),
75
+ ('PRINT "unterminated GOTO', b' \xf1 "unterminated GOTO'),
76
+ ("REM this has PRINT GOTO 10", b" \xf4 this has PRINT GOTO 10"),
77
+ ("DATA 1,2,PRINT,3:X=4", b" \xdc 1,2,PRINT,3:X=4"),
78
+ ("*CAT", b" *CAT"),
79
+ ("*FX 21,0", b" *FX 21,0"),
80
+ ("X=3*4", b" X=3*4"),
81
+ ("PRINT~&FF", b" \xf1~&FF"),
82
+ ("VDU 26,12;0;", b" \xef 26,12;0;"),
83
+ ("TIMER", b" TIMER"),
84
+ ("TRUEELSE", b" TRUEELSE"),
85
+ ("GDIV40", b" GDIV40"),
86
+ ("TOTAL", b" \xb8TAL"),
87
+ ("DIVMOD", b" \x81\x83"),
88
+ ("TRUE+", b" \xb9+"),
89
+ ("text%?p%=ASC(MID$(A$,i%,1))", b" text%?p%=\x97(\xc1A$,i%,1))"),
90
+ ("?(a%+1)=&FF", b" ?(a%+1)=&FF"),
91
+ ("A=1:B=2:PRINT A,B", b" A=1:B=2:\xf1 A,B"),
92
+ ("COLOUR 129:CLS", b" \xfb 129:\xdb"),
93
+ ("MODE 7:VDU 23", b" \xeb 7:\xef 23"),
94
+ ]
95
+
96
+
97
+ @pytest.mark.parametrize(("source", "expected_body"), _GOLDEN, ids=[v[0] for v in _GOLDEN])
98
+ def test_tokenises_like_the_basic_ii_rom(source, expected_body):
99
+ program = tokenise("10 " + source)
100
+ assert program[4 : program[3]] == expected_body
@@ -0,0 +1,54 @@
1
+ """Golden de-tokeniser vectors from the genuine BBC BASIC II ROM.
2
+
3
+ Generated by scripts/gen_rom_detok_golden.py from basictool's 6502
4
+ emulation of the BASIC II ROM and committed as static data: the ROM's
5
+ LIST output is the authority for what de-tokenising must produce (the
6
+ expected text is rebuilt in our plain line-number style from the ROM's
7
+ LIST bodies). basictool is NOT a test dependency.
8
+ """
9
+
10
+ import pytest
11
+ from oaknut.basic import detokenise
12
+
13
+ # (tokenised program bytes, expected de-tokenised text).
14
+ _GOLDEN = [
15
+ (b'\r\x00\n\x16 \xf1 "Hello, world!"\r\xff', '10 PRINT "Hello, world!"\n'),
16
+ (b"\r\x00\n\x0b \xe5 \x8dDd@\r\xff", "10 GOTO 100\n"),
17
+ (b"\r\x00\n\r \xe4 \x8ddH@:\xf8\r\xff", "10 GOSUB 200:RETURN\n"),
18
+ (b"\r\x00\n\x19 \xee X \xe5 \x8dTJ@,\x8dTT@,\x8dT^@\r\xff", "10 ON X GOTO 10,20,30\n"),
19
+ (b"\r\x00\n\x18 \xe7 A=1 \x8c \x8dTr@ \x8b \x8dT|@\r\xff", "10 IF A=1 THEN 50 ELSE 60\n"),
20
+ (b"\r\x00\n\x11 \xf7 \x8dDd@:\xf3 A,B\r\xff", "10 RESTORE 100:READ A,B\n"),
21
+ (b"\r\x00\n\x17 \xe3 I=1 \xb8 10 \x88 2:\xed I\r\xff", "10 FOR I=1 TO 10 STEP 2:NEXT I\n"),
22
+ (b"\r\x00\n\x0c \xf5:\xfd X>5\r\xff", "10 REPEAT:UNTIL X>5\n"),
23
+ (b"\r\x00\n\x0c \xd0=&2000\r\xff", "10 PAGE=&2000\n"),
24
+ (b"\r\x00\n\x08 X=\x90\r\xff", "10 X=PAGE\n"),
25
+ (b"\r\x00\n\n \xcf#3=0\r\xff", "10 PTR#3=0\n"),
26
+ (b"\r\x00\n\x0c \xd1=0:X=\x91\r\xff", "10 TIME=0:X=TIME\n"),
27
+ (b"\r\x00\n\x10 \xd3=&7C00:\xd2=\x90\r\xff", "10 HIMEM=&7C00:LOMEM=PAGE\n"),
28
+ (b'\r\x00\n\x15 A$=\xbd(34)+"quote"\r\xff', '10 A$=CHR$(34)+"quote"\n'),
29
+ (b'\r\x00\n\x1d \xf1 "GOTO 10 is text here"\r\xff', '10 PRINT "GOTO 10 is text here"\n'),
30
+ (b"\r\x00\n\x16 X=&FF+&1A2B \x80 &F0\r\xff", "10 X=&FF+&1A2B AND &F0\n"),
31
+ (b"\r\x00\n\x10 Y=1.5E3+.25\r\xff", "10 Y=1.5E3+.25\n"),
32
+ (
33
+ b"\r\x00\n$ \xf4 this REM has PRINT GOTO in it\r\xff",
34
+ "10 REM this REM has PRINT GOTO in it\n",
35
+ ),
36
+ (b"\r\x00\n\x18 \xdc 1,2,PRINT,3,hello\r\xff", "10 DATA 1,2,PRINT,3,hello\n"),
37
+ (b"\r\x00\n\t *CAT\r\xff", "10 *CAT\n"),
38
+ (b"\r\x00\n\x14 \xef 23,255;0;0;0;\r\xff", "10 VDU 23,255;0;0;0;\n"),
39
+ (b"\r\x00\n\x13 \xf2draw(x%,y%):\xe1\r\xff", "10 PROCdraw(x%,y%):ENDPROC\n"),
40
+ (b"\r\x00\n\x11 \xdd \xa4sq(x)=x*x\r\xff", "10 DEF FNsq(x)=x*x\n"),
41
+ (b"\r\x00\n\x11 L%=\xa9(A$)+\xb1-\x9c\r\xff", "10 L%=LEN(A$)+POS-COUNT\n"),
42
+ (b"\r\x00\n\x12 X=\xa8(\xb3(1)*6)+1\r\xff", "10 X=INT(RND(1)*6)+1\n"),
43
+ (b"\r\x00\n\n \xf1~&AB\r\xff", "10 PRINT~&AB\n"),
44
+ (b"\r\x00\n\x14 @%=&90A:\xf1 \x8a5);X\r\xff", "10 @%=&90A:PRINT TAB(5);X\n"),
45
+ (b"\r\x00\n\x12 \xfb 129:\xe6 0,1:\xdb\r\xff", "10 COLOUR 129:GCOL 0,1:CLS\n"),
46
+ (b"\r\x00\n\x10 \xeb 7:\xf1'\x8a10)'\r\xff", "10 MODE 7:PRINT'TAB(10)'\n"),
47
+ (b"\r\x00\n\x17 ?(addr%+1)=base%?2\r\xff", "10 ?(addr%+1)=base%?2\n"),
48
+ (b'\r\x00\n\x19 \xf1 "unterminated GOTO\r\xff', '10 PRINT "unterminated GOTO\n'),
49
+ ]
50
+
51
+
52
+ @pytest.mark.parametrize(("program", "expected"), _GOLDEN, ids=range(len(_GOLDEN)))
53
+ def test_detokenises_like_the_basic_ii_rom(program, expected):
54
+ assert detokenise(program) == expected
@@ -1,100 +0,0 @@
1
- """Golden tokeniser vectors from the genuine BBC BASIC II ROM.
2
-
3
- Generated by scripts (basictool's 6502 emulation of the BASIC II ROM)
4
- and committed as static data: the ROM is the authority for what the
5
- tokeniser must produce, and this test holds it to that. basictool is
6
- NOT a test dependency — regenerate with scripts/gen_rom_golden.py if the
7
- vector set changes.
8
- """
9
-
10
- import pytest
11
- from oaknut.basic import tokenise
12
-
13
- # (source after the line number, tokenised body the ROM produces).
14
- _GOLDEN = [
15
- ('PRINT "Hi"', b' \xf1 "Hi"'),
16
- ('END', b' \xe0'),
17
- ('GOTO 100', b' \xe5 \x8dDd@'),
18
- ('FOR I=1 TO 10 STEP 2', b' \xe3 I=1 \xb8 10 \x88 2'),
19
- ('NEXT I', b' \xed I'),
20
- ('REPEAT', b' \xf5'),
21
- ('UNTIL X>5', b' \xfd X>5'),
22
- ('IF A=1 THEN PRINT A', b' \xe7 A=1 \x8c \xf1 A'),
23
- ('X=INT(RND(1)*6)+1', b' X=\xa8(\xb3(1)*6)+1'),
24
- ('PROCdraw(x%,y%)', b' \xf2draw(x%,y%)'),
25
- ('DEF PROCgbpb(A$,T%)', b' \xdd \xf2gbpb(A$,T%)'),
26
- ('DEF FNsq(x)=x*x', b' \xdd \xa4sq(x)=x*x'),
27
- ('=FNsq(3)', b' =\xa4sq(3)'),
28
- ('P.', b' \xf1'),
29
- ('PR.', b' \xf1'),
30
- ('PRO.', b' \xf2'),
31
- ('F.', b' \xe3'),
32
- ('N.', b' \xed'),
33
- ('G.100', b' \xe5\x8dDd@'),
34
- ('REP.', b' \xf5'),
35
- ('AND0', b' \x80\x8dT@@'),
36
- ('TO1', b' \xb8\x8dTA@'),
37
- ('PRINT1', b' \xf11'),
38
- ('DIV9', b' \x81\x8dTI@'),
39
- ('SIN1', b' \xb5\x8dTA@'),
40
- ('TAB(1', b' \x8a\x8dTA@'),
41
- ('X=2', b' X=2'),
42
- ('5', b' \x8dTE@'),
43
- ('GOTO0', b' \xe5\x8dT@@'),
44
- ('GOTO1', b' \xe5\x8dTA@'),
45
- ('GOTO 10', b' \xe5 \x8dTJ@'),
46
- ('GOTO 32767', b' \xe5 \x8d`\x7f\x7f'),
47
- ('GOSUB 100', b' \xe4 \x8dDd@'),
48
- ('RESTORE 100,200', b' \xf7 \x8dDd@,\x8ddH@'),
49
- ('ON X GOTO 10,20,30', b' \xee X \xe5 \x8dTJ@,\x8dTT@,\x8dT^@'),
50
- ('IF A THEN 100 ELSE 200', b' \xe7 A \x8c \x8dDd@ \x8b \x8ddH@'),
51
- ('GOTO X', b' \xe5 X'),
52
- ('GOTO -5', b' \xe5 -5'),
53
- ('RESTORE', b' \xf7'),
54
- ('TRACE 500', b' \xfc \x8ddtA'),
55
- ('RENUMBER 100,10', b' \xcc \x8dDd@,\x8dTJ@'),
56
- ('PAGE=&2000', b' \xd0=&2000'),
57
- ('X=PAGE', b' X=\x90'),
58
- ('PRINT PAGE', b' \xf1 \x90'),
59
- ('TIME=0', b' \xd1=0'),
60
- ('X=TIME', b' X=\x91'),
61
- ('PTR#3=0', b' \xcf#3=0'),
62
- ('X=PTR#3', b' X=\x8f#3'),
63
- ('HIMEM=&7C00', b' \xd3=&7C00'),
64
- ('LOMEM=PAGE', b' \xd2=\x90'),
65
- ('? ERR PAGE', b' ? \x9f \x90'),
66
- ('GET$;,PTR', b' \xbe;,\x8f'),
67
- ('INSTR(A$,B$) PTR', b' \xa7A$,B$) \x8f'),
68
- ('OPENOUT LOMEM', b' \xae \xd2'),
69
- ('SQR RAD PTR', b' \xb6 \xb2 \xcf'),
70
- ('RND * SQR ENDPROC', b' \xb3 * SQR ENDPROC'),
71
- ('A$="he""llo"', b' A$="he""llo"'),
72
- ('PRINT "GOTO 10"', b' \xf1 "GOTO 10"'),
73
- ('X=&FF+&1A2B', b' X=&FF+&1A2B'),
74
- ('Y=1.5E3+.25', b' Y=1.5E3+.25'),
75
- ('PRINT "unterminated GOTO', b' \xf1 "unterminated GOTO'),
76
- ('REM this has PRINT GOTO 10', b' \xf4 this has PRINT GOTO 10'),
77
- ('DATA 1,2,PRINT,3:X=4', b' \xdc 1,2,PRINT,3:X=4'),
78
- ('*CAT', b' *CAT'),
79
- ('*FX 21,0', b' *FX 21,0'),
80
- ('X=3*4', b' X=3*4'),
81
- ('PRINT~&FF', b' \xf1~&FF'),
82
- ('VDU 26,12;0;', b' \xef 26,12;0;'),
83
- ('TIMER', b' TIMER'),
84
- ('TRUEELSE', b' TRUEELSE'),
85
- ('GDIV40', b' GDIV40'),
86
- ('TOTAL', b' \xb8TAL'),
87
- ('DIVMOD', b' \x81\x83'),
88
- ('TRUE+', b' \xb9+'),
89
- ('text%?p%=ASC(MID$(A$,i%,1))', b' text%?p%=\x97(\xc1A$,i%,1))'),
90
- ('?(a%+1)=&FF', b' ?(a%+1)=&FF'),
91
- ('A=1:B=2:PRINT A,B', b' A=1:B=2:\xf1 A,B'),
92
- ('COLOUR 129:CLS', b' \xfb 129:\xdb'),
93
- ('MODE 7:VDU 23', b' \xeb 7:\xef 23'),
94
- ]
95
-
96
-
97
- @pytest.mark.parametrize(("source", "expected_body"), _GOLDEN, ids=[v[0] for v in _GOLDEN])
98
- def test_tokenises_like_the_basic_ii_rom(source, expected_body):
99
- program = tokenise("10 " + source)
100
- assert program[4 : program[3]] == expected_body
@@ -1,51 +0,0 @@
1
- """Golden de-tokeniser vectors from the genuine BBC BASIC II ROM.
2
-
3
- Generated by scripts/gen_rom_detok_golden.py from basictool's 6502
4
- emulation of the BASIC II ROM and committed as static data: the ROM's
5
- LIST output is the authority for what de-tokenising must produce (the
6
- expected text is rebuilt in our plain line-number style from the ROM's
7
- LIST bodies). basictool is NOT a test dependency.
8
- """
9
-
10
- import pytest
11
- from oaknut.basic import detokenise
12
-
13
- # (tokenised program bytes, expected de-tokenised text).
14
- _GOLDEN = [
15
- (b'\r\x00\n\x16 \xf1 "Hello, world!"\r\xff', '10 PRINT "Hello, world!"\n'),
16
- (b'\r\x00\n\x0b \xe5 \x8dDd@\r\xff', '10 GOTO 100\n'),
17
- (b'\r\x00\n\r \xe4 \x8ddH@:\xf8\r\xff', '10 GOSUB 200:RETURN\n'),
18
- (b'\r\x00\n\x19 \xee X \xe5 \x8dTJ@,\x8dTT@,\x8dT^@\r\xff', '10 ON X GOTO 10,20,30\n'),
19
- (b'\r\x00\n\x18 \xe7 A=1 \x8c \x8dTr@ \x8b \x8dT|@\r\xff', '10 IF A=1 THEN 50 ELSE 60\n'),
20
- (b'\r\x00\n\x11 \xf7 \x8dDd@:\xf3 A,B\r\xff', '10 RESTORE 100:READ A,B\n'),
21
- (b'\r\x00\n\x17 \xe3 I=1 \xb8 10 \x88 2:\xed I\r\xff', '10 FOR I=1 TO 10 STEP 2:NEXT I\n'),
22
- (b'\r\x00\n\x0c \xf5:\xfd X>5\r\xff', '10 REPEAT:UNTIL X>5\n'),
23
- (b'\r\x00\n\x0c \xd0=&2000\r\xff', '10 PAGE=&2000\n'),
24
- (b'\r\x00\n\x08 X=\x90\r\xff', '10 X=PAGE\n'),
25
- (b'\r\x00\n\n \xcf#3=0\r\xff', '10 PTR#3=0\n'),
26
- (b'\r\x00\n\x0c \xd1=0:X=\x91\r\xff', '10 TIME=0:X=TIME\n'),
27
- (b'\r\x00\n\x10 \xd3=&7C00:\xd2=\x90\r\xff', '10 HIMEM=&7C00:LOMEM=PAGE\n'),
28
- (b'\r\x00\n\x15 A$=\xbd(34)+"quote"\r\xff', '10 A$=CHR$(34)+"quote"\n'),
29
- (b'\r\x00\n\x1d \xf1 "GOTO 10 is text here"\r\xff', '10 PRINT "GOTO 10 is text here"\n'),
30
- (b'\r\x00\n\x16 X=&FF+&1A2B \x80 &F0\r\xff', '10 X=&FF+&1A2B AND &F0\n'),
31
- (b'\r\x00\n\x10 Y=1.5E3+.25\r\xff', '10 Y=1.5E3+.25\n'),
32
- (b'\r\x00\n$ \xf4 this REM has PRINT GOTO in it\r\xff', '10 REM this REM has PRINT GOTO in it\n'),
33
- (b'\r\x00\n\x18 \xdc 1,2,PRINT,3,hello\r\xff', '10 DATA 1,2,PRINT,3,hello\n'),
34
- (b'\r\x00\n\t *CAT\r\xff', '10 *CAT\n'),
35
- (b'\r\x00\n\x14 \xef 23,255;0;0;0;\r\xff', '10 VDU 23,255;0;0;0;\n'),
36
- (b'\r\x00\n\x13 \xf2draw(x%,y%):\xe1\r\xff', '10 PROCdraw(x%,y%):ENDPROC\n'),
37
- (b'\r\x00\n\x11 \xdd \xa4sq(x)=x*x\r\xff', '10 DEF FNsq(x)=x*x\n'),
38
- (b'\r\x00\n\x11 L%=\xa9(A$)+\xb1-\x9c\r\xff', '10 L%=LEN(A$)+POS-COUNT\n'),
39
- (b'\r\x00\n\x12 X=\xa8(\xb3(1)*6)+1\r\xff', '10 X=INT(RND(1)*6)+1\n'),
40
- (b'\r\x00\n\n \xf1~&AB\r\xff', '10 PRINT~&AB\n'),
41
- (b'\r\x00\n\x14 @%=&90A:\xf1 \x8a5);X\r\xff', '10 @%=&90A:PRINT TAB(5);X\n'),
42
- (b'\r\x00\n\x12 \xfb 129:\xe6 0,1:\xdb\r\xff', '10 COLOUR 129:GCOL 0,1:CLS\n'),
43
- (b"\r\x00\n\x10 \xeb 7:\xf1'\x8a10)'\r\xff", "10 MODE 7:PRINT'TAB(10)'\n"),
44
- (b'\r\x00\n\x17 ?(addr%+1)=base%?2\r\xff', '10 ?(addr%+1)=base%?2\n'),
45
- (b'\r\x00\n\x19 \xf1 "unterminated GOTO\r\xff', '10 PRINT "unterminated GOTO\n'),
46
- ]
47
-
48
-
49
- @pytest.mark.parametrize(("program", "expected"), _GOLDEN, ids=range(len(_GOLDEN)))
50
- def test_detokenises_like_the_basic_ii_rom(program, expected):
51
- assert detokenise(program) == expected
File without changes
File without changes