-
-
Notifications
You must be signed in to change notification settings - Fork 375
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Clear minimum requirements for a script to install arbitrary project …
…requirements
- Loading branch information
Showing
5 changed files
with
157 additions
and
102 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -82,7 +82,8 @@ jobs: | |
run: | | ||
python -m pip install --upgrade pip | ||
python -m pip install --upgrade setuptools build wheel | ||
python tests/install_dep.py tox --extra dev | ||
# Utility script installs tox as defined in pyproject.toml | ||
python install_requirement.py tox --extra dev | ||
- name: Test | ||
id: test | ||
|
@@ -126,7 +127,8 @@ jobs: | |
run: | | ||
python -m pip install --upgrade pip | ||
python -m pip install --upgrade setuptools build wheel | ||
python tests/install_dep.py tox --extra dev | ||
# Utility script installs tox as defined in pyproject.toml | ||
python install_requirement.py tox --extra dev | ||
- name: Retrieve Coverage Data | ||
uses: actions/[email protected] | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
# install_requirement.py - Install a requirement from a PEP 517 project | ||
# | ||
# Usage | ||
# ----- | ||
# $ python install_requirement.py [-h] [--extra EXTRA] [--project-root PROJECT_ROOT] [requirements ...] | ||
# | ||
# Install one or more PEP 517 project defined requirements | ||
# | ||
# positional arguments: | ||
# requirements List of project requirements to install. Any project | ||
# requirements that start with any of these values will | ||
# be installed. For instance, including 'pytest' in this | ||
# list would install both pytest and pytest-xdist. | ||
# | ||
# options: | ||
# -h, --help show this help message and exit | ||
# --extra EXTRA Name of the extra where the requirements are defined | ||
# --project-root PROJECT_ROOT | ||
# File path to the root of the project. The current | ||
# directory is used by default. | ||
# | ||
# Purpose | ||
# ------- | ||
# Installs one or more requested requirements as defined for the project. | ||
# | ||
# In certain workflows, such as automated coverage reporting, the coverage | ||
# dependencies must be installed first. Since a project's requirements are often | ||
# pinned to specific versions to ensure consistency for the project regardless of the | ||
# environment, the coverage dependencies that are installed should match those pinned | ||
# for the project. | ||
# | ||
# A simple method to accomplish this is ``pip install .[dev]`` in which ``pip`` will | ||
# build the source and install the project with all its defined requirements. However, | ||
# this is very inefficient when only one or a few specific requirements are needed. | ||
# | ||
# Therefore, this script will evaluate the requirements defined in the project's | ||
# metadata and install the ones matching those being requested to be installed. | ||
|
||
from __future__ import annotations | ||
|
||
import subprocess | ||
import sys | ||
from argparse import ArgumentParser, RawDescriptionHelpFormatter | ||
from pathlib import Path | ||
from shutil import get_terminal_size | ||
|
||
from build.util import project_wheel_metadata | ||
from packaging.requirements import Requirement | ||
|
||
|
||
def parse_args(): | ||
width = max(min(get_terminal_size().columns, 80) - 2, 20) | ||
parser = ArgumentParser( | ||
prog="Requirement Installer", | ||
description="Installs one or more PEP 517 project defined requirements", | ||
formatter_class=lambda prog: RawDescriptionHelpFormatter(prog, width=width), | ||
) | ||
parser.add_argument( | ||
"requirements", | ||
type=str, | ||
nargs="*", | ||
help=( | ||
"List of project requirements to install. Any project requirements that " | ||
"start with any of these values will be installed. For instance, including " | ||
"'pytest' in this list would install both pytest and pytest-xdist." | ||
), | ||
) | ||
parser.add_argument( | ||
"--extra", | ||
type=str, | ||
default="", | ||
help="Name of the extra where the requirements are defined", | ||
) | ||
parser.add_argument( | ||
"--project-root", | ||
type=Path, | ||
default=".", | ||
help=( | ||
"File path to the root of the project. The current directory is used by " | ||
"default." | ||
), | ||
) | ||
return parser.parse_args() | ||
|
||
|
||
def gather_requirements( | ||
project_root: str | Path, | ||
requested_requirements: list[str], | ||
extra_name: str = "", | ||
) -> list[Requirement]: | ||
"""Identifies one or more matching requirements from a project.""" | ||
if not requested_requirements: | ||
raise ValueError("No requirements requested for install") | ||
|
||
project_root = Path(project_root).resolve() | ||
project_metadata = project_wheel_metadata(project_root, isolated=False) | ||
project_reqs = [ | ||
req | ||
for req in map(Requirement, project_metadata.get_all("Requires-Dist")) | ||
if not req.marker or req.marker.evaluate(environment={"extra": extra_name}) | ||
] | ||
|
||
found_requirements = [ | ||
requirement | ||
for requirement in project_reqs | ||
if any(requirement.name.startswith(dep) for dep in requested_requirements) | ||
] | ||
|
||
if not found_requirements: | ||
deps_str = "\n ".join(d.name for d in project_reqs) | ||
print(f"One or more requirements not found; considered:\n {deps_str}") | ||
|
||
return found_requirements | ||
|
||
|
||
def install_requirements(requirements: list[Requirement]): | ||
"""Install requirements from PyPI.""" | ||
for requirement in requirements: | ||
requirement_str = f"{requirement.name}{requirement.specifier}" | ||
print(f"Installing {requirement_str}...") | ||
subprocess.run( | ||
[ | ||
sys.executable, | ||
"-m", | ||
"pip", | ||
"install", | ||
"--upgrade", | ||
requirement_str, | ||
], | ||
check=True, | ||
) | ||
|
||
return 0 | ||
|
||
|
||
def main(): | ||
args = parse_args() | ||
sys.exit( | ||
install_requirements( | ||
requirements=gather_requirements( | ||
project_root=args.project_root, | ||
requested_requirements=args.requirements, | ||
extra_name=args.extra, | ||
) | ||
) | ||
) | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters