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
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ jobs:
python-version: ["3.10", "3.13"]

steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v5
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v5
with:
python-version: ${{ matrix.python-version }}

Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## [Unreleased]

### Changed
* Migrate from `optional-dependencies` to PEP 735 `dependency-groups` for hatch environments (@mathematicalmichael, #145)
* All development dependencies now use the standard `[dependency-groups]` table instead of `[project.optional-dependencies]`
* Hatch environments now use `dependency-groups = [...]` instead of `features = [...]`
* This change requires Hatch 1.16+ (which supports dependency groups in builder and non-dev environments)
* When `use_hatch_envs=False`, optional-dependencies are still used for backward compatibility

## [v0.6.8]

### Fixed
Expand Down
90 changes: 77 additions & 13 deletions template/pyproject.toml.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -69,21 +69,18 @@ Documentation = "{{ dev_platform_url }}/{{ username }}/{{ project_slug }}/blob/m
{%- endif %}
Download = "https://pypi.org/project/{{ project_slug }}/#files"

{%- if not use_hatch_envs %}
Copy link
Member

Choose a reason for hiding this comment

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

@mathematicalmichael optional-dependencies are for optional features in a package. so they shouldn't be correlated with development-groups which are for development dependencies. I suggest we still include them/remove the conditional here and just add a comment about how they are used.

Suggested change
{%- if not use_hatch_envs %}

Copy link
Member

Choose a reason for hiding this comment

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

i'm trying to find the end of this condition - but we may just need to move it down.

Choose a reason for hiding this comment

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

line 128

Copy link
Member

Choose a reason for hiding this comment

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

thanks!! yes i think project.optional-dependencies can just be empty in our pyproject.toml template with a comment that the user can use that for optional features if they have them. but it shouldn't correlate to hatch environments because it's just a core pyproject.toml feature and development groups just allow us to separate feature dependencies from development.

Please let me know if i'm missing your intention here!

[project.optional-dependencies]
# The groups below should be in the [development-groups] table
# They are here now because hatch hasn't released support for them but plans to
# in Mid November 2025.
# When not using hatch environments, we keep optional-dependencies for backward compatibility
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# When not using hatch environments, we keep optional-dependencies for backward compatibility
# Optional-dependency table can be used to features that support optional functionality in your package. Remove this section if you don't have optional features in your package!

dev = [
Copy link
Member

Choose a reason for hiding this comment

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

I think this entire section can get moved to development groups but i may be wrong! it's hard to wrap my head around a jinja template in a pr!! but dev definitely belongs in development-groups not optional-dependencies

Copy link
Author

@mathematicalmichael mathematicalmichael Jan 16, 2026

Choose a reason for hiding this comment

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

yeah they are really annoying to review w/o indents, aren't they?

there's already an equivalent dep group for dev on 137 - so yeah perhaps this can get yeeted / was just left over.

Copy link
Member

Choose a reason for hiding this comment

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

they are!!

"hatch",
"pre-commit",
{%- if not use_hatch_envs %}
"{{ package_name }}[
{%- if documentation!="" %}docs,{% endif -%}
{%- if use_test %}tests,{% endif -%}
{%- if use_lint %}style,{% endif -%}
{%- if use_types %}types,{% endif -%}
audit]",
{%- endif %}
]

