Skip to content
Open
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
eef04fe
Add support for workspaces
ofek Jun 2, 2024
0058046
Merge remote-tracking branch 'refs/remotes/origin/master' into worksp…
cjames23 Sep 22, 2025
9fc0f41
Fix not all reqs convert to deps issue
cjames23 Sep 22, 2025
c0152b2
Additional fixes for Requirements to Dependency conversion, fix tests…
cjames23 Sep 25, 2025
4dc4339
Fixing tests and typing issues found during workspaces dev.
cjames23 Sep 26, 2025
203b30e
Fix failing tests, handle subprocess buffering causing output issues …
cjames23 Sep 26, 2025
ef09d2a
Remove unused type-ignore comments
cjames23 Sep 26, 2025
9079b2a
Remove 3.8 from test workflows.
cjames23 Sep 26, 2025
83b119d
Drop 3.8 in classifiers and add changelogs to history docs.
cjames23 Sep 26, 2025
74d0daa
Fix requires-python for hatch
cjames23 Sep 26, 2025
4260722
Fix history doc for unreleased changes
cjames23 Sep 27, 2025
f448239
Fix: self test needed to have an additional arg, history doc formatting
cjames23 Sep 27, 2025
9c7a880
Update docs/history/hatch.md
cjames23 Sep 27, 2025
9d83042
Update docs/history/hatchling.md
cjames23 Sep 27, 2025
0a4b9af
Fix: formatting
cjames23 Sep 27, 2025
6215222
Merge remote-tracking branch 'origin/master'
cjames23 Sep 27, 2025
00f6c98
Merge remote-tracking branch 'origin/master' into workspaces
cjames23 Sep 27, 2025
6f07123
Add Workspace config model and tests for workspace support.
cjames23 Oct 3, 2025
e089124
Add workspace aware project discovery
cjames23 Oct 3, 2025
49640c1
Add support for workspaces
ofek Jun 2, 2024
2e82dc2
Fix not all reqs convert to deps issue
cjames23 Sep 22, 2025
1251148
Additional fixes for Requirements to Dependency conversion, fix tests…
cjames23 Sep 25, 2025
405a31b
Fixing tests and typing issues found during workspaces dev.
cjames23 Sep 26, 2025
29d8169
Drop 3.8 in classifiers and add changelogs to history docs.
cjames23 Sep 26, 2025
53f0a70
Fix history doc for unreleased changes
cjames23 Sep 27, 2025
ecb2bad
Fix: self test needed to have an additional arg, history doc formatting
cjames23 Sep 27, 2025
a81b13e
Fix: formatting
cjames23 Sep 27, 2025
6d3bd15
Add Workspace config model and tests for workspace support.
cjames23 Oct 3, 2025
c76dcd5
Add workspace aware project discovery
cjames23 Oct 3, 2025
5146231
Formatting changes and minor clean up
cjames23 Oct 5, 2025
ba36be5
Clean up merge conflicts with rebase and formatting
cjames23 Oct 5, 2025
98cd0e6
Merge remote-tracking branch 'origin/workspaces' into workspaces
cjames23 Oct 5, 2025
91e117c
Fix circular dependency, add properties to WorkspaceConfig,
cjames23 Oct 8, 2025
176faee
Utilize new workspace support for CI
cjames23 Oct 8, 2025
7c48bf9
Fix broad exception issue and other ruff errors
cjames23 Oct 8, 2025
8be9965
Change type for WorkspaceMember last_modified
cjames23 Oct 8, 2025
fa5272a
Move to using hatch env create to dogfood workspace support
cjames23 Oct 8, 2025
2ebe183
Revert workflow changes.
cjames23 Oct 8, 2025
88138e5
Fix member discovery issues.
cjames23 Oct 9, 2025
8416d4a
Fix formatting for dependencies block
cjames23 Oct 9, 2025
da10a4f
Handle case where project level workspace is not configured only envi…
cjames23 Oct 9, 2025
9a54b4d
Fix docs missing reference, update doc dependencies to handle depreca…
cjames23 Oct 9, 2025
af09e9a
Dogfooding workspaces with CI
cjames23 Oct 10, 2025
8261fce
Dogfood testing workspaces in CI
cjames23 Oct 10, 2025
aaa232c
Remove unnecessary scripts for dogfooding
cjames23 Oct 10, 2025
56e809b
Fix to ensure env creation happens before verifying hatchling
cjames23 Oct 10, 2025
89d105e
Revert changes for CI
cjames23 Oct 10, 2025
5502734
Change error type to enable dogfooding workspaces in CI
cjames23 Oct 14, 2025
1a5d7ae
Add back dependency on hatchling
cjames23 Oct 14, 2025
f55f61a
Add conflict logic for workspace members that are also declared depen…
cjames23 Oct 22, 2025
a136f72
Fix formatting for changes
cjames23 Oct 22, 2025
718be5e
Fix typing issues for project config
cjames23 Oct 22, 2025
b140f13
Fix bug for logic handling conflicts
cjames23 Oct 22, 2025
36570a8
Chore: Formatting
cjames23 Oct 22, 2025
07b4b90
Merge branch 'master' into workspaces
cjames23 Oct 22, 2025
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
4 changes: 4 additions & 0 deletions docs/history/hatchling.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

