pyproject_api-1.6.1/.pre-commit-config.yaml0000644000000000000000000000231413615410400015570 0ustar00repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit rev: "v0.0.286" hooks: - id: ruff exclude: src/pyproject_api/_backend.py args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/psf/black rev: 23.7.0 hooks: - id: black - repo: https://github.com/tox-dev/tox-ini-fmt rev: "1.3.1" hooks: - id: tox-ini-fmt args: ["-p", "fix"] - repo: https://github.com/tox-dev/pyproject-fmt rev: "1.1.0" hooks: - id: pyproject-fmt additional_dependencies: ["tox>=4.10"] - repo: https://github.com/pre-commit/mirrors-prettier rev: "v3.0.2" hooks: - id: prettier args: ["--print-width=120", "--prose-wrap=always"] - repo: https://github.com/asottile/blacken-docs rev: 1.16.0 hooks: - id: blacken-docs additional_dependencies: [black==23.7] - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 hooks: - id: rst-backticks - repo: meta hooks: - id: check-hooks-apply - id: check-useless-excludes pyproject_api-1.6.1/.readthedocs.yml0000644000000000000000000000035013615410400014373 0ustar00version: 2 build: os: ubuntu-22.04 tools: python: "3" python: install: - method: pip path: . extra_requirements: - docs sphinx: builder: html configuration: docs/conf.py fail_on_warning: true pyproject_api-1.6.1/CODE_OF_CONDUCT.md0000644000000000000000000000626413615410400014116 0ustar00# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: - Using welcoming and inclusive language - Being respectful of differing viewpoints and experiences - Gracefully accepting constructive criticism - Focusing on what is best for the community - Showing empathy towards other community members Examples of unacceptable behavior by participants include: - The use of sexualized language or imagery and unwelcome sexual attention or advances - Trolling, insulting/derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or electronic address, without explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at tox-dev@python.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html][version] [homepage]: https://www.contributor-covenant.org/ [version]: https://www.contributor-covenant.org/version/1/4/ pyproject_api-1.6.1/tox.ini0000644000000000000000000000407613615410400012631 0ustar00[tox] requires = tox>=4.2 env_list = fix py312 py311 py310 py39 py38 py37 type docs pkg_meta skip_missing_interpreters = true [testenv] description = run the tests with pytest under {envname} package = wheel wheel_build_env = .pkg extras = testing pass_env = PYTEST_* SSL_CERT_FILE set_env = COVERAGE_FILE = {env:COVERAGE_FILE:{toxworkdir}{/}.coverage.{envname}} commands = pytest {tty:--color=yes} {posargs: --no-cov-on-fail --cov-context=test \ --cov={envsitepackagesdir}{/}pyproject_api --cov={toxinidir}{/}tests --cov-config={toxinidir}{/}pyproject.toml \ --cov-report=term-missing:skip-covered --cov-report=html:{envtmpdir}{/}htmlcov \ --cov-report=xml:{toxworkdir}{/}coverage.{envname}.xml --junitxml={toxworkdir}{/}junit.{envname}.xml \ tests} labels = test [testenv:fix] description = run formatter and linters skip_install = true deps = pre-commit>=3.3.3 pass_env = {[testenv]passenv} PROGRAMDATA commands = pre-commit run --all-files --show-diff-on-failure {tty:--color=always} {posargs} [testenv:type] description = run type check on code base deps = mypy==1.5.1 set_env = {tty:MYPY_FORCE_COLOR = 1} commands = mypy src/pyproject_api --strict mypy tests --strict [testenv:docs] description = build documentation extras = docs commands = sphinx-build -d "{envtmpdir}{/}doctree" docs "{toxworkdir}{/}docs_out" --color -b html {posargs} -W python -c 'print(r"documentation available under file://{toxworkdir}{/}docs_out{/}index.html")' [testenv:pkg_meta] description = check that the long description is valid skip_install = true deps = build[virtualenv]>=0.10 check-wheel-contents>=0.4 twine>=4.0.2 commands = python -m build -o {envtmpdir} -s -w . twine check --strict {envtmpdir}{/}* check-wheel-contents --no-config {envtmpdir} [testenv:dev] description = dev environment with all deps at {envdir} package = editable extras = docs testing commands = python -m pip list --format=columns python -c "print(r'{envpython}')" pyproject_api-1.6.1/.github/dependabot.yml0000644000000000000000000000016513615410400015501 0ustar00version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" pyproject_api-1.6.1/.github/release.yml0000644000000000000000000000011413615410400015006 0ustar00changelog: exclude: authors: - dependabot - pre-commit-ci pyproject_api-1.6.1/.github/workflows/check.yml0000644000000000000000000000456213615410400016513 0ustar00name: check on: push: tags-ignore: ["**"] pull_request: schedule: - cron: "0 8 * * *" concurrency: group: check-${{ github.ref }} cancel-in-progress: true jobs: test: name: test ${{ matrix.py }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: py: - "3.12.0-rc.1" - "3.11" - "3.10" - "3.9" - "3.8" os: - ubuntu-latest - windows-latest - macos-latest steps: - name: Setup python for tox uses: actions/setup-python@v4 with: python-version: "3.11" - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Install tox run: python -m pip install tox - name: Setup python for test ${{ matrix.py }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.py }} - name: Pick environment to run run: | import os; import platform; import sys; from pathlib import Path env = f'TOXENV=py{"" if platform.python_implementation() == "CPython" else "py"}3{sys.version_info.minor}' print(f"Picked: {env} for {sys.version} based of {sys.executable}") with Path(os.environ["GITHUB_ENV"]).open("ta") as file_handler: file_handler.write(env) shell: python - name: Setup test suite run: tox -vv --notest - name: Run test suite run: tox --skip-pkg-install env: PYTEST_ADDOPTS: "-vv --durations=20" CI_RUN: "yes" DIFF_AGAINST: HEAD check: name: tox env ${{ matrix.tox_env }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: tox_env: - type - dev - docs - pkg_meta os: - ubuntu-latest - windows-latest exclude: - { os: windows-latest, tox_env: pkg_meta } steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Setup Python 3.11 uses: actions/setup-python@v4 with: python-version: "3.11" - name: Install tox run: python -m pip install tox - name: Run check for ${{ matrix.tox_env }} run: tox -e ${{ matrix.tox_env }} env: UPGRADE_ADVISORY: "yes" pyproject_api-1.6.1/.github/workflows/release.yml0000644000000000000000000000121213615410400017043 0ustar00name: Release to PyPI on: push: tags: ["*"] jobs: release: runs-on: ubuntu-latest environment: name: release url: https://pypi.org/p/pyproject-api permissions: id-token: write steps: - name: Setup python to build package uses: actions/setup-python@v4 with: python-version: "3.11" - name: Install build run: python -m pip install build - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Build package run: pyproject-build -s -w . -o dist - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@v1.8.10 pyproject_api-1.6.1/docs/changelog.rst0000644000000000000000000000420513615410400014721 0ustar00Release History =============== v1.6.1 - (2023-08-29) --------------------- - Fix :meth:`pyproject_api.Frontend.metadata_from_built` only extracts one of the dist info files. v1.6.0 - (2023-08-29) --------------------- - Remove ``build_`` from ``prepare_metadata_for_build_`` to allow separate config parametrization and instead add :meth:`pyproject_api.Frontend.metadata_from_built` the user can call when the prepare fails. Pass ``None`` for ``metadata_directory`` for such temporary wheel builds. v1.5.4 - (2023-08-17) --------------------- - Make sure that the order of Requires-Dist does not matter v1.5.3 - (2023-07-06) --------------------- - Fix ``read_line`` to raise ``EOFError`` if nothing was read v1.5.2 - (2023-06-14) --------------------- - Use ruff for linting. - Drop 2.7 test run. v1.5.1 - (2023-03-12) --------------------- - docs: set html_last_updated_fmt to format string v1.5.0 - (2023-01-17) --------------------- - When getting metadata from a built wheel, do not pass ``metadata_directory`` to ``build_wheel``, which forces the backend to generate the metadata - by :user:`masenf`. (`#47 `_) v1.4.0 - (2022-01-04) --------------------- - Add minimal CLI for debugging v1.3.0 - (2022-01-03) --------------------- - Do not allow exceptions to propagate in backend v1.2.1 - (2022-12-04) --------------------- - Fix Python 2 incompatibility on the backend - Allow skipping prepare metadata for the full build by returning None as basename v1.2.0 - (2022-12-04) --------------------- - Expose which optional hooks are present or missing via :meth:`pyproject_api.Frontend.optional_hooks` v1.1.2 - (2022-10-30) --------------------- - Fix editable classes not exported at root level v1.1.1 - (2022-09-10) --------------------- - Add missed ``wheel`` as test dependency v1.1.0 - (2022-09-10) --------------------- - PEP-660 support v1.0.0 - (2022-09-10) --------------------- - Use hatchling as build backend - 3.11 support v0.0.1 - (2021-12-30) --------------------- - Drop Python 3.6 support v0.1.0 - (2021-10-21) --------------------- - first version pyproject_api-1.6.1/docs/conf.py0000644000000000000000000000231113615410400013533 0ustar00# noqa: D100 from __future__ import annotations from pyproject_api import __version__ project = name = "pyproject_api" company = "tox-dev" copyright = f"{company}" # noqa: A001 version, release = __version__, __version__.split("+")[0] extensions = [ "sphinx.ext.autosectionlabel", "sphinx.ext.extlinks", "sphinx.ext.autodoc", "sphinx_autodoc_typehints", "sphinx.ext.viewcode", "sphinx.ext.intersphinx", ] master_doc, source_suffix = "index", ".rst" html_theme = "furo" html_title, html_last_updated_fmt = "pyproject-api docs", "%Y-%m-%dT%H:%M:%S" pygments_style, pygments_dark_style = "sphinx", "monokai" autoclass_content, autodoc_typehints = "both", "none" autodoc_default_options = {"members": True, "member-order": "bysource", "undoc-members": True, "show-inheritance": True} inheritance_alias = {} extlinks = { "issue": ("https://github.com/tox-dev/pyproject-api/issues/%s", "#%s"), "pull": ("https://github.com/tox-dev/pyproject-api/pull/%s", "PR #%s"), "user": ("https://github.com/%s", "@%s"), } intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "packaging": ("https://packaging.pypa.io/en/latest", None), } nitpicky = True nitpick_ignore = [] pyproject_api-1.6.1/docs/index.rst0000644000000000000000000000213113615410400014075 0ustar00``pyproject-api`` ================= ``pyproject-api`` aims to abstract away interaction with ``pyproject.toml`` style projects in a flexible way. API +++ .. currentmodule:: pyproject_api .. autodata:: __version__ Frontend -------- .. autoclass:: Frontend .. autoclass:: OptionalHooks Exceptions ---------- Backend failed ~~~~~~~~~~~~~~ .. autoclass:: BackendFailed Results ------- Build source distribution requires ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: RequiresBuildSdistResult Build wheel requires ~~~~~~~~~~~~~~~~~~~~ .. autoclass:: RequiresBuildWheelResult Editable requires ~~~~~~~~~~~~~~~~~ .. autoclass:: RequiresBuildEditableResult Wheel metadata ~~~~~~~~~~~~~~ .. autoclass:: MetadataForBuildWheelResult Editable metadata ~~~~~~~~~~~~~~~~~ .. autoclass:: MetadataForBuildEditableResult Source distribution ~~~~~~~~~~~~~~~~~~~ .. autoclass:: SdistResult Editable ~~~~~~~~ .. autoclass:: EditableResult Wheel ~~~~~ .. autoclass:: WheelResult Fresh subprocess frontend ------------------------- .. autoclass:: SubprocessFrontend .. toctree:: :hidden: self changelog pyproject_api-1.6.1/src/pyproject_api/__init__.py0000644000000000000000000000156613615410400017067 0ustar00"""PyProject API interface.""" from __future__ import annotations from ._frontend import ( BackendFailed, CmdStatus, EditableResult, Frontend, MetadataForBuildEditableResult, MetadataForBuildWheelResult, OptionalHooks, RequiresBuildEditableResult, RequiresBuildSdistResult, RequiresBuildWheelResult, SdistResult, WheelResult, ) from ._version import version from ._via_fresh_subprocess import SubprocessFrontend #: semantic version of the project __version__ = version __all__ = [ "__version__", "Frontend", "BackendFailed", "CmdStatus", "RequiresBuildSdistResult", "RequiresBuildWheelResult", "RequiresBuildEditableResult", "MetadataForBuildWheelResult", "MetadataForBuildEditableResult", "SdistResult", "WheelResult", "EditableResult", "SubprocessFrontend", "OptionalHooks", ] pyproject_api-1.6.1/src/pyproject_api/__main__.py0000644000000000000000000000533113615410400017042 0ustar00from __future__ import annotations # noqa: D100 import argparse import os import pathlib import sys from typing import TYPE_CHECKING from ._via_fresh_subprocess import SubprocessFrontend if TYPE_CHECKING: from ._frontend import EditableResult, SdistResult, WheelResult def main_parser() -> argparse.ArgumentParser: # noqa: D103 parser = argparse.ArgumentParser( description=( "A pyproject.toml-based build frontend. " "This is mainly useful for debugging PEP-517 backends. " "This frontend will not do things like install required build dependencies." ), ) parser.add_argument( "srcdir", type=pathlib.Path, nargs="?", default=pathlib.Path.cwd(), help="source directory (defaults to current directory)", ) parser.add_argument( "--sdist", "-s", dest="distributions", action="append_const", const="sdist", default=[], help="build a source distribution", ) parser.add_argument( "--wheel", "-w", dest="distributions", action="append_const", const="wheel", help="build a wheel distribution", ) parser.add_argument( "--editable", "-e", dest="distributions", action="append_const", const="editable", help="build an editable wheel distribution", ) parser.add_argument( "--outdir", "-o", type=pathlib.Path, help=f"output directory (defaults to {{srcdir}}{os.sep}dist)", ) return parser def main(argv: list[str]) -> None: # noqa: D103 parser = main_parser() args = parser.parse_args(argv) outdir = args.outdir or args.srcdir / "dist" # we intentionally do not build editable distributions by default distributions = args.distributions or ["sdist", "wheel"] frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(args.srcdir)[:-1]) res: SdistResult | WheelResult | EditableResult if "sdist" in distributions: print("Building sdist...") # noqa: T201 res = frontend.build_sdist(outdir) print(res.out) # noqa: T201 print(res.err, file=sys.stderr) # noqa: T201 if "wheel" in distributions: print("Building wheel...") # noqa: T201 res = frontend.build_wheel(outdir) print(res.out) # noqa: T201 print(res.err, file=sys.stderr) # noqa: T201 if "editable" in distributions: print("Building editable wheel...") # noqa: T201 res = frontend.build_editable(outdir) print(res.out) # noqa: T201 print(res.err, file=sys.stderr) # noqa: T201 if __name__ == "__main__": main(sys.argv[1:]) pyproject_api-1.6.1/src/pyproject_api/_backend.py0000644000000000000000000001107213615410400017047 0ustar00"""Handles communication on the backend side between frontend and backend Please keep this file Python 2.7 compatible. See https://tox.readthedocs.io/en/rewrite/development.html#code-style-guide """ from __future__ import print_function import importlib import json import os import sys import traceback class MissingCommand(TypeError): """Missing command""" class BackendProxy: def __init__(self, backend_module, backend_obj): self.backend_module = backend_module self.backend_object = backend_obj backend = importlib.import_module(self.backend_module) if self.backend_object: backend = getattr(backend, self.backend_object) self.backend = backend def __call__(self, name, *args, **kwargs): on_object = self if name.startswith("_") else self.backend if not hasattr(on_object, name): raise MissingCommand("{!r} has no attribute {!r}".format(on_object, name)) return getattr(on_object, name)(*args, **kwargs) def __str__(self): return "{}(backend={})".format(self.__class__.__name__, self.backend) def _exit(self): return 0 def _optional_hooks(self): return { k: hasattr(self.backend, k) for k in ( "get_requires_for_build_sdist", "prepare_metadata_for_build_wheel", "get_requires_for_build_wheel", "build_editable", "get_requires_for_build_editable", "prepare_metadata_for_build_editable", ) } def flush(): sys.stderr.flush() sys.stdout.flush() def run(argv): reuse_process = argv[0].lower() == "true" try: backend_proxy = BackendProxy(argv[1], None if len(argv) == 2 else argv[2]) except BaseException: print("failed to start backend", file=sys.stderr) raise else: print("started backend {}".format(backend_proxy), file=sys.stdout) finally: flush() # pragma: no branch while True: content = read_line() if not content: continue flush() # flush any output generated before try: if sys.version_info[0] == 2: # pragma: no branch # python 2 does not support loading from bytearray content = content.decode() # pragma: no cover parsed_message = json.loads(content) result_file = parsed_message["result"] except Exception: # ignore messages that are not valid JSON and contain a valid result path print("Backend: incorrect request to backend: {}".format(content), file=sys.stderr) flush() else: result = {} try: cmd = parsed_message["cmd"] print("Backend: run command {} with args {}".format(cmd, parsed_message["kwargs"])) outcome = backend_proxy(parsed_message["cmd"], **parsed_message["kwargs"]) result["return"] = outcome if cmd == "_exit": break except BaseException as exception: result["code"] = exception.code if isinstance(exception, SystemExit) else 1 result["exc_type"] = exception.__class__.__name__ result["exc_msg"] = str(exception) if not isinstance(exception, MissingCommand): # for missing command do not print stack traceback.print_exc() finally: try: with open(result_file, "w") as file_handler: json.dump(result, file_handler) except Exception: traceback.print_exc() finally: # used as done marker by frontend print("Backend: Wrote response {} to {}".format(result, result_file)) flush() # pragma: no branch if reuse_process is False: # pragma: no branch # no test for reuse process in root test env break return 0 def read_line(fd=0): # for some reason input() seems to break (hangs forever) so instead we read byte by byte the unbuffered stream content = bytearray() while True: char = os.read(fd, 1) if not char: if not content: raise EOFError("EOF without reading anything") # we didn't get a line at all, let the caller know break # pragma: no cover if char == b"\n": break if char != b"\r": content += char return content if __name__ == "__main__": sys.exit(run(sys.argv[1:])) pyproject_api-1.6.1/src/pyproject_api/_backend.pyi0000644000000000000000000000102713615410400017217 0ustar00from typing import Any, Sequence class MissingCommand(TypeError): ... # noqa: N818 class BackendProxy: backend_module: str backend_object: str | None backend: Any def __init__(self, backend_module: str, backend_obj: str | None) -> None: ... def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any: ... def _exit(self) -> None: ... def _optional_commands(self) -> dict[str, bool]: ... def run(argv: Sequence[str]) -> int: ... def read_line(fd: int = 0) -> bytearray: ... def flush() -> None: ... pyproject_api-1.6.1/src/pyproject_api/_frontend.py0000644000000000000000000005070613615410400017306 0ustar00"""Build frontend for PEP-517.""" from __future__ import annotations import json import sys from abc import ABC, abstractmethod from contextlib import contextmanager from pathlib import Path from tempfile import NamedTemporaryFile, TemporaryDirectory from time import sleep from typing import Any, Dict, Iterator, List, Literal, NamedTuple, NoReturn, Optional, TypedDict, cast from zipfile import ZipFile from packaging.requirements import Requirement from pyproject_api._util import ensure_empty_dir if sys.version_info >= (3, 11): # pragma: no cover (py311+) import tomllib else: # pragma: no cover (py311+) import tomli as tomllib _HERE = Path(__file__).parent ConfigSettings = Optional[Dict[str, Any]] class OptionalHooks(TypedDict, total=True): """A flag indicating if the backend supports the optional hook or not.""" get_requires_for_build_sdist: bool prepare_metadata_for_build_wheel: bool get_requires_for_build_wheel: bool build_editable: bool get_requires_for_build_editable: bool prepare_metadata_for_build_editable: bool class CmdStatus(ABC): @property @abstractmethod def done(self) -> bool: """:return: truthful when the command finished running""" raise NotImplementedError @abstractmethod def out_err(self) -> tuple[str, str]: """:return: standard output and standard error text""" raise NotImplementedError class RequiresBuildSdistResult(NamedTuple): """Information collected while acquiring the source distribution build dependencies.""" #: wheel build dependencies requires: tuple[Requirement, ...] #: backend standard output while acquiring the source distribution build dependencies out: str #: backend standard output while acquiring the source distribution build dependencies err: str class RequiresBuildWheelResult(NamedTuple): """Information collected while acquiring the wheel build dependencies.""" #: wheel build dependencies requires: tuple[Requirement, ...] #: backend standard output while acquiring the wheel build dependencies out: str #: backend standard error while acquiring the wheel build dependencies err: str class RequiresBuildEditableResult(NamedTuple): """Information collected while acquiring the wheel build dependencies.""" #: editable wheel build dependencies requires: tuple[Requirement, ...] #: backend standard output while acquiring the editable wheel build dependencies out: str #: backend standard error while acquiring the editable wheel build dependencies err: str class MetadataForBuildWheelResult(NamedTuple): """Information collected while acquiring the wheel metadata.""" #: path to the wheel metadata metadata: Path #: backend standard output while generating the wheel metadata out: str #: backend standard output while generating the wheel metadata err: str class MetadataForBuildEditableResult(NamedTuple): """Information collected while acquiring the editable metadata.""" #: path to the wheel metadata metadata: Path #: backend standard output while generating the editable wheel metadata out: str #: backend standard output while generating the editable wheel metadata err: str class SdistResult(NamedTuple): """Information collected while building a source distribution.""" #: path to the built source distribution sdist: Path #: backend standard output while building the source distribution out: str #: backend standard output while building the source distribution err: str class WheelResult(NamedTuple): """Information collected while building a wheel.""" #: path to the built wheel artifact wheel: Path #: backend standard output while building the wheel out: str #: backend standard error while building the wheel err: str class EditableResult(NamedTuple): """Information collected while building an editable wheel.""" #: path to the built wheel artifact wheel: Path #: backend standard output while building the wheel out: str #: backend standard error while building the wheel err: str class BackendFailed(RuntimeError): # noqa: N818 """An error of the build backend.""" def __init__(self, result: dict[str, Any], out: str, err: str) -> None: super().__init__() #: standard output collected while running the command self.out = out #: standard error collected while running the command self.err = err #: exit code of the command self.code: int = result.get("code", -2) #: the type of exception thrown self.exc_type: str = result.get("exc_type", "missing Exception type") #: the string representation of the exception thrown self.exc_msg: str = result.get("exc_msg", "missing Exception message") def __str__(self) -> str: return ( f"packaging backend failed{'' if self.code is None else f' (code={self.code})'}, " f"with {self.exc_type}: {self.exc_msg}\n{self.err}{self.out}" ).rstrip() def __repr__(self) -> str: return ( f"{self.__class__.__name__}(" f"result=dict(code={self.code}, exc_type={self.exc_type!r},exc_msg={self.exc_msg!r})," f" out={self.out!r}, err={self.err!r})" ) class Frontend(ABC): """Abstract base class for a pyproject frontend.""" #: backend key when the ``pyproject.toml`` does not specify it LEGACY_BUILD_BACKEND: str = "setuptools.build_meta:__legacy__" #: backend requirements when the ``pyproject.toml`` does not specify it LEGACY_REQUIRES: tuple[Requirement, ...] = (Requirement("setuptools >= 40.8.0"), Requirement("wheel")) def __init__( # noqa: PLR0913 self, root: Path, backend_paths: tuple[Path, ...], backend_module: str, backend_obj: str | None, requires: tuple[Requirement, ...], reuse_backend: bool = True, # noqa: FBT001, FBT002 ) -> None: """ Create a new frontend. :param root: the root path of the project :param backend_paths: paths to provision as available to import from for the build backend :param backend_module: the module where the backend lives :param backend_obj: the backend object key (will be lookup up within the backend module) :param requires: build requirements for the backend :param reuse_backend: a flag indicating if the communication channel should be kept alive between messages """ self._root = root self._backend_paths = backend_paths self._backend_module = backend_module self._backend_obj = backend_obj self.requires: tuple[Requirement, ...] = requires self._reuse_backend = reuse_backend self._optional_hooks: OptionalHooks | None = None @classmethod def create_args_from_folder( cls, folder: Path, ) -> tuple[Path, tuple[Path, ...], str, str | None, tuple[Requirement, ...], bool]: """ Frontend creation arguments from a python project folder (thould have a ``pypyproject.toml`` file per PEP-518). :param folder: the python project folder :return: the frontend creation args E.g., to create a frontend from a python project folder: .. code:: python frontend = Frontend(*Frontend.create_args_from_folder(project_folder)) """ py_project_toml = folder / "pyproject.toml" if py_project_toml.exists(): with py_project_toml.open("rb") as file_handler: py_project = tomllib.load(file_handler) build_system = py_project.get("build-system", {}) if "backend-path" in build_system: backend_paths: tuple[Path, ...] = tuple(folder / p for p in build_system["backend-path"]) else: backend_paths = () if "requires" in build_system: requires: tuple[Requirement, ...] = tuple(Requirement(r) for r in build_system.get("requires")) else: requires = cls.LEGACY_REQUIRES build_backend = build_system.get("build-backend", cls.LEGACY_BUILD_BACKEND) else: backend_paths = () requires = cls.LEGACY_REQUIRES build_backend = cls.LEGACY_BUILD_BACKEND paths = build_backend.split(":") backend_module: str = paths[0] backend_obj: str | None = paths[1] if len(paths) > 1 else None return folder, backend_paths, backend_module, backend_obj, requires, True @property def backend(self) -> str: """:return: backend key""" return f"{self._backend_module}{f':{self._backend_obj}' if self._backend_obj else ''}" @property def backend_args(self) -> list[str]: """:return: startup arguments for a backend""" result: list[str] = [str(_HERE / "_backend.py"), str(self._reuse_backend), self._backend_module] if self._backend_obj: result.append(self._backend_obj) return result @property def optional_hooks(self) -> OptionalHooks: """:return: a dictionary indicating if the optional hook is supported or not""" if self._optional_hooks is None: result, _, __ = self._send("_optional_hooks") self._optional_hooks = result return self._optional_hooks def get_requires_for_build_sdist(self, config_settings: ConfigSettings | None = None) -> RequiresBuildSdistResult: """ Get build requirements for a source distribution (per PEP-517). :param config_settings: run arguments :return: outcome """ if self.optional_hooks["get_requires_for_build_sdist"]: result, out, err = self._send(cmd="get_requires_for_build_sdist", config_settings=config_settings) else: result, out, err = [], "", "" if not isinstance(result, list) or not all(isinstance(i, str) for i in result): self._unexpected_response("get_requires_for_build_sdist", result, "list of string", out, err) return RequiresBuildSdistResult(tuple(Requirement(r) for r in cast(List[str], result)), out, err) def get_requires_for_build_wheel(self, config_settings: ConfigSettings | None = None) -> RequiresBuildWheelResult: """ Get build requirements for a wheel (per PEP-517). :param config_settings: run arguments :return: outcome """ if self.optional_hooks["get_requires_for_build_wheel"]: result, out, err = self._send(cmd="get_requires_for_build_wheel", config_settings=config_settings) else: result, out, err = [], "", "" if not isinstance(result, list) or not all(isinstance(i, str) for i in result): self._unexpected_response("get_requires_for_build_wheel", result, "list of string", out, err) return RequiresBuildWheelResult(tuple(Requirement(r) for r in cast(List[str], result)), out, err) def get_requires_for_build_editable( self, config_settings: ConfigSettings | None = None, ) -> RequiresBuildEditableResult: """ Get build requirements for an editable wheel build (per PEP-660). :param config_settings: run arguments :return: outcome """ if self.optional_hooks["get_requires_for_build_editable"]: result, out, err = self._send(cmd="get_requires_for_build_editable", config_settings=config_settings) else: result, out, err = [], "", "" if not isinstance(result, list) or not all(isinstance(i, str) for i in result): self._unexpected_response("get_requires_for_build_editable", result, "list of string", out, err) return RequiresBuildEditableResult(tuple(Requirement(r) for r in cast(List[str], result)), out, err) def prepare_metadata_for_build_wheel( self, metadata_directory: Path, config_settings: ConfigSettings | None = None, ) -> MetadataForBuildWheelResult | None: """ Build wheel metadata (per PEP-517). :param metadata_directory: where to generate the metadata :param config_settings: build arguments :return: metadata generation result """ self._check_metadata_dir(metadata_directory) basename: str | None = None if self.optional_hooks["prepare_metadata_for_build_wheel"]: basename, out, err = self._send( cmd="prepare_metadata_for_build_wheel", metadata_directory=metadata_directory, config_settings=config_settings, ) if basename is None: return None if not isinstance(basename, str): self._unexpected_response("prepare_metadata_for_build_wheel", basename, str, out, err) return MetadataForBuildWheelResult(metadata_directory / basename, out, err) def _check_metadata_dir(self, metadata_directory: Path) -> None: if metadata_directory == self._root: msg = f"the project root and the metadata directory can't be the same {self._root}" raise RuntimeError(msg) if metadata_directory.exists(): # start with fresh ensure_empty_dir(metadata_directory) metadata_directory.mkdir(parents=True, exist_ok=True) def prepare_metadata_for_build_editable( self, metadata_directory: Path, config_settings: ConfigSettings | None = None, ) -> MetadataForBuildEditableResult | None: """ Build editable wheel metadata (per PEP-660). :param metadata_directory: where to generate the metadata :param config_settings: build arguments :return: metadata generation result """ self._check_metadata_dir(metadata_directory) basename: str | None = None if self.optional_hooks["prepare_metadata_for_build_editable"]: basename, out, err = self._send( cmd="prepare_metadata_for_build_editable", metadata_directory=metadata_directory, config_settings=config_settings, ) if basename is None: return None if not isinstance(basename, str): self._unexpected_response("prepare_metadata_for_build_wheel", basename, str, out, err) result = metadata_directory / basename return MetadataForBuildEditableResult(result, out, err) def build_sdist(self, sdist_directory: Path, config_settings: ConfigSettings | None = None) -> SdistResult: """ Build a source distribution (per PEP-517). :param sdist_directory: the folder where to build the source distribution :param config_settings: build arguments :return: source distribution build result """ sdist_directory.mkdir(parents=True, exist_ok=True) basename, out, err = self._send( cmd="build_sdist", sdist_directory=sdist_directory, config_settings=config_settings, ) if not isinstance(basename, str): self._unexpected_response("build_sdist", basename, str, out, err) return SdistResult(sdist_directory / basename, out, err) def build_wheel( self, wheel_directory: Path, config_settings: ConfigSettings | None = None, metadata_directory: Path | None = None, ) -> WheelResult: """ Build a wheel file (per PEP-517). :param wheel_directory: the folder where to build the wheel :param config_settings: build arguments :param metadata_directory: wheel metadata folder :return: wheel build result """ wheel_directory.mkdir(parents=True, exist_ok=True) basename, out, err = self._send( cmd="build_wheel", wheel_directory=wheel_directory, config_settings=config_settings, metadata_directory=metadata_directory, ) if not isinstance(basename, str): self._unexpected_response("build_wheel", basename, str, out, err) return WheelResult(wheel_directory / basename, out, err) def build_editable( self, wheel_directory: Path, config_settings: ConfigSettings | None = None, metadata_directory: Path | None = None, ) -> EditableResult: """ Build an editable wheel file (per PEP-660). :param wheel_directory: the folder where to build the editable wheel :param config_settings: build arguments :param metadata_directory: wheel metadata folder :return: wheel build result """ wheel_directory.mkdir(parents=True, exist_ok=True) basename, out, err = self._send( cmd="build_editable", wheel_directory=wheel_directory, config_settings=config_settings, metadata_directory=metadata_directory, ) if not isinstance(basename, str): self._unexpected_response("build_editable", basename, str, out, err) return EditableResult(wheel_directory / basename, out, err) def _unexpected_response( # noqa: PLR0913 self, cmd: str, got: Any, expected_type: Any, out: str, err: str, ) -> NoReturn: msg = f"{cmd!r} on {self.backend!r} returned {got!r} but expected type {expected_type!r}" raise BackendFailed({"code": None, "exc_type": TypeError.__name__, "exc_msg": msg}, out, err) def metadata_from_built( self, metadata_directory: Path, target: Literal["wheel", "editable"], config_settings: ConfigSettings | None = None, ) -> tuple[Path, str, str]: """ Create metadata from building the wheel (use when the prepare endpoints are not present or don't work). :param metadata_directory: directory where to put the metadata :param target: the type of wheel metadata to build :param config_settings: config settings to pass in to the build endpoint :return: """ hook = getattr(self, f"build_{target}") with self._wheel_directory() as wheel_directory: result: EditableResult | WheelResult = hook(wheel_directory, config_settings) wheel = result.wheel if not wheel.exists(): msg = f"missing wheel file return by backed {wheel!r}" raise RuntimeError(msg) out, err = result.out, result.err extract_to = str(metadata_directory) basename = None with ZipFile(str(wheel), "r") as zip_file: for name in zip_file.namelist(): # pragma: no branch root = Path(name).parts[0] if root.endswith(".dist-info"): basename = root zip_file.extract(name, extract_to) if basename is None: # pragma: no branch msg = f"no .dist-info found inside generated wheel {wheel}" raise RuntimeError(msg) return metadata_directory / basename, out, err @contextmanager def _wheel_directory(self) -> Iterator[Path]: with TemporaryDirectory() as wheel_directory: yield Path(wheel_directory) def _send(self, cmd: str, **kwargs: Any) -> tuple[Any, str, str]: with NamedTemporaryFile(prefix=f"pep517_{cmd}-") as result_file_marker: result_file = Path(result_file_marker.name).with_suffix(".json") msg = json.dumps( { "cmd": cmd, "kwargs": {k: (str(v) if isinstance(v, Path) else v) for k, v in kwargs.items()}, "result": str(result_file), }, ) with self._send_msg(cmd, result_file, msg) as status: while not status.done: # pragma: no branch sleep(0.001) # wait a bit for things to happen if result_file.exists(): try: with result_file.open("rt") as result_handler: result = json.load(result_handler) finally: result_file.unlink() else: result = { "code": 1, "exc_type": "RuntimeError", "exc_msg": f"Backend response file {result_file} is missing", } out, err = status.out_err() if "return" in result: return result["return"], out, err raise BackendFailed(result, out, err) @abstractmethod @contextmanager def _send_msg(self, cmd: str, result_file: Path, msg: str) -> Iterator[CmdStatus]: raise NotImplementedError pyproject_api-1.6.1/src/pyproject_api/_util.py0000644000000000000000000000110613615410400016432 0ustar00from __future__ import annotations from shutil import rmtree from typing import TYPE_CHECKING if TYPE_CHECKING: from pathlib import Path def ensure_empty_dir(path: Path) -> None: if path.exists(): if path.is_dir(): for sub_path in path.iterdir(): if sub_path.is_dir(): rmtree(sub_path, ignore_errors=True) else: sub_path.unlink() else: path.unlink() path.mkdir() else: path.mkdir(parents=True) __all__ = [ "ensure_empty_dir", ] pyproject_api-1.6.1/src/pyproject_api/_version.py0000644000000000000000000000024013615410400017140 0ustar00# file generated by setuptools_scm # don't change, don't track in version control __version__ = version = '1.6.1' __version_tuple__ = version_tuple = (1, 6, 1) pyproject_api-1.6.1/src/pyproject_api/_via_fresh_subprocess.py0000644000000000000000000000547713615410400021712 0ustar00from __future__ import annotations import os import sys from contextlib import contextmanager from subprocess import PIPE, Popen from threading import Thread from typing import IO, TYPE_CHECKING, Any, Iterator, Tuple, cast from ._frontend import CmdStatus, Frontend if TYPE_CHECKING: from pathlib import Path from packaging.requirements import Requirement class SubprocessCmdStatus(CmdStatus, Thread): def __init__(self, process: Popen[str]) -> None: super().__init__() self.process = process self._out_err: tuple[str, str] | None = None self.start() def run(self) -> None: self._out_err = self.process.communicate() @property def done(self) -> bool: return self.process.returncode is not None def out_err(self) -> tuple[str, str]: return cast(Tuple[str, str], self._out_err) class SubprocessFrontend(Frontend): """A frontend that creates fresh subprocess at every call to communicate with the backend.""" def __init__( # noqa: PLR0913 self, root: Path, backend_paths: tuple[Path, ...], backend_module: str, backend_obj: str | None, requires: tuple[Requirement, ...], ) -> None: """ Create a subprocess frontend. :param root: the root path to the built project :param backend_paths: paths that are available on the python path for the backend :param backend_module: module where the backend is located :param backend_obj: object within the backend module identifying the backend :param requires: seed requirements for the backend """ super().__init__(root, backend_paths, backend_module, backend_obj, requires, reuse_backend=False) self.executable = sys.executable @contextmanager def _send_msg(self, cmd: str, result_file: Path, msg: str) -> Iterator[SubprocessCmdStatus]: # noqa: ARG002 env = os.environ.copy() backend = os.pathsep.join(str(i) for i in self._backend_paths).strip() if backend: env["PYTHONPATH"] = backend process = Popen( args=[self.executable, *self.backend_args], stdout=PIPE, stderr=PIPE, stdin=PIPE, universal_newlines=True, cwd=self._root, env=env, ) cast(IO[str], process.stdin).write(f"{os.linesep}{msg}{os.linesep}") yield SubprocessCmdStatus(process) def send_cmd(self, cmd: str, **kwargs: Any) -> tuple[Any, str, str]: """ Send a command to the backend. :param cmd: the command to send :param kwargs: keyword arguments to the backend :return: a tuple of: backend response, standard output text, standard error text """ return self._send(cmd, **kwargs) __all__ = ("SubprocessFrontend",) pyproject_api-1.6.1/src/pyproject_api/py.typed0000644000000000000000000000000013615410400016433 0ustar00pyproject_api-1.6.1/tests/_build_sdist.py0000644000000000000000000000000013615410400015455 0ustar00pyproject_api-1.6.1/tests/test_backend.py0000644000000000000000000001423113615410400015452 0ustar00from __future__ import annotations import json import os from typing import TYPE_CHECKING, Any import pytest from pyproject_api._backend import BackendProxy, read_line, run if TYPE_CHECKING: from pathlib import Path import pytest_mock def test_invalid_module(capsys: pytest.CaptureFixture[str]) -> None: with pytest.raises(ImportError): run([str(False), "an.invalid.module"]) captured = capsys.readouterr() assert "failed to start backend" in captured.err def test_invalid_request(mocker: pytest_mock.MockerFixture, capsys: pytest.CaptureFixture[str]) -> None: """Validate behavior when an invalid request is issued.""" command = "invalid json" backend_proxy = mocker.MagicMock(spec=BackendProxy) backend_proxy.return_value = "dummy_result" backend_proxy.__str__.return_value = "FakeBackendProxy" mocker.patch("pyproject_api._backend.BackendProxy", return_value=backend_proxy) mocker.patch("pyproject_api._backend.read_line", return_value=bytearray(command, "utf-8")) ret = run([str(False), "a.dummy.module"]) assert ret == 0 captured = capsys.readouterr() assert "started backend " in captured.out assert "Backend: incorrect request to backend: " in captured.err def test_exception(mocker: pytest_mock.MockerFixture, capsys: pytest.CaptureFixture[str], tmp_path: Path) -> None: """Ensure an exception in the backend is not bubbled up.""" result = str(tmp_path / "result") command = json.dumps({"cmd": "dummy_command", "kwargs": {"foo": "bar"}, "result": result}) backend_proxy = mocker.MagicMock(spec=BackendProxy) backend_proxy.side_effect = SystemExit(1) backend_proxy.__str__.return_value = "FakeBackendProxy" mocker.patch("pyproject_api._backend.BackendProxy", return_value=backend_proxy) mocker.patch("pyproject_api._backend.read_line", return_value=bytearray(command, "utf-8")) ret = run([str(False), "a.dummy.module"]) # We still return 0 and write a result file. The exception should *not* bubble up assert ret == 0 captured = capsys.readouterr() assert "started backend FakeBackendProxy" in captured.out assert "Backend: run command dummy_command with args {'foo': 'bar'}" in captured.out assert "Backend: Wrote response " in captured.out assert "SystemExit: 1" in captured.err def test_valid_request(mocker: pytest_mock.MockerFixture, capsys: pytest.CaptureFixture[str], tmp_path: Path) -> None: """Validate the "success" path.""" result = str(tmp_path / "result") command = json.dumps({"cmd": "dummy_command", "kwargs": {"foo": "bar"}, "result": result}) backend_proxy = mocker.MagicMock(spec=BackendProxy) backend_proxy.return_value = "dummy-result" backend_proxy.__str__.return_value = "FakeBackendProxy" mocker.patch("pyproject_api._backend.BackendProxy", return_value=backend_proxy) mocker.patch("pyproject_api._backend.read_line", return_value=bytearray(command, "utf-8")) ret = run([str(False), "a.dummy.module"]) assert ret == 0 captured = capsys.readouterr() assert "started backend FakeBackendProxy" in captured.out assert "Backend: run command dummy_command with args {'foo': 'bar'}" in captured.out assert "Backend: Wrote response " in captured.out assert not captured.err def test_reuse_process(mocker: pytest_mock.MockerFixture, capsys: pytest.CaptureFixture[str], tmp_path: Path) -> None: """Validate behavior when reusing the backend proxy process. There are a couple of things we'd like to check here: - Ensure we can actually reuse the process. - Ensure an exception in a call to the backend does not affect subsequent calls. - Ensure we can exit safely by calling the '_exit' command. """ results = [ str(tmp_path / "result_a"), str(tmp_path / "result_b"), str(tmp_path / "result_c"), str(tmp_path / "result_d"), ] commands = [ json.dumps({"cmd": "dummy_command_a", "kwargs": {"foo": "bar"}, "result": results[0]}), json.dumps({"cmd": "dummy_command_b", "kwargs": {"baz": "qux"}, "result": results[1]}), json.dumps({"cmd": "dummy_command_c", "kwargs": {"win": "wow"}, "result": results[2]}), json.dumps({"cmd": "_exit", "kwargs": {}, "result": results[3]}), ] def fake_backend(name: str, *args: Any, **kwargs: Any) -> Any: # noqa: ARG001 if name == "dummy_command_b": raise SystemExit(2) return "dummy-result" backend_proxy = mocker.MagicMock(spec=BackendProxy) backend_proxy.side_effect = fake_backend backend_proxy.__str__.return_value = "FakeBackendProxy" mocker.patch("pyproject_api._backend.BackendProxy", return_value=backend_proxy) mocker.patch("pyproject_api._backend.read_line", side_effect=[bytearray(x, "utf-8") for x in commands]) ret = run([str(True), "a.dummy.module"]) # We still return 0 and write a result file. The exception should *not* bubble up and all commands should execute. # It is the responsibility of the caller to handle errors. assert ret == 0 captured = capsys.readouterr() assert "started backend FakeBackendProxy" in captured.out assert "Backend: run command dummy_command_a with args {'foo': 'bar'}" in captured.out assert "Backend: run command dummy_command_b with args {'baz': 'qux'}" in captured.out assert "Backend: run command dummy_command_c with args {'win': 'wow'}" in captured.out assert "SystemExit: 2" in captured.err def test_read_line_success() -> None: r, w = os.pipe() try: line_in = b"this is a line\r\n" os.write(w, line_in) line_out = read_line(fd=r) assert line_out == bytearray(b"this is a line") finally: os.close(r) os.close(w) def test_read_line_eof_before_newline() -> None: r, w = os.pipe() try: line_in = b"this is a line" os.write(w, line_in) os.close(w) line_out = read_line(fd=r) assert line_out == bytearray(b"this is a line") finally: os.close(r) def test_read_line_eof_at_the_beginning() -> None: r, w = os.pipe() try: os.close(w) with pytest.raises(EOFError): read_line(fd=r) finally: os.close(r) pyproject_api-1.6.1/tests/test_frontend.py0000644000000000000000000003523213615410400015706 0ustar00from __future__ import annotations from pathlib import Path from textwrap import dedent from typing import Callable, Literal import pytest from packaging.requirements import Requirement from pyproject_api._frontend import BackendFailed from pyproject_api._via_fresh_subprocess import SubprocessFrontend @pytest.fixture() def local_builder(tmp_path: Path) -> Callable[[str], Path]: def _f(content: str) -> Path: toml = '[build-system]\nrequires=[]\nbuild-backend = "build_tester"\nbackend-path=["."]' (tmp_path / "pyproject.toml").write_text(toml) (tmp_path / "build_tester.py").write_text(dedent(content)) return tmp_path return _f def test_missing_backend(local_builder: Callable[[str], Path]) -> None: tmp_path = local_builder("") toml = tmp_path / "pyproject.toml" toml.write_text('[build-system]\nrequires=[]\nbuild-backend = "build_tester"') frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(tmp_path)[:-1]) with pytest.raises(BackendFailed) as context: frontend.build_wheel(tmp_path / "wheel") exc = context.value assert exc.exc_type == "RuntimeError" assert exc.code == 1 assert "failed to start backend" in exc.err assert "ModuleNotFoundError: No module named " in exc.err @pytest.mark.parametrize("cmd", ["build_wheel", "build_sdist"]) def test_missing_required_cmd(cmd: str, local_builder: Callable[[str], Path]) -> None: tmp_path = local_builder("") frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(tmp_path)[:-1]) with pytest.raises(BackendFailed) as context: getattr(frontend, cmd)(tmp_path) exc = context.value assert f"has no attribute '{cmd}'" in exc.exc_msg assert exc.exc_type == "MissingCommand" def test_empty_pyproject(tmp_path: Path) -> None: (tmp_path / "pyproject.toml").write_text("[build-system]") root, backend_paths, backend_module, backend_obj, requires, _ = SubprocessFrontend.create_args_from_folder(tmp_path) assert root == tmp_path assert backend_paths == () assert backend_module == "setuptools.build_meta" assert backend_obj == "__legacy__" for left, right in zip(requires, (Requirement("setuptools>=40.8.0"), Requirement("wheel"))): assert isinstance(left, Requirement) assert str(left) == str(right) @pytest.fixture(scope="session") def demo_pkg_inline() -> Path: return Path(__file__).absolute().parent / "demo_pkg_inline" def test_backend_no_prepare_wheel(tmp_path: Path, demo_pkg_inline: Path) -> None: frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1]) result = frontend.prepare_metadata_for_build_wheel(tmp_path) assert result is None def test_backend_build_sdist_demo_pkg_inline(tmp_path: Path, demo_pkg_inline: Path) -> None: frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1]) result = frontend.build_sdist(sdist_directory=tmp_path) assert result.sdist == tmp_path / "demo_pkg_inline-1.0.0.tar.gz" def test_backend_obj(tmp_path: Path) -> None: toml = """ [build-system] requires=[] build-backend = "build.api:backend:" backend-path=["."] """ api = """ class A: def get_requires_for_build_sdist(self, config_settings=None): return ["a"] backend = A() """ (tmp_path / "pyproject.toml").write_text(dedent(toml)) build = tmp_path / "build" build.mkdir() (build / "__init__.py").write_text("") (build / "api.py").write_text(dedent(api)) frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(tmp_path)[:-1]) result = frontend.get_requires_for_build_sdist() for left, right in zip(result.requires, (Requirement("a"),)): assert isinstance(left, Requirement) assert str(left) == str(right) @pytest.mark.parametrize("of_type", ["wheel", "sdist"]) def test_get_requires_for_build_missing(of_type: str, local_builder: Callable[[str], Path]) -> None: tmp_path = local_builder("") frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(tmp_path)[:-1]) result = getattr(frontend, f"get_requires_for_build_{of_type}")() assert result.requires == () @pytest.mark.parametrize("of_type", ["sdist", "wheel"]) def test_bad_return_type_get_requires_for_build(of_type: str, local_builder: Callable[[str], Path]) -> None: tmp_path = local_builder(f"def get_requires_for_build_{of_type}(config_settings=None): return 1") frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(tmp_path)[:-1]) with pytest.raises(BackendFailed) as context: getattr(frontend, f"get_requires_for_build_{of_type}")() exc = context.value msg = f"'get_requires_for_build_{of_type}' on 'build_tester' returned 1 but expected type 'list of string'" assert exc.exc_msg == msg assert exc.exc_type == "TypeError" def test_bad_return_type_build_sdist(local_builder: Callable[[str], Path]) -> None: tmp_path = local_builder("def build_sdist(sdist_directory, config_settings=None): return 1") frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(tmp_path)[:-1]) with pytest.raises(BackendFailed) as context: frontend.build_sdist(tmp_path) exc = context.value assert exc.exc_msg == f"'build_sdist' on 'build_tester' returned 1 but expected type {str!r}" assert exc.exc_type == "TypeError" def test_bad_return_type_build_wheel(local_builder: Callable[[str], Path]) -> None: txt = "def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): return 1" tmp_path = local_builder(txt) frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(tmp_path)[:-1]) with pytest.raises(BackendFailed) as context: frontend.build_wheel(tmp_path) exc = context.value assert exc.exc_msg == f"'build_wheel' on 'build_tester' returned 1 but expected type {str!r}" assert exc.exc_type == "TypeError" def test_bad_return_type_prepare_metadata_for_build_wheel(local_builder: Callable[[str], Path]) -> None: tmp_path = local_builder("def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None): return 1") frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(tmp_path)[:-1]) with pytest.raises(BackendFailed) as context: frontend.prepare_metadata_for_build_wheel(tmp_path / "meta") exc = context.value assert exc.exc_type == "TypeError" assert exc.exc_msg == f"'prepare_metadata_for_build_wheel' on 'build_tester' returned 1 but expected type {str!r}" def test_prepare_metadata_for_build_wheel_meta_is_root(local_builder: Callable[[str], Path]) -> None: tmp_path = local_builder("def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None): return 1") frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(tmp_path)[:-1]) with pytest.raises(RuntimeError) as context: frontend.prepare_metadata_for_build_wheel(tmp_path) assert str(context.value) == f"the project root and the metadata directory can't be the same {tmp_path}" def test_no_wheel_prepare_metadata_for_build_wheel(local_builder: Callable[[str], Path]) -> None: txt = "def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): return 'out'" tmp_path = local_builder(txt) frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(tmp_path)[:-1]) with pytest.raises(RuntimeError, match="missing wheel file return by backed *"): frontend.metadata_from_built(tmp_path, "wheel") @pytest.mark.parametrize("target", ["wheel", "editable"]) def test_metadata_from_built_wheel( demo_pkg_inline: Path, tmp_path: Path, target: Literal["wheel", "editable"], monkeypatch: pytest.MonkeyPatch, ) -> None: frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1]) monkeypatch.chdir(tmp_path) path, out, err = frontend.metadata_from_built(tmp_path, target) assert path == tmp_path / "demo_pkg_inline-1.0.0.dist-info" assert {p.name for p in path.iterdir()} == {"top_level.txt", "WHEEL", "RECORD", "METADATA"} assert f" build_{target}" in out assert not err def test_bad_wheel_metadata_from_built_wheel(local_builder: Callable[[str], Path]) -> None: txt = """ import sys from pathlib import Path from zipfile import ZipFile def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): path = Path(wheel_directory) / "out" with ZipFile(str(path), "w") as zip_file_handler: pass print(f"created wheel {path}") return path.name """ tmp_path = local_builder(txt) frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(tmp_path)[:-1]) with pytest.raises(RuntimeError, match="no .dist-info found inside generated wheel*"): frontend.metadata_from_built(tmp_path, "wheel") def test_create_no_pyproject(tmp_path: Path) -> None: result = SubprocessFrontend.create_args_from_folder(tmp_path) assert len(result) == 6 assert result[0] == tmp_path assert result[1] == () assert result[2] == "setuptools.build_meta" assert result[3] == "__legacy__" assert all(isinstance(i, Requirement) for i in result[4]) assert [str(i) for i in result[4]] == ["setuptools>=40.8.0", "wheel"] assert result[5] is True def test_backend_get_requires_for_build_editable(demo_pkg_inline: Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("HAS_REQUIRES_EDITABLE", "1") monkeypatch.delenv("REQUIRES_EDITABLE_BAD_RETURN", raising=False) frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1]) result = frontend.get_requires_for_build_editable() assert [str(i) for i in result.requires] == ["editables"] assert isinstance(result.requires[0], Requirement) assert " get_requires_for_build_editable " in result.out assert not result.err def test_backend_get_requires_for_build_editable_miss(demo_pkg_inline: Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("HAS_REQUIRES_EDITABLE", raising=False) frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1]) result = frontend.get_requires_for_build_editable() assert not result.requires assert not result.out assert not result.err def test_backend_get_requires_for_build_editable_bad(demo_pkg_inline: Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("HAS_REQUIRES_EDITABLE", "1") monkeypatch.setenv("REQUIRES_EDITABLE_BAD_RETURN", "1") frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1]) with pytest.raises(BackendFailed) as context: frontend.get_requires_for_build_editable() exc = context.value assert exc.code is None assert not exc.err assert " get_requires_for_build_editable " in exc.out assert not exc.args assert exc.exc_type == "TypeError" assert exc.exc_msg == "'get_requires_for_build_editable' on 'build' returned [1] but expected type 'list of string'" def test_backend_prepare_editable(tmp_path: Path, demo_pkg_inline: Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("HAS_PREPARE_EDITABLE", "1") monkeypatch.delenv("PREPARE_EDITABLE_BAD", raising=False) frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1]) result = frontend.prepare_metadata_for_build_editable(tmp_path) assert result is not None assert result.metadata.name == "demo_pkg_inline-1.0.0.dist-info" assert " prepare_metadata_for_build_editable " in result.out assert " build_editable " not in result.out assert not result.err def test_backend_prepare_editable_miss(tmp_path: Path, demo_pkg_inline: Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("HAS_PREPARE_EDITABLE", raising=False) monkeypatch.delenv("BUILD_EDITABLE_BAD", raising=False) frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1]) result = frontend.prepare_metadata_for_build_editable(tmp_path) assert result is None def test_backend_prepare_editable_bad(tmp_path: Path, demo_pkg_inline: Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("HAS_PREPARE_EDITABLE", "1") monkeypatch.setenv("PREPARE_EDITABLE_BAD", "1") frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1]) with pytest.raises(BackendFailed) as context: frontend.prepare_metadata_for_build_editable(tmp_path) exc = context.value assert exc.code is None assert not exc.err assert " prepare_metadata_for_build_editable " in exc.out assert not exc.args assert exc.exc_type == "TypeError" assert exc.exc_msg == "'prepare_metadata_for_build_wheel' on 'build' returned 1 but expected type " def test_backend_build_editable(tmp_path: Path, demo_pkg_inline: Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("BUILD_EDITABLE_BAD", raising=False) monkeypatch.setenv("HAS_PREPARE_EDITABLE", "1") frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1]) meta = tmp_path / "meta" res = frontend.prepare_metadata_for_build_editable(meta) assert res is not None metadata = res.metadata assert metadata is not None assert metadata.name == "demo_pkg_inline-1.0.0.dist-info" result = frontend.build_editable(tmp_path, metadata_directory=meta) assert result.wheel.name == "demo_pkg_inline-1.0.0-py3-none-any.whl" assert " build_editable " in result.out assert not result.err def test_backend_build_wheel(tmp_path: Path, demo_pkg_inline: Path) -> None: frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1]) result = frontend.build_wheel(tmp_path) assert result.wheel.name == "demo_pkg_inline-1.0.0-py3-none-any.whl" assert " build_wheel " in result.out assert not result.err def test_backend_build_editable_bad(tmp_path: Path, demo_pkg_inline: Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("BUILD_EDITABLE_BAD", "1") frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1]) with pytest.raises(BackendFailed) as context: frontend.build_editable(tmp_path) exc = context.value assert exc.code is None assert not exc.err assert " build_editable " in exc.out assert not exc.args assert exc.exc_type == "TypeError" assert exc.exc_msg == "'build_editable' on 'build' returned 1 but expected type " pyproject_api-1.6.1/tests/test_frontend_setuptools.py0000644000000000000000000001507013615410400020205 0ustar00from __future__ import annotations import sys from contextlib import contextmanager from stat import S_IWGRP, S_IWOTH, S_IWUSR from typing import TYPE_CHECKING, Iterator, NamedTuple import pytest from packaging.requirements import Requirement from pyproject_api._frontend import BackendFailed from pyproject_api._via_fresh_subprocess import SubprocessFrontend if TYPE_CHECKING: from pathlib import Path from _pytest.tmpdir import TempPathFactory from pytest_mock import MockerFixture from importlib.metadata import Distribution, EntryPoint @pytest.fixture(scope="session") def frontend_setuptools(tmp_path_factory: TempPathFactory) -> SubprocessFrontend: prj = tmp_path_factory.mktemp("proj") (prj / "pyproject.toml").write_text( '[build-system]\nrequires=["setuptools","wheel"]\nbuild-backend = "setuptools.build_meta"', ) cfg = """ [metadata] name = demo version = 1.0 [options] packages = demo install_requires = requests>2 magic>3 [options.entry_points] console_scripts = demo_exe = demo:a """ (prj / "setup.cfg").write_text(cfg) (prj / "setup.py").write_text("from setuptools import setup; setup()") demo = prj / "demo" demo.mkdir() (demo / "__init__.py").write_text("def a(): print('ok')") args = SubprocessFrontend.create_args_from_folder(prj) return SubprocessFrontend(*args[:-1]) def test_setuptools_get_requires_for_build_sdist(frontend_setuptools: SubprocessFrontend) -> None: result = frontend_setuptools.get_requires_for_build_sdist() assert result.requires == () assert isinstance(result.out, str) assert isinstance(result.err, str) def test_setuptools_get_requires_for_build_wheel(frontend_setuptools: SubprocessFrontend) -> None: result = frontend_setuptools.get_requires_for_build_wheel() for left, right in zip(result.requires, (Requirement("wheel"),)): assert isinstance(left, Requirement) assert str(left) == str(right) assert isinstance(result.out, str) assert isinstance(result.err, str) def test_setuptools_prepare_metadata_for_build_wheel(frontend_setuptools: SubprocessFrontend, tmp_path: Path) -> None: meta = tmp_path / "meta" result = frontend_setuptools.prepare_metadata_for_build_wheel(metadata_directory=meta) assert result is not None dist = Distribution.at(str(result.metadata)) assert list(dist.entry_points) == [EntryPoint(name="demo_exe", value="demo:a", group="console_scripts")] assert dist.version == "1.0" assert dist.metadata["Name"] == "demo" values = [v for k, v in dist.metadata.items() if k == "Requires-Dist"] # type: ignore[attr-defined] # ignore because "PackageMetadata" has no attribute "items" assert sorted(values) == ["magic >3", "requests >2"] assert isinstance(result.out, str) assert isinstance(result.err, str) # call it again regenerates it because frontend always deletes earlier content before = result.metadata.stat().st_mtime result = frontend_setuptools.prepare_metadata_for_build_wheel(metadata_directory=meta) assert result is not None after = result.metadata.stat().st_mtime assert after > before def test_setuptools_build_sdist(frontend_setuptools: SubprocessFrontend, tmp_path: Path) -> None: result = frontend_setuptools.build_sdist(tmp_path) sdist = result.sdist assert sdist.exists() assert sdist.is_file() assert sdist.name == "demo-1.0.tar.gz" assert isinstance(result.out, str) assert isinstance(result.err, str) def test_setuptools_build_wheel(frontend_setuptools: SubprocessFrontend, tmp_path: Path) -> None: result = frontend_setuptools.build_wheel(tmp_path) wheel = result.wheel assert wheel.exists() assert wheel.is_file() assert wheel.name == "demo-1.0-py3-none-any.whl" assert isinstance(result.out, str) assert isinstance(result.err, str) def test_setuptools_exit(frontend_setuptools: SubprocessFrontend) -> None: result, out, err = frontend_setuptools.send_cmd("_exit") assert isinstance(out, str) assert isinstance(err, str) assert result == 0 def test_setuptools_missing_command(frontend_setuptools: SubprocessFrontend) -> None: with pytest.raises(BackendFailed): frontend_setuptools.send_cmd("missing_command") def test_setuptools_exception(frontend_setuptools: SubprocessFrontend) -> None: with pytest.raises(BackendFailed) as context: frontend_setuptools.send_cmd("build_wheel") assert isinstance(context.value.out, str) assert isinstance(context.value.err, str) assert context.value.exc_type == "TypeError" prefix = "_BuildMetaBackend." if sys.version_info >= (3, 10) else "" msg = f"{prefix}build_wheel() missing 1 required positional argument: 'wheel_directory'" assert context.value.exc_msg == msg assert context.value.code == 1 assert context.value.args == () assert repr(context.value) assert str(context.value) assert repr(context.value) != str(context.value) def test_bad_message(frontend_setuptools: SubprocessFrontend, tmp_path: Path) -> None: with frontend_setuptools._send_msg("bad_cmd", tmp_path / "a", "{{") as status: # noqa: SLF001 while not status.done: # pragma: no branch pass out, err = status.out_err() assert out assert "Backend: incorrect request to backend: bytearray(b'{{')" in err class _Result(NamedTuple): name: str def test_result_missing(frontend_setuptools: SubprocessFrontend, tmp_path: Path, mocker: MockerFixture) -> None: @contextmanager def named_temporary_file(prefix: str) -> Iterator[_Result]: write = S_IWUSR | S_IWGRP | S_IWOTH base = tmp_path / prefix result = base.with_suffix(".json") result.write_text("") result.chmod(result.stat().st_mode & ~write) # force json write to fail due to R/O patch = mocker.patch("pyproject_api._frontend.Path.exists", return_value=False) # make it missing try: yield _Result(str(base)) finally: patch.stop() result.chmod(result.stat().st_mode | write) # cleanup result.unlink() mocker.patch("pyproject_api._frontend.NamedTemporaryFile", named_temporary_file) with pytest.raises(BackendFailed) as context: frontend_setuptools.send_cmd("_exit") exc = context.value assert exc.exc_msg == f"Backend response file {tmp_path / 'pep517__exit-.json'} is missing" assert exc.exc_type == "RuntimeError" assert exc.code == 1 assert "Traceback" in exc.err assert "PermissionError" in exc.err pyproject_api-1.6.1/tests/test_main.py0000644000000000000000000000617613615410400015020 0ustar00from __future__ import annotations from pathlib import Path from typing import TYPE_CHECKING import pytest import pyproject_api.__main__ from pyproject_api._frontend import EditableResult, SdistResult, WheelResult if TYPE_CHECKING: import pytest_mock @pytest.mark.parametrize( ("cli_args", "srcdir", "outdir", "hooks"), [ ( [], Path.cwd(), Path.cwd() / "dist", ["build_sdist", "build_wheel"], ), ( ["src"], Path("src"), Path("src") / "dist", ["build_sdist", "build_wheel"], ), ( ["-o", "out"], Path.cwd(), Path("out"), ["build_sdist", "build_wheel"], ), ( ["-s"], Path.cwd(), Path.cwd() / "dist", ["build_sdist"], ), ( ["-w"], Path.cwd(), Path.cwd() / "dist", ["build_wheel"], ), ( ["-e"], Path.cwd(), Path.cwd() / "dist", ["build_editable"], ), ( ["-s", "-w"], Path.cwd(), Path.cwd() / "dist", ["build_sdist", "build_wheel"], ), ], ) def test_parse_args( # noqa: PLR0913 mocker: pytest_mock.MockerFixture, capsys: pytest.CaptureFixture[str], cli_args: list[str], srcdir: Path, outdir: Path, hooks: list[str], ) -> None: subprocess_frontend = mocker.patch("pyproject_api.__main__.SubprocessFrontend", autospec=True) subprocess_frontend.create_args_from_folder.return_value = (srcdir, (), "foo.bar", "baz", (), True) subprocess_frontend.return_value.build_sdist.return_value = SdistResult( sdist=outdir / "foo.whl", out="sdist out", err="sdist err", ) subprocess_frontend.return_value.build_wheel.return_value = WheelResult( wheel=outdir / "foo.whl", out="wheel out", err="wheel err", ) subprocess_frontend.return_value.build_editable.return_value = EditableResult( wheel=outdir / "foo.whl", out="editable wheel out", err="editable wheel err", ) pyproject_api.__main__.main(cli_args) subprocess_frontend.create_args_from_folder.assert_called_once_with(srcdir) captured = capsys.readouterr() if "build_sdist" in hooks: assert "Building sdist..." in captured.out subprocess_frontend.return_value.build_sdist.assert_called_once_with(outdir) assert "sdist out" in captured.out assert "sdist err" in captured.err if "build_wheel" in hooks: assert "Building wheel..." in captured.out subprocess_frontend.return_value.build_wheel.assert_called_once_with(outdir) assert "wheel out" in captured.out assert "wheel err" in captured.err if "build_editable" in hooks: assert "Building editable wheel..." in captured.out subprocess_frontend.return_value.build_editable.assert_called_once_with(outdir) assert "editable wheel out" in captured.out assert "editable wheel err" in captured.err pyproject_api-1.6.1/tests/test_util.py0000644000000000000000000000212513615410400015037 0ustar00from __future__ import annotations from typing import TYPE_CHECKING from pyproject_api._util import ensure_empty_dir if TYPE_CHECKING: from pathlib import Path def test_ensure_empty_dir_on_empty(tmp_path: Path) -> None: ensure_empty_dir(tmp_path) assert list(tmp_path.iterdir()) == [] def test_ensure_empty_dir_on_path_missing(tmp_path: Path) -> None: path = tmp_path / "a" ensure_empty_dir(path) assert list(path.iterdir()) == [] def test_ensure_empty_dir_on_path_file(tmp_path: Path) -> None: path = tmp_path / "a" path.write_text("") ensure_empty_dir(path) assert list(path.iterdir()) == [] def test_ensure_empty_dir_on_path_folder(tmp_path: Path) -> None: """ ├──  a │ ├──  a │ └──  b │ └──  c └──  d """ path = tmp_path / "a" path.mkdir() (path / "a").write_text("") sub_dir = path / "b" sub_dir.mkdir() (sub_dir / "c").write_text("") (tmp_path / "d").write_text("") ensure_empty_dir(tmp_path) assert list(tmp_path.iterdir()) == [] pyproject_api-1.6.1/tests/test_version.py0000644000000000000000000000020313615410400015542 0ustar00from __future__ import annotations def test_version() -> None: from pyproject_api import __version__ assert __version__ pyproject_api-1.6.1/tests/demo_pkg_inline/build.py0000644000000000000000000001037313615410400017251 0ustar00""" Please keep this file Python 2.7 compatible. See https://tox.readthedocs.io/en/rewrite/development.html#code-style-guide """ from __future__ import annotations import os import sys import tarfile from pathlib import Path from textwrap import dedent from zipfile import ZipFile name = "demo_pkg_inline" pkg_name = name.replace("_", "-") version = "1.0.0" dist_info = f"{name}-{version}.dist-info" logic = f"{name}/__init__.py" metadata_file = f"{dist_info}/METADATA" wheel = f"{dist_info}/WHEEL" record = f"{dist_info}/RECORD" content = { logic: f"def do():\n print('greetings from {name}')", } metadata = { metadata_file: f""" Metadata-Version: 2.1 Name: {pkg_name} Version: {version} Summary: UNKNOWN Home-page: UNKNOWN Author: UNKNOWN Author-email: UNKNOWN License: UNKNOWN Platform: UNKNOWN UNKNOWN """, wheel: f""" Wheel-Version: 1.0 Generator: {name}-{version} Root-Is-Purelib: true Tag: py{sys.version_info[0]}-none-any """, f"{dist_info}/top_level.txt": name, record: f""" {name}/__init__.py,, {dist_info}/METADATA,, {dist_info}/WHEEL,, {dist_info}/top_level.txt,, {dist_info}/RECORD,, """, } def build_wheel( wheel_directory: str, metadata_directory: str | None = None, config_settings: dict[str, str] | None = None, # noqa: ARG001 ) -> str: base_name = f"{name}-{version}-py{sys.version_info[0]}-none-any.whl" path = Path(wheel_directory) / base_name with ZipFile(str(path), "w") as zip_file_handler: for arc_name, data in content.items(): zip_file_handler.writestr(arc_name, dedent(data).strip()) if metadata_directory is not None: for sub_directory, _, filenames in os.walk(metadata_directory): for filename in filenames: zip_file_handler.write( str(Path(metadata_directory) / sub_directory / filename), str(Path(sub_directory) / filename), ) else: for arc_name, data in metadata.items(): zip_file_handler.writestr(arc_name, dedent(data).strip()) print(f"created wheel {path}") # noqa: T201 return base_name def get_requires_for_build_wheel(config_settings: dict[str, str] | None = None) -> list[str]: # noqa: ARG001 return [] # pragma: no cover # only executed in non-host pythons def build_sdist(sdist_directory: str, config_settings: dict[str, str] | None = None) -> str: # noqa: ARG001 result = f"{name}-{version}.tar.gz" with tarfile.open(str(Path(sdist_directory) / result), "w:gz") as tar: root = Path(__file__).parent tar.add(str(root / "build.py"), "build.py") tar.add(str(root / "pyproject.toml"), "pyproject.toml") return result def get_requires_for_build_sdist(config_settings: dict[str, str] | None = None) -> list[str]: # noqa: ARG001 return [] # pragma: no cover # only executed in non-host pythons if "HAS_REQUIRES_EDITABLE" in os.environ: def get_requires_for_build_editable(config_settings: dict[str, str] | None = None) -> list[str]: # noqa: ARG001 return [1] if "REQUIRES_EDITABLE_BAD_RETURN" in os.environ else ["editables"] # type: ignore[list-item] if "HAS_PREPARE_EDITABLE" in os.environ: def prepare_metadata_for_build_editable( metadata_directory: str, config_settings: dict[str, str] | None = None, # noqa: ARG001 ) -> str: dest = Path(metadata_directory) / dist_info dest.mkdir(parents=True) for arc_name, data in metadata.items(): (dest.parent / arc_name).write_text(dedent(data).strip()) print(f"created metadata {dest}") # noqa: T201 if "PREPARE_EDITABLE_BAD" in os.environ: return 1 # type: ignore[return-value] # checking bad type on purpose return dist_info def build_editable( wheel_directory: str, metadata_directory: str | None = None, config_settings: dict[str, str] | None = None, ) -> str: if "BUILD_EDITABLE_BAD" in os.environ: return 1 # type: ignore[return-value] # checking bad type on purpose return build_wheel(wheel_directory, metadata_directory, config_settings) pyproject_api-1.6.1/tests/demo_pkg_inline/pyproject.toml0000644000000000000000000000017613615410400020514 0ustar00[build-system] # noqa: D100 build-backend = "build" requires = [ ] backend-path = [ ".", ] [tool.black] line-length = 120 pyproject_api-1.6.1/.gitignore0000644000000000000000000000014013615410400013272 0ustar00*.py[cod] *.swp __pycache__ /src/pyproject_api/_version.py build dist *.egg-info .tox /.*_cache pyproject_api-1.6.1/LICENSE0000644000000000000000000000177713615410400012330 0ustar00Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pyproject_api-1.6.1/README.md0000644000000000000000000000152613615410400012572 0ustar00# [`pyproject-api`](https://pyproject-api.readthedocs.io/en/latest/) [![PyPI](https://img.shields.io/pypi/v/pyproject-api?style=flat-square)](https://pypi.org/project/pyproject-api/) [![Supported Python versions](https://img.shields.io/pypi/pyversions/pyproject-api.svg)](https://pypi.org/project/pyproject-api/) [![Downloads](https://static.pepy.tech/badge/pyproject-api/month)](https://pepy.tech/project/pyproject-api) [![check](https://github.com/tox-dev/pyproject-api/actions/workflows/check.yml/badge.svg)](https://github.com/tox-dev/pyproject-api/actions/workflows/check.yml) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Documentation Status](https://readthedocs.org/projects/pyproject-api/badge/?version=latest)](https://pyproject-api.readthedocs.io/en/latest/?badge=latest) pyproject_api-1.6.1/pyproject.toml0000644000000000000000000000655413615410400014235 0ustar00[build-system] build-backend = "hatchling.build" requires = [ "hatch-vcs>=0.3", "hatchling>=1.18", ] [project] name = "pyproject-api" description = "API to interact with the python pyproject.toml based projects" readme.content-type = "text/markdown" readme.file = "README.md" keywords = [ "environments", "isolated", "testing", "virtual", ] license = "MIT" maintainers = [{ name = "Bernát Gábor", email = "gaborjbernat@gmail.com" }] authors = [{ name = "Bernát Gábor", email = "gaborjbernat@gmail.com" }] requires-python = ">=3.8" classifiers = [ "Development Status :: 5 - Production/Stable", "Framework :: tox", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Testing", "Topic :: Utilities", ] dynamic = [ "version", ] dependencies = [ "packaging>=23.1", 'tomli>=2.0.1; python_version < "3.11"', ] optional-dependencies.docs = [ "furo>=2023.8.19", "sphinx<7.2", "sphinx-autodoc-typehints>=1.24", ] optional-dependencies.testing = [ "covdefaults>=2.3", "pytest>=7.4", "pytest-cov>=4.1", "pytest-mock>=3.11.1", "setuptools>=68.1.2", "wheel>=0.41.2", ] urls.Homepage = "http://pyproject_api.readthedocs.org" urls.Source = "https://github.com/tox-dev/pyproject-api" urls.Tracker = "https://github.com/tox-dev/pyproject-api/issues" [tool.hatch] build.hooks.vcs.version-file = "src/pyproject_api/_version.py" version.source = "vcs" [tool.black] line-length = 120 [tool.ruff] select = ["ALL"] line-length = 120 target-version = "py38" isort = {known-first-party = ["pyproject_api"], required-imports = ["from __future__ import annotations"]} ignore = [ "INP001", # no implicit namespaces here "ANN101", # Missing type annotation for `self` in method "ANN102", # Missing type annotation for `cls` in classmethod" "ANN401", # Dynamically typed expressions "D203", # `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible "D212", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible "S104", # Possible binding to all interface ] [tool.ruff.per-file-ignores] "tests/**/*.py" = [ "S101", # asserts allowed in tests... "FBT", # don"t care about booleans as positional arguments in tests "INP001", # no implicit namespace "D", # don"t care about documentation in tests "S603", # `subprocess` call: check for execution of untrusted input "PLR2004", # Magic value used in comparison, consider replacing with a constant variable ] [tool.coverage] html.show_contexts = true html.skip_covered = false paths.source = [ "src", ".tox*/*/lib/python*/site-packages", ".tox*/pypy*/site-packages", ".tox*\\*\\Lib\\site-packages", "*/src", "*\\src", ] report.omit = [] run.parallel = true run.plugins = ["covdefaults"] [tool.mypy] python_version = "3.11" show_error_codes = true strict = true overrides = [{ module = ["virtualenv.*"], ignore_missing_imports = true }] pyproject_api-1.6.1/PKG-INFO0000644000000000000000000000536313615410400012413 0ustar00Metadata-Version: 2.1 Name: pyproject-api Version: 1.6.1 Summary: API to interact with the python pyproject.toml based projects Project-URL: Homepage, http://pyproject_api.readthedocs.org Project-URL: Source, https://github.com/tox-dev/pyproject-api Project-URL: Tracker, https://github.com/tox-dev/pyproject-api/issues Author-email: Bernát Gábor Maintainer-email: Bernát Gábor License-Expression: MIT License-File: LICENSE Keywords: environments,isolated,testing,virtual Classifier: Development Status :: 5 - Production/Stable Classifier: Framework :: tox Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: MacOS :: MacOS X Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: POSIX Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Utilities Requires-Python: >=3.8 Requires-Dist: packaging>=23.1 Requires-Dist: tomli>=2.0.1; python_version < '3.11' Provides-Extra: docs Requires-Dist: furo>=2023.8.19; extra == 'docs' Requires-Dist: sphinx-autodoc-typehints>=1.24; extra == 'docs' Requires-Dist: sphinx<7.2; extra == 'docs' Provides-Extra: testing Requires-Dist: covdefaults>=2.3; extra == 'testing' Requires-Dist: pytest-cov>=4.1; extra == 'testing' Requires-Dist: pytest-mock>=3.11.1; extra == 'testing' Requires-Dist: pytest>=7.4; extra == 'testing' Requires-Dist: setuptools>=68.1.2; extra == 'testing' Requires-Dist: wheel>=0.41.2; extra == 'testing' Description-Content-Type: text/markdown # [`pyproject-api`](https://pyproject-api.readthedocs.io/en/latest/) [![PyPI](https://img.shields.io/pypi/v/pyproject-api?style=flat-square)](https://pypi.org/project/pyproject-api/) [![Supported Python versions](https://img.shields.io/pypi/pyversions/pyproject-api.svg)](https://pypi.org/project/pyproject-api/) [![Downloads](https://static.pepy.tech/badge/pyproject-api/month)](https://pepy.tech/project/pyproject-api) [![check](https://github.com/tox-dev/pyproject-api/actions/workflows/check.yml/badge.svg)](https://github.com/tox-dev/pyproject-api/actions/workflows/check.yml) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Documentation Status](https://readthedocs.org/projects/pyproject-api/badge/?version=latest)](https://pyproject-api.readthedocs.io/en/latest/?badge=latest)