Skip to content

Commit

Permalink
Clear minimum requirements for a script to install arbitrary project …
Browse files Browse the repository at this point in the history
…requirements
  • Loading branch information
rmartin16 committed May 24, 2023
1 parent 85f6cab commit 9bbabd5
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 102 deletions.
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
3 changes: 2 additions & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ include README.rst
include AUTHORS
include LICENSE
include docs/spelling_wordlist
include tests/integrations/xcode/simctl/README
include install_requirement.py
include pyproject.toml
include tests/integrations/xcode/simctl/README
include tox.ini
include .git-blame-ignore-revs
include .pre-commit-config.yaml
Expand Down
150 changes: 150 additions & 0 deletions install_requirement.py
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()
98 changes: 0 additions & 98 deletions tests/install_dep.py

This file was deleted.

2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ setenv =
deps = build
commands_pre =
python --version
python {tox_root}{/}tests{/}install_dep.py coverage --extra dev --project-root {tox_root}
python {tox_root}{/}install_requirement.py coverage --extra dev --project-root {tox_root}
commands =
-python -m coverage combine {env:COMBINE_FLAGS}
html: python -m coverage html --skip-covered --skip-empty
Expand Down

0 comments on commit 9bbabd5

Please sign in to comment.