Skip to content

Commit 26f2630

Browse files
committed
fix hatchling source path detection
1 parent 109eadf commit 26f2630

2 files changed

Lines changed: 90 additions & 11 deletions

File tree

plux/build/hatchling.py

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,11 @@ def find_packages(self) -> t.Iterable[str]:
114114
for relative_package_path in package_paths:
115115
package_name = os.path.basename(relative_package_path)
116116

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

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

142144
@property
143145
def path(self) -> str:
146+
"""Return the sources root — the directory under which the package names are located.
147+
148+
This is used by ``PluginFromPackageFinder._list_module_names`` to construct the
149+
file-system path for each package name, so it must point to the directory that
150+
*contains* the top-level packages (not the project root in general).
151+
152+
Hatchling's ``sources`` dict maps ``{source_dir: dest_dir_in_wheel}``:
153+
154+
- ``{"": "src"}`` — explicit src-layout: source root is ``src/``
155+
- ``{"localstack-core/": ""}`` — packages in a subdirectory: source root is
156+
``localstack-core/`` (the key, not the value)
157+
- ``{"": ""}`` — packages directly in the project root
158+
"""
159+
root = self.builder_config.root
160+
144161
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[""]
149-
else:
150-
LOG.warning("plux doesn't know how to resolve multiple sources directories")
151-
where = self.builder_config.root
152-
153-
return where
162+
return root
163+
164+
source_root = self.builder_config.sources.get("")
165+
if source_root:
166+
# Explicit mapping: "" -> "src" (or similar). The value is the source dir.
167+
return os.path.join(root, source_root)
168+
169+
# No "" key. The keys themselves are source directories (e.g. "localstack-core/").
170+
# Filter out any empty-string key that slipped through, strip trailing separators.
171+
source_dirs = [k.rstrip("/") for k in self.builder_config.sources.keys() if k]
172+
if len(source_dirs) == 1:
173+
return os.path.join(root, source_dirs[0])
174+
175+
if source_dirs:
176+
LOG.warning("plux doesn't know how to resolve multiple sources directories")
177+
return root
154178

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

tests/build/test_hatchling.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,3 +372,58 @@ 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+
assert path == os.path.join(str(tmp_path), "localstack-core")
411+
412+
def test_path_with_empty_sources(self, tmp_path):
413+
"""When sources is empty, path falls back to the project root."""
414+
finder = self._make_finder(sources={}, root=str(tmp_path))
415+
416+
assert finder.path == str(tmp_path)
417+
418+
def test_path_with_packages_in_root(self, tmp_path):
419+
"""When packages are in the project root (sources = {'': ''}), path returns root."""
420+
finder = self._make_finder(sources={"": ""}, root=str(tmp_path))
421+
422+
assert finder.path == str(tmp_path)
423+
424+
def test_path_with_src_layout(self, tmp_path):
425+
"""When sources maps '' -> 'src', path returns the sources root joined to the project root."""
426+
import os
427+
finder = self._make_finder(sources={"": "src"}, root=str(tmp_path))
428+
429+
assert finder.path == os.path.join(str(tmp_path), "src")

0 commit comments

Comments
 (0)