Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,16 @@ jobs:
python ./builder/build_pymeos_functions.py
pip install .

- name: OO codegen coverage gate
# Hard-gate the meos-idl.json-driven OO method-family generator: it
# is offline (vendored catalog, no compiled MEOS), so the check is
# that every catalogued function is emitted or explicitly counted
# (zero unaccounted) and all six type families are covered. The
# pytest run below additionally asserts the committed Draft preview
# stays in lock-step with the generator and reuses real backings.
run: |
python tools/oo_codegen/codegen.py --check

- name: Test PyMEOS with pytest
run: |
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${{ matrix.ld_prefix }}/lib
Expand Down
154 changes: 154 additions & 0 deletions tests/test_oo_codegen_coverage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"""Coverage + backing gate for the meos-idl.json-driven OO codegen.

This is the high-level analogue of ``tests/test_portable_parity.py``: it
asserts that ``tools/oo_codegen/codegen.py`` accounts for EVERY catalogued
function (emitted as a regular method or counted under a named exclusion --
zero unaccounted), covers all six in-scope temporal type families, and that
every overload the committed Draft preview dispatches to is the EXACT
``pymeos_cffi`` backing symbol present in the vendored catalog (reuse the
operator's own backing -- equivalence by construction, never reimplemented).

It runs fully offline (no compiled binding): the catalog is the vendored
``tools/oo_codegen/meos-idl.json`` and the preview is read by AST without
importing it.

python3 -m pytest tests/test_oo_codegen_coverage.py
python3 tests/test_oo_codegen_coverage.py
"""

import ast
import json
import py_compile
import sys
import unittest
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
TOOLS = ROOT / "tools" / "oo_codegen"
PREVIEW = TOOLS / "_preview"
sys.path.insert(0, str(TOOLS))

import codegen as cg # the generator under test

IN_SCOPE = ["cbuffer", "geo", "npoint", "pose", "rgeo", "temporal"]


def _idl():
return json.loads((TOOLS / "meos-idl.json").read_text())


def _collected():
return cg.collect(_idl())


def _preview_tables():
"""AST-read every committed preview module without importing it.

Returns ``{family: {method_name: {arg_key: backing_symbol}}}`` plus the
set of module paths seen, so the committed Draft can be checked against
the generator without a compiled binding.
"""
out = {}
for path in sorted(PREVIEW.glob("*_methods.py")):
family = path.stem.replace("_methods", "")
tree = ast.parse(path.read_text())
methods = {}
for cls in (n for n in tree.body if isinstance(n, ast.ClassDef)):
if not cls.name.startswith("T"):
continue
for fn in (n for n in cls.body if isinstance(n, ast.FunctionDef)):
table = None
for stmt in fn.body:
if (
isinstance(stmt, ast.Assign)
and getattr(stmt.targets[0], "id", "") == "_disp"
):
table = ast.literal_eval(stmt.value)
if table is not None:
methods[fn.name] = table
out[family] = methods
return out


class CoverageAccounting(unittest.TestCase):
"""Every catalogued function is emitted or explicitly counted."""

def test_zero_unaccounted(self):
idl = _idl()
_, st = cg.collect(idl)
total = len(idl["functions"])
accounted = st.emitted_overloads + st.datum + st.irregular + st.out_of_scope
self.assertEqual(
accounted,
total,
f"{total - accounted} functions neither emitted nor counted",
)

def test_all_six_families_present(self):
fams, _ = _collected()
self.assertEqual(sorted(fams), IN_SCOPE)
empty = sorted(f for f, m in fams.items() if not m)
self.assertEqual(empty, [], f"families with no methods: {empty}")


class CommittedPreview(unittest.TestCase):
"""The committed Draft preview must compile, parse, and stay in
lock-step with the generator (catches hand edits / stale drift)."""

def test_every_module_compiles(self):
mods = sorted(PREVIEW.glob("*_methods.py"))
self.assertEqual(len(mods), 6, f"expected 6 preview modules, found {len(mods)}")
for path in mods:
py_compile.compile(str(path), doraise=True)

def test_every_entry_is_a_valid_symbol(self):
for family, methods in _preview_tables().items():
for meth, table in methods.items():
self.assertTrue(table, f"{family}.{meth}: empty dispatch")
for key, sym in table.items():
self.assertRegex(
sym,
r"^[a-z][a-z0-9_]+$",
f"{family}.{meth}[{key}]: bad symbol {sym!r}",
)

def test_committed_matches_generator(self):
fams, _ = _collected()
expected = {
fam: {name: m.overloads for name, m in meths.items()}
for fam, meths in fams.items()
}
self.assertEqual(
_preview_tables(),
expected,
"committed _preview/ is out of sync -- regenerate with "
"`python3 tools/oo_codegen/codegen.py && black tools/oo_codegen`",
)


class BackingValidity(unittest.TestCase):
"""Every symbol the preview dispatches to is a real catalogued
function -- the generated method reuses the operator's own backing,
so it is identical by construction (never reimplemented)."""

