-
-
Notifications
You must be signed in to change notification settings - Fork 307
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
feat: hatch deps sync #1094
base: master
Are you sure you want to change the base?
feat: hatch deps sync #1094
Conversation
I am about to release but this will definitely go in the next, thank you! |
Sounds great. Super excited for the upcoming release. Regarding `hatch deps add` example code
"""
`hatch dep add` PoC
This code leverages tomlkit to parse the pyproject.toml file which is necessary
because it preserves style and comments.
"""
import pathlib
from typing import Optional, Tuple
import httpx
import packaging.requirements
import packaging.version
import tomlkit
def get_toml_dependencies(
toml_doc: tomlkit.TOMLDocument, environment: str
) -> Tuple[tomlkit.TOMLDocument, tomlkit.array]:
"""
Return an environment's dependencies from a TOMLDocument.
This function will perform some basic validation to ensure that the
TOMLDocument is structured in a way that we expect. If the TOMLDocument
does not contain the expected structure, a ValueError will be raised.
The table returned with the TOMLDocument is a ref of the internal data
structure, so any changes made to it will be reflected in the original
document.
Parameters
----------
toml_doc: tomlkit.TOMLDocument
The TOMLDocument to parse.
environment: str
The environment to parse.
Returns
-------
Tuple[tomlkit.TOMLDocument, tomlkit.array]
The TOMLDocument and the array of dependencies.
"""
toml_data = toml_doc.copy()
if toml_data.get("hatch"):
hatch_toml = toml_data["hatch"]
elif toml_data.get("tool", {}).get("hatch"):
hatch_toml = toml_data["tool"]["hatch"]
else:
raise ValueError("No hatch section found in config file.")
if not hatch_toml.get("envs", {}).get(environment):
raise ValueError(f"No environment {environment} found in config file.")
environment = hatch_toml["envs"][environment]
existing_dependencies = environment.get("dependencies", tomlkit.array())
return toml_data, existing_dependencies
def lookup_requirement(
requirement: packaging.requirements.Requirement,
) -> packaging.requirements.Requirement:
"""
Lookup a requirement on the PyPI API.
When a requirement is specified without a version, the latest version is
returned from PyPI with the `~=` specifier.
"""
response = httpx.get(f"https://pypi.org/pypi/{requirement.name}/json")
response.raise_for_status()
data = response.json()
if not requirement.specifier:
all_releases = [packaging.version.Version(version) for version in data["releases"]]
greatest_release = max(all_releases)
return packaging.requirements.Requirement(f"{requirement.name}~={greatest_release}")
else:
if requirement.specifier not in data["releases"]:
raise ValueError(f"Requirement {requirement} not found on PyPI.")
return requirement
def add_dependency(
toml_doc: tomlkit.TOMLDocument,
requirement: packaging.requirements.Requirement,
environment: str,
) -> Optional[tomlkit.TOMLDocument]:
"""
Add a dependency to a TOMLDocument if necessary
Behavior:
- If the dependency is already present with the same specifier provided,
no changes are made.
- If the dependency is already present and no specifier is provided, the
no changes are made.
- If the dependency is already present with a different specifier provided,
the specifier is updated.
- If the dependency is not present, it is added with the specifier provided
or the latest version specifier if no specifier is provided.
Returns
-------
Optional[tomlkit.TOMLDocument]
The TOMLDocument with the dependency added, or None if no changes were
made.
"""
toml_doc, existing_dependencies = get_toml_dependencies(
toml_doc=toml_doc, environment=environment
)
requirement_list = [packaging.requirements.Requirement(dep) for dep in existing_dependencies]
existing_requirements = {req.name: req for req in requirement_list}
matching_requirement = existing_requirements.get(requirement.name)
if matching_requirement and not requirement.specifier:
return None
elif matching_requirement and matching_requirement.specifier == requirement.specifier:
return None
elif matching_requirement and matching_requirement.specifier != requirement.specifier:
existing_dependencies.remove(str(matching_requirement))
requirement_to_add = lookup_requirement(requirement)
existing_dependencies.append(str(requirement_to_add))
return toml_doc
def add_requirement_to_toml(
toml_file: pathlib.Path,
requirement: str,
environment: str,
) -> None:
"""
Add a requirement to the pyproject.toml file if necessary.
This function represents the core functionality of the `hatch dep add`
command.
"""
toml_data = tomlkit.parse(toml_file.read_text())
new_requirement = packaging.requirements.Requirement(requirement)
updated_toml = add_dependency(
toml_doc=toml_data, requirement=new_requirement, environment=environment
)
if updated_toml:
tomlkit.dump(updated_toml, toml_file.open(mode="w"))
if __name__ == "__main__":
toml_file = pathlib.Path.home() / "git" / "hatch-pip-compile" / "pyproject.toml"
add_requirement_to_toml(
toml_file=toml_file,
requirement="flake8",
environment="lint",
) |
Greetings. At the moment I am choosing tools for the team and the priority is Hutch. The absence of this feature adds complexity to the work. Are there any problems using this feature? |
Adds a new CLI command:
deps sync
This saves me from running something like
hatch env run --env docs -- python --version
to initiate an environment sync. I've also found that being able to runhatch dep sync --all
can be especially useful. This would be particularly helpful for my plugin, hatch-pip-compile, which would allow this command to sync all lockfiles.This is a quick and dirty implementation, I extracted the
env run
implementation into a function and removed the last part where it actually runs the command. I did this in the easiest way I could come up with to demonstrate the functionality - I'd be happy to refactor if it's a feature you're interested in.Related: