From d2c27ecca4318c820007f26bfe08507544484fd0 Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Thu, 3 Sep 2026 00:45:13 +0200 Subject: [PATCH] Drop from a member name the prefix that classified it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A method's `ooName` is its function name with the class's token dropped, and the token tried is the class's own lower-cased name. A class whose C prefix is something else keeps that prefix in every member: `Geometry` is reached by `geom_*`, `Geography` by `geog_*` and `GeomSet` by `geoset_*`, so their members read `geomBuffer`, `geogFromHexewkb`, `geosetStartValue` — the class named twice, once by the receiver and once inside the call. The classifier already knows which prefix matched, since that is how it chose the class, so the name drops that prefix too, longest match first. 55 members across those three classes lose it and no class ends up holding two methods of one name. One token goes, not every repetition of it: `set_set_subspan` keeps its `setSubspan`, the inner `set` being part of what the member says. --- .github/workflows/pytest.yml | 2 +- parser/object_model.py | 21 ++++++++++++++------- tests/test_object_model.py | 17 +++++++++++++++++ 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index babdf51..70b0180 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -96,7 +96,7 @@ jobs: # carries, or a change to them is not exercised until after it merges. # Consumers use the action; this repository owns the rules. - name: Refuse a skip, and a suite that shrank - run: tools/check-test-outcome.py "$RUNNER_TEMP/pytest.log" --min-tests 292 + run: tools/check-test-outcome.py "$RUNNER_TEMP/pytest.log" --min-tests 293 # The rules earn their place by refusing a log that carries what they # name. Both fixtures are written here rather than tracked, and the diff --git a/parser/object_model.py b/parser/object_model.py index 102e48e..ed7698f 100644 --- a/parser/object_model.py +++ b/parser/object_model.py @@ -214,11 +214,18 @@ def _role(fn_name: str) -> str: _OONAME_OVERRIDES: dict[str, str] = {} -def _strip_class_token(fn_name: str, cls: str) -> str: +def _strip_class_token(fn_name: str, cls: str, prefix: str = "") -> str: """Drop the class prefix the function-name object model encodes, leaving - the bare member name. Tries the class's own lower-cased token first, then - the generic superclass tokens, longest match wins.""" - tokens = sorted({cls.lower(), *_GENERIC_TOKENS}, key=len, reverse=True) + the bare member name. Tries the prefix the classifier matched and the + class's own lower-cased token, then the generic superclass tokens, longest + match wins. + + The matched prefix is what a class whose C prefix is not its lower-cased + name is reached by — `geom_*` for Geometry, `geog_*` for Geography — and + without it those members keep the prefix twice over, as `geomBuffer` on a + Geometry.""" + tokens = sorted({cls.lower(), prefix, *_GENERIC_TOKENS} - {""}, + key=len, reverse=True) for tok in tokens: if fn_name == tok: return "" @@ -241,11 +248,11 @@ def _camel(member: str) -> str: return "".join(out) -def _ooname(fn_name: str, cls: str) -> str: +def _ooname(fn_name: str, cls: str, prefix: str = "") -> str: """Canonical camelCase OO method name for a classified function.""" if fn_name in _OONAME_OVERRIDES: return _OONAME_OVERRIDES[fn_name] - return _camel(_strip_class_token(fn_name, cls)) + return _camel(_strip_class_token(fn_name, cls, prefix)) def _oo_excluded(fn: dict, role: str) -> bool: @@ -533,7 +540,7 @@ def attach_object_model(idl: dict, path: Path, role = _role(name) method = {"function": name, "role": role, "scope": tgt["scope"], "backing": name, - "ooName": _ooname(name, cls)} + "ooName": _ooname(name, cls, pref)} if _oo_excluded(fn, role): method["ooExclude"] = True sugar = [] diff --git a/tests/test_object_model.py b/tests/test_object_model.py index 3595809..ae44175 100644 --- a/tests/test_object_model.py +++ b/tests/test_object_model.py @@ -195,6 +195,23 @@ def test_internal_api_methods_are_excluded(self): self.assertNotIn("ooExclude", meths["temporal_num_instants"]) self.assertTrue(meths["temporal_inst_n"].get("ooExclude")) + def test_ooname_drops_the_prefix_the_classifier_matched(self): + # A class reached by a prefix that is not its lower-cased name — + # `geom_*` for Geometry, `geog_*` for Geography, `geoset_*` for + # GeomSet — otherwise keeps that prefix in the member name, so the + # method reads `geometry.geomBuffer()`. + om = attach_object_model({"functions": [ + {"name": "geom_from_hexewkb"}, {"name": "geog_from_hexewkb"}, + {"name": "geoset_start_value"}, {"name": "set_set_subspan"}, + ]}, MODEL, None)["objectModel"] + names = {m["function"]: m["ooName"] + for s in om["classes"].values() for m in s["methods"]} + self.assertEqual(names["geom_from_hexewkb"], "fromHexewkb") + self.assertEqual(names["geog_from_hexewkb"], "fromHexewkb") + self.assertEqual(names["geoset_start_value"], "startValue") + # One token is dropped, not every repetition of it. + self.assertEqual(names["set_set_subspan"], "setSubspan") + def test_class_ctype_comes_from_the_receiver(self): # A receiver-role method takes the value it is called on first, so its # pointee names what the class's instances are — which is what a