Skip to content
Merged
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: 9 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@

## 5.3.2 (unreleased)

<!-- Add future changes here -->
- 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)
Expand Down
5 changes: 5 additions & 0 deletions EXTENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 34 additions & 7 deletions src/mxdev/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<namespace>:". 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

Expand Down
100 changes: 100 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""``[<namespace>-section]`` is still recognized as a hook section, with a deprecation warning.

Before the colon-as-delimiter fix, the convention was ``[<namespace>-section]``.
We keep that working for compatibility but log a deprecation pointing users to
the new ``<namespace>: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
Loading