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 backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ dependencies = [
"packaging>=24.2",
"pathspec>=0.10.1",
"pluggy>=1.0.0",
"tomlkit>=0.11.1",
"tomli>=1.2.2; python_version < '3.11'",
"trove-classifiers",
]
Expand Down
27 changes: 24 additions & 3 deletions backend/src/hatchling/cli/version/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
def version_impl(
*,
called_by_app: bool, # noqa: ARG001
force: bool,
desired_version: str,
) -> None:
import os
Expand All @@ -22,11 +23,30 @@ def version_impl(
metadata = ProjectMetadata(root, plugin_manager)

if "version" in metadata.config.get("project", {}):
import tomlkit

project_file_path = os.path.join(root, "pyproject.toml")

with open(project_file_path, encoding="utf-8") as fr:
raw_config = tomlkit.parse(fr.read())
version = raw_config["project"]["version"]

if desired_version:
app.abort("Cannot set version when it is statically defined by the `project.version` field")
from hatchling.version.scheme.standard import StandardScheme

scheme_config = {"validate-bump": False} if force else {}
scheme = StandardScheme(root, scheme_config)
updated_version = scheme.update(desired_version, version, {})
raw_config["project"]["version"] = updated_version

with open(project_file_path, "w", encoding="utf-8") as fw:
fw.write(tomlkit.dumps(raw_config))

app.display_info(f"Old: {version}")
app.display_info(f"New: {updated_version}")
else:
app.display(metadata.core.version)
return
app.display(version)
return

source = metadata.hatch.version.source

Expand All @@ -48,4 +68,5 @@ def version_command(subparsers: argparse._SubParsersAction, defaults: Any) -> No
parser = subparsers.add_parser("version")
parser.add_argument("desired_version", default="", nargs="?", **defaults)
parser.add_argument("--app", dest="called_by_app", action="store_true", help=argparse.SUPPRESS)
parser.add_argument("--force", action="store_true", help="Allow an explicit downgrading version to be given")
parser.set_defaults(func=version_impl)
23 changes: 20 additions & 3 deletions src/hatch/cli/version/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,28 @@ def version(app: Application, *, desired_version: str | None, force: bool):
app.abort(f"Project {app.project.chosen_name} (not a project)")

if "version" in app.project.metadata.config.get("project", {}):
import tomlkit

project_file_path = app.project.location / "pyproject.toml"

raw_config = tomlkit.parse(project_file_path.read_text("utf-8"))
version = raw_config["project"]["version"]

if desired_version:
app.abort("Cannot set version when it is statically defined by the `project.version` field")
from hatchling.version.scheme.standard import StandardScheme

scheme_config = {"validate-bump": False} if force else {}
scheme = StandardScheme(str(app.project.location), scheme_config)
updated_version = scheme.update(desired_version, version, {})
raw_config["project"]["version"] = updated_version

project_file_path.write_text(tomlkit.dumps(raw_config), "utf-8")

app.display_info(f"Old: {version}")
app.display_info(f"New: {updated_version}")
else:
app.display(app.project.metadata.config["project"]["version"])
return
app.display(version)
return

from hatch.config.constants import VersionEnvVars
from hatch.project.constants import BUILD_BACKEND
Expand Down
62 changes: 60 additions & 2 deletions tests/cli/version/test_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,52 @@ def test_show_static(hatch, temp_dir):

def test_set_static(hatch, helpers, temp_dir):
project_name = "My.App"
test_comment = "# This is a test comment to assure version bumps do not erase comments."

with temp_dir.as_cwd():
hatch("new", project_name)

path = temp_dir / "my-app"
pyproject = path / "pyproject.toml"
data_path = temp_dir / "data"
data_path.mkdir()

# To test if formatting and comments are preserved after static set
# Regular Project.save_config does not preserve it
with open(pyproject, "r+", encoding="utf-8") as f:
lines = list(f)
for i, line in enumerate(lines):
if line.startswith("dynamic"):
lines[i] = 'version = "1.2.3"\n'
lines.append(test_comment)
f.seek(0)
f.writelines(lines)

with path.as_cwd(env_vars={ConfigEnvVars.DATA: str(data_path)}):
result = hatch("version", "minor,rc")

assert result.exit_code == 0, result.output
assert result.output == helpers.dedent(
"""
Old: 1.2.3
New: 1.3.0rc0
"""
)

project = Project(path)
assert project.raw_config["project"]["version"] == "1.3.0rc0", "should update static version"

with open(pyproject, encoding="utf-8") as fr:
for line in fr:
if line.startswith("version"):
version_line = line
last_line = line
assert version_line == 'version = "1.3.0rc0"\n', "should preserve pyproject formatting"
assert last_line == test_comment, "should preserve pyproject comments"


def test_set_static_downgrade(hatch, helpers, temp_dir):
project_name = "My.App"

with temp_dir.as_cwd():
hatch("new", project_name)
Expand All @@ -362,16 +408,28 @@ def test_set_static(hatch, helpers, temp_dir):
config["project"]["dynamic"].remove("version")
project.save_config(config)

# This one fails, because it's a downgrade without --force
with path.as_cwd(env_vars={ConfigEnvVars.DATA: str(data_path)}):
result = hatch("version", "minor,rc")
result = hatch("version", "1.2.0", catch_exceptions=True)

assert result.exit_code == 1, result.output
assert str(result.exception) == "Version `1.2.0` is not higher than the original version `1.2.3`"

# Try again, this time with --force
with path.as_cwd(env_vars={ConfigEnvVars.DATA: str(data_path)}):
result = hatch("version", "--force", "1.2.0")

assert result.exit_code == 0, result.output
assert result.output == helpers.dedent(
"""
Cannot set version when it is statically defined by the `project.version` field
Old: 1.2.3
New: 1.2.0
"""
)

project = Project(path)
assert project.raw_config["project"]["version"] == "1.2.0", "should force update static version"


@pytest.mark.requires_internet
@pytest.mark.usefixtures("mock_backend_process")
Expand Down