Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pin project dependencies during hatch build #760

Merged
merged 10 commits into from
Nov 26, 2024
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ ENV/
env.bak/
venv.bak/
.vscode
.idea

# Rope project settings
.ropeproject
Expand Down
7 changes: 6 additions & 1 deletion .zenodo.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@
"orcid": "https://orcid.org/0000-0002-5545-1736",
"affiliation": "CSIRO, Australia",
"name": "Shrestha, Durga"
}
},
{
"orcid": "https://orcid.org/0009-0002-8569-1439",
"affiliation": "Independent Contributor, Australia",
"name": "Bishop, Sam"
}
],
"license": "Apache-2.0",

Expand Down
125 changes: 125 additions & 0 deletions hatch_build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import pathlib
from typing import Optional

import tomlkit
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
from hatchling.metadata.plugin.interface import MetadataHookInterface


class PinVersionsMetadataHook(MetadataHookInterface):
"""
Update the dependency metadata that `hatch` uses to set the dependencies of wheel artefacts.

This is invoked by `hatch` when `hatch build` is run as part of constructing the package metadata
before it moves on to executing the next appropriate build step, be it sdist or wheels.
"""

def update(self, metadata):
change_count = 0
# Overlay the pinned dependencies, replacing dependencies if they already exist.
for pinned_dep in self.config["config"]["pinned_dependencies"]:
techdragon marked this conversation as resolved.
Show resolved Hide resolved
pinned_dep_name = pinned_dep.split(" ")[0]
tennlee marked this conversation as resolved.
Show resolved Hide resolved
for dep in metadata["dependencies"]:
dep_name = dep.split(" ")[0]
# If the dependency is already in the list, replace it with the pinned version.
if dep_name == pinned_dep_name:
techdragon marked this conversation as resolved.
Show resolved Hide resolved
index = metadata["dependencies"].index(dep)
metadata["dependencies"][index] = pinned_dep
change_count += 1
break
print(
f"Updated {change_count} dependencies in hatch's internal dependency metadata"
f" to pinned versions from config."
)


class PinVersionsBuildHook(BuildHookInterface):
"""
Temporarily edit the dependencies in the pyproject.toml file to use the desired pinned versions
when using `hatch` to build a package for release.

When `hatch` performs a build in response to running the `hatch build` build command, its plugin system
will call the initialize method of this hook before the build process starts. This allows the hook to
update the dependencies in the pyproject.toml file before the build starts. The finalize method is called
after the build has completed to restore the original dependencies.
"""

PLUGIN_NAME = "pin-during-build"

original_dependencies: Optional[list] = None
made_changes: bool = False

def initialize(self, version, build_data):
# To avoid affecting the wheel METADATA file, we only run this hook's logic when
# the build system is building a sdist artefact.

print()
if self.build_config.builder.PLUGIN_NAME != "sdist":
print("Building wheel artefact. ")
print("This uses hatch's internal dependency metadata.")
print()
return
else:
print("Building sdist artefact.")
print("This does not use hatch's internal dependency metadata.")
print()
print("Updating pyproject.toml to contain the correct versions of pinned dependencies.")

# Get the pinned dependencies, this is a list of package specifier strings that are the dependencies to pin.
pinned_dependencies = self.metadata.hatch.metadata.hook_config["custom"]["config"]["pinned_dependencies"]

# Load the toml file
pyproject_file = pathlib.Path("pyproject.toml")
toml_data = tomlkit.loads(pyproject_file.read_text())

# Get the dependencies from the toml file, this is a list of package specifier strings.
dependencies = toml_data["project"]["dependencies"]

# Save the original dependencies for later.
self.original_dependencies = dependencies.copy()

change_count = 0

# Update dependencies with pinned versions if they are in the configuration and
# only if the un-pinned version is present.
for pinned_dep in pinned_dependencies:
pinned_dep_name = pinned_dep.split(" ")[0]
for dep in dependencies:
dep_name = dep.split(" ")[0]
# If the dependency is already in the list, replace it with the pinned version.
if dep_name == pinned_dep_name:
index = dependencies.index(dep)
dependencies[index] = pinned_dep
change_count += 1
break

# If we made any changes, write the changes back to the pyroject.toml file for the build.
if change_count > 0:
tennlee marked this conversation as resolved.
Show resolved Hide resolved
# Write the changes back to the file.
pyproject_file.write_text(tomlkit.dumps(toml_data))
print(
f"Updated {change_count} dependencies to pinned versions in the pyproject.toml "
f"which will be incorporated into the final sdist artefact."
)
# Set a flag to restore the original dependencies after the build.
self.made_changes = True
else:
print("No dependencies were changed to pinned version.")

# Update the build data with the pinned dependencies so it populates the METADATA file in the wheel artefact.
build_data["dependencies"] = dependencies

return super().initialize(version, build_data)

def finalize(self, version, build_data, artefact_path):
# If we have made changes restore the original dependencies after the build to keep the git repo clean.
tennlee marked this conversation as resolved.
Show resolved Hide resolved
if self.made_changes:
pyproject_file = pathlib.Path("pyproject.toml")
toml_data = tomlkit.loads(pyproject_file.read_text())
toml_data["project"]["dependencies"] = self.original_dependencies
pyproject_file.write_text(tomlkit.dumps(toml_data))
print()
print("Build of sdist artefact completed.")
print("Restored original dependencies in pyproject.toml file.")
print("This puts the git repo back to its original state.")
print()
25 changes: 18 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,7 @@ classifiers = [
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
]
dependencies = [
"xarray",
"pandas",
"scipy",
"bottleneck",
"scikit-learn",
]
dependencies = ["xarray", "pandas", "scipy", "bottleneck", "scikit-learn"]

[project.optional-dependencies]
dev = [
Expand Down Expand Up @@ -77,6 +71,23 @@ exclude = [
"/docs/",
"/tests/"
]
dependencies = [
"tomlkit",
]

[tool.hatch.build.hooks.custom]
override = true # This is required to activate the build hook.

[tool.hatch.metadata.hooks.custom.config]
# The pinned versions of these dependencies will be subsituted during package builds.
# Pinned versions will only be substitued for packages that are already in this package's dependencies list.
pinned_dependencies = [
"xarray ~= 2024.1",
"pandas ~= 2.0",
"scipy ~= 1.1",
"bottleneck ~= 1.3",
"scikit-learn ~= 1.4",
]

[tool.hatch.version]
path = "src/scores/__init__.py"
Expand Down
Loading