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