Skip to content

Commit

Permalink
Merge pull request #7 from sbidoul/optimize-static-deps
Browse files Browse the repository at this point in the history
Performance optimization when dependencies are static
  • Loading branch information
sbidoul authored Sep 22, 2023
2 parents e0f404e + a9a3f1e commit fa43d53
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 5 deletions.
8 changes: 7 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ build-backend = "hatchling.build"

[project]
name = "pyproject-dependencies"
dependencies = ["build", "packaging"]
dependencies = [
"build",
"packaging",
"pyproject-metadata",
"tomli ; python_version<'3.11'",
"typing_extensions ; python_version<'3.8'",
]
requires-python = ">=3.7"
readme = "README.md"
authors = [
Expand Down
64 changes: 60 additions & 4 deletions src/pyproject_dependencies/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
import subprocess
import sys
from pathlib import Path
from typing import Mapping, Optional, Sequence
from typing import Any, List, Mapping, Optional, Sequence, TypeVar, Union

from build import BuildBackendException
from build.util import project_wheel_metadata
from packaging.requirements import Requirement
from packaging.utils import canonicalize_name
from pyproject_metadata import RFC822Message, StandardMetadata

from .compat import Protocol, tomllib


def subprocess_runner(
Expand All @@ -36,6 +38,58 @@ def subprocess_runner(
raise subprocess.CalledProcessError(res.returncode, cmd)


_T = TypeVar("_T")


class BasicPackageMetadata(Protocol):
"""A subset of the importlib.metadata.PackageMetadata protocol."""

def __getitem__(self, key: str) -> str:
... # pragma: no cover

def get_all(self, name: str, failobj: _T) -> Union[List[Any], _T]:
"""
Return all values associated with a possibly multi-valued key.
"""


class RFC822MessageAdapter(BasicPackageMetadata):
def __init__(self, rfc822_message: RFC822Message):
self._rfc822_message = rfc822_message

def __getitem__(self, key: str) -> str:
value = self._rfc822_message.headers[key]
if len(value) > 1:
raise KeyError(f"multiple values for key {key!r}")
return value[0]

def get_all(self, name: str, failobj: _T) -> Union[List[Any], _T]:
return self._rfc822_message.headers.get(name, failobj)


def pyproject_metadata(
project_path: Path,
) -> Optional[BasicPackageMetadata]:
"""Obtain metadata for a project using pyproject.toml.
Return None if the dependencies are not static or if the project does not use
pyproject.toml.
"""
pyproject_path = project_path / "pyproject.toml"
if not pyproject_path.is_file():
return None
parsed_pyproject = tomllib.loads(pyproject_path.read_text(encoding="utf-8"))
if "project" not in parsed_pyproject:
return None
metadata = StandardMetadata.from_pyproject(parsed_pyproject)
if (
"dependencies" in metadata.dynamic
or "optional-dependencies" in metadata.dynamic
):
return None
return RFC822MessageAdapter(metadata.as_rfc822())


def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
Expand Down Expand Up @@ -94,12 +148,14 @@ def main() -> None:
metadata_by_project_name = {}
for project_path in project_paths:
try:
project_metadata = project_wheel_metadata(
project_metadata = pyproject_metadata(
project_path
) or project_wheel_metadata(
project_path,
not args.no_isolation,
runner=subprocess_runner,
)
except BuildBackendException as e:
except Exception as e:
if args.ignore_build_errors:
print(
f"Warning: ignoring build error in {project_path.resolve()}: {e}",
Expand Down
14 changes: 14 additions & 0 deletions src/pyproject_dependencies/compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import sys

if sys.version_info < (3, 8):
from typing_extensions import Protocol
else:
from typing import Protocol

if sys.version_info < (3, 11):
import tomli as tomllib
else:
import tomllib


__all__ = ["tomllib", "Protocol"]
39 changes: 39 additions & 0 deletions tests/test_pyproject_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,27 @@ def _make_project(tmp_path: Path, name: str, deps: List[str]) -> None:
)


def _make_legacy_project(tmp_path: Path, name: str, deps: List[str]) -> None:
assert len(deps) > 0
project_path = tmp_path / name
project_path.mkdir()
setup_py = project_path / "setup.py"
deps_str = '", "'.join(deps)
setup_py.write_text(
textwrap.dedent(
f"""
from setuptools import setup
setup(
name="{name}",
version="1.0",
install_requires=["{deps_str}"],
)
"""
)
)


def test_basic(tmp_path: Path) -> None:
_make_project(tmp_path, name="p1", deps=["d1", "d2"])
_make_project(tmp_path, name="p2", deps=["d1", "d3", "p1"])
Expand All @@ -41,6 +62,24 @@ def test_basic(tmp_path: Path) -> None:
assert result.stdout == "d1\nd2\nd3\n"


def test_basic_legacy(tmp_path: Path) -> None:
_make_legacy_project(tmp_path, name="p1", deps=["d1", "d2"])
_make_legacy_project(tmp_path, name="p2", deps=["d1", "d3", "p1"])
result = subprocess.run(
[
sys.executable,
"-m",
"pyproject_dependencies",
str(tmp_path / "p1"),
str(tmp_path / "p2" / "setup.py"),
],
check=True,
text=True,
capture_output=True,
)
assert result.stdout == "d1\nd2\nd3\n"


def test_name_filter(tmp_path: Path) -> None:
_make_project(tmp_path, name="p1", deps=["D1", "d2"])
_make_project(tmp_path, name="p2", deps=["d1", "d3", "p1"])
Expand Down

0 comments on commit fa43d53

Please # to comment.