Skip to content

Commit 0f2c191

Browse files
committed
Fix hook namespace swallowing packages with a matching name prefix
Section ownership used an unanchored str.startswith match, so a hook namespace like "uv" silently classified any section whose name merely started with "uv" (e.g. uvst.addon, uvxs.addon) as a hook section and dropped it from the package list - never checked out, never written to requirements, and with no diagnostic. Anchor the match: a section belongs to a hook only when its name is exactly the namespace or is prefixed with "<namespace>:". A colon cannot occur in a package name, so it unambiguously separates hook sections from package sections and removes the false-positive class permanently. This is an alternative to #87, which worked around the bug by renaming the uv hook's namespace to "__uv__". Fixing the matching logic instead keeps the clean "uv" namespace and protects every current and future hook, not just this one. Hook config sections previously named [namespace-section] must be renamed to [namespace:section]; EXTENDING.md updated accordingly. The only known hook (uv) owns no mx.ini sections, so the practical impact is nil.
1 parent 394e0b3 commit 0f2c191

4 files changed

Lines changed: 85 additions & 5 deletions

File tree

CHANGES.md

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

33
## 5.4.0 (unreleased)
44

5-
<!-- Add future changes here -->
5+
- Fix: A hook namespace no longer swallows packages whose name merely starts with it.
6+
Section ownership now requires an exact `[namespace]` match or a `[namespace:subsection]`
7+
prefix (the `:` separator cannot occur in a package name), so a hook like `uv` no longer
8+
silently drops packages such as `uvst.addon`. Hook config sections previously written as
9+
`[namespace-section]` must be renamed to `[namespace:section]`.
10+
[jensens]
611

712

813
## 5.3.0 (2026-05-29)

EXTENDING.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,19 @@ To avoid naming conflicts, all hook-related settings and config sections must be
1111

1212
It is recommended to use the package name containing the hook as a namespace.
1313

14+
Settings keys in `[settings]` and package sections are namespaced with a `namespace-` prefix.
15+
Dedicated hook config sections are named either exactly `[namespace]` or `[namespace:subsection]`.
16+
The `:` separator is used on purpose: it cannot occur in a package name, so a hook namespace
17+
can never accidentally claim a package section whose name merely starts with the same letters
18+
(e.g. a `uv` hook must not swallow a package named `uvst.addon`).
19+
1420
This looks like so:
1521

1622
```INI
1723
[settings]
1824
myextension-global_setting = 1
1925

20-
[myextension-section]
26+
[myextension:section]
2127
setting = value
2228

2329
[foo.bar]
@@ -49,7 +55,7 @@ class MyExtension(Hook):
4955

5056
# Example: Access hook-specific sections
5157
for section_name, section_config in state.configuration.hooks.items():
52-
if section_name.startswith('myextension-'):
58+
if section_name == 'myextension' or section_name.startswith('myextension:'):
5359
# Process your hook's configuration
5460
pass
5561

@@ -100,7 +106,7 @@ Replace:
100106

101107
- Use your package name as namespace prefix
102108
- All settings: `namespace-setting_name`
103-
- All sections: `[namespace-section]`
109+
- All sections: `[namespace]` or `[namespace:section]` (the `:` separator cannot occur in a package name, so it prevents collisions with package sections)
104110
- This prevents conflicts with other hooks
105111

106112
## Example Use Cases

src/mxdev/config.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,14 @@ def __init__(
109109
self.ignore_keys.append(line)
110110

111111
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").
112118
for hook in hooks:
113-
if name.startswith(hook.namespace):
119+
if name == hook.namespace or name.startswith(f"{hook.namespace}:"):
114120
return True
115121
return False
116122

tests/test_config.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,3 +311,66 @@ def test_config_parse_multiple_pushurls(tmp_path):
311311
# package2 should have single pushurl (no pushurls list)
312312
assert "pushurls" not in config.packages["package2"]
313313
assert config.packages["package2"]["pushurl"] == "git@github.com:test/repo2.git"
314+
315+
316+
def test_package_name_starting_with_hook_namespace_is_not_swallowed(tmp_path):
317+
"""A package whose name merely starts with a hook namespace stays a package.
318+
319+
Regression: with the ``uv`` hook (namespace ``"uv"``) registered, packages
320+
like ``uvst.addon`` were silently classified as hook sections and dropped
321+
from ``config.packages`` because section ownership used an unanchored
322+
``str.startswith`` match.
323+
"""
324+
from mxdev.config import Configuration
325+
from mxdev.hooks import Hook
326+
327+
class UvHook(Hook):
328+
namespace = "uv"
329+
330+
config_content = """
331+
[settings]
332+
requirements-in = requirements.txt
333+
334+
[uvst.addon]
335+
url = https://github.com/example/uvst.addon.git
336+
"""
337+
config_file = tmp_path / "mx.ini"
338+
config_file.write_text(config_content)
339+
340+
config = Configuration(str(config_file), hooks=[UvHook()])
341+
342+
assert "uvst.addon" in config.packages
343+
assert "uvst.addon" not in config.hooks
344+
345+
346+
def test_hook_section_with_namespace_delimiter_belongs_to_hook(tmp_path):
347+
"""Sections named exactly ``<namespace>`` or prefixed ``<namespace>:`` are hook sections.
348+
349+
A colon cannot occur in a package name, so it is the unambiguous delimiter
350+
separating hook sections from package sections.
351+
"""
352+
from mxdev.config import Configuration
353+
from mxdev.hooks import Hook
354+
355+
class UvHook(Hook):
356+
namespace = "uv"
357+
358+
config_content = """
359+
[settings]
360+
requirements-in = requirements.txt
361+
362+
[uv]
363+
some-setting = value
364+
365+
[uv:sources]
366+
another = thing
367+
"""
368+
config_file = tmp_path / "mx.ini"
369+
config_file.write_text(config_content)
370+
371+
config = Configuration(str(config_file), hooks=[UvHook()])
372+
373+
assert "uv" in config.hooks
374+
assert "uv:sources" in config.hooks
375+
assert "uv" not in config.packages
376+
assert "uv:sources" not in config.packages

0 commit comments

Comments
 (0)