def test_every_symbol_is_a_real_idl_function(self):
names = {f["name"] for f in _idl()["functions"]}
unknown = sorted(
{
sym
for methods in _preview_tables().values()
for table in methods.values()
for sym in table.values()
if sym not in names
}
)
self.assertEqual(
unknown,
[],
f"preview dispatches to {len(unknown)} symbol(s) absent from "
f"meos-idl.json: {unknown[:10]}",
)


if __name__ == "__main__":
unittest.main(verbosity=2)
1 change: 1 addition & 0 deletions tools/oo_codegen/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__pycache__/
107 changes: 107 additions & 0 deletions tools/oo_codegen/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# PyMEOS OO method-family code generator

`codegen.py` reads the vendored `meos-idl.json` (the signature catalog
produced by the [MEOS-API](https://github.com/MobilityDB/MEOS-API) parser
and consumed verbatim by every binding) and emits one idiomatic PyMEOS
**OO method** per *regular* method-family member into
`tools/oo_codegen/_preview/<family>_methods.py`.

It is the high-level analogue of two existing ecosystem artifacts:

* **GoMEOS PR #2** (`refactor/codegen-meos-idl`, `tools/codegen.py` →
`tools/_preview/`) — the non-destructive Draft-artifact discipline: a
vendored IDL, output written to an underscore-prefixed directory the
package never imports, explicit *counted* exclusions instead of silent
gaps, and a coverage report.
* **PyMEOS PR #87** (`tools/portable_aliases/generate.py`) — the in-repo
content model: every generated callable dispatches to the **exact
`pymeos_cffi` backing function** the hand-written surface uses. Nothing
is reimplemented, so each generated method is identical to its
hand-written counterpart by construction.

## Running

```
python3 tools/oo_codegen/codegen.py # regenerate the preview
python3 tools/oo_codegen/codegen.py --check # accounting gate (CI)
```

The generator is offline and import-safe: it reads only the vendored JSON
and never needs a compiled MEOS.

## Coverage today

2833 catalogued functions, fully accounted for:

| Bucket | Count |
|---|---|
| Emitted regular methods | **107** (407 typed overloads) across **all 6** families |
| &nbsp;&nbsp;cbuffer | 30 methods / 103 overloads |
| &nbsp;&nbsp;geo | 29 / 83 |
| &nbsp;&nbsp;npoint | 12 / 35 |
| &nbsp;&nbsp;pose | 12 / 35 |
| &nbsp;&nbsp;rgeo | 10 / 30 |
| &nbsp;&nbsp;temporal | 14 / 121 |
| Hand-written core (see below) | 864 |
| Datum-bearing internal helpers (excluded) | 43 |
| Out of scope (non-temporal-family headers) | 1519 |

Zero unaccounted: every function is either emitted or counted under an
explicit, named exclusion. `--check` fails the build otherwise.

## What is generated vs hand-written (the hybrid, matching GoMEOS)

The hand-written PyMEOS types spell out, uniformly across every temporal
type, a set of **regular** families whose entire behaviour is a thin
isinstance ladder forwarding `self._inner` + the unwrapped argument to a
typed MEOS overload and post-processing the result deterministically. That
regularity is exactly what a signature catalog can synthesise:

* **comparison** — `always/ever_equal`, `*_not_equal`, `temporal_*`
* **spatial relationship** — ever (`is_ever_*`, `ever_intersects/touches`),
always (`is_always_*`, `always_intersects/touches`), temporal
(`contains/covers/disjoint/within_distance/intersects/touches`)
* **distance** — `distance`, `nearest_approach_distance/instant`,
`shortest_line`
* **restriction** — `at`, `minus`

The **irregular core stays hand-written** because it needs per-function
editorial decisions a signature alone does not carry: constructors
(`from_*`), conversions (`to_*`, `as_wkt/ewkt`), accessors and transforms
(`srid`, `round`, `transform`, …) and I/O (`read_from_cursor`). These are
counted (864) and named, never silently dropped — the same discipline
GoMEOS applies to its Datum-bearing exclusions.

## Non-destructive Draft artifact

`tools/oo_codegen/_preview/` is **never imported by the `pymeos`
package** — the directory is underscore-prefixed and the modules carry a
self-contained, lazily-resolved `pymeos_cffi` shim so they import cleanly
without the compiled binding. The preview exists so a reviewer can diff
this uniform, deterministic surface against the hand-written
`pymeos/main/*.py` before any staged migration. It is intentionally
committed (like GoMEOS's `tools/_preview/*.go`).

## Black

The committed preview is formatted with Black (`~= 24`, the repo's
`black.yml` pin). After regenerating, re-apply it before committing:

```
python3 tools/oo_codegen/codegen.py
black tools/oo_codegen
```

The CI gate (`--check`) asserts accounting and family coverage, not
formatting, so a raw regenerate never breaks the build.

## Refreshing the IDL

`meos-idl.json` is vendored from `PyMEOS-CFFI` (`builder/meos-idl.json`,
the `bump/meos-1.4` line). When MEOS bumps, copy the regenerated catalog
back and re-run:

```
cp ../PyMEOS-CFFI/builder/meos-idl.json tools/oo_codegen/meos-idl.json
python3 tools/oo_codegen/codegen.py && black tools/oo_codegen
```
Loading