Skip to content

Commit 760aaa3

Browse files
authored
fix hatchling source path detection (#45)
1 parent 109eadf commit 760aaa3

3 files changed

Lines changed: 96 additions & 11 deletions

File tree

plux/build/hatchling.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ class HatchlingPackageFinder(PackageFinder):
8181
Uses hatchling's BuilderConfig abstraction to enumerate packages.
8282
8383
TODO: include/exclude configuration of packages in hatch needs more thorough testing with different scenarios.
84+
TODO: hatchling supports path rewrites with the sources config, which is currently not supported
8485
"""
8586

8687
builder_config: BuilderConfig
@@ -114,9 +115,9 @@ def find_packages(self) -> t.Iterable[str]:
114115
for relative_package_path in package_paths:
115116
package_name = os.path.basename(relative_package_path)
116117

117-
package_path = os.path.join(
118-
self.path, relative_package_path
119-
) # build package path within sources root
118+
# Package paths in hatchling are always relative to the project root, so we join
119+
# with the project root (not self.path, which is the sources root).
120+
package_path = str(os.path.join(self.builder_config.root, relative_package_path))
120121
if not os.path.isdir(package_path):
121122
continue
122123

@@ -141,16 +142,38 @@ def find_packages(self) -> t.Iterable[str]:
141142

142143
@property
143144
def path(self) -> str:
145+
"""Return the sources root — the directory under which the package names are located.
146+
147+
This is used by ``PluginFromPackageFinder._list_module_names`` to construct the
148+
file-system path for each package name, so it must point to the directory that
149+
*contains* the top-level packages (not the project root in general).
150+
151+
Hatchling's ``sources`` dict maps ``{source_dir: dest_dir_in_wheel}``:
152+
- ``{"localstack-core/": ""}`` — packages in a subdirectory: source root is
153+
``localstack-core/`` (the key, not the value)
154+
- ``{"": ""}`` — packages directly in the project root
155+
"""
156+
root = self.builder_config.root
157+
158+
# If no sources are configured, we assume the sources root is the project root
144159
if not self.builder_config.sources:
145-
where = self.builder_config.root
146-
else:
147-
if self.builder_config.sources[""]:
148-
where = self.builder_config.sources[""]
160+
return root
161+
162+
# The keys themselves are source directories (e.g. "localstack-core/").
163+
# Filter out any empty-string keys, strip trailing separators.
164+
source_dirs = {k.rstrip("/"): v for k, v in self.builder_config.sources.items() if k}
165+
if len(source_dirs) == 1:
166+
source_dir, dest_dir = next(iter(source_dirs.items()))
167+
if dest_dir:
168+
LOG.warning(
169+
"plux doesn't know how to resolve sources with non-empty destination directories, using root dir."
170+
)
149171
else:
150-
LOG.warning("plux doesn't know how to resolve multiple sources directories")
151-
where = self.builder_config.root
172+
return os.path.join(root, str(source_dir))
152173

153-
return where
174+
if source_dirs:
175+
LOG.warning("plux doesn't know how to resolve multiple sources directories, using root dir.")
176+
return root
154177

155178
def filter_packages(self, packages: t.Iterable[str]) -> t.Iterable[str]:
156179
return [item for item in packages if not self.exclude(item) and self.include(item)]

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ setuptools = [
3535
"setuptools"
3636
]
3737
dev = [
38+
"plux[hatchling,setuptools]",
3839
"build",
39-
"setuptools",
4040
"pytest==8.4.1",
4141
"ruff==0.9.1",
4242
"mypy",

tests/build/test_hatchling.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,3 +372,65 @@ def test_hatch_register_metadata_hook():
372372
hook_class = hatch_register_metadata_hook()
373373

374374
assert hook_class is PluxMetadataHook
375+
376+
377+
class TestHatchlingPackageFinderPath:
378+
"""Tests for HatchlingPackageFinder.path property."""
379+
380+
def _make_finder(self, sources, root, packages=None):
381+
pytest.importorskip("hatchling")
382+
from unittest.mock import MagicMock
383+
from plux.build.hatchling import HatchlingPackageFinder
384+
385+
builder_config = MagicMock()
386+
builder_config.sources = sources
387+
builder_config.root = root
388+
builder_config.packages = packages or []
389+
return HatchlingPackageFinder(builder_config)
390+
391+
def test_path_with_packages_in_subdirectory(self, tmp_path):
392+
"""Regression test: KeyError when packages live in a subdirectory.
393+
394+
When hatchling is configured with ``packages = ["localstack-core/localstack"]``,
395+
builder_config.sources becomes ``{'localstack-core/': ''}`` — the key is the
396+
source directory, not ``''``. The old code did ``sources[""]`` which raised a
397+
KeyError. The fix must return the sources root (``{root}/localstack-core``)
398+
so that ``_list_module_names`` can build correct file-system paths for each
399+
discovered package name.
400+
"""
401+
finder = self._make_finder(
402+
sources={"localstack-core/": ""},
403+
root=str(tmp_path),
404+
packages=["localstack-core/localstack"],
405+
)
406+
407+
# Must not raise KeyError, and must return the sources root directory
408+
path = finder.path
409+
import os
410+
411+
assert path == os.path.join(str(tmp_path), "localstack-core")
412+
413+
def test_path_with_empty_sources(self, tmp_path):
414+
"""When sources is empty, path falls back to the project root."""
415+
finder = self._make_finder(sources={}, root=str(tmp_path))
416+
417+
assert finder.path == str(tmp_path)
418+
419+
def test_path_with_packages_in_root(self, tmp_path):
420+
"""When packages are in the project root (sources = {'': ''}), path returns root."""
421+
finder = self._make_finder(sources={"": ""}, root=str(tmp_path))
422+
423+
assert finder.path == str(tmp_path)
424+
425+
def test_path_single_source_non_empty_dest_dir_falls_back_to_root(self, tmp_path):
426+
"""When a single source has a non-empty dest dir, fall back to project root.
427+
428+
A mapping like ``{"src/": "lib/"}`` means the wheel destination is remapped,
429+
which plux cannot reason about — so it must return the project root and warn.
430+
"""
431+
432+
finder = self._make_finder(sources={"src/": "lib/"}, root=str(tmp_path))
433+
434+
path = finder.path
435+
436+
assert path == str(tmp_path)

0 commit comments

Comments
 (0)