Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/history/hatch.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

- Fixes hatch shell type error for keep_env.
- SBOM documentation for including SBOM files in `sdist`
- Fixes workspace member detection to properly handle shared path prefixes.

## [1.16.3](https://github.com/pypa/hatch/releases/tag/hatch-v1.16.3) - 2026-01-20 ## {: #hatch-v1.16.3 }

Expand Down
2 changes: 1 addition & 1 deletion src/hatch/env/plugin/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -1238,7 +1238,7 @@ def members(self) -> list[WorkspaceMember]:
path_spec = data["path"]
normalized_path = os.path.normpath(os.path.join(root, path_spec))
absolute_path = os.path.abspath(normalized_path)
shared_prefix = os.path.commonprefix([root, absolute_path])
shared_prefix = os.path.commonpath([root, absolute_path])
relative_path = os.path.relpath(absolute_path, shared_prefix)

# Now we have the necessary information to perform an optimized glob search for members
Expand Down
53 changes: 53 additions & 0 deletions tests/env/plugin/test_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -3221,6 +3221,59 @@ def test_correct(self, hatch, temp_dir, isolated_data_dir, platform, global_appl
assert members[1].project.location == member2_path
assert members[2].project.location == member3_path

def test_member_outside_root_with_shared_prefix(self, temp_dir, isolated_data_dir, platform, global_application):
"""Verify correct workspace member discovery with shared path prefix.

os.path.commonprefix works character-by-character, so for paths that
share a partial directory name (e.g. 'local_app' and 'lib_member' both
start with 'l'), it would return an invalid path like '.../l' instead
of the true common ancestor directory. os.path.commonpath correctly
returns the nearest common directory, which is what we need as the
base for the member glob search.

Example of the mismatch:
os.path.commonprefix(['/usr/lib', '/usr/local/lib']) == '/usr/l'
os.path.commonpath(['/usr/lib', '/usr/local/lib']) == '/usr'
"""
project_root = temp_dir / "local_app"
project_root.mkdir()

member_path = temp_dir / "lib_member"
member_path.mkdir()
(member_path / "pyproject.toml").write_text(
"""\
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "lib-member"
version = "0.1.0"
"""
)

config = {
"project": {"name": "my_app", "version": "0.0.1"},
"tool": {"hatch": {"envs": {"default": {"workspace": {"members": [{"path": "../lib_member"}]}}}}},
}
project = Project(project_root, config=config)
environment = MockEnvironment(
project_root,
project.metadata,
"default",
project.config.envs["default"],
{},
isolated_data_dir,
isolated_data_dir,
platform,
0,
global_application,
)

members = environment.workspace.members
assert len(members) == 1
assert members[0].project.location == member_path


class TestWorkspaceDependencies:
def test_basic(self, temp_dir, isolated_data_dir, platform, global_application):
Expand Down
Loading