Skip to content

Commit 4b35681

Browse files
tobixenclaude
andcommitted
fix: fix --name registry lookup (case and sys.path/sys.modules shadowing)
Two bugs prevented --name from finding servers in the caldav test registry: 1. The caldav library capitalised its server names (Radicale, Xandikos). The registry lookup now falls back to a case-insensitive search when the exact key is not found. 2. The caldav-server-tester's own tests/ package shadows the caldav project's tests/test_servers module — either via sys.modules (if already imported) or via '' (CWD) in sys.path. Switch from `from tests.test_servers import …` to loading the module directly with importlib.util.spec_from_file_location, bypassing sys.path resolution entirely. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7f98e25 commit 4b35681

3 files changed

Lines changed: 89 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,17 @@ This file should adhere to [Keep a Changelog](https://keepachangelog.com/en/1.1.
66

77
This project should adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), though some earlier releases may be incompatible with the SemVer standard.
88

9-
## [Unreleased]
9+
## [1.0.1] - 2026-03-19
10+
11+
### Fixed
12+
- `--name radicale` (and other lowercase names) failed to find servers in the caldav test registry after the caldav library renamed its server entries to capitalised names (`Radicale`, `Xandikos`). The registry lookup is now case-insensitive.
13+
- `--name` registry lookup silently returned nothing when the caldav-server-tester's own `tests/` package shadowed the caldav project's `tests/test_servers` in `sys.modules` or via the CWD entry in `sys.path`. The registry is now loaded via `importlib` using the explicit file path, bypassing `sys.path` resolution.
1014

1115
### Documentation
12-
- USAGE.md: updated `--format text` section to reflect current multi-line output format and actual support-level values; added `unknown` status
13-
- USAGE.md: added guide for contributing a new server profile to `caldav/compatibility_hints.py`
14-
- USAGE.md: added guide for storing checker results in `~/.config/caldav/calendar.conf` (named profile, inline features, and base+overrides patterns)
16+
* Updated USAGE.md
17+
* `--format text` section to reflect current multi-line output format and actual support-level values; added `unknown` status
18+
* added guide for contributing a new server profile to `caldav/compatibility_hints.py`
19+
* added guide for storing checker results in `~/.config/caldav/calendar.conf` (named profile, inline features, and base+overrides patterns)
1520

1621
## [1.0.0] - 2026-03-15
1722

src/caldav_server_tester/caldav_server_tester.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,44 @@ def _find_caldav_test_registry():
4242
]
4343

4444
for root in candidates:
45-
if (root / "tests" / "test_servers" / "__init__.py").exists():
46-
if str(root) not in sys.path:
47-
sys.path.insert(0, str(root))
48-
try:
49-
from tests.test_servers import get_registry
50-
51-
return get_registry()
52-
except ImportError:
53-
pass
45+
ts_init = root / "tests" / "test_servers" / "__init__.py"
46+
if not ts_init.exists():
47+
continue
48+
## Use importlib to load directly from the file path, bypassing
49+
## sys.path resolution entirely. A plain `from tests.test_servers
50+
## import …` fails when another tests/ directory (e.g. this project's
51+
## own tests/ via CWD or editable install) appears earlier in sys.path.
52+
import importlib.util
53+
54+
tests_spec = importlib.util.spec_from_file_location(
55+
"tests",
56+
str(root / "tests" / "__init__.py"),
57+
submodule_search_locations=[str(root / "tests")],
58+
)
59+
ts_spec = importlib.util.spec_from_file_location(
60+
"tests.test_servers",
61+
str(ts_init),
62+
submodule_search_locations=[str(ts_init.parent)],
63+
)
64+
if tests_spec is None or ts_spec is None:
65+
continue
66+
67+
_saved = {k: sys.modules.pop(k) for k in list(sys.modules) if k == "tests" or k.startswith("tests.")}
68+
try:
69+
tests_mod = importlib.util.module_from_spec(tests_spec)
70+
sys.modules["tests"] = tests_mod
71+
tests_spec.loader.exec_module(tests_mod) # type: ignore[union-attr]
72+
73+
ts_mod = importlib.util.module_from_spec(ts_spec)
74+
sys.modules["tests.test_servers"] = ts_mod
75+
ts_spec.loader.exec_module(ts_mod) # type: ignore[union-attr]
76+
77+
return ts_mod.get_registry()
78+
except Exception:
79+
for k in list(sys.modules):
80+
if k == "tests" or k.startswith("tests."):
81+
del sys.modules[k]
82+
sys.modules.update(_saved)
5483

5584
return None
5685

tests/test_cli.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
"""Tests for the CLI (caldav_server_tester.py click application)"""
22

3+
import sys
4+
import types
35
from unittest.mock import MagicMock, patch
46

57
from click.testing import CliRunner
68

7-
from caldav_server_tester.caldav_server_tester import check_server_compatibility
9+
from caldav_server_tester.caldav_server_tester import (
10+
_find_caldav_test_registry,
11+
check_server_compatibility,
12+
)
813

914

1015
class TestCliConfigSection:
@@ -135,6 +140,42 @@ def test_name_in_registry_uses_registry(self) -> None:
135140
)
136141
mock_check.assert_called_once()
137142

143+
def test_find_registry_works_when_tests_shadowed_in_sys_modules(self) -> None:
144+
"""Registry discovery must succeed even when sys.modules['tests'] points elsewhere.
145+
146+
When the CLI is installed and run, its own tests/ package ends up in
147+
sys.modules before _find_caldav_test_registry() runs. That used to
148+
shadow the caldav project's tests/test_servers and cause the function
149+
to return None.
150+
"""
151+
# Only meaningful if caldav is checked out as source (has tests/test_servers)
152+
from pathlib import Path
153+
154+
import caldav
155+
156+
caldav_root = Path(caldav.__file__).parent.parent
157+
if not (caldav_root / "tests" / "test_servers" / "__init__.py").exists():
158+
return # skip — caldav is not a source checkout
159+
160+
# Inject a fake conflicting 'tests' module that has no test_servers attr
161+
fake_tests = types.ModuleType("tests")
162+
fake_tests.__path__ = ["/some/unrelated/tests"]
163+
164+
saved_tests = {k: sys.modules.pop(k) for k in list(sys.modules) if k == "tests" or k.startswith("tests.")}
165+
sys.modules["tests"] = fake_tests
166+
try:
167+
registry = _find_caldav_test_registry()
168+
finally:
169+
for k in list(sys.modules):
170+
if k == "tests" or k.startswith("tests."):
171+
del sys.modules[k]
172+
sys.modules.update(saved_tests)
173+
174+
assert registry is not None, (
175+
"_find_caldav_test_registry() returned None even though caldav source is available; "
176+
"the 'tests' shadowing bug was not fixed"
177+
)
178+
138179
def test_name_lookup_is_case_insensitive(self) -> None:
139180
"""--name radicale should match a registry entry named 'Radicale'"""
140181
runner = CliRunner()

0 commit comments

Comments
 (0)