pyproject_api-1.9.0/.pre-commit-config.yaml0000644000000000000000000000173013615410400015573 0ustar00repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema rev: 0.31.0 hooks: - id: check-github-workflows args: [ "--verbose" ] - repo: https://github.com/codespell-project/codespell rev: v2.3.0 hooks: - id: codespell additional_dependencies: ["tomli>=2.2.1"] - repo: https://github.com/tox-dev/tox-ini-fmt rev: "1.4.1" hooks: - id: tox-ini-fmt args: ["-p", "fix"] - repo: https://github.com/tox-dev/pyproject-fmt rev: "v2.5.0" hooks: - id: pyproject-fmt - repo: https://github.com/astral-sh/ruff-pre-commit rev: "v0.9.2" hooks: - id: ruff-format - id: ruff args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"] - repo: meta hooks: - id: check-hooks-apply - id: check-useless-excludes pyproject_api-1.9.0/.readthedocs.yml0000644000000000000000000000035013615410400014375 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.9.0/CODE_OF_CONDUCT.md0000644000000000000000000000626413615410400014120 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.9.0/tox.ini0000644000000000000000000000427113615410400012630 0ustar00[tox] requires = tox>=4.23.2 tox-uv>=1.17 env_list = fix 3.13 3.12 3.11 3.10 3.9 docs type pkg_meta skip_missing_interpreters = true [testenv] description = run the unit tests with pytest under {base_python} package = wheel wheel_build_env = .pkg extras = testing pass_env = DIFF_AGAINST PYTEST_* set_env = COVERAGE_FILE = {work_dir}/.coverage.{env_name} COVERAGE_PROCESS_START = {tox_root}/pyproject.toml _COVERAGE_SRC = {env_site_packages_dir}/sphinx_argparse_cli commands = pytest {tty:--color=yes} {posargs: \ --junitxml {work_dir}{/}junit.{env_name}.xml --cov {env_site_packages_dir}{/}pyproject_api \ --cov {tox_root}{/}tests --cov-fail-under=100 \ --cov-config=pyproject.toml --no-cov-on-fail --cov-report term-missing:skip-covered --cov-context=test \ --cov-report html:{env_tmp_dir}{/}htmlcov --cov-report xml:{work_dir}{/}coverage.{env_name}.xml \ tests} [testenv:fix] description = run static analysis and style check using flake8 package = skip deps = pre-commit-uv>=4.1.4 pass_env = HOMEPATH PROGRAMDATA commands = pre-commit run --all-files --show-diff-on-failure python -c 'print("hint: run {envdir}/bin/pre-commit install to add checks as pre-commit hook")' [testenv:docs] description = build documentation extras = docs commands = sphinx-build -d "{env_tmp_dir}{/}doc_tree" docs "{work_dir}{/}docs_out" --color -b html {posargs: -W} python -c 'print(r"documentation available under file://{work_dir}{/}docs_out{/}index.html")' [testenv:type] description = run type check on code base deps = mypy==1.14.1 set_env = {tty:MYPY_FORCE_COLOR = 1} commands = mypy src mypy tests [testenv:pkg_meta] description = check that the long description is valid skip_install = true deps = check-wheel-contents>=0.6.1 twine>=6.0.1 uv>=0.5.18 commands = uv build --sdist --wheel --out-dir {env_tmp_dir} . twine check {env_tmp_dir}{/}* check-wheel-contents --no-config {env_tmp_dir} [testenv:dev] description = generate a DEV environment package = editable extras = testing commands = uv pip tree python -c 'import sys; print(sys.executable)' pyproject_api-1.9.0/.github/FUNDING.yml0000644000000000000000000000003713615410400014466 0ustar00tidelift: "pypi/pyproject-api" pyproject_api-1.9.0/.github/dependabot.yml0000644000000000000000000000016513615410400015503 0ustar00version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" pyproject_api-1.9.0/.github/release.yml0000644000000000000000000000011413615410400015010 0ustar00changelog: exclude: authors: - dependabot - pre-commit-ci pyproject_api-1.9.0/.github/workflows/check.yaml0000644000000000000000000000346213615410400016654 0ustar00name: check on: workflow_dispatch: push: branches: ["main"] tags-ignore: ["**"] pull_request: schedule: - cron: "0 8 * * *" concurrency: group: check-${{ github.ref }} cancel-in-progress: true jobs: test: name: test ${{ matrix.env }} - ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: env: - "3.13" - "3.12" - "3.11" - "3.10" - "3.9" - type - dev - pkg_meta os: - ubuntu-latest - windows-latest - macos-latest exclude: - { os: macos-latest, env: "type" } - { os: macos-latest, env: "dev" } - { os: macos-latest, env: "pkg_meta" } steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install the latest version of uv uses: astral-sh/setup-uv@v5 with: enable-cache: true cache-dependency-glob: "pyproject.toml" github-token: ${{ secrets.GITHUB_TOKEN }} - name: Add .local/bin to Windows PATH if: runner.os == 'Windows' shell: bash run: echo "$USERPROFILE/.local/bin" >> $GITHUB_PATH - name: Install tox run: uv tool install --python-preference only-managed --python 3.13 tox --with tox-uv - name: Install Python if: startsWith(matrix.env, '3.') && matrix.env != '3.13' run: uv python install --python-preference only-managed ${{ matrix.env }} - name: Setup test suite run: tox run -vv --notest --skip-missing-interpreters false -e ${{ matrix.env }} - name: Run test suite run: tox run --skip-pkg-install -e ${{ matrix.env }} env: PYTEST_ADDOPTS: "-vv --durations=20" DIFF_AGAINST: HEAD pyproject_api-1.9.0/.github/workflows/release.yaml0000644000000000000000000000237213615410400017216 0ustar00name: Release to PyPI on: push: tags: ["*"] env: dists-artifact-name: python-package-distributions jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install the latest version of uv uses: astral-sh/setup-uv@v5 with: enable-cache: true cache-dependency-glob: "pyproject.toml" github-token: ${{ secrets.GITHUB_TOKEN }} - name: Build package run: uv build --python 3.13 --python-preference only-managed --sdist --wheel . --out-dir dist - name: Store the distribution packages uses: actions/upload-artifact@v4 with: name: ${{ env.dists-artifact-name }} path: dist/* release: needs: - build runs-on: ubuntu-latest environment: name: release url: https://pypi.org/project/pyproject-api/${{ github.ref_name }} permissions: id-token: write steps: - name: Download all the dists uses: actions/download-artifact@v4 with: name: ${{ env.dists-artifact-name }} path: dist/ - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@v1.12.3 with: attestations: true pyproject_api-1.9.0/docs/api.rst0000644000000000000000000000162613615410400013551 0ustar00 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 pyproject_api-1.9.0/docs/conf.py0000644000000000000000000000436013615410400013543 0ustar00# noqa: D100 from __future__ import annotations from typing import TYPE_CHECKING from sphinx.domains.python import PythonDomain from pyproject_api import __version__ if TYPE_CHECKING: from docutils.nodes import Element from sphinx.application import Sphinx from sphinx.builders import Builder from sphinx.environment import BuildEnvironment project = name = "pyproject_api" company = "tox-dev" project_copyright = f"{company}" 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 = [] def setup(app: Sphinx) -> None: # noqa: D103 class PatchedPythonDomain(PythonDomain): def resolve_xref( # noqa: PLR0913,PLR0917 self, env: BuildEnvironment, fromdocname: str, builder: Builder, type: str, # noqa: A002 target: str, node: resolve_xref, contnode: Element, ) -> Element: # fixup some wrongly resolved mappings mapping = { "pathlib._local.Path": "pathlib.Path", } if target in mapping: target = node["reftarget"] = mapping[target] return super().resolve_xref(env, fromdocname, builder, type, target, node, contnode) app.add_domain(PatchedPythonDomain, override=True) pyproject_api-1.9.0/docs/index.rst0000644000000000000000000000027513615410400014106 0ustar00``pyproject-api`` ================= ``pyproject-api`` aims to abstract away interaction with ``pyproject.toml`` style projects in a flexible way. .. toctree:: :hidden: self api pyproject_api-1.9.0/src/pyproject_api/__init__.py0000644000000000000000000000156713615410400017072 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__ = [ "BackendFailed", "CmdStatus", "EditableResult", "Frontend", "MetadataForBuildEditableResult", "MetadataForBuildWheelResult", "OptionalHooks", "RequiresBuildEditableResult", "RequiresBuildSdistResult", "RequiresBuildWheelResult", "SdistResult", "SubprocessFrontend", "WheelResult", "__version__", ] pyproject_api-1.9.0/src/pyproject_api/__main__.py0000644000000000000000000000533113615410400017044 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.9.0/src/pyproject_api/_backend.py0000644000000000000000000001146513615410400017057 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 """ import importlib import json import locale import os import sys import traceback class MissingCommand(TypeError): # noqa: N818 """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): msg = f"{on_object!r} has no attribute {name!r}" raise MissingCommand(msg) return getattr(on_object, name)(*args, **kwargs) def __str__(self): return f"{self.__class__.__name__}(backend={self.backend})" def _exit(self): # noqa: PLR6301 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): # noqa: C901, PLR0912, PLR0915 reuse_process = argv[0].lower() == "true" try: backend_proxy = BackendProxy(argv[1], None if len(argv) == 2 else argv[2]) # noqa: PLR2004 except BaseException: print("failed to start backend", file=sys.stderr) raise else: print(f"started backend {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: # python 2 does not support loading from bytearray if sys.version_info[0] == 2: # pragma: no branch # noqa: PLR2004 content = content.decode() # pragma: no cover parsed_message = json.loads(content) result_file = parsed_message["result"] except Exception: # noqa: BLE001 # ignore messages that are not valid JSON and contain a valid result path print(f"Backend: incorrect request to backend: {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: # noqa: BLE001 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: encoding = locale.getpreferredencoding(do_setlocale=False) with open(result_file, "w", encoding=encoding) as file_handler: # noqa: PTH123 json.dump(result, file_handler) except Exception: # noqa: BLE001 traceback.print_exc() finally: # used as done marker by frontend print(f"Backend: Wrote response {result} to {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: msg = "EOF without reading anything" raise EOFError(msg) # 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.9.0/src/pyproject_api/_backend.pyi0000644000000000000000000000106313615410400017221 0ustar00from collections.abc import Sequence from typing import Any 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.9.0/src/pyproject_api/_frontend.py0000644000000000000000000005101313615410400017300 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 TYPE_CHECKING, Any, Literal, NamedTuple, NoReturn, Optional, TypedDict, cast from zipfile import ZipFile from packaging.requirements import Requirement from pyproject_api._util import ensure_empty_dir if TYPE_CHECKING: from collections.abc import Iterator 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, PLR0917 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( 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]: # noqa: PLR6301 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.9.0/src/pyproject_api/_util.py0000644000000000000000000000110613615410400016434 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.9.0/src/pyproject_api/_version.py0000644000000000000000000000063313615410400017150 0ustar00# file generated by setuptools_scm # don't change, don't track in version control TYPE_CHECKING = False if TYPE_CHECKING: from typing import Tuple, Union VERSION_TUPLE = Tuple[Union[int, str], ...] else: VERSION_TUPLE = object version: str __version__: str __version_tuple__: VERSION_TUPLE version_tuple: VERSION_TUPLE __version__ = version = '1.9.0' __version_tuple__ = version_tuple = (1, 9, 0) pyproject_api-1.9.0/src/pyproject_api/_via_fresh_subprocess.py0000644000000000000000000000553013615410400021702 0ustar00from __future__ import annotations import os import sys from contextlib import contextmanager from subprocess import PIPE, Popen # noqa: S404 from threading import Thread from typing import IO, TYPE_CHECKING, Any, cast from ._frontend import CmdStatus, Frontend if TYPE_CHECKING: from collections.abc import Iterator 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__( 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.9.0/src/pyproject_api/py.typed0000644000000000000000000000000013615410400016435 0ustar00pyproject_api-1.9.0/tests/_build_sdist.py0000644000000000000000000000000013615410400015457 0ustar00pyproject_api-1.9.0/tests/test_backend.py0000644000000000000000000001423113615410400015454 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.9.0/tests/test_frontend.py0000644000000000000000000003523213615410400015710 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=r"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=r"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.9.0/tests/test_frontend_setuptools.py0000644000000000000000000001500613615410400020206 0ustar00from __future__ import annotations import sys from contextlib import contextmanager from stat import S_IWGRP, S_IWOTH, S_IWUSR from typing import TYPE_CHECKING, NamedTuple import pytest from pyproject_api._frontend import BackendFailed from pyproject_api._via_fresh_subprocess import SubprocessFrontend if TYPE_CHECKING: from collections.abc import Iterator 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() assert not result.requires 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" expected = ["magic>3", "requests>2"] if sys.version_info[0:2] > (3, 8) else ["magic >3", "requests >2"] assert sorted(values) == expected 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.9.0/tests/test_main.py0000644000000000000000000000615513615410400015017 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( 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.9.0/tests/test_util.py0000644000000000000000000000212513615410400015041 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.9.0/tests/test_version.py0000644000000000000000000000022413615410400015547 0ustar00from __future__ import annotations def test_version() -> None: from pyproject_api import __version__ # noqa: PLC0415 assert __version__ pyproject_api-1.9.0/tests/demo_pkg_inline/build.py0000644000000000000000000001037413615410400017254 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.9.0/tests/demo_pkg_inline/pyproject.toml0000644000000000000000000000017513615410400020515 0ustar00[build-system] # noqa: D100 build-backend = "build" requires = [ ] backend-path = [ ".", ] [tool.black] line-length = 120 pyproject_api-1.9.0/.gitignore0000644000000000000000000000014013615410400013274 0ustar00*.py[cod] *.swp __pycache__ /src/pyproject_api/_version.py build dist *.egg-info .tox /.*_cache pyproject_api-1.9.0/LICENSE0000644000000000000000000000177713615410400012332 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.9.0/README.md0000644000000000000000000000134713615410400012575 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.yaml/badge.svg)](https://github.com/tox-dev/pyproject-api/actions/workflows/check.yaml) [![Documentation Status](https://readthedocs.org/projects/pyproject-api/badge/?version=latest)](https://pyproject-api.readthedocs.io/en/latest/?badge=latest) pyproject_api-1.9.0/pyproject.toml0000644000000000000000000001004413615410400014224 0ustar00[build-system] build-backend = "hatchling.build" requires = [ "hatch-vcs>=0.4", "hatchling>=1.27", ] [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.9" 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.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Testing", "Topic :: Utilities", ] dynamic = [ "version", ] dependencies = [ "packaging>=24.2", "tomli>=2.2.1; python_version<'3.11'", ] optional-dependencies.docs = [ "furo>=2024.8.6", "sphinx-autodoc-typehints>=3", ] optional-dependencies.testing = [ "covdefaults>=2.3", "pytest>=8.3.4", "pytest-cov>=6", "pytest-mock>=3.14", "setuptools>=75.8", ] urls.Changelog = "https://github.com/tox-dev/pyproject-api/releases" urls.Homepage = "https://pyproject-api.readthedocs.io" 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.ruff] line-length = 120 format.preview = true format.docstring-code-line-length = 100 format.docstring-code-format = true lint.select = [ "ALL", ] lint.ignore = [ "ANN401", # Dynamically typed expressions "COM812", # Conflict with formatter "CPY", # No copyright statements "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 "DOC", # no restructuredtext support "INP001", # no implicit namespaces here "ISC001", # Conflict with formatter "S104", # Possible binding to all interface ] lint.per-file-ignores."src/pyproject_api/_backend.py" = [ "ANN", "I002", "T201", "UP", ] # no type annotations lint.per-file-ignores."src/pyproject_api/_backend.pyi" = [ "E301", "E302", ] # https://github.com/astral-sh/ruff/issues/10077 lint.per-file-ignores."tests/**/*.py" = [ "D", # don't care about documentation in tests "FBT", # don't care about booleans as positional arguments in tests "INP001", # no implicit namespace "PLC2701", # private imports "PLR0913", # too many positional arguments "PLR0917", # too many positional arguments "PLR2004", # Magic value used in comparison, consider replacing with a constant variable "S101", # asserts allowed in tests "S603", # `subprocess` call: check for execution of untrusted input ] lint.isort = { known-first-party = [ "pyproject_api", ], required-imports = [ "from __future__ import annotations", ] } lint.preview = true [tool.codespell] builtin = "clear,usage,en-GB_to_en-US" write-changes = true count = true [tool.pyproject-fmt] max_supported_python = "3.13" [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.12" show_error_codes = true strict = true overrides = [ { module = [ "virtualenv.*", ], ignore_missing_imports = true }, ] pyproject_api-1.9.0/PKG-INFO0000644000000000000000000000515313615410400012412 0ustar00Metadata-Version: 2.4 Name: pyproject-api Version: 1.9.0 Summary: API to interact with the python pyproject.toml based projects Project-URL: Changelog, https://github.com/tox-dev/pyproject-api/releases Project-URL: Homepage, https://pyproject-api.readthedocs.io 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.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Utilities Requires-Python: >=3.9 Requires-Dist: packaging>=24.2 Requires-Dist: tomli>=2.2.1; python_version < '3.11' Provides-Extra: docs Requires-Dist: furo>=2024.8.6; extra == 'docs' Requires-Dist: sphinx-autodoc-typehints>=3; extra == 'docs' Provides-Extra: testing Requires-Dist: covdefaults>=2.3; extra == 'testing' Requires-Dist: pytest-cov>=6; extra == 'testing' Requires-Dist: pytest-mock>=3.14; extra == 'testing' Requires-Dist: pytest>=8.3.4; extra == 'testing' Requires-Dist: setuptools>=75.8; 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.yaml/badge.svg)](https://github.com/tox-dev/pyproject-api/actions/workflows/check.yaml) [![Documentation Status](https://readthedocs.org/projects/pyproject-api/badge/?version=latest)](https://pyproject-api.readthedocs.io/en/latest/?badge=latest)