- Drop support for Python 3.8

***Changed:***

- Drop support for Python 3.8

## [1.27.0](https://github.com/pypa/hatch/releases/tag/hatchling-v1.27.0) - 2024-11-26 ## {: #hatchling-v1.27.0 }

***Added:***
Expand Down
1 change: 1 addition & 0 deletions docs/meta/authors.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@
- Olga Matoula [:material-github:](https://github.com/olgarithms) [:material-twitter:](https://twitter.com/olgarithms_)
- Philip Blair [:material-email:](mailto:[email protected])
- Robert Rosca [:material-github:](https://github.com/robertrosca)
- Cary Hawkins [:material-github](https://github.com/cjames23)
14 changes: 4 additions & 10 deletions hatch.toml
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
[workpace]
members = ["hatch/*"]

[envs.hatch-static-analysis]
config-path = "ruff_defaults.toml"

[envs.default]
installer = "uv"
post-install-commands = [
"uv pip install {verbosity:flag:-1} -e ./backend",
]

[envs.hatch-test]
extra-dependencies = [
"filelock",
"flit-core",
"hatchling",
"pyfakefs",
"trustme",
# Hatchling dynamic dependency
"editables",
]
post-install-commands = [
"pip install {verbosity:flag:-1} -e ./backend",
]
extra-args = ["--dist", "worksteal"]

[envs.hatch-test.extra-scripts]
Expand Down Expand Up @@ -125,9 +121,7 @@ update-ruff = [
[envs.release]
detached = true
installer = "uv"
dependencies = [
"hatchling",
]

[envs.release.scripts]
bump = "python scripts/bump.py {args}"
github = "python scripts/release_github.py {args}"
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ classifiers = [
]
dependencies = [
"click>=8.0.6",
"hatchling>=1.24.2",
"httpx>=0.22.0",
"hyperlink>=21.0.0",
"keyring>=23.5.0",
Expand All @@ -53,7 +52,7 @@ dependencies = [
"tomlkit>=0.11.1",
"userpath~=1.7",
"uv>=0.5.23",
"virtualenv>=20.26.6",
"virtualenv>=20.26.6,<20.35.0",
"zstandard<1",
]
dynamic = ["version"]
Expand All @@ -68,6 +67,9 @@ Source = "https://github.com/pypa/hatch"
[project.scripts]
hatch = "hatch.cli:main"

[tool.hatch.workspace]
members = ["backend"]

[tool.hatch.version]
source = "vcs"

Expand Down
48 changes: 46 additions & 2 deletions src/hatch/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,38 @@
from hatch.utils.fs import Path


def find_workspace_root(path: Path) -> Path | None:
"""Find workspace root by traversing up from given path."""
from hatch.utils.toml import load_toml_file

current = path
while current.parent != current:
# Check hatch.toml first
hatch_toml = current / "hatch.toml"
if hatch_toml.exists() and _has_workspace_config(load_toml_file, str(hatch_toml), "workspace"):
return current

# Then check pyproject.toml
pyproject = current / "pyproject.toml"
if pyproject.exists() and _has_workspace_config(load_toml_file, str(pyproject), "tool.hatch.workspace"):
Copy link
Contributor

Choose a reason for hiding this comment

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

We never actually use the config_path parameter here. I'd suggest that we remove the helper function and just load in this function directly or keep the helper function if you're concerned about random errors and have it return an empty dictionary instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the primary reason for doing this was just to catch errors separately for the file loading to check if there is a workspace configuration. There might be a better way to do this but this seemed clean to avoid catching errors of the same type that might not be related to the file loading.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let me know if you think this should still be changed

return current

current = current.parent
return None


def _has_workspace_config(load_func, file_path: str, config_path: str) -> bool:
"""Check if file has workspace configuration, returning False on any error."""
try:
config = load_func(file_path)
if config_path == "workspace":
return bool(config.get("workspace"))
# "tool.hatch.workspace"
return bool(config.get("tool", {}).get("hatch", {}).get("workspace"))
except (OSError, ValueError, TypeError, KeyError):
return False


@click.group(
context_settings={"help_option_names": ["-h", "--help"], "max_content_width": 120}, invoke_without_command=True
)
Expand Down Expand Up @@ -170,8 +202,20 @@ def hatch(ctx: click.Context, env_name, project, verbose, quiet, color, interact
app.project.set_app(app)
return

app.project = Project(Path.cwd())
app.project.set_app(app)
# Discover workspace-aware project
workspace_root = find_workspace_root(Path.cwd())
if workspace_root:
# Create project from workspace root with workspace context
app.project = Project(workspace_root, locate=False)
app.project.set_app(app)
# Set current member context if we're in a member directory
current_dir = Path.cwd()
if current_dir != workspace_root:
app.project.current_member_path = current_dir
else:
# No workspace, use current directory as before
app.project = Project(Path.cwd())
app.project.set_app(app)

if app.config.mode == "local":
return
Expand Down
13 changes: 7 additions & 6 deletions src/hatch/cli/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
if TYPE_CHECKING:
from collections.abc import Generator

from packaging.requirements import Requirement

from hatch.dep.core import Dependency
from hatch.env.plugin.interface import EnvironmentInterface


Expand Down Expand Up @@ -141,11 +140,11 @@ def ensure_environment_plugin_dependencies(self) -> None:
self.project.config.env_requires_complex, wait_message="Syncing environment plugin requirements"
)

def ensure_plugin_dependencies(self, dependencies: list[Requirement], *, wait_message: str) -> None:
def ensure_plugin_dependencies(self, dependencies: list[Dependency], *, wait_message: str) -> None:
if not dependencies:
return

from hatch.dep.sync import dependencies_in_sync
from hatch.dep.sync import InstalledDistributions
from hatch.env.utils import add_verbosity_flag

if app_path := os.environ.get("PYAPP"):
Expand All @@ -154,12 +153,14 @@ def ensure_plugin_dependencies(self, dependencies: list[Requirement], *, wait_me
management_command = os.environ["PYAPP_COMMAND_NAME"]
executable = self.platform.check_command_output([app_path, management_command, "python-path"]).strip()
python_info = PythonInfo(self.platform, executable=executable)
if dependencies_in_sync(dependencies, sys_path=python_info.sys_path):
distributions = InstalledDistributions(sys_path=python_info.sys_path)
if distributions.dependencies_in_sync(dependencies):
return

pip_command = [app_path, management_command, "pip"]
else:
if dependencies_in_sync(dependencies):
distributions = InstalledDistributions()
if distributions.dependencies_in_sync(dependencies):
return

pip_command = [sys.executable, "-u", "-m", "pip"]
Expand Down
5 changes: 2 additions & 3 deletions src/hatch/cli/dep/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@ def table(app, project_only, env_only, show_lines, force_ascii):
"""Enumerate dependencies in a tabular format."""
app.ensure_environment_plugin_dependencies()

from packaging.requirements import Requirement

from hatch.dep.core import Dependency
from hatch.utils.dep import get_complex_dependencies, get_normalized_dependencies, normalize_marker_quoting

environment = app.project.get_environment()
Expand All @@ -76,7 +75,7 @@ def table(app, project_only, env_only, show_lines, force_ascii):
if not all_requirements:
continue

normalized_requirements = [Requirement(d) for d in get_normalized_dependencies(all_requirements)]
normalized_requirements = [Dependency(d) for d in get_normalized_dependencies(all_requirements)]

columns = {"Name": {}, "URL": {}, "Versions": {}, "Markers": {}, "Features": {}}
for i, requirement in enumerate(normalized_requirements):
Expand Down
7 changes: 3 additions & 4 deletions src/hatch/cli/env/show.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,7 @@ def show(
app.display(json.dumps(contextual_config, separators=(",", ":")))
return

from packaging.requirements import InvalidRequirement, Requirement

from hatch.dep.core import Dependency, InvalidDependencyError
from hatchling.metadata.utils import get_normalized_dependency, normalize_project_name

if internal:
Expand Down Expand Up @@ -126,8 +125,8 @@ def show(
normalized_dependencies = set()
for dependency in dependencies:
try:
req = Requirement(dependency)
except InvalidRequirement:
req = Dependency(dependency)
except InvalidDependencyError:
normalized_dependencies.add(dependency)
else:
normalized_dependencies.add(get_normalized_dependency(req))
Expand Down
37 changes: 37 additions & 0 deletions src/hatch/dep/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from __future__ import annotations

from functools import cached_property

from packaging.requirements import InvalidRequirement, Requirement

from hatch.utils.fs import Path

InvalidDependencyError = InvalidRequirement


class Dependency(Requirement):
def __init__(self, s: str, *, editable: bool = False) -> None:
super().__init__(s)

if editable and self.url is None:
message = f"Editable dependency must refer to a local path: {s}"
raise InvalidDependencyError(message)

self.__editable = editable

@property
def editable(self) -> bool:
return self.__editable

@cached_property
def path(self) -> Path | None:
if self.url is None:
return None

import hyperlink

uri = hyperlink.parse(self.url)
if uri.scheme != "file":
return None

return Path.from_uri(self.url)
Loading
Loading