Skip to content

Commit 5c324c4

Browse files
shigarovclaude
andcommitted
Close remaining migration-plan items, release prep 0.3.0
Implements the items left open after auditing plans/PYREGTAB_MIGRATION_PLAN.md against the shipped state: - AtpMatcher.match_many(pattern, syntaxes, context_items=None) (plan §4.5): parallel batch matching on an internal scoped-thread pool with the GIL released; degrades to the sequential GIL path when the pattern holds Python callbacks; on error no input table is modified. The single-table fast path is refactored into the shared match_core_no_gil helper. - Recordset.to_csv(path=None, *, sep=",", missing="") (plan §4.1): RFC 4180 quoting, returns the text or writes the file. - clippy is now blocking in CI (-D warnings); fixed the five outstanding lints (entry API, manual_contains, needless_range_loop, type_complexity alias, documented allow for large_enum_variant on CellAst). - New manual "differential" CI job (workflow_dispatch, plan §7.3): builds jRegTab at the pinned v0.4.1 tag, dumps reference recordsets, compares cell-by-cell via tools/differential.py. - Plan document marked up: phases checked off, new §11 records what was closed now and what is deliberately deferred (criterion/JMH benchmarks, proptest, miri, pure-Python fallback). - Version bumped to 0.3.0 (Cargo.toml/lock, pyproject, __version__, docs); README/api docs describe the new APIs; suite is now 1908 tests, green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent ffd571d commit 5c324c4

16 files changed

Lines changed: 379 additions & 70 deletions

File tree

.github/workflows/ci.yml

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
branches: [main]
66
tags: ["v*"]
77
pull_request:
8+
workflow_dispatch:
89

910
jobs:
1011
grammar-sync:
@@ -36,11 +37,11 @@ jobs:
3637
with:
3738
python-version: "3.10"
3839
- uses: dtolnay/rust-toolchain@stable
39-
- name: Lint (rustfmt + clippy)
40+
with:
41+
components: clippy
42+
- name: Lint (clippy, blocking)
4043
if: matrix.os == 'ubuntu-latest'
41-
run: |
42-
rustup component add clippy
43-
cargo clippy --all-targets -- -D warnings || true
44+
run: cargo clippy --all-targets -- -D warnings
4445
- name: Build and install
4546
run: |
4647
pip install maturin pytest
@@ -49,6 +50,47 @@ jobs:
4950
- name: Run test suite (150 tasks + conformance + round-trip)
5051
run: pytest tests -q
5152