docs = [
Expand All @@ -104,7 +101,6 @@ docs = [

build = [
"pip-audit",
"twine",
]

{%- if use_test %}
Expand All @@ -129,6 +125,67 @@ types = [
"mypy",
]
{%- endif %}
{%- endif %}

{%- if use_hatch_envs %}
################################################################################
# Dependency Groups (PEP 735)
################################################################################

[dependency-groups]
# Development tools that are used across multiple environments
dev = [
"hatch",
"pre-commit",
]

{%- if documentation != "no" %}
docs = [
{%- if documentation == "sphinx" %}
"sphinx~=8.0",
"myst-parser>=4.0",
"pydata-sphinx-theme~=0.16",
"sphinx-autobuild>=2024.10.3",
"sphinx-autoapi>=3.6.0",
"sphinx_design>=0.6.1",
"sphinx-copybutton>=0.5.2",
{%- elif documentation == "mkdocs" %}
"mkdocs-material ~=9.5",
"mkdocstrings[python] ~=0.24",
"mkdocs-awesome-pages-plugin ~=2.9",
{% endif %}
]

{%- endif %}
build = [
"pip-audit",
"twine",
]

{%- if use_test %}
tests = [
"pytest",
"pytest-cov",
"pytest-raises",
"pytest-randomly",
"pytest-xdist",
]

{%- endif %}
{%- if use_lint %}
style = [
"pydoclint",
"ruff",
]

{%- endif %}
{%- if use_types %}
types = [
"mypy",
]

{%- endif %}
{%- endif %}


################################################################################
Expand Down Expand Up @@ -276,9 +333,16 @@ installer = "uv"
# This table installs the tools you need to test and build your package
[tool.hatch.envs.build]
description = """Test the installation the package."""
features = [
builder = true
dependency-groups = [
"build",
]
# Explicit dependencies are required as a workaround for dependency-groups
# not being installed in builder environments (see https://github.com/pypa/hatch/issues/2152)
# This ensures twine is available when scripts run
dependencies = [

Choose a reason for hiding this comment

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

this irks me

"twine",
]
Comment on lines +340 to +345
Copy link
Member

Choose a reason for hiding this comment

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

I need to test this again - the pr to fix this was merged, but when I tested this last, it worked as long as builder was set to true in the environment definition here. Let me do a sanity check on it - @mathematicalmichael, did you run through the template and try it with Hatch 1.16.2 without adding twine explicitly? I'm sorry if I missed a discussion of this, but I thought it worked, but you just had to make it a builder envt. it's possible i just had the newest commit/fix installed and didn't realize it but i thought that was the patch for now - builder=true.

Choose a reason for hiding this comment

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

yup. in commit history i tried to get rid of twine explicitly a few times and it kept failing. the only other workaround was to pip install it outside (when installing hatch) - equally bad.

i only pushed up stuff that passed locally to CI. everything else failed.

detached = true

# This table installs created the command hatch run install:check which will build and check your package.
Expand All @@ -293,7 +357,7 @@ check = [
{%- if use_test %}
[tool.hatch.envs.test]
description = """Run the test suite."""
features = [
dependency-groups = [
"tests",
]

Expand All @@ -309,8 +373,8 @@ run = "pytest {args:--cov={{ package_name }} --cov-report=term-missing --cov-rep
# This sets up a hatch environment with associated dependencies that need to be installed
[tool.hatch.envs.docs]
description = """Build or serve the documentation."""
# Install optional dependency test for docs
features = [
# Install dependency group for docs
dependency-groups = [
"docs",
]

Expand All @@ -333,7 +397,7 @@ serve = ["sphinx-autobuild docs --watch src/{{ package_name }} {args:-b html doc

[tool.hatch.envs.style]
description = """Check the code and documentation style."""
features = [
dependency-groups = [
"style",
]
detached = true
Expand All @@ -349,7 +413,7 @@ check = ["docstrings", "code"]

[tool.hatch.envs.audit]
description = """Check dependencies for security vulnerabilities."""
features = [
dependency-groups = [
"build",
]

Expand All @@ -361,7 +425,7 @@ check = ["pip-audit"]
#--------------- Typing ---------------#
[tool.hatch.envs.types]
description = """Check the static types of the codebase."""
features = ["types"]
dependency-groups = ["types"]

[tool.hatch.envs.types.scripts]
check = "mypy src/{{ package_name }}"
Expand Down
55 changes: 53 additions & 2 deletions tests/test_template_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,14 @@ def test_template_suite(
generated: Callable[..., Path],
) -> None:
"""Expect that the test suite passes for the initialized template."""
project_dir = generated()
# Explicitly enable hatch environments to ensure build environment exists
project_dir = generated(use_hatch_envs=True)

# Run the local test suite.
run_command("hatch build --clean", project_dir)
# Use hatch run build:check to ensure we use the build environment
# (Hatch 1.16+ requires builder environments to have builder=true)
# This runs "hatch build --clean" and "twine check" in the build environment
run_command("hatch run build:check", project_dir)
run_command(f"hatch run +py={sys.version_info.major}.{sys.version_info.minor} test:run", project_dir)
run_command("hatch run style:check", project_dir)

Expand Down Expand Up @@ -254,3 +258,50 @@ def test_non_hatch_deps(
if documentation != "no":
assert "docs" in optional_deps
assert any(dep.startswith(documentation) for dep in optional_deps["docs"])


def test_hatch_deps_groups(
documentation: str,
generated: Callable[..., Path],
) -> None:
"""When using hatch environments, we should use dependency-groups (PEP 735)."""
project = generated(
use_hatch_envs=True,
use_lint=True,
use_types=True,
use_test=True,
use_git=False,
documentation=documentation,
)

pyproject_file = project / "pyproject.toml"
with pyproject_file.open("rb") as pfile:
pyproject = tomllib.load(pfile)

# validate pyproject.toml file if present
validator_api.Validator()(pyproject)

# When using hatch_envs, dependencies should be in dependency-groups, not optional-dependencies
assert "dependency-groups" in pyproject
dep_groups = pyproject["dependency-groups"]

# Check that expected groups exist
groups = ("dev", "tests", "style", "types", "build")
assert all(group in dep_groups for group in groups)

# Check that docs group exists if documentation is enabled
if documentation != "no":
assert "docs" in dep_groups
assert any(dep.startswith(documentation) for dep in dep_groups["docs"])

# Verify that hatch environments use dependency-groups
if "tool" in pyproject and "hatch" in pyproject["tool"]:
hatch_envs = pyproject["tool"]["hatch"].get("envs", {})
for env_name, env_config in hatch_envs.items():
if env_name != "default" and isinstance(env_config, dict):
# Check that environments use dependency-groups instead of features
if "dependency-groups" in env_config:
# Verify the dependency-groups reference valid groups
env_dep_groups = env_config["dependency-groups"]
for group in env_dep_groups:
assert group in dep_groups, f"Environment {env_name} references unknown dependency group: {group}"