Skip to content

Commit adfd17e

Browse files
authored
Re-accept hyphen-delimited hook sections with deprecation warning (#89)
5.3.1 anchored hook section matching strictly on ":", which broke downstream hooks (notably mxmake) that emit [namespace-section] sections. Re-accept the historical "-" delimiter for backward compatibility, log a one-time deprecation warning per offending section, and normalize hook namespaces that bake a trailing "-" into the namespace string itself (e.g. namespace = "mxmake-"). Recognized hook section forms (in order of preference): [namespace] exact match [namespace:subsection] colon delimiter, canonical [namespace-subsection] hyphen delimiter, deprecated, logs warning The original bug stays fixed: a package whose name merely starts with the namespace without a delimiter (e.g. uvst.addon for namespace "uv") is still a package, not a hook section.
1 parent 62e76e5 commit adfd17e

4 files changed

Lines changed: 148 additions & 8 deletions

File tree

CHANGES.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@
22

33
## 5.3.2 (unreleased)
44

5-
<!-- Add future changes here -->
5+
- Fix: Restore backward compatibility for hook config sections named
6+
`[namespace-subsection]`. After 5.3.1 anchored section matching strictly on
7+
`:`, downstream hooks (notably `mxmake`) that emit `[mxmake-env]`-style
8+
sections broke. The matcher now accepts the historical `-` delimiter too,
9+
logging a one-time deprecation warning per offending section. Hook classes
10+
that declared a trailing delimiter in their namespace (e.g. `namespace = "mxmake-"`)
11+
are normalized: the trailing `-` is stripped and the hook author is warned.
12+
Migration: rename `[ns-section]` to `[ns:section]` and set `namespace = "ns"`.
13+
[jensens]
614

715

816
## 5.3.1 (2026-05-29)

EXTENDING.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ The `:` separator is used on purpose: it cannot occur in a package name, so a ho
1717
can never accidentally claim a package section whose name merely starts with the same letters
1818
(e.g. a `uv` hook must not swallow a package named `uvst.addon`).
1919

20+
The historical `[namespace-subsection]` form is still accepted for backward compatibility
21+
but logs a deprecation warning and should be migrated to `[namespace:subsection]`. Hooks
22+
that declared their namespace with a trailing delimiter (e.g. `namespace = "mxmake-"`) keep
23+
working — the trailing `-` is stripped — and the hook author is warned to drop it.
24+
2025
This looks like so:
2126

2227
```INI

src/mxdev/config.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,15 +108,42 @@ def __init__(
108108
if line:
109109
self.ignore_keys.append(line)
110110

111+
# Canonical delimiter is ":" (cannot appear in a Python distribution
112+
# name, so it never collides with a package section). "-" is the
113+
# historical delimiter and is still accepted, but emits a deprecation
114+
# warning. Legacy hooks declared their namespace with the trailing
115+
# delimiter baked in (e.g. namespace = "mxmake-"); we normalize that
116+
# too and warn the hook author.
117+
_warned: set[str] = set()
118+
119+
def _warn_once(key: str, message: str) -> None:
120+
if key in _warned:
121+
return
122+
_warned.add(key)
123+
logger.warning(message)
124+
111125
def is_ns_member(name) -> bool:
112-
# A section belongs to a hook only when its name is exactly the
113-
# hook namespace or is prefixed with "<namespace>:". The colon
114-
# cannot occur in a package name, so it unambiguously separates
115-
# hook sections from package sections and avoids swallowing
116-
# packages that merely start with the namespace (e.g. a "uv" hook
117-
# must not claim a package named "uvst.addon").
118126
for hook in hooks:
119-
if name == hook.namespace or name.startswith(f"{hook.namespace}:"):
127+
ns = hook.namespace
128+
effective_ns = ns.rstrip("-")
129+
if effective_ns != ns:
130+
_warn_once(
131+
f"ns:{ns}",
132+
f"Hook '{type(hook).__name__}' declares namespace='{ns}' with a "
133+
f"trailing '-'; this form is deprecated. Use namespace='{effective_ns}' "
134+
f"and the ':' section delimiter (e.g. [{effective_ns}:section]).",
135+
)
136+
if name == effective_ns:
137+
return True
138+
if name.startswith(f"{effective_ns}:"):
139+
return True
140+
if name.startswith(f"{effective_ns}-"):
141+
subsection = name[len(effective_ns) + 1 :]
142+
_warn_once(
143+
f"section:{name}",
144+
f"Hook section '[{name}]' uses the deprecated '-' delimiter; "
145+
f"rename to '[{effective_ns}:{subsection}]'.",
146+
)
120147
return True
121148
return False
122149

tests/test_config.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,3 +374,103 @@ class UvHook(Hook):
374374
assert "uv:sources" in config.hooks
375375
assert "uv" not in config.packages
376376
assert "uv:sources" not in config.packages
377+
378+
379+
def test_hook_section_with_hyphen_delimiter_is_supported_but_deprecated(tmp_path, caplog):
380+
"""``[<namespace>-section]`` is still recognized as a hook section, with a deprecation warning.
381+
382+
Before the colon-as-delimiter fix, the convention was ``[<namespace>-section]``.
383+
We keep that working for compatibility but log a deprecation pointing users to
384+
the new ``<namespace>:section`` form.
385+
"""
386+
from mxdev.config import Configuration
387+
from mxdev.hooks import Hook
388+
389+
import logging
390+
391+
class MxmakeHook(Hook):
392+
namespace = "mxmake"
393+
394+
config_content = """
395+
[settings]
396+
requirements-in = requirements.txt
397+
398+
[mxmake-env]
399+
some-setting = value
400+
"""
401+
config_file = tmp_path / "mx.ini"
402+
config_file.write_text(config_content)
403+
404+
with caplog.at_level(logging.WARNING, logger="mxdev"):
405+
config = Configuration(str(config_file), hooks=[MxmakeHook()])
406+
407+
assert "mxmake-env" in config.hooks
408+
assert "mxmake-env" not in config.packages
409+
assert any(
410+
"mxmake-env" in r.message and "deprecated" in r.message.lower() for r in caplog.records
411+
), f"Expected deprecation warning for hyphen-delimited section, got: {[r.message for r in caplog.records]}"
412+
413+
414+
def test_legacy_namespace_with_trailing_hyphen_is_normalized(tmp_path, caplog):
415+
"""A hook declaring ``namespace = 'mxmake-'`` (trailing hyphen baked in) still works.
416+
417+
Legacy hooks (e.g. mxmake) declared their namespace with the delimiter included.
418+
The matcher strips a trailing hyphen so the effective namespace is ``mxmake``,
419+
keeping ``[mxmake-env]`` and ``[mxmake:env]`` both recognized.
420+
"""
421+
from mxdev.config import Configuration
422+
from mxdev.hooks import Hook
423+
424+
import logging
425+
426+
class LegacyMxmakeHook(Hook):
427+
namespace = "mxmake-"
428+
429+
config_content = """
430+
[settings]
431+
requirements-in = requirements.txt
432+
433+
[mxmake-env]
434+
some-setting = value
435+
436+
[mxmake:files]
437+
other-setting = value
438+
"""
439+
config_file = tmp_path / "mx.ini"
440+
config_file.write_text(config_content)
441+
442+
with caplog.at_level(logging.WARNING, logger="mxdev"):
443+
config = Configuration(str(config_file), hooks=[LegacyMxmakeHook()])
444+
445+
assert "mxmake-env" in config.hooks
446+
assert "mxmake:files" in config.hooks
447+
assert "mxmake-env" not in config.packages
448+
assert "mxmake:files" not in config.packages
449+
450+
451+
def test_hyphen_prefix_without_delimiter_is_still_a_package(tmp_path):
452+
"""The original ``uvst.addon`` bug stays fixed even with hyphen support enabled.
453+
454+
A package whose name starts with the namespace but has no delimiter
455+
(``uvst.addon`` for namespace ``uv``) must remain a package.
456+
"""
457+
from mxdev.config import Configuration
458+
from mxdev.hooks import Hook
459+
460+
class UvHook(Hook):
461+
namespace = "uv"
462+
463+
config_content = """
464+
[settings]
465+
requirements-in = requirements.txt
466+
467+
[uvst.addon]
468+
url = https://github.com/example/uvst.addon.git
469+
"""
470+
config_file = tmp_path / "mx.ini"
471+
config_file.write_text(config_content)
472+
473+
config = Configuration(str(config_file), hooks=[UvHook()])
474+
475+
assert "uvst.addon" in config.packages
476+
assert "uvst.addon" not in config.hooks

0 commit comments

Comments
 (0)