diff --git a/CHANGES.md b/CHANGES.md index 7bb2ab5..827962e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,15 @@ ## 5.3.2 (unreleased) - +- Fix: Restore backward compatibility for hook config sections named + `[namespace-subsection]`. After 5.3.1 anchored section matching strictly on + `:`, downstream hooks (notably `mxmake`) that emit `[mxmake-env]`-style + sections broke. The matcher now accepts the historical `-` delimiter too, + logging a one-time deprecation warning per offending section. Hook classes + that declared a trailing delimiter in their namespace (e.g. `namespace = "mxmake-"`) + are normalized: the trailing `-` is stripped and the hook author is warned. + Migration: rename `[ns-section]` to `[ns:section]` and set `namespace = "ns"`. + [jensens] ## 5.3.1 (2026-05-29) diff --git a/EXTENDING.md b/EXTENDING.md index d251da6..ef53238 100644 --- a/EXTENDING.md +++ b/EXTENDING.md @@ -17,6 +17,11 @@ The `:` separator is used on purpose: it cannot occur in a package name, so a ho can never accidentally claim a package section whose name merely starts with the same letters (e.g. a `uv` hook must not swallow a package named `uvst.addon`). +The historical `[namespace-subsection]` form is still accepted for backward compatibility +but logs a deprecation warning and should be migrated to `[namespace:subsection]`. Hooks +that declared their namespace with a trailing delimiter (e.g. `namespace = "mxmake-"`) keep +working — the trailing `-` is stripped — and the hook author is warned to drop it. + This looks like so: ```INI diff --git a/src/mxdev/config.py b/src/mxdev/config.py index 61ec767..33169f7 100644 --- a/src/mxdev/config.py +++ b/src/mxdev/config.py @@ -108,15 +108,42 @@ def __init__( if line: self.ignore_keys.append(line) + # Canonical delimiter is ":" (cannot appear in a Python distribution + # name, so it never collides with a package section). "-" is the + # historical delimiter and is still accepted, but emits a deprecation + # warning. Legacy hooks declared their namespace with the trailing + # delimiter baked in (e.g. namespace = "mxmake-"); we normalize that + # too and warn the hook author. + _warned: set[str] = set() + + def _warn_once(key: str, message: str) -> None: + if key in _warned: + return + _warned.add(key) + logger.warning(message) + def is_ns_member(name) -> bool: - # A section belongs to a hook only when its name is exactly the - # hook namespace or is prefixed with ":". The colon - # cannot occur in a package name, so it unambiguously separates - # hook sections from package sections and avoids swallowing - # packages that merely start with the namespace (e.g. a "uv" hook - # must not claim a package named "uvst.addon"). for hook in hooks: - if name == hook.namespace or name.startswith(f"{hook.namespace}:"): + ns = hook.namespace + effective_ns = ns.rstrip("-") + if effective_ns != ns: + _warn_once( + f"ns:{ns}", + f"Hook '{type(hook).__name__}' declares namespace='{ns}' with a " + f"trailing '-'; this form is deprecated. Use namespace='{effective_ns}' " + f"and the ':' section delimiter (e.g. [{effective_ns}:section]).", + ) + if name == effective_ns: + return True + if name.startswith(f"{effective_ns}:"): + return True + if name.startswith(f"{effective_ns}-"): + subsection = name[len(effective_ns) + 1 :] + _warn_once( + f"section:{name}", + f"Hook section '[{name}]' uses the deprecated '-' delimiter; " + f"rename to '[{effective_ns}:{subsection}]'.", + ) return True return False diff --git a/tests/test_config.py b/tests/test_config.py index b7a48e0..a1341d9 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -374,3 +374,103 @@ class UvHook(Hook): assert "uv:sources" in config.hooks assert "uv" not in config.packages assert "uv:sources" not in config.packages + + +def test_hook_section_with_hyphen_delimiter_is_supported_but_deprecated(tmp_path, caplog): + """``[-section]`` is still recognized as a hook section, with a deprecation warning. + + Before the colon-as-delimiter fix, the convention was ``[-section]``. + We keep that working for compatibility but log a deprecation pointing users to + the new ``:section`` form. + """ + from mxdev.config import Configuration + from mxdev.hooks import Hook + + import logging + + class MxmakeHook(Hook): + namespace = "mxmake" + + config_content = """ +[settings] +requirements-in = requirements.txt + +[mxmake-env] +some-setting = value +""" + config_file = tmp_path / "mx.ini" + config_file.write_text(config_content) + + with caplog.at_level(logging.WARNING, logger="mxdev"): + config = Configuration(str(config_file), hooks=[MxmakeHook()]) + + assert "mxmake-env" in config.hooks + assert "mxmake-env" not in config.packages + assert any( + "mxmake-env" in r.message and "deprecated" in r.message.lower() for r in caplog.records + ), f"Expected deprecation warning for hyphen-delimited section, got: {[r.message for r in caplog.records]}" + + +def test_legacy_namespace_with_trailing_hyphen_is_normalized(tmp_path, caplog): + """A hook declaring ``namespace = 'mxmake-'`` (trailing hyphen baked in) still works. + + Legacy hooks (e.g. mxmake) declared their namespace with the delimiter included. + The matcher strips a trailing hyphen so the effective namespace is ``mxmake``, + keeping ``[mxmake-env]`` and ``[mxmake:env]`` both recognized. + """ + from mxdev.config import Configuration + from mxdev.hooks import Hook + + import logging + + class LegacyMxmakeHook(Hook): + namespace = "mxmake-" + + config_content = """ +[settings] +requirements-in = requirements.txt + +[mxmake-env] +some-setting = value + +[mxmake:files] +other-setting = value +""" + config_file = tmp_path / "mx.ini" + config_file.write_text(config_content) + + with caplog.at_level(logging.WARNING, logger="mxdev"): + config = Configuration(str(config_file), hooks=[LegacyMxmakeHook()]) + + assert "mxmake-env" in config.hooks + assert "mxmake:files" in config.hooks + assert "mxmake-env" not in config.packages + assert "mxmake:files" not in config.packages + + +def test_hyphen_prefix_without_delimiter_is_still_a_package(tmp_path): + """The original ``uvst.addon`` bug stays fixed even with hyphen support enabled. + + A package whose name starts with the namespace but has no delimiter + (``uvst.addon`` for namespace ``uv``) must remain a package. + """ + from mxdev.config import Configuration + from mxdev.hooks import Hook + + class UvHook(Hook): + namespace = "uv" + + config_content = """ +[settings] +requirements-in = requirements.txt + +[uvst.addon] +url = https://github.com/example/uvst.addon.git +""" + config_file = tmp_path / "mx.ini" + config_file.write_text(config_content) + + config = Configuration(str(config_file), hooks=[UvHook()]) + + assert "uvst.addon" in config.packages + assert "uvst.addon" not in config.hooks