nc-gcode-interpreter 0.2.6__tar.gz → 0.2.7__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 (125) hide show
  1. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/.github/workflows/build-and-release.yml +3 -1
  2. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/CHANGELOG.md +27 -0
  3. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/Cargo.lock +1 -1
  4. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/Cargo.toml +1 -1
  5. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/Development.md +27 -0
  6. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/PKG-INFO +1 -1
  7. nc_gcode_interpreter-0.2.7/build.rs +62 -0
  8. nc_gcode_interpreter-0.2.7/build_backend/nc_build_backend.py +116 -0
  9. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/ggroups/generate_g_commands.py +2 -2
  10. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/pyproject.toml +24 -1
  11. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/python/nc_gcode_interpreter/__init__.py +27 -21
  12. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/python/nc_gcode_interpreter/_internal.pyi +18 -19
  13. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/python/nc_gcode_interpreter/cli.py +2 -1
  14. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/python/nc_gcode_interpreter/viz.py +1 -1
  15. nc_gcode_interpreter-0.2.7/python/tests/nc_source.py +11 -0
  16. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/python/tests/test_batch_variables.py +42 -53
  17. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/python/tests/test_batches.py +5 -5
  18. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/python/tests/test_diagnostics.py +1 -1
  19. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/python/tests/test_expected_output.py +3 -2
  20. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/python/tests/test_field_defects.py +0 -1
  21. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/python/tests/test_flags.py +1 -1
  22. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/python/tests/test_flatten.py +8 -2
  23. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/python/tests/test_jumps.py +42 -51
  24. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/python/tests/test_streaming.py +29 -36
  25. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/src/main.rs +1 -1
  26. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/.gitignore +0 -0
  27. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/CONTRIBUTING.md +0 -0
  28. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/LICENSE +0 -0
  29. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/README.md +0 -0
  30. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/TODO.md +0 -0
  31. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/docs/resumable-interpretation-design.md +0 -0
  32. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/docs/sinumerik-execution-model.md +0 -0
  33. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/actual_position.csv +0 -0
  34. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/actual_position.mpf +0 -0
  35. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/arc.csv +0 -0
  36. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/arc.mpf +0 -0
  37. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/arrays.csv +0 -0
  38. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/arrays.mpf +0 -0
  39. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/axis_index_assignment.csv +0 -0
  40. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/axis_index_assignment.mpf +0 -0
  41. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/basic_math.csv +0 -0
  42. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/basic_math.mpf +0 -0
  43. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/case.csv +0 -0
  44. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/case.mpf +0 -0
  45. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/case_insensitive_variables.csv +0 -0
  46. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/case_insensitive_variables.mpf +0 -0
  47. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/custom_vars.csv +0 -0
  48. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/custom_vars.mpf +0 -0
  49. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/def_string.csv +0 -0
  50. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/def_string.mpf +0 -0
  51. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/defaults.csv +0 -0
  52. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/defaults.mpf +0 -0
  53. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/edge_cases.csv +0 -0
  54. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/edge_cases.mpf +0 -0
  55. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/flattening/flatten_demo.mpf +0 -0
  56. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/flattening/flatten_demo_flattened.csv +0 -0
  57. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/flattening/flatten_demo_raw.csv +0 -0
  58. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/for_loop.csv +0 -0
  59. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/for_loop.mpf +0 -0
  60. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/function_calls.csv +0 -0
  61. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/function_calls.mpf +0 -0
  62. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/goto.csv +0 -0
  63. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/goto.mpf +0 -0
  64. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/if_statement.csv +0 -0
  65. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/if_statement.mpf +0 -0
  66. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/increment.csv +0 -0
  67. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/increment.mpf +0 -0
  68. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/logic_operators.csv +0 -0
  69. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/logic_operators.mpf +0 -0
  70. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/loop.csv +0 -0
  71. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/loop.mpf +0 -0
  72. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/multiple_m_codes.csv +0 -0
  73. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/multiple_m_codes.mpf +0 -0
  74. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/precedence.csv +0 -0
  75. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/precedence.mpf +0 -0
  76. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/r_param.csv +0 -0
  77. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/r_param.mpf +0 -0
  78. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/repeat.csv +0 -0
  79. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/repeat.mpf +0 -0
  80. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/simple.csv +0 -0
  81. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/simple.mpf +0 -0
  82. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/spline.csv +0 -0
  83. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/spline.mpf +0 -0
  84. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/tool.csv +0 -0
  85. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/tool.mpf +0 -0
  86. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/trans.csv +0 -0
  87. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/trans.mpf +0 -0
  88. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/trans2.csv +0 -0
  89. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/trans2.mpf +0 -0
  90. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/trans_ic.csv +0 -0
  91. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/trans_ic.mpf +0 -0
  92. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/trans_reset.csv +0 -0
  93. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/trans_reset.mpf +0 -0
  94. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/trig.csv +0 -0
  95. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/trig.mpf +0 -0
  96. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/variables.csv +0 -0
  97. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/examples/variables.mpf +0 -0
  98. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/python/example/minimal.py +0 -0
  99. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/python/example/streaming.py +0 -0
  100. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/python/nc_gcode_interpreter/ggroups.json +0 -0
  101. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/python/nc_gcode_interpreter/py.typed +0 -0
  102. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/python/tests/test_arithmetic_functions.py +0 -0
  103. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/python/tests/test_g_groups.py +0 -0
  104. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/python/tests/test_g_vocabulary.py +0 -0
  105. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/python/tests/test_real_comparisons.py +0 -0
  106. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/python/tests/test_stage1.py +0 -0
  107. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/python/tests/test_string_operations.py +0 -0
  108. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/python/tests/test_structured_errors.py +0 -0
  109. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/python/tests/test_unsupported.py +0 -0
  110. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/rustfmt.toml +0 -0
  111. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/src/errors.rs +0 -0
  112. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/src/flatten.rs +0 -0
  113. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/src/grammar.pest +0 -0
  114. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/src/interpret_rules.rs +0 -0
  115. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/src/interpreter.rs +0 -0
  116. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/src/lib.rs +0 -0
  117. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/src/line_driver.rs +0 -0
  118. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/src/modal_groups.rs +0 -0
  119. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/src/output.rs +0 -0
  120. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/src/state.rs +0 -0
  121. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/src/structure_scan.rs +0 -0
  122. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/src/types.rs +0 -0
  123. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/src/vm.rs +0 -0
  124. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/tests/cli.rs +0 -0
  125. {nc_gcode_interpreter-0.2.6 → nc_gcode_interpreter-0.2.7}/uv.lock +0 -0
@@ -35,7 +35,9 @@ jobs:
35
35
  with:
36
36
  python-version: 3.13
37
37
  - name: Install Python linter
38
- run: pip install ruff
38
+ # Pinned: an unpinned ruff silently widens its rule set on upgrade and
39
+ # turns the job red on code nobody touched. Bump deliberately.
40
+ run: pip install ruff==0.16.6
39
41
  - name: Lint Python code with ruff
40
42
  run: ruff check .
41
43
 
@@ -4,6 +4,33 @@ Notable changes to **nc-gcode-interpreter**. The format loosely follows
4
4
  [Keep a Changelog](https://keepachangelog.com/); versions are git tags,
5
5
  released to PyPI.
6
6
 
7
+ ## [v0.2.7] - 2026-09-08
8
+
9
+ ### Changed
10
+
11
+ - Local and editable builds now derive their version from `git describe`
12
+ instead of reporting the `0.0.0.dev0` placeholder: an editable checkout three
13
+ commits past `v0.2.6` reports `0.2.6.dev3+<sha>`. A small in-tree PEP 517
14
+ backend (`build_backend/nc_build_backend.py`) stamps the version onto the
15
+ `Cargo.toml` placeholder and hands off to maturin; the release CI, which
16
+ already writes the tag version into `Cargo.toml`, is untouched. Downstream
17
+ consumers installing this package editable now get a real version to pin
18
+ against and a signal that they are running a local edit (#65).
19
+ - `nc-gcode-interpreter --version` now reports the actual build version. It was
20
+ a hard-coded `1.0` string literal that had never matched any release; a
21
+ `build.rs` now derives the same `git describe` version for plain `cargo`
22
+ builds (which don't go through the PEP 517 shim) and the CLI reads it, so a
23
+ dev build prints e.g. `0.2.6-dev.1+ge5f2306` and a release build prints the
24
+ tag.
25
+ - Ruff's implicit default rule set widened in 0.16 and turned the lint job red
26
+ on 64 findings in untouched code. Pin the rule selection in `pyproject.toml`
27
+ and the ruff version in CI, so a linter upgrade adds rules only when asked,
28
+ and fix the findings. Mostly mechanical (`X | None`, `list`/`dict` builtins,
29
+ import sorting, explicit `zip(strict=)`). The rule set carries no `ignore`
30
+ list: the one rule that would have needed an exception, `FLY002`, is instead
31
+ satisfied by an `nc_lines(*blocks)` test helper, which keeps the NC test
32
+ programs one block per line with their explanatory comments intact.
33
+
7
34
  ## [v0.2.6] - 2026-07-09
8
35
 
9
36
  ### Added
@@ -506,7 +506,7 @@ checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
506
506
 
507
507
  [[package]]
508
508
  name = "nc-gcode-interpreter"
509
- version = "0.2.6"
509
+ version = "0.2.7"
510
510
  dependencies = [
511
511
  "arrow-array",
512
512
  "arrow-data",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "nc-gcode-interpreter"
3
- version = "0.2.6"
3
+ version = "0.2.7"
4
4
  edition = "2021"
5
5
  readme = "README.md"
6
6
 
@@ -30,6 +30,33 @@ cargo build --release
30
30
  maturin develop --release --uv
31
31
  ```
32
32
 
33
+ ### Versioning
34
+
35
+ `Cargo.toml` holds the placeholder version `0.0.0-dev`; the release workflow
36
+ rewrites it from the git tag before building the wheels, so the released
37
+ version always comes from the tag and never from a committed number.
38
+
39
+ Local builds get a version too. `build_backend/nc_build_backend.py` is an
40
+ in-tree PEP 517 backend that wraps maturin: when it sees the placeholder and
41
+ finds a git checkout with tags, it stamps
42
+ `git describe --tags --long --dirty` onto the `Cargo.toml` version as
43
+ `0.2.6-dev.3+g<sha>` (maturin normalises that to the PEP 440
44
+ `0.2.6.dev3+<sha>`), builds, then restores `Cargo.toml` and `Cargo.lock`. So an
45
+ editable `uv sync` reports the commit it was built from rather than
46
+ `0.0.0.dev0` - which is what lets a downstream consumer notice it is running a
47
+ local edit of the interpreter rather than a release.
48
+
49
+ Any non-placeholder version in `Cargo.toml` is left alone, so the CI release
50
+ path is unaffected. Without git (an unpacked sdist, say) the placeholder
51
+ stands.
52
+
53
+ A plain `cargo build` doesn't go through that shim, so `build.rs` derives the
54
+ same version independently and exposes it as `NC_GCODE_INTERPRETER_VERSION`,
55
+ which is what the CLI's `--version` reports. It applies the same rules: an
56
+ already-stamped `Cargo.toml` version wins, and without git the placeholder
57
+ stands. The CLI therefore shows cargo's semver spelling
58
+ (`0.2.6-dev.1+ge5f2306`), not maturin's PEP 440 normalisation of it.
59
+
33
60
  ### Release profile
34
61
 
35
62
  `[profile.release]` currently sets no `strip`/`lto` overrides. `strip = true` was
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nc-gcode-interpreter
3
- Version: 0.2.6
3
+ Version: 0.2.7
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: 3.12
@@ -0,0 +1,62 @@
1
+ //! Give `nc-gcode-interpreter --version` a real version in local builds.
2
+ //!
3
+ //! `Cargo.toml` carries the placeholder version `0.0.0-dev`, which the release
4
+ //! CI rewrites from the git tag. A plain `cargo build` in a checkout doesn't go
5
+ //! through the PEP 517 shim in `build_backend/` that does the same for wheels,
6
+ //! so without this the CLI would report `0.0.0-dev` for every dev build. Derive
7
+ //! the same `git describe`-based version here and expose it as
8
+ //! `NC_GCODE_INTERPRETER_VERSION`; fall back to the `Cargo.toml` version when
9
+ //! there is no git checkout (an unpacked sdist) or the version is already
10
+ //! stamped.
11
+
12
+ use std::process::Command;
13
+
14
+ const PLACEHOLDER: &str = "0.0.0-dev";
15
+
16
+ fn main() {
17
+ // Re-derive when the checkout moves: HEAD (branch switch), the index
18
+ // (commits, staging) and the tags. A bare unstaged edit won't retrigger
19
+ // the build script, so a stale `.dirty` suffix is possible; it clears on
20
+ // the next `cargo build` that recompiles anything.
21
+ println!("cargo:rerun-if-changed=.git/HEAD");
22
+ println!("cargo:rerun-if-changed=.git/index");
23
+ println!("cargo:rerun-if-changed=.git/refs/tags");
24
+
25
+ let cargo_version = env!("CARGO_PKG_VERSION");
26
+ let version = if cargo_version == PLACEHOLDER {
27
+ git_version().unwrap_or_else(|| cargo_version.to_string())
28
+ } else {
29
+ cargo_version.to_string()
30
+ };
31
+ println!("cargo:rustc-env=NC_GCODE_INTERPRETER_VERSION={version}");
32
+ }
33
+
34
+ /// `v0.2.6-3-g1234abc[-dirty]` -> `0.2.6-dev.3+g1234abc[.dirty]`, or the bare
35
+ /// tag when we are exactly on a clean tag.
36
+ fn git_version() -> Option<String> {
37
+ let output = Command::new("git")
38
+ .args(["describe", "--tags", "--long", "--dirty", "--match", "v[0-9]*"])
39
+ .output()
40
+ .ok()?;
41
+ if !output.status.success() {
42
+ return None;
43
+ }
44
+ let described = String::from_utf8(output.stdout).ok()?;
45
+ let described = described.trim().strip_prefix('v')?;
46
+
47
+ let (described, dirty) = match described.strip_suffix("-dirty") {
48
+ Some(rest) => (rest, true),
49
+ None => (described, false),
50
+ };
51
+ // Split off the trailing `-<distance>-g<sha>`; the tag itself may contain
52
+ // `-`, so scan from the right.
53
+ let (rest, sha) = described.rsplit_once('-')?;
54
+ let (tag, distance) = rest.rsplit_once('-')?;
55
+ let distance: u32 = distance.parse().ok()?;
56
+
57
+ if distance == 0 && !dirty {
58
+ return Some(tag.to_string());
59
+ }
60
+ let local = if dirty { format!("{sha}.dirty") } else { sha.to_string() };
61
+ Some(format!("{tag}-dev.{distance}+{local}"))
62
+ }
@@ -0,0 +1,116 @@
1
+ """In-tree PEP 517 backend: give local builds a version from the git tag.
2
+
3
+ `Cargo.toml` carries the placeholder version `0.0.0-dev`, which the release CI
4
+ rewrites from the tag before building. Without this shim every non-release
5
+ build -- notably an editable `uv sync` in a sibling checkout -- reports
6
+ `0.0.0.dev0`, which is useless as a version floor and gives a downstream
7
+ consumer no signal that its interpreter is a local edit rather than a release.
8
+
9
+ So: when the placeholder is still in place and we are inside a git checkout
10
+ with tags, rewrite it to a semver dev version derived from `git describe`
11
+ (`0.2.6-dev.3+g1234abc`), which maturin normalises to the PEP 440
12
+ `0.2.6.dev3+g1234abc`. Any other version in `Cargo.toml` is left alone, so the
13
+ CI release path is untouched.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import re
19
+ import subprocess
20
+ from contextlib import contextmanager
21
+ from pathlib import Path
22
+
23
+ from maturin import build_editable as _maturin_build_editable
24
+ from maturin import build_sdist as _maturin_build_sdist
25
+ from maturin import build_wheel as _maturin_build_wheel
26
+
27
+ # The hooks this module does not wrap are re-exported unchanged; the rest are
28
+ # imported under an alias and overridden below.
29
+ from maturin import ( # noqa: F401
30
+ get_requires_for_build_editable,
31
+ get_requires_for_build_sdist,
32
+ get_requires_for_build_wheel,
33
+ )
34
+ from maturin import (
35
+ prepare_metadata_for_build_editable as _maturin_prepare_metadata_for_build_editable,
36
+ )
37
+ from maturin import (
38
+ prepare_metadata_for_build_wheel as _maturin_prepare_metadata_for_build_wheel,
39
+ )
40
+
41
+ PLACEHOLDER = "0.0.0-dev"
42
+ _VERSION_LINE = re.compile(r'^version = ".*"$', re.MULTILINE)
43
+ # v0.2.6-3-g1234abc[-dirty]
44
+ _DESCRIBE = re.compile(r"^v(?P<tag>.+)-(?P<distance>\d+)-g(?P<sha>[0-9a-f]+)(?P<dirty>-dirty)?$")
45
+
46
+
47
+ def _git_version(root: Path) -> str | None:
48
+ """Semver dev version from `git describe`, or None if it can't be derived."""
49
+ try:
50
+ described = subprocess.run(
51
+ ["git", "describe", "--tags", "--long", "--dirty", "--match", "v[0-9]*"],
52
+ cwd=root,
53
+ capture_output=True,
54
+ text=True,
55
+ check=True,
56
+ ).stdout.strip()
57
+ except (OSError, subprocess.CalledProcessError):
58
+ return None
59
+ match = _DESCRIBE.match(described)
60
+ if not match:
61
+ return None
62
+ tag, distance, sha = match["tag"], int(match["distance"]), match["sha"]
63
+ if distance == 0 and not match["dirty"]:
64
+ return tag
65
+ local = sha if not match["dirty"] else f"{sha}.dirty"
66
+ return f"{tag}-dev.{distance}+{local}"
67
+
68
+
69
+ @contextmanager
70
+ def _versioned_cargo_toml():
71
+ root = Path(__file__).resolve().parent.parent
72
+ cargo_toml = root / "Cargo.toml"
73
+ cargo_lock = root / "Cargo.lock"
74
+ original = cargo_toml.read_text()
75
+ if f'version = "{PLACEHOLDER}"' not in original:
76
+ # CI already stamped the release version; nothing to do.
77
+ yield
78
+ return
79
+ version = _git_version(root)
80
+ if version is None:
81
+ yield
82
+ return
83
+ lock_original = cargo_lock.read_text() if cargo_lock.exists() else None
84
+ cargo_toml.write_text(_VERSION_LINE.sub(f'version = "{version}"', original, count=1))
85
+ try:
86
+ yield
87
+ finally:
88
+ cargo_toml.write_text(original)
89
+ # cargo rewrites the lock to match the patched version during the build.
90
+ if lock_original is not None:
91
+ cargo_lock.write_text(lock_original)
92
+
93
+
94
+ def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
95
+ with _versioned_cargo_toml():
96
+ return _maturin_build_wheel(wheel_directory, config_settings, metadata_directory)
97
+
98
+
99
+ def build_editable(wheel_directory, config_settings=None, metadata_directory=None):
100
+ with _versioned_cargo_toml():
101
+ return _maturin_build_editable(wheel_directory, config_settings, metadata_directory)
102
+
103
+
104
+ def build_sdist(sdist_directory, config_settings=None):
105
+ with _versioned_cargo_toml():
106
+ return _maturin_build_sdist(sdist_directory, config_settings)
107
+
108
+
109
+ def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):
110
+ with _versioned_cargo_toml():
111
+ return _maturin_prepare_metadata_for_build_wheel(metadata_directory, config_settings)
112
+
113
+
114
+ def prepare_metadata_for_build_editable(metadata_directory, config_settings=None):
115
+ with _versioned_cargo_toml():
116
+ return _maturin_prepare_metadata_for_build_editable(metadata_directory, config_settings)
@@ -6,13 +6,13 @@
6
6
  #
7
7
  # Run from the repository root or this directory:
8
8
  # python ggroups/generate_g_commands.py
9
- from pathlib import Path
10
9
  import json
10
+ from pathlib import Path
11
11
 
12
12
  json_file = Path(__file__).parent.parent / "python/nc_gcode_interpreter/ggroups.json"
13
13
  out_file = Path(__file__).parent.parent / "src/modal_groups.rs"
14
14
 
15
- with open(json_file, "r") as file:
15
+ with open(json_file) as file:
16
16
  g_groups = json.load(file)
17
17
 
18
18
  modal = [g["short_name"] for g in g_groups if g["effectiveness"] == "modal"]
@@ -1,6 +1,10 @@
1
1
  [build-system]
2
2
  requires = ["maturin>=1.7,<2.0"]
3
- build-backend = "maturin"
3
+ # In-tree shim around maturin: stamps a `git describe` version onto the
4
+ # Cargo.toml placeholder so local/editable builds report e.g.
5
+ # 0.2.6.dev3+g1234abc instead of 0.0.0.dev0. See build_backend/.
6
+ build-backend = "nc_build_backend"
7
+ backend-path = ["build_backend"]
4
8
 
5
9
  [project]
6
10
  name = "nc-gcode-interpreter"
@@ -51,6 +55,25 @@ Homepage = "https://github.com/CEAD-group/nc-gcode-interpreter"
51
55
  Repository = "https://github.com/CEAD-group/nc-gcode-interpreter.git"
52
56
  Documentation = "https://github.com/CEAD-group/nc-gcode-interpreter/blob/main/README.md"
53
57
 
58
+ [tool.ruff]
59
+ target-version = "py312"
60
+
61
+ [tool.ruff.lint]
62
+ # Pin the rule set explicitly. `ruff check .` previously ran on ruff's implicit
63
+ # defaults, so when ruff 0.16 widened them the lint job went red on 64
64
+ # pre-existing findings in code nobody had touched. An explicit select means a
65
+ # ruff upgrade adds rules only when we ask for them.
66
+ select = [
67
+ "E4", "E7", "E9", "F", # ruff's historical defaults
68
+ "I", # import sorting
69
+ "UP", # pyupgrade
70
+ "B", # bugbear
71
+ "C4", # comprehensions
72
+ "PIE",
73
+ "RUF",
74
+ "FLY",
75
+ ]
76
+
54
77
  [tool.mypy]
55
78
  # The viz extra's dependencies are optional: mypy runs in CI without them.
56
79
  [[tool.mypy.overrides]]
@@ -1,14 +1,17 @@
1
- from typing import Protocol, Iterator
1
+ import json
2
2
  import os
3
+ from collections.abc import Callable, Iterator
4
+ from pathlib import Path
5
+ from typing import Any, Protocol, TypedDict
6
+
3
7
  import polars as pl
4
- from ._internal import nc_to_rows as _nc_to_rows
8
+
9
+ from ._internal import (
10
+ NcError,
11
+ __doc__, # noqa: F401
12
+ )
5
13
  from ._internal import nc_to_batches as _nc_to_batches
6
- from ._internal import NcError
7
- from ._internal import __doc__ # noqa: F401
8
- import json
9
- from pathlib import Path
10
- from typing import TypedDict, TypeVar, Any, Generic
11
- from collections.abc import Callable
14
+ from ._internal import nc_to_rows as _nc_to_rows
12
15
 
13
16
 
14
17
  # Define TextFileLike Protocol
@@ -17,12 +20,12 @@ class TextFileLike(Protocol):
17
20
 
18
21
 
19
22
  __all__ = [
23
+ "NcError",
24
+ "dataframe_to_nc",
25
+ "nc_to_batches",
20
26
  "nc_to_dataframe",
21
27
  "nc_to_rows",
22
- "nc_to_batches",
23
28
  "sanitize_dataframe",
24
- "dataframe_to_nc",
25
- "NcError",
26
29
  ]
27
30
 
28
31
  # Batch size nc_to_dataframe uses internally when draining the batch stream it
@@ -171,14 +174,11 @@ def nc_to_dataframe(
171
174
  return pl.concat(frames, how="diagonal").select(frames[-1].columns), state
172
175
 
173
176
 
174
- _T = TypeVar("_T")
175
-
176
-
177
- class _classproperty(Generic[_T]):
178
- def __init__(self, fget: Callable[[Any], _T]) -> None:
177
+ class _classproperty[T]:
178
+ def __init__(self, fget: Callable[[Any], T]) -> None:
179
179
  self.fget = fget
180
180
 
181
- def __get__(self, instance: Any, owner: type[Any]) -> _T:
181
+ def __get__(self, instance: Any, owner: type[Any]) -> T:
182
182
  return self.fget(owner)
183
183
 
184
184
 
@@ -295,10 +295,16 @@ def sanitize_dataframe(
295
295
  # Canonical column order.
296
296
  order: list[str] = []
297
297
  for name in (
298
- ["N"] + modal + non_modal + known_axes
299
- + sorted(c for c in value_columns if c not in known_axes and c not in block_addresses)
300
- + block_addresses
301
- + ["T", "M", "non_returning_function_call", "comment"]
298
+ "N",
299
+ *modal,
300
+ *non_modal,
301
+ *known_axes,
302
+ *sorted(c for c in value_columns if c not in known_axes and c not in block_addresses),
303
+ *block_addresses,
304
+ "T",
305
+ "M",
306
+ "non_returning_function_call",
307
+ "comment",
302
308
  ):
303
309
  if name in df.columns and name not in order:
304
310
  order.append(name)
@@ -1,4 +1,5 @@
1
- from typing import Any, Iterator, Optional, List, Dict, Tuple
1
+ from collections.abc import Iterator
2
+ from typing import Any
2
3
 
3
4
  # Type stubs for the compiled Rust extension `nc_gcode_interpreter._internal`.
4
5
  # mypy cannot introspect the pyo3 module, so the public entry points the Python
@@ -13,39 +14,38 @@ class NcError(ValueError):
13
14
  #: ``"unexpected_axis"``, ``"undefined_variable"``), for branching without
14
15
  #: matching the formatted message.
15
16
  kind: str
16
- line: Optional[int]
17
- column: Optional[int]
18
- context: Optional[str]
19
- line_text: Optional[str]
17
+ line: int | None
18
+ column: int | None
19
+ context: str | None
20
+ line_text: str | None
20
21
 
21
22
  def nc_to_rows(
22
23
  input: str,
23
- initial_state: Optional[str] = None,
24
- axis_identifiers: Optional[List[str]] = None,
25
- extra_axes: Optional[List[str]] = None,
24
+ initial_state: str | None = None,
25
+ axis_identifiers: list[str] | None = None,
26
+ extra_axes: list[str] | None = None,
26
27
  iteration_limit: int = 10000,
27
28
  forward_fill: bool = True,
28
29
  include_variables: bool = False,
29
- axis_index_map: Optional[Dict[str, int]] = None,
30
+ axis_index_map: dict[str, int] | None = None,
30
31
  allow_undefined_variables: bool = False,
31
32
  input_is_path: bool = False,
32
- flatten_tolerance: Optional[float] = None,
33
- ) -> Iterator[Tuple[Any, ...]]:
33
+ flatten_tolerance: float | None = None,
34
+ ) -> Iterator[tuple[Any, ...]]:
34
35
  """Interpret an NC program lazily into ``(line_no, row[, variables])`` tuples."""
35
- ...
36
36
 
37
37
  def nc_to_batches(
38
38
  input: str,
39
39
  batch_size: int = 500_000,
40
- initial_state: Optional[str] = None,
41
- axis_identifiers: Optional[List[str]] = None,
42
- extra_axes: Optional[List[str]] = None,
40
+ initial_state: str | None = None,
41
+ axis_identifiers: list[str] | None = None,
42
+ extra_axes: list[str] | None = None,
43
43
  iteration_limit: int = 10000,
44
44
  disable_forward_fill: bool = False,
45
- axis_index_map: Optional[Dict[str, int]] = None,
45
+ axis_index_map: dict[str, int] | None = None,
46
46
  allow_undefined_variables: bool = False,
47
47
  input_is_path: bool = False,
48
- flatten_tolerance: Optional[float] = None,
48
+ flatten_tolerance: float | None = None,
49
49
  include_line_numbers: bool = False,
50
50
  include_variables: bool = False,
51
51
  ) -> Any:
@@ -56,6 +56,5 @@ def nc_to_batches(
56
56
  - when ``include_variables`` is set - ``variable_events`` (an Arrow batch of
57
57
  ``row_idx`` / ``name_id`` / ``value``) and ``variable_names`` (list[str]).
58
58
  """
59
- ...
60
59
 
61
- __all__ = ["nc_to_rows", "nc_to_batches"]
60
+ __all__ = ["nc_to_batches", "nc_to_rows"]
@@ -128,8 +128,9 @@ def main(argv: list[str] | None = None) -> int:
128
128
  args = build_parser().parse_args(argv)
129
129
 
130
130
  try:
131
- from .viz import view_toolpath
132
131
  import threejs_viewer # noqa: F401 - fail here, before interpreting
132
+
133
+ from .viz import view_toolpath
133
134
  except ImportError:
134
135
  print(
135
136
  "nc-view needs the viz extra: pip install 'nc-gcode-interpreter[viz]'",
@@ -84,7 +84,7 @@ def toolpath_arrays(
84
84
  bead_width: float | None = None,
85
85
  bead_height: float | None = None,
86
86
  default_feed: float = 1000.0,
87
- ) -> "tuple[np.ndarray, np.ndarray | None]":
87
+ ) -> tuple[np.ndarray, np.ndarray | None]:
88
88
  """Convert an interpreter DataFrame to threejs-viewer toolpath arrays.
89
89
 
90
90
  Returns ``(data, colors)``: ``data`` is the ``(N, 6)`` float64
@@ -0,0 +1,11 @@
1
+ """Helper for writing multi-line NC programs in tests."""
2
+
3
+
4
+ def nc_lines(*lines: str) -> str:
5
+ """Join NC blocks into a program, one block per line.
6
+
7
+ Written as varargs rather than `"\\n".join([...])` so each block can carry
8
+ its own trailing `#` comment explaining what it contributes to the expected
9
+ output - which is most of why these programs are written a line at a time.
10
+ """
11
+ return "\n".join(lines)