Skip to content
Merged
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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you rebase and merge? I am about to cut a release for hatch and if you can get this updated inside the release tag for the next release I will include these updates as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can rebase it, but the branch is up to date, so I'm not sure what you mean.


## [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