53+
differential:
54+
# Differential test against the reference Java implementation (plan
55+
# §7.3): compiles jRegTab at the pinned tag, dumps its recordsets for all
56+
# task variants, and compares them cell-by-cell with pyRegTab. Manual
57+
# trigger (workflow_dispatch) because it needs the JREGTAB_TOKEN secret
58+
# with read access to the private regtab/jregtab repository.
59+
if: github.event_name == 'workflow_dispatch'
60+
runs-on: ubuntu-latest
61+
steps:
62+
- uses: actions/checkout@v4
63+
- name: Check out jRegTab at the pinned release
64+
uses: actions/checkout@v4
65+
with:
66+
repository: regtab/jregtab
67+
ref: v0.4.1
68+
token: ${{ secrets.JREGTAB_TOKEN }}
69+
path: jregtab
70+
- uses: actions/setup-java@v4
71+
with:
72+
distribution: temurin
73+
java-version: "21"
74+
cache: maven
75+
- uses: actions/setup-python@v5
76+
with:
77+
python-version: "3.12"
78+
- name: Dump reference recordsets from jRegTab
79+
run: |
80+
cp tools/RecordsetDumpMain.java jregtab/src/test/java/ru/icc/regtab/
81+
mvn -q -f jregtab/pom.xml -DskipTests test-compile \
82+
dependency:build-classpath -Dmdep.outputFile="$PWD/cp.txt"
83+
java -cp "jregtab/target/test-classes:jregtab/target/classes:$(cat cp.txt)" \
84+
ru.icc.regtab.RecordsetDumpMain tests/fixtures/tasks conformance/positive dump.jsonl
85+
- uses: dtolnay/rust-toolchain@stable
86+
- name: Build and install pyRegTab
87+
run: |
88+
pip install maturin
89+
maturin build --release --out dist
90+
pip install --no-index --find-links dist pyregtab
91+
- name: Compare recordsets cell-by-cell
92+
run: python tools/differential.py dump.jsonl
93+
5294
docs:
5395
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
5496
needs: test

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "pyregtab"
3-
version = "0.2.0"
3+
version = "0.3.0"
44
edition = "2021"
55
description = "Native core of pyRegTab: RTL compiler, ATP matcher and table interpreter"
66
license = "MIT"

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ and interprets the match into a relational **recordset**:
1616
TableSyntax → RtlCompiler/TablePattern → AtpMatcher → TableInterpreter → Recordset
1717
```
1818

19-
**pyRegTab 0.2.0 ≙ jRegTab 0.4.1** (same API, same semantics, same test
19+
**pyRegTab 0.3.0 ≙ jRegTab 0.4.1** (same API, same semantics, same test
2020
corpus; jRegTab 0.4.1 changes only the Java build over 0.4.0), including the
2121
embedded RTL DSL `pyregtab.dsl` — a port of jRegTab's `ru.icc.regtab.dsl`
22-
(added upstream in jRegTab 0.3.0).
22+
(added upstream in jRegTab 0.3.0). Python-side extras on top of the Java API:
23+
`AtpMatcher.match_many` (parallel batch matching), `Recordset.to_pandas()`
24+
and `Recordset.to_csv()`.
2325

2426
## Installation
2527

@@ -140,7 +142,7 @@ Rust (`pyregtab._core`, built with [PyO3](https://pyo3.rs) and
140142

141143
## Testing
142144

143-
`pytest tests` runs (1 904 tests):
145+
`pytest tests` runs (1 908 tests):
144146

145147
- the full benchmark suite — tasks 001–150 (Foofah, RegTab, Baikal),
146148
every fixture variant, **both** via RTL patterns and via ATP patterns
@@ -156,7 +158,7 @@ Rust (`pyregtab._core`, built with [PyO3](https://pyo3.rs) and
156158
- RTL↔ATP round-trip for tasks 001–050;
157159
- API unit tests (syntax layer, extractors, EXT bindings, custom
158160
predicates, transformations, interpreter options, GIL-released batch
159-
matching from a thread pool).
161+
matching from a thread pool and via `AtpMatcher.match_many`).
160162

161163
`cargo test` additionally runs the conformance corpus and an end-to-end
162164
smoke test against the native core alone. Differential testing against the

docs/api.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ core `pyregtab._core` (Rust). Full signatures are in the bundled type stubs
1212
|---|---|
1313
| `RtlCompiler.compile(rtl, bindings=None)` / `pyregtab.compile(...)` | RTL string → `TablePattern`; raises `RtlCompileError` with source position |
1414
| `AtpMatcher.match(pattern, syntax, context_items=None)` | `TablePattern` × `TableSyntax``InterpretableTable \| None` |
15+
| `AtpMatcher.match_many(pattern, syntaxes, context_items=None)` | batch form of `match`: `list[InterpretableTable \| None]` in input order, matched in parallel on an internal thread pool (GIL released) |
1516
| `TableInterpreter().interpret(itm)` | `InterpretableTable``Recordset` |
1617
| `AtpToRtlSerializer.serialize(pattern)` | `TablePattern` → canonical RTL string |
1718
| `TablePattern.transform(rs)` | applies the pattern's recordset transformations |
@@ -31,7 +32,10 @@ rs = (
3132

3233
When the pattern contains no Python callbacks (no `EXT`/custom predicates),
3334
`AtpMatcher.match` and `TableInterpreter.interpret` release the GIL — batch
34-
processing parallelizes with a plain `ThreadPoolExecutor`.
35+
processing parallelizes with a plain `ThreadPoolExecutor`, or use
36+
`AtpMatcher.match_many`, which fans the tables out over an internal thread
37+
pool itself. With Python callbacks present, `match_many` degrades to matching
38+
sequentially under the GIL, like `match`.
3539

3640
## ITM: syntactic layer
3741

@@ -50,7 +54,7 @@ processing parallelizes with a plain `ThreadPoolExecutor`.
5054
| `TableSemantics` | `cell_derived_items()`, `context_derived_items()` |
5155
| `CellDerivedItem` | `.str`, `.tags`, `.index`, `.cell`; passed to filter callbacks |
5256
| `ContextDerivedItem(s, ItemType, const_value=None)` | external context items for `AtpMatcher.match` |
53-
| `Recordset` | `.schema`, `.records`, `len(rs)`, `rs[i]`, `to_pandas()` |
57+
| `Recordset` | `.schema`, `.records`, `len(rs)`, `rs[i]`, `to_pandas()`, `to_csv(path=None, sep=",", missing="")` (RFC 4180; returns the CSV text when `path` is None) |
5458
| `Record` | `rec[attr]`, `rec[i]`, `.get(...)`, `.values()`; missing value → `None` |
5559
| `Schema` | `.attributes`, `index_of`, `contains` |
5660

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,4 @@ Requires **Python 3.10+**; binary wheels for Windows, Linux, and macOS.
109109
---
110110

111111
!!! note "Status"
112-
Current release: **0.2.0** (feature parity with jRegTab 0.4.1) · License: **MIT** · [PyPI](https://pypi.org/project/pyregtab/) · [GitHub](https://github.com/regtab/pyregtab)
112+
Current release: **0.3.0** (feature parity with jRegTab 0.4.1) · License: **MIT** · [PyPI](https://pypi.org/project/pyregtab/) · [GitHub](https://github.com/regtab/pyregtab)

plans/PYREGTAB_MIGRATION_PLAN.md

Lines changed: 63 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# План миграции jRegTab → pyRegTab
22

3-
**Статус:** ЧЕРНОВИК v2 (ANTLR4-требование ослаблено; ядро — Rust, альтернатива — C++)
3+
**Статус:** РЕАЛИЗОВАН (вариант A — Rust; сверка выполнена 2026-07-10, итог и
4+
осознанно отложенные пункты — в §11)
45
**Дата:** 2026-07-06 (v2 — того же дня; v1 с обязательным ANTLR4 заменена целиком)
56
**Исходный проект:** `d:\YandexDisk\code2\jregtab` (Java 21, v0.1.2-SNAPSHOT)
67
**Целевой проект:** pyRegTab — Python-пакет с нативным ядром, публикуемый на PyPI
@@ -379,57 +380,60 @@ conformance-корпус зелёный в обоих проектах**.
379380
Оценки — в неделях чистой работы одного разработчика; фазы 2–5 частично параллелизуемы.
380381

381382
### Фаза 0 — Решения и прототип (1 нед.)
382-
- [ ] Аудит регулярок во всех фикстурах/тестах → выбор `regex` vs `fancy-regex`.
383-
- [ ] Прототип парсера: лексер (logos) + рекурсивный спуск для 2–3 правил `RTL.g4`,
383+
- [x] Аудит регулярок во всех фикстурах/тестах → выбор `regex` vs `fancy-regex`.
384+
- [x] Прототип парсера: лексер (logos) + рекурсивный спуск для 2–3 правил `RTL.g4`,
384385
разбор 5–10 реальных RTL-строк.
385-
- [ ] Прототип wheel: maturin + PyO3, пустой `_core` с одним классом, сборка
386+
- [x] Прототип wheel: maturin + PyO3, пустой `_core` с одним классом, сборка
386387
wheels на Windows и Linux (GitHub Actions, maturin-action).
387-
- [ ] Утвердить стиль API (properties vs `set_*`; §4.6) и имя пакета на PyPI.
388-
- [ ] **Точка выхода**: при блокере — переключение на C++ (вариант B), §3.B.
388+
- [x] Утвердить стиль API (properties vs `set_*`; §4.6) и имя пакета на PyPI.
389+
- [x] **Точка выхода**: при блокере — переключение на C++ (вариант B), §3.B.
389390
- Результат: утверждённые решения, репозиторий-скелет `pyregtab`.
390391

391392
### Фаза 1 — ITM syntax + Recordset (1–1.5 нед.)
392-
- [ ] Порт `itm.syntax` (11 классов) на Rust: арена + индексы (§4.2).
393-
- [ ] Порт `recordset` (3 класса).
394-
- [ ] PyO3-биндинги (handle-объекты) + Python-обёртки `syntax.py`, `recordset.py`.
395-
- [ ] Юнит-тесты (создание таблиц, subtables/subrows, форматирование ячеек).
393+
- [x] Порт `itm.syntax` (11 классов) на Rust: арена + индексы (§4.2).
394+
- [x] Порт `recordset` (3 класса).
395+
- [x] PyO3-биндинги (handle-объекты) + Python-обёртки `syntax.py`, `recordset.py`.
396+
- [x] Юнит-тесты (создание таблиц, subtables/subrows, форматирование ячеек).
396397

397398
### Фаза 2 — ATP spec (1–1.5 нед.)
398-
- [ ] Порт `atp.spec` (18 классов) на Rust-enum'ы/структуры: паттерны, content specs,
399+
- [x] Порт `atp.spec` (18 классов) на Rust-enum'ы/структуры: паттерны, content specs,
399400
`ActionSpec`, `ProviderSpec`, `FilterTerm`, `Quantifier`, `StringExtractor`, предикаты.
400-
- [ ] Custom-предикаты с Python-callable (§4.4).
401-
- [ ] Python-фабрики, повторяющие Java-фабрики; docstrings из Javadoc.
401+
- [x] Custom-предикаты с Python-callable (§4.4).
402+
- [x] Python-фабрики, повторяющие Java-фабрики; docstrings из Javadoc.
402403

403404
### Фаза 3 — Matcher + ITM semantics (2 нед., самая сложная)
404-
- [ ] Порт `itm.semantics` (items, providers, operations, `WorkingState`).
405-
- [ ] Порт `atp.match` (`SyntaxMatcher`, `SemanticConstructor`, `AtpMatcher`).
406-
- [ ] Первые сквозные тесты: задачи 001–020 через ATP-паттерны (без RTL).
405+
- [x] Порт `itm.semantics` (items, providers, operations, `WorkingState`).
406+
- [x] Порт `atp.match` (`SyntaxMatcher`, `SemanticConstructor`, `AtpMatcher`).
407+
- [x] Первые сквозные тесты: задачи 001–020 через ATP-паттерны (без RTL).
407408

408409
### Фаза 4 — Interpreter (1 нед.)
409-
- [ ] Порт `interpret` (4 фазы, стратегии, `MissingValueHandler`, трансформации
410+
- [x] Порт `interpret` (4 фазы, стратегии, `MissingValueHandler`, трансформации
410411
`WhitespaceNormalization` / `FieldSplitting` / `SchemaReordering` / `AnchorAttributeAtPosition`).
411-
- [ ] Сквозные ATP-тесты для всех 150 задач зелёные.
412+
- [x] Сквозные ATP-тесты для всех 150 задач зелёные.
412413

413414
### Фаза 5 — RTL: парсер, построитель ATP, сериализатор (1.5–2 нед.)
414-
- [ ] Полный лексер + рекурсивный спуск по всем правилам `RTL.g4` (§5.1).
415-
- [ ] Порт логики `ATPBuilder`, `ProviderTemplateResolver`, `StringExtractorFactory`;
415+
- [x] Полный лексер + рекурсивный спуск по всем правилам `RTL.g4` (§5.1).
416+
- [x] Порт логики `ATPBuilder`, `ProviderTemplateResolver`, `StringExtractorFactory`;
416417
`RtlCompiler` / `RtlCompileError` с диагностикой line:col.
417-
- [ ] Порт `AtpToRtlSerializer`; round-trip тест.
418-
- [ ] Сборка conformance-корпуса (позитив из тестов jregtab, негатив — по веткам
418+
- [x] Порт `AtpToRtlSerializer`; round-trip тест.
419+
- [x] Сборка conformance-корпуса (позитив из тестов jregtab, негатив — по веткам
419420
ошибок парсера); job для jregtab-CI.
420-
- [ ] RTL-тесты для всех 150 задач зелёные (итого 1 500 вариантов).
421+
- [x] RTL-тесты для всех 150 задач зелёные (итого 1 500 вариантов).
421422

422423
### Фаза 6 — Дифференциальная проверка и полировка API (1 нед.)
423-
- [ ] Дифф-прогон против jRegTab (§7.3), устранение расхождений.
424-
- [ ] GIL-release (`allow_threads`), `match_many`-бенчмарк, сравнение скорости
425-
с jRegTab (criterion ↔ JMH).
426-
- [ ] `to_pandas()`, `__repr__`, типизация (`py.typed`, stubs для `_core`).
424+
- [x] Дифф-прогон против jRegTab (§7.3), устранение расхождений
425+
(750/750 вариантов идентичны; ручной CI-job `differential` добавлен).
426+
- [x] GIL-release (`allow_threads`) и `AtpMatcher.match_many` (внутренний пул
427+
потоков, реализован в 0.3.0). *Не сделано:* бенчмарк-сравнение скорости
428+
с jRegTab (criterion ↔ JMH) — см. §11.
429+
- [x] `to_pandas()`, `to_csv()`, `__repr__`, типизация (`py.typed`, stubs для `_core`).
427430

428431
### Фаза 7 — CI/CD, документация, релиз (1 нед.)
429-
- [ ] maturin-action матрица wheels, публикация на TestPyPI → PyPI (`pyregtab 0.1.0`).
430-
- [ ] mkdocs-сайт (адаптация docs jregtab: getting-started, RTL reference — общий,
432+
- [x] maturin-action матрица wheels, публикация на PyPI (`pyregtab 0.1.0`,
433+
2026-07-07; сразу через trusted publishing, без промежуточного TestPyPI).
434+
- [x] mkdocs-сайт (адаптация docs jregtab: getting-started, RTL reference — общий,
431435
API reference — Python).
432-
- [ ] README с примером из §4.6; документ о процессе эволюции RTL (§5.2) — в оба репо.
436+
- [x] README с примером из §4.6; документ о процессе эволюции RTL (§5.2) — в оба репо.
433437

434438
**Итого: ~8.5–10 недель.**
435439

@@ -460,3 +464,32 @@ conformance-корпус зелёный в обоих проектах**.
460464
в 0.1 (`Bindings`, `RtlCompiler.compile(rtl, bindings)`). Отложено на будущее:
461465
перегрузка операторов Python (`+`/`*` квантификаторы) и корпусный
462466
DSL-генератор в `tools/translate_atp.py --dsl`.
467+
468+
---
469+
470+
## 11. Статус реализации (сверка 2026-07-10)
471+
472+
План реализован: вариант A (Rust + PyO3 + maturin), релизы `pyregtab` 0.1.0 →
473+
0.1.1 → 0.2.0 на PyPI, паритет с jRegTab 0.4.1. Критерий готовности §7 выполнен:
474+
1 500/1 500 вариантов зелёные, дифф с jRegTab пуст (750/750), conformance-корпус
475+
зелёный в CI обоих проектов. Все открытые вопросы §10 решены (Rust; `cell.text`
476+
property + `set_text()`; имя `pyregtab`; корпус копиями с пином; Python ≥ 3.10;
477+
pure-Python-фолбэк не делается — достаточно sdist).
478+
479+
Сверх плана: embedded RTL DSL (`pyregtab.dsl`, 0.2.0) и IDE-поддержка RTL
480+
(`ide/`, зеркало jRegTab 0.4.0).
481+
482+
Закрыто при сверке (0.3.0): `AtpMatcher.match_many` (§4.5) — параллельный
483+
батчевый матчинг с внутренним пулом потоков без GIL; `Recordset.to_csv()`
484+
(§4.1); clippy в CI стал блокирующим (§7.7); ручной CI-job `differential`
485+
(workflow_dispatch, §7.3).
486+
487+
Осознанно отложено (не блокирует паритет и релизы):
488+
489+
- бенчмарки скорости против jRegTab (criterion ↔ JMH, фаза 6) — отдельный
490+
перфоманс-проект;
491+
- property-based тесты парсера (proptest, §7.6) и выборочный `cargo miri`
492+
(§7.7) — покрытие обеспечивают conformance-корпус и 1 500 вариантов;
493+
- pure-Python fallback (§10.6) — отклонён;
494+
- рукописный лексер вместо `logos` и единый `__init__.py`-реэкспорт вместо
495+
расписанных в §4.1 модулей Python-слоя — эквивалентные упрощения, не долг.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "maturin"
44

55
[project]
66
name = "pyregtab"
7-
version = "0.2.0"
7+
version = "0.3.0"
88
description = "pyRegTab: pattern-based extraction of recordsets from tables (RTL / ATP / ITM)"
99
readme = "README.md"
1010
license = { text = "MIT" }

python/pyregtab/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373

7474
from pyregtab import dsl
7575

76-
__version__ = "0.2.0"
76+
__version__ = "0.3.0"
7777

7878
__all__ = [
7979
"TableSyntax", "Cell", "Row", "Subrow", "Subtable", "GridPosition",

python/pyregtab/_core.pyi

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Type stubs for the native core `pyregtab._core`."""
22

3+
import os
34
from enum import Enum
45
from typing import Any, Callable, Iterable, Mapping, Optional, Sequence, Union
56

@@ -204,6 +205,13 @@ class Recordset:
204205
def get(self, index: int) -> Record: ...
205206
def __getitem__(self, index: int) -> Record: ...
206207
def to_pandas(self) -> Any: ...
208+
def to_csv(
209+
self,
210+
path: Optional[Union[str, os.PathLike[str]]] = None,
211+
*,
212+
sep: str = ",",
213+
missing: str = "",
214+
) -> Optional[str]: ...
207215

208216
# ---------------------------------------------------------------- spec
209217

@@ -658,6 +666,12 @@ class AtpMatcher:
658666
syntax: TableSyntax,
659667
context_items: Optional[Sequence[ContextDerivedItem]] = None,
660668
) -> Optional[InterpretableTable]: ...
669+
@staticmethod
670+
def match_many(
671+
atp: TablePattern,
672+
syntaxes: Sequence[TableSyntax],
673+
context_items: Optional[Sequence[ContextDerivedItem]] = None,
674+
) -> list[Optional[InterpretableTable]]: ...
661675

662676
class TableInterpreter:
663677
def __init__(self) -> None: ...

0 commit comments

Comments
 (0)