pax_global_header00006660000000000000000000000064141207316700014513gustar00rootroot0000000000000052 comment=c940180b6ace5210e6f00149d45b1358f39fb274 build-0.7.0/000077500000000000000000000000001412073167000126165ustar00rootroot00000000000000build-0.7.0/.dockerignore000066400000000000000000000000571412073167000152740ustar00rootroot00000000000000.tox .*_cache *.egg-info Dockerfile build dist build-0.7.0/.github/000077500000000000000000000000001412073167000141565ustar00rootroot00000000000000build-0.7.0/.github/CODEOWNERS000066400000000000000000000000441412073167000155470ustar00rootroot00000000000000* @FFY00 @gaborbernat @layday build-0.7.0/.github/workflows/000077500000000000000000000000001412073167000162135ustar00rootroot00000000000000build-0.7.0/.github/workflows/check.yml000066400000000000000000000011431412073167000200120ustar00rootroot00000000000000name: check on: push: branches: - main pull_request: branches: - main schedule: - cron: "0 8 * * *" jobs: docs: runs-on: ubuntu-latest env: PY_COLORS: 1 TOX_PARALLEL_NO_SPINNER: 1 steps: - uses: actions/checkout@v2 - name: Setup Python 3.9 uses: actions/setup-python@v2 with: python-version: 3.9 - name: Install tox run: python -m pip install tox - name: Setup run environment run: tox -vv --notest -e docs - name: Run check for type run: tox -e docs --skip-pkg-install build-0.7.0/.github/workflows/test.yml000066400000000000000000000057651412073167000177320ustar00rootroot00000000000000name: test on: push: branches: - main paths-ignore: - 'docs/**' - '*.md' pull_request: branches: - main paths-ignore: - 'docs/**' - '*.md' schedule: - cron: "0 8 * * *" jobs: pytest: runs-on: ${{ matrix.os }}-latest env: PYTEST_ADDOPTS: "--run-integration --showlocals -vv --durations=10 --reruns 5 --only-rerun subprocess.CalledProcessError" strategy: fail-fast: false matrix: os: - ubuntu - macos - windows py: - '3.10-dev' - '3.9' - '3.8' - '3.7' - '3.6' - 'pypy3' steps: - uses: actions/checkout@v2 - name: Setup python for test ${{ matrix.py }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.py }} - name: Pick environment to run run: | import platform import os import sys if platform.python_implementation() == "PyPy": base = f"pypy{sys.version_info.major}" else: base = f"py{sys.version_info.major}{sys.version_info.minor}" env = f"BASE={base}\n" print(f"Picked:\n{env}for {sys.version}") with open(os.environ["GITHUB_ENV"], "a", encoding="utf-8") as file: file.write(env) shell: python - name: Setup python for tox uses: actions/setup-python@v2 with: python-version: 3.9 - name: Install tox run: python -m pip install tox - name: Setup test suite run: tox -vv --notest -e ${{env.BASE}},${{env.BASE}}-path,${{env.BASE}}-sdist,${{env.BASE}}-wheel - name: Run test suite via tox run: tox -e ${{env.BASE}} --skip-pkg-install - name: Run test suite via path run: tox -e ${{env.BASE}}-path --skip-pkg-install - name: Run test suite via sdist run: tox -e ${{env.BASE}}-sdist --skip-pkg-install - name: Run test suite via wheel run: tox -e ${{env.BASE}}-wheel --skip-pkg-install - name: Run minimum version test run: tox -e ${{env.BASE}}-min - name: Rename coverage report file run: mv ".tox/coverage.${BASE}.xml" .tox/coverage.xml shell: bash - uses: codecov/codecov-action@v1 if: ${{ always() }} env: PYTHON: ${{ matrix.python }} with: file: ./.tox/coverage.xml flags: tests env_vars: PYTHON name: ${{ matrix.py }} - ${{ matrix.os }} type: runs-on: ubuntu-latest env: PY_COLORS: 1 TOX_PARALLEL_NO_SPINNER: 1 steps: - uses: actions/checkout@v2 - name: Setup Python 3.9 uses: actions/setup-python@v2 with: python-version: 3.9 - name: Install tox run: python -m pip install tox - name: Setup run environment run: tox -vv --notest -e type - name: Run check for type run: tox -e type --skip-pkg-install build-0.7.0/.gitignore000066400000000000000000000004261412073167000146100ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # Distribution / packaging build/lib/ dist/ *.egg-info/ .eggs # Testing and coverage .cache .nox/ .mypy_cache/ .pytest_cache htmlcov/ .coverage coverage.xml tests/integration/ .integration-sources .tox Dockerfile build-0.7.0/.pre-commit-config.yaml000066400000000000000000000020061412073167000170750ustar00rootroot00000000000000ci: autofix_prs: false autoupdate_commit_msg: 'pre-commit: bump repositories' repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.0.1 hooks: - id: check-ast - id: check-builtin-literals - id: check-docstring-first - id: check-merge-conflict - id: check-yaml - id: check-toml exclude: tests/packages/test-(bad-syntax|no-permission)/pyproject.toml - id: debug-statements - id: end-of-file-fixer - id: trailing-whitespace - id: double-quote-string-fixer - repo: https://github.com/asottile/pyupgrade rev: v2.26.0 hooks: - id: pyupgrade args: ["--py36-plus"] - repo: https://github.com/psf/black rev: 21.8b0 hooks: - id: black - repo: https://github.com/PyCQA/isort rev: 5.9.3 hooks: - id: isort - repo: https://github.com/asottile/setup-cfg-fmt rev: v1.17.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/PyCQA/flake8 rev: "3.9.2" hooks: - id: flake8 additional_dependencies: ["flake8-bugbear == 20.1.4"] language_version: python3.9 build-0.7.0/.readthedocs.yml000066400000000000000000000001571412073167000157070ustar00rootroot00000000000000version: 2 python: version: 3.8 install: - method: pip path: . extra_requirements: [docs] build-0.7.0/CHANGELOG.rst000066400000000000000000000237511412073167000146470ustar00rootroot00000000000000+++++++++ Changelog +++++++++ 0.7.0 (16-09-2021) ================== - Add ``build.util`` module with an high-level utility API (`PR #340`_) .. _PR #340: https://github.com/pypa/build/pull/340 0.6.0.post1 (05-08-2021) ======================== - Fix compability with Python 3.6 and 3.7 (`PR #339`_, Fixes `#338`_) .. _PR #339: https://github.com/pypa/build/pull/339 .. _#338: https://github.com/pypa/build/issues/338 0.6.0 (02-08-2021) ================== - Improved output (`PR #333`_, Fixes `#142`_) - The CLI now honnors `NO_COLOR`_ (`PR #333`_) - The CLI can now be forced to colorize the output by setting the ``FORCE_COLOR`` environment variable (`PR #335`_) - Added logging to ``build`` and ``build.env`` (`PR #333`_) - Switch to a TOML v1 compliant parser (`PR #336`_, Fixes `#308`_) Breaking Changes ---------------- - Dropped support for Python 2 and 3.5. .. _PR #333: https://github.com/pypa/build/pull/333 .. _PR #335: https://github.com/pypa/build/pull/335 .. _PR #336: https://github.com/pypa/build/pull/336 .. _#142: https://github.com/pypa/build/issues/142 .. _#308: https://github.com/pypa/build/issues/308 .. _NO_COLOR: https://no-color.org 0.5.1 (22-06-2021) ================== - Fix invoking the backend on an inexistent output directory with multiple levels (`PR #318`_, Fixes `#316`_) - When building wheels via sdists, use an isolated temporary directory (`PR #321`_, Fixes `#320`_) .. _PR #318: https://github.com/pypa/build/pull/318 .. _PR #321: https://github.com/pypa/build/pull/321 .. _#316: https://github.com/pypa/build/issues/316 .. _#320: https://github.com/pypa/build/issues/320 0.5.0 (19-06-2021) ================== - Add ``ProjectBuilder.metadata_path`` helper (`PR #303`_, Fixes `#301`_) - Added a ``build.__main__.build_package_via_sdist`` method (`PR #304`_) - Use appropriate installation scheme for Apple Python venvs (`PR #314`_, Fixes `#310`_) Breaking Changes ---------------- - Binary distributions are now built via the sdist by default in the CLI (`PR #304`_, Fixes `#257`_) - ``python -m build`` will now build a sdist, extract it, and build a wheel from the source - As a side-effect of `PR #304`_, ``build.__main__.build_package`` no longer does CLI error handling (print nice message and exit the program) - Importing ``build.__main__`` no longer has any side-effects, it no longer overrides ``warnings.showwarning`` or runs ``colorama.init`` on import (`PR #312`_) .. _PR #303: https://github.com/pypa/build/pull/303 .. _PR #304: https://github.com/pypa/build/pull/304 .. _PR #312: https://github.com/pypa/build/pull/312 .. _PR #314: https://github.com/pypa/build/pull/314 .. _#257: https://github.com/pypa/build/issues/257 .. _#301: https://github.com/pypa/build/issues/301 .. _#310: https://github.com/pypa/build/issues/310 0.4.0 (23-05-2021) ================== - Validate that the supplied source directory is valid (`PR #260`_, Fixes `#259`_) - Set and test minimum versions of build's runtime dependencies (`PR #267`_, Fixes `#263`_) - Use symlinks on creating venv's when available (`PR #274`_, Fixes `#271`_) - Error sooner if pip upgrade is required and fails (`PR #288`_, Fixes `#256`_) - Add a ``runner`` argument to ``ProjectBuilder`` (`PR #290`_, Fixes `#289`_) - Hide irrelevant ``pep517`` error traceback and improve error messages (`PR #296`_) - Try to use ``colorama`` to fix colors on Windows (`PR #300`_) .. _PR #260: https://github.com/pypa/build/pull/260 .. _PR #267: https://github.com/pypa/build/pull/267 .. _PR #274: https://github.com/pypa/build/pull/274 .. _PR #288: https://github.com/pypa/build/pull/288 .. _PR #290: https://github.com/pypa/build/pull/290 .. _PR #296: https://github.com/pypa/build/pull/296 .. _PR #300: https://github.com/pypa/build/pull/300 .. _#256: https://github.com/pypa/build/issues/256 .. _#259: https://github.com/pypa/build/issues/259 .. _#263: https://github.com/pypa/build/issues/263 .. _#271: https://github.com/pypa/build/issues/271 .. _#289: https://github.com/pypa/build/issues/289 Breaking Changes ---------------- - As a side-effect of `PR #260`_, projects not containing either a ``pyproject.toml`` or ``setup.py`` will be reported as invalid. This affects projects specifying only a ``setup.cfg``, such projects are recommended to add a ``pyproject.toml``. The new behavior is on par with what pip currently does, so if you are affected by this, your project should not be pip installable. - The ``--skip-depencencies`` option has been renamed to ``--skip-dependency-check`` (`PR #297`_) - The ``skip_dependencies`` argument of ``build.__main__.build_package`` has been renamed to ``skip_dependency_check`` (`PR #297`_) - ``build.ConfigSettings`` has been renamed to ``build.ConfigSettingsType`` (`PR #298`_) - ``build.ProjectBuilder.build_dependencies`` to ``build.ProjectBuilder.build_system_requires`` (`PR #284`_, Fixes `#182`_) - ``build.ProjectBuilder.get_dependencies`` to ``build.ProjectBuilder.get_requires_for_build`` (`PR #284`_, Fixes `#182`_) .. _PR #284: https://github.com/pypa/build/pull/284 .. _PR #297: https://github.com/pypa/build/pull/297 .. _PR #298: https://github.com/pypa/build/pull/298 .. _#182: https://github.com/pypa/build/issues/182 0.3.1 (09-03-2021) ================== - Support direct usage from pipx run in 0.16.1.0+ (`PR #247`_) - Use UTF-8 encoding when reading pyproject.toml (`PR #251`_, Fixes `#250`_) .. _PR #247: https://github.com/pypa/build/pull/247 .. _PR #251: https://github.com/pypa/build/pull/251 .. _#250: https://github.com/pypa/build/issues/250 0.3.0 (19-02-2021) ================== - Upgrade pip based on venv pip version, avoids error on Debian Python 3.6.5-3.8 or issues installing wheels on Big Sur (`PR #229`_, `PR #230`_, Fixes `#228`_) - Build dependencies in isolation, instead of in the build environment (`PR #232`_, Fixes `#231`_) - Fallback on venv if virtualenv is too old (`PR #241`_) - Add metadata preparation hook (`PR #217`_, Fixes `#130`_) .. _PR #217: https://github.com/pypa/build/pull/217 .. _PR #229: https://github.com/pypa/build/pull/229 .. _PR #230: https://github.com/pypa/build/pull/230 .. _PR #232: https://github.com/pypa/build/pull/232 .. _PR #241: https://github.com/pypa/build/pull/241 .. _#130: https://github.com/pypa/build/issues/130 .. _#228: https://github.com/pypa/build/issues/228 .. _#231: https://github.com/pypa/build/issues/231 0.2.1 (09-02-2021) ================== - Fix error from unrecognised pip flag on Python 3.6.0 to 3.6.5 (`PR #227`_, Fixes `#226`_) .. _PR #227: https://github.com/pypa/build/pull/227 .. _#226: https://github.com/pypa/build/issues/226 0.2.0 (07-02-2021) ================== - Check dependencies recursively (`PR #183`_, Fixes `#25`_) - Build wheel and sdist distributions in separate environments, as they may have different dependencies (`PR #195`_, Fixes `#194`_) - Add support for pre-releases in ``check_dependency`` (`PR #204`_, Fixes `#191`_) - Fixes console scripts not being available during build (`PR #221`_, Fixes `#214`_) - Do not add the default backend requirements to ``requires`` when no backend is specified (`PR #177`_, Fixes `#107`_) - Return the sdist name in ``ProjectBuild.build`` (`PR #197`_) - Improve documentation (`PR #178`_, `PR #203`_) - Add changelog (`PR #219`_, Fixes `#169`_) Breaking changes ---------------- - Move ``config_settings`` argument to the hook calls (`PR #218`_, Fixes `#216`_) .. _PR #177: https://github.com/pypa/build/pull/177 .. _PR #178: https://github.com/pypa/build/pull/178 .. _PR #183: https://github.com/pypa/build/pull/183 .. _PR #195: https://github.com/pypa/build/pull/195 .. _PR #197: https://github.com/pypa/build/pull/197 .. _PR #203: https://github.com/pypa/build/pull/203 .. _PR #204: https://github.com/pypa/build/pull/204 .. _PR #218: https://github.com/pypa/build/pull/218 .. _PR #219: https://github.com/pypa/build/pull/219 .. _PR #221: https://github.com/pypa/build/pull/221 .. _#25: https://github.com/pypa/build/issues/25 .. _#107: https://github.com/pypa/build/issues/107 .. _#109: https://github.com/pypa/build/issues/109 .. _#169: https://github.com/pypa/build/issues/169 .. _#191: https://github.com/pypa/build/issues/191 .. _#194: https://github.com/pypa/build/issues/194 .. _#214: https://github.com/pypa/build/issues/214 .. _#216: https://github.com/pypa/build/issues/216 0.1.0 (29-10-2020) ================== - Moved the upstream to PyPA - Fixed building with isolation in a virtual environment - Added env.IsolatedEnv abstract class - Added env.IsolatedEnvBuilder (replaces env.IsolatedEnvironment usages) - Added python_executable argument to the ProjectBuilder constructor - Added --version/-V option to the CLI - Added support for Python 3.9 - Added py.typed marker - Various miscelaneous fixes in the virtual environment creation - Many general improvements in the documentation - Documentation moved to the furo theme - Updated the CoC to the PSF CoC, which PyPA has adopted Breaking changes ---------------- - Renamed the entrypoint script to pyproject-build - Removed default arguments from all paths in ProjectBuilder - Removed ProjectBuilder.hook - Renamed __main__.build to __main__.build_package - Changed the default outdir value to {srcdir}/dest - Removed env.IsolatedEnvironment 0.0.4 (08-09-2020) ================== - Packages are now built in isolation by default - Added --no-isolation/-n flag to build in the current environment - Add --config-setting/-C option to pass options to the backend - Add IsolatedEnvironment class - Fix creating the output directory if it doesn't exit - Fix building with in-tree backends - Fix broken entrypoint script (python-build) - Add warning about incomplete verification when verifying extras - Automatically detect typos in the build system table - Minor documentation improvements 0.0.3.1 (10-06-2020) ==================== - Fix bug preventing the CLI from being invoked - Improved documentation 0.0.3 (09-06-2020) ================== - Misc improvements - Added documentation 0.0.2 (29-05-2020) ================== - Add setuptools as a default fallback backend - Fix extras handling in requirement strings 0.0.1 (17-05-2020) ================== - Initial release build-0.7.0/LICENSE000066400000000000000000000021311412073167000136200ustar00rootroot00000000000000Copyright © 2019 Filipe Laíns Permission 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 (including the next paragraph) 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. build-0.7.0/README.md000066400000000000000000000031701412073167000140760ustar00rootroot00000000000000# build [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/pypa/build/main.svg)](https://results.pre-commit.ci/latest/github/pypa/build/main) [![CI check](https://github.com/pypa/build/workflows/check/badge.svg)](https://github.com/pypa/build/actions) [![CI test](https://github.com/pypa/build/actions/workflows/test.yml/badge.svg)](https://github.com/pypa/build/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/pypa/build/branch/main/graph/badge.svg)](https://codecov.io/gh/pypa/build) [![Documentation Status](https://readthedocs.org/projects/pypa-build/badge/?version=latest)](https://pypa-build.readthedocs.io/en/latest/?badge=latest) [![PyPI version](https://badge.fury.io/py/build.svg)](https://pypi.org/project/build/) [![Discord](https://img.shields.io/discord/803025117553754132?label=Discord%20chat%20%23build&style=flat-square)](https://discord.gg/pypa) A simple, correct PEP517 package builder. See the [documentation](https://pypa-build.readthedocs.io/en/latest/) for more information. ### Installation `build` can be installed via `pip` or an equivalent via: ```console $ pip install build ``` ### Usage ```console $ python -m build ``` This will build the package in an isolated environment, generating a source-distribution and wheel in the directory `dist/`. See the [documentation](https://pypa-build.readthedocs.io/en/latest/) for full information. ### Code of Conduct Everyone interacting in the build's codebase, issue trackers, chat rooms, and mailing lists is expected to follow the [PSF Code of Conduct]. [psf code of conduct]: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md build-0.7.0/codecov.yml000066400000000000000000000000171412073167000147610ustar00rootroot00000000000000comment: false build-0.7.0/docs/000077500000000000000000000000001412073167000135465ustar00rootroot00000000000000build-0.7.0/docs/api.rst000066400000000000000000000004611412073167000150520ustar00rootroot00000000000000API Documentation ================= This project exposes 2 modules: ``build`` module ---------------- .. automodule:: build :members: :undoc-members: :show-inheritance: ``build.env`` module -------------------- .. automodule:: build.env :members: :undoc-members: :show-inheritance: build-0.7.0/docs/changelog.rst000077700000000000000000000000001412073167000204562../CHANGELOG.rstustar00rootroot00000000000000build-0.7.0/docs/conf.py000066400000000000000000000041711412073167000150500ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. import build # -- Project information ----------------------------------------------------- project = 'build' copyright = '2020, Filipe Laíns' author = 'Filipe Laíns' # The short X.Y version version = build.__version__ # The full version, including alpha/beta/rc tags release = build.__version__ # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx_autodoc_typehints', 'sphinx_argparse_cli', ] intersphinx_mapping = { 'python': ('https://docs.python.org/3/', None), } # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [] default_role = 'any' # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'furo' html_title = f'build {version}' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named 'default.css' will overwrite the builtin 'default.css'. # html_static_path = ['_static'] autoclass_content = 'both' build-0.7.0/docs/differences.rst000066400000000000000000000016451412073167000165630ustar00rootroot00000000000000============================ Differences from other tools ============================ ``pep517.build`` ---------------- build implements a CLI tailored to end users. ``pep517.build`` contained a proof-of-concept of a :pep:`517` frontend. It *"implement[ed] essentially the simplest possible frontend tool, to exercise and illustrate how the core functionality can be used"*. It has since been `deprecated and is scheduled for removal`_. ``setup.py sdist bdist_wheel`` ------------------------------ build is roughly the equivalent of ``setup.py sdist bdist_wheel`` but with :pep:`517` support, allowing use with projects that don't use setuptools. .. _deprecated and is scheduled for removal: https://github.com/pypa/pep517/pull/83 Custom Behaviors ---------------- Fallback Backend ^^^^^^^^^^^^^^^^ As recommended in :pep:`517`, if no backend is specified, ``build`` will fallback to ``setuptools.build_meta:__legacy__``. build-0.7.0/docs/index.rst000066400000000000000000000017641412073167000154170ustar00rootroot00000000000000:hide-toc: ***** build ***** A simple, correct :pep:`517` package builder. build will invoke the :pep:`517` hooks to build a distribution package. It is a simple build tool and does not perform any dependency management. .. sphinx_argparse_cli:: :module: build.__main__ :func: main_parser :prog: python -m build :title: python -m build :usage_width: 97 .. note:: A ``pyproject-build`` CLI script is also available, so that tools such as pipx_ can use it. By default build will build the package in an isolated environment, but this behavior can be disabled with ``--no-isolation``. .. toctree:: :hidden: mission differences .. toctree:: :caption: Usage :hidden: installation changelog api .. toctree:: :caption: Contributing :hidden: test_suite .. toctree:: :caption: Project Links :hidden: Source Code Issue Tracker .. _pipx: https://github.com/pipxproject/pipx build-0.7.0/docs/installation.rst000066400000000000000000000041631412073167000170050ustar00rootroot00000000000000============ Installation ============ You can download a tarball_ from Github, checkout the latest `git tag`_ or fetch the artifacts from `project page`_ on PyPI. The recommended way is to checkout the git tags, as they are PGP signed with one of the following keys: - |3DCE51D60930EBA47858BA4146F633CBB0EB4BF2|_ *(Filipe Laíns)* ``build`` may also be installed via `pip`_ or an equivalent: .. code-block:: sh $ pip install build .. tip:: If you prefer, or are already using virtualenv_ in your workflow, you can install ``build`` with the optional ``virtualenv`` depedency: .. code-block:: sh $ pip install 'build[virtualenv]' this way, ``build`` will use virtualenv_ for isolation, instead of venv_. This can be particularly useful, for example, when using automation tools that rely on virtualenv_, such as tox_, or when your operating system's Python package does not include venv_ in the standard installation (such as some versions of Ubuntu). Bootstrapping ============= This package can build itself with only the ``toml`` and ``pep517`` dependencies. The ``--skip-dependency-check`` flag should be used in this case. We have a dependency on tomli_, but toml_ can be used instead, which may make bootstraping easier. Compatibility ============= ``build`` is verified to be compatible with the following Python versions: - 2.7 - 3.5 - 3.6 - 3.7 - 3.8 - 3.9 - PyPy(2) - PyPy3 .. _pipx: https://github.com/pipxproject/pipx .. _pip: https://github.com/pypa/pip .. _PyPI: https://pypi.org/ .. _tox: https://tox.readthedocs.org/ .. _virtualenv: https://virtualenv.pypa.io .. _venv: https://docs.python.org/3/library/venv.html .. _tarball: https://github.com/pypa/build/releases .. _git tag: https://github.com/pypa/build/tags .. _project page: https://pypi.org/project/build/ .. _tomli: https://github.com/hukkin/tomli .. _toml: https://github.com/uiri/toml .. |3DCE51D60930EBA47858BA4146F633CBB0EB4BF2| replace:: ``3DCE51D60930EBA47858BA4146F633CBB0EB4BF2`` .. _3DCE51D60930EBA47858BA4146F633CBB0EB4BF2: https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3dce51d60930eba47858ba4146f633cbb0eb4bf2 build-0.7.0/docs/mission.rst000066400000000000000000000014461412073167000157660ustar00rootroot00000000000000================= Mission Statement ================= In the Python ecosystem, the build system tools and the package management are very intertwined. While it might be useful for user to be able to access all this capabilities in a single project (such as pip_), there are several usecases where this is not desirable. The main being custom environments (outside PyPI_) or situations where the user does its own package management, such as Linux distributions. This project aims to fit the "building packages hole" for such use-cases in :pep:`517`/:pep:`518` workflows. As it is intended to be used by users that do their own package management, we will try to keep dependencies to a minimum, in order to try make bootstrapping easier. .. _pip: https://github.com/pypa/pip .. _PyPI: https://pypi.org/ build-0.7.0/docs/test_suite.rst000066400000000000000000000040411412073167000164670ustar00rootroot00000000000000********** Test Suite ********** Due to its nature, ``build`` has a somewhat complex test suite, which we will try to go through in this document. Firstly, there are two set of tests, unit tests and integration tests. In unit tests, we test the actual code implementation. In integration tests, we test ``build`` on a few real world projects; this is mostly a sanity test. Integration tests take a long time to run, and are not very helpful tracking down issues, so they are **disabled by default**. They can be enabled by passing either ``--run-integration`` or ``--only-integration`` arguments to pytest, where the latter will disable the unit tests and only run the integration ones. Even though these tests are disabled by default, they will be run in CI, where test suite run durations are not a big issue. To run the test suite we use ``tox``, which automates running the test suite on different environments: .. code-block:: console tox You can find out more about how to run ``tox`` and its arguments in the `tox documentation`_. We have a fairly large environment matrix. We run tests for all supported Python versions and implementations, and with the module being invoked from path, sdist install, or wheel install. Additionally, we have an environment for type checking, and one to produce the documentation. There are some other extra environments, like checking the code with the minimum version of each dependency. Some examples commands for this project: - Run type checking: ``tox -e type`` - Only run unit tests against Python 3.9: ``tox -e py39`` - Run both unit and integration tests: ``tox -- --run-integration`` - Only run integration tests: ``tox -- --only-integration`` - Only run integration tests with parallel tasks: ``tox -- -n auto --only-integration`` - Only run unit tests against Python 3.9 with the module installed via wheel: ``tox -e py39-wheel`` We have CI testing, where we the test suite across all supported operating systems, and have test coverage reports. .. _tox documentation: https://tox.readthedocs.io/ build-0.7.0/pyproject.toml000066400000000000000000000006171412073167000155360ustar00rootroot00000000000000[build-system] requires = ['setuptools >= 40.8.0', 'wheel'] build-backend = 'setuptools.build_meta' [tool.black] line-length = 127 skip-string-normalization = true target-version = ['py39', 'py38', 'py37', 'py36'] [tool.isort] profile = "black" lines_between_types = 1 lines_after_imports = 2 line_length = 127 known_first_party = "build" skip = [] # "build" is included in the default skip list build-0.7.0/setup.cfg000066400000000000000000000047221412073167000144440ustar00rootroot00000000000000[metadata] name = build version = 0.7.0 description = A simple, correct PEP517 package builder long_description = file: README.md long_description_content_type = text/markdown author = Filipe Laíns author_email = lains@riseup.net license = MIT license_file = LICENSE classifiers = License :: OSI Approved :: MIT License Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy project_urls = homepage = https://github.com/pypa/build changelog = https://pypa-build.readthedocs.io/en/stable/changelog.html [options] packages = find: install_requires = packaging>=19.0 pep517>=0.9.1 tomli>=1.0.0 # toml can be used instead -- in case it makes bootstraping easier colorama;os_name == "nt" # not actually a runtime dependency, only supplied as there is not "recomended dependency" support importlib-metadata>=0.22;python_version < "3.8" python_requires = >=3.6 package_dir = =src [options.packages.find] where = src [options.entry_points] console_scripts = pyproject-build = build.__main__:entrypoint pipx.run = build = build.__main__:entrypoint [options.extras_require] docs = furo>=2020.11.19b18 sphinx~=3.0 sphinx-argparse-cli>=1.5 sphinx-autodoc-typehints>=1.10 test = filelock>=3 pytest>=6.2.4 pytest-cov>=2 pytest-mock>=2 pytest-rerunfailures>=9.1 pytest-xdist>=1.34 setuptools>=42.0.0 toml>=0.10.0 wheel>=0.36.0 typing = importlib-metadata>=4.6.4 mypy==0.910 typing-extensions>=3.7.4.3 virtualenv = virtualenv>=20.0.35 [options.package_data] build = py.typed [tool:pytest] addopts = --strict-config --strict-markers xfail_strict = True junit_family = xunit2 norecursedirs = tests/integration/* markers = isolated filterwarnings = error [flake8] max-line-length = 127 max-complexity = 10 extend-ignore = E203 [mypy] ignore_missing_imports = True strict = True [coverage:run] omit = setup.py *bin/pyproject-build *bin\pyproject-build.exe [coverage:report] exclude_lines = \#\s*pragma: no cover ^\s*raise NotImplementedError\b [coverage:paths] source = src */site-packages *\site-packages [coverage:html] show_contexts = true build-0.7.0/setup.py000066400000000000000000000000471412073167000143310ustar00rootroot00000000000000from setuptools import setup setup() build-0.7.0/src/000077500000000000000000000000001412073167000134055ustar00rootroot00000000000000build-0.7.0/src/build/000077500000000000000000000000001412073167000145045ustar00rootroot00000000000000build-0.7.0/src/build/__init__.py000066400000000000000000000400551412073167000166210ustar00rootroot00000000000000# SPDX-License-Identifier: MIT """ build - A simple, correct PEP 517 package builder """ __version__ = '0.7.0' import contextlib import difflib import logging import os import re import subprocess import sys import types import warnings import zipfile from collections import OrderedDict from typing import ( AbstractSet, Any, Callable, Dict, Iterator, Mapping, MutableMapping, Optional, Sequence, Set, Tuple, Type, Union, ) import pep517.wrappers TOMLDecodeError: Type[Exception] toml_loads: Callable[[str], MutableMapping[str, Any]] try: from tomli import TOMLDecodeError from tomli import loads as toml_loads except ModuleNotFoundError: # pragma: no cover from toml import TomlDecodeError as TOMLDecodeError # type: ignore from toml import loads as toml_loads # type: ignore RunnerType = Callable[[Sequence[str], Optional[str], Optional[Mapping[str, str]]], None] ConfigSettingsType = Mapping[str, Union[str, Sequence[str]]] _ExcInfoType = Union[Tuple[Type[BaseException], BaseException, types.TracebackType], Tuple[None, None, None]] _WHEEL_NAME_REGEX = re.compile( r'(?P.+)-(?P.+)' r'(-(?P.+))?-(?P.+)' r'-(?P.+)-(?P.+)\.whl' ) _DEFAULT_BACKEND = { 'build-backend': 'setuptools.build_meta:__legacy__', 'requires': ['setuptools >= 40.8.0', 'wheel'], } _logger = logging.getLogger('build') class BuildException(Exception): """ Exception raised by ProjectBuilder """ class BuildBackendException(Exception): """ Exception raised when the backend fails """ def __init__( self, exception: Exception, description: Optional[str] = None, exc_info: _ExcInfoType = (None, None, None) ) -> None: super().__init__() self.exception: Exception = exception self.exc_info: _ExcInfoType = exc_info self._description = description def __str__(self) -> str: if self._description: return self._description return f'Backend operation failed: {self.exception!r}' class TypoWarning(Warning): """ Warning raised when a potential typo is found """ def _validate_source_directory(srcdir: str) -> None: if not os.path.isdir(srcdir): raise BuildException(f'Source {srcdir} is not a directory') pyproject_toml = os.path.join(srcdir, 'pyproject.toml') setup_py = os.path.join(srcdir, 'setup.py') if not os.path.exists(pyproject_toml) and not os.path.exists(setup_py): raise BuildException(f'Source {srcdir} does not appear to be a Python project: no pyproject.toml or setup.py') def check_dependency( req_string: str, ancestral_req_strings: Tuple[str, ...] = (), parent_extras: AbstractSet[str] = frozenset() ) -> Iterator[Tuple[str, ...]]: """ Verify that a dependency and all of its dependencies are met. :param req_string: Requirement string :param parent_extras: Extras (eg. "test" in myproject[test]) :yields: Unmet dependencies """ import packaging.requirements if sys.version_info >= (3, 8): import importlib.metadata as importlib_metadata else: import importlib_metadata req = packaging.requirements.Requirement(req_string) if req.marker: extras = frozenset(('',)).union(parent_extras) # a requirement can have multiple extras but ``evaluate`` can # only check one at a time. if all(not req.marker.evaluate(environment={'extra': e}) for e in extras): # if the marker conditions are not met, we pretend that the # dependency is satisfied. return try: dist = importlib_metadata.distribution(req.name) # type: ignore[no-untyped-call] except importlib_metadata.PackageNotFoundError: # dependency is not installed in the environment. yield ancestral_req_strings + (req_string,) else: if req.specifier and not req.specifier.contains(dist.version, prereleases=True): # the installed version is incompatible. yield ancestral_req_strings + (req_string,) elif dist.requires: for other_req_string in dist.requires: # yields transitive dependencies that are not satisfied. yield from check_dependency(other_req_string, ancestral_req_strings + (req_string,), req.extras) def _find_typo(dictionary: Mapping[str, str], expected: str) -> None: if expected not in dictionary: for obj in dictionary: if difflib.SequenceMatcher(None, expected, obj).ratio() >= 0.8: warnings.warn( f"Found '{obj}' in pyproject.toml, did you mean '{expected}'?", TypoWarning, ) @contextlib.contextmanager def _working_directory(path: str) -> Iterator[None]: current = os.getcwd() os.chdir(path) try: yield finally: os.chdir(current) class ProjectBuilder: """ The PEP 517 consumer API. """ def __init__( self, srcdir: str, python_executable: str = sys.executable, scripts_dir: Optional[str] = None, runner: RunnerType = pep517.wrappers.default_subprocess_runner, ) -> None: """ :param srcdir: The source directory :param scripts_dir: The location of the scripts dir (defaults to the folder where the python executable lives) :param python_executable: The python executable where the backend lives :param runner: An alternative runner for backend subprocesses The 'runner', if provided, must accept the following arguments: - cmd: a list of strings representing the command and arguments to execute, as would be passed to e.g. 'subprocess.check_call'. - cwd: a string representing the working directory that must be used for the subprocess. Corresponds to the provided srcdir. - extra_environ: a dict mapping environment variable names to values which must be set for the subprocess execution. The default runner simply calls the backend hooks in a subprocess, writing backend output to stdout/stderr. """ self.srcdir: str = os.path.abspath(srcdir) _validate_source_directory(srcdir) spec_file = os.path.join(srcdir, 'pyproject.toml') try: with open(spec_file, 'rb') as f: spec = toml_loads(f.read().decode()) except FileNotFoundError: spec = {} except PermissionError as e: raise BuildException(f"{e.strerror}: '{e.filename}' ") except TOMLDecodeError as e: raise BuildException(f'Failed to parse {spec_file}: {e} ') build_system = spec.get('build-system') # if pyproject.toml is missing (per PEP 517) or [build-system] is missing (per PEP 518), # use default values. if build_system is None: _find_typo(spec, 'build-system') build_system = _DEFAULT_BACKEND # if [build-system] is present, it must have a ``requires`` field (per PEP 518). elif 'requires' not in build_system: _find_typo(build_system, 'requires') raise BuildException(f"Missing 'build-system.requires' in {spec_file}") # if ``build-backend`` is missing, inject the legacy setuptools backend # but leave ``requires`` alone to emulate pip. elif 'build-backend' not in build_system: _find_typo(build_system, 'build-backend') build_system['build-backend'] = _DEFAULT_BACKEND['build-backend'] self._build_system = build_system self._backend = self._build_system['build-backend'] self._scripts_dir = scripts_dir self._hook_runner = runner self._hook = pep517.wrappers.Pep517HookCaller( self.srcdir, self._backend, backend_path=self._build_system.get('backend-path'), python_executable=python_executable, runner=self._runner, ) def _runner( self, cmd: Sequence[str], cwd: Optional[str] = None, extra_environ: Optional[Mapping[str, str]] = None ) -> None: # if script dir is specified must be inserted at the start of PATH (avoid duplicate path while doing so) if self.scripts_dir is not None: paths: Dict[str, None] = OrderedDict() paths[str(self.scripts_dir)] = None if 'PATH' in os.environ: paths.update((i, None) for i in os.environ['PATH'].split(os.pathsep)) extra_environ = {} if extra_environ is None else dict(extra_environ) extra_environ['PATH'] = os.pathsep.join(paths) self._hook_runner(cmd, cwd, extra_environ) @property def python_executable(self) -> str: """ The Python executable used to invoke the backend. """ # make mypy happy exe: str = self._hook.python_executable return exe @python_executable.setter def python_executable(self, value: str) -> None: self._hook.python_executable = value @property def scripts_dir(self) -> Optional[str]: """ The folder where the scripts are stored for the python executable. """ return self._scripts_dir @scripts_dir.setter def scripts_dir(self, value: Optional[str]) -> None: self._scripts_dir = value @property def build_system_requires(self) -> Set[str]: """ The dependencies defined in the ``pyproject.toml``'s ``build-system.requires`` field or the default build dependencies if ``pyproject.toml`` is missing or ``build-system`` is undefined. """ return set(self._build_system['requires']) def get_requires_for_build(self, distribution: str, config_settings: Optional[ConfigSettingsType] = None) -> Set[str]: """ Return the dependencies defined by the backend in addition to :attr:`build_system_requires` for a given distribution. :param distribution: Distribution to get the dependencies of (``sdist`` or ``wheel``) :param config_settings: Config settings for the build backend """ self.log(f'Getting dependencies for {distribution}...') hook_name = f'get_requires_for_build_{distribution}' get_requires = getattr(self._hook, hook_name) with self._handle_backend(hook_name): return set(get_requires(config_settings)) def check_dependencies( self, distribution: str, config_settings: Optional[ConfigSettingsType] = None ) -> Set[Tuple[str, ...]]: """ Return the dependencies which are not satisfied from the combined set of :attr:`build_system_requires` and :meth:`get_requires_for_build` for a given distribution. :param distribution: Distribution to check (``sdist`` or ``wheel``) :param config_settings: Config settings for the build backend :returns: Set of variable-length unmet dependency tuples """ dependencies = self.get_requires_for_build(distribution, config_settings).union(self.build_system_requires) return {u for d in dependencies for u in check_dependency(d)} def prepare( self, distribution: str, output_directory: str, config_settings: Optional[ConfigSettingsType] = None ) -> Optional[str]: """ Prepare metadata for a distribution. :param distribution: Distribution to build (must be ``wheel``) :param output_directory: Directory to put the prepared metadata in :param config_settings: Config settings for the build backend :returns: The full path to the prepared metadata directory """ self.log(f'Getting metadata for {distribution}...') try: return self._call_backend( f'prepare_metadata_for_build_{distribution}', output_directory, config_settings, _allow_fallback=False, ) except BuildBackendException as exception: if isinstance(exception.exception, pep517.wrappers.HookMissing): return None raise def build( self, distribution: str, output_directory: str, config_settings: Optional[ConfigSettingsType] = None, metadata_directory: Optional[str] = None, ) -> str: """ Build a distribution. :param distribution: Distribution to build (``sdist`` or ``wheel``) :param output_directory: Directory to put the built distribution in :param config_settings: Config settings for the build backend :param metadata_directory: If provided, should be the return value of a previous ``prepare`` call on the same ``distribution`` kind :returns: The full path to the built distribution """ self.log(f'Building {distribution}...') kwargs = {} if metadata_directory is None else {'metadata_directory': metadata_directory} return self._call_backend(f'build_{distribution}', output_directory, config_settings, **kwargs) def metadata_path(self, output_directory: str) -> str: """ Generates the metadata directory of a distribution and returns its path. If the backend does not support the ``prepare_metadata_for_build_wheel`` hook, a wheel will be built and the metadata extracted. :param output_directory: Directory to put the metadata distribution in """ # prepare_metadata hook metadata = self.prepare('wheel', output_directory) if metadata is not None: return metadata # fallback to build_wheel hook wheel = self.build('wheel', output_directory) match = _WHEEL_NAME_REGEX.match(os.path.basename(wheel)) if not match: raise ValueError('Invalid wheel') distinfo = f"{match['distribution']}-{match['version']}.dist-info" member_prefix = f'{distinfo}/' with zipfile.ZipFile(wheel) as w: w.extractall( output_directory, (member for member in w.namelist() if member.startswith(member_prefix)), ) return os.path.join(output_directory, distinfo) def _call_backend( self, hook_name: str, outdir: str, config_settings: Optional[ConfigSettingsType] = None, **kwargs: Any ) -> str: outdir = os.path.abspath(outdir) callback = getattr(self._hook, hook_name) if os.path.exists(outdir): if not os.path.isdir(outdir): raise BuildException(f"Build path '{outdir}' exists and is not a directory") else: os.makedirs(outdir) with self._handle_backend(hook_name): basename: str = callback(outdir, config_settings, **kwargs) return os.path.join(outdir, basename) @contextlib.contextmanager def _handle_backend(self, hook: str) -> Iterator[None]: with _working_directory(self.srcdir): try: yield except pep517.wrappers.BackendUnavailable as exception: raise BuildBackendException( exception, f"Backend '{self._backend}' is not available.", sys.exc_info(), ) except subprocess.CalledProcessError as exception: raise BuildBackendException(exception, f'Backend subproccess exited when trying to invoke {hook}') except Exception as exception: raise BuildBackendException(exception, exc_info=sys.exc_info()) @staticmethod def log(message: str) -> None: """ Prints message The default implementation uses the logging module but this function can be overwritten by users to have a different implementation. :param msg: Message to output """ if sys.version_info >= (3, 8): _logger.log(logging.INFO, message, stacklevel=2) else: _logger.log(logging.INFO, message) __all__ = ( '__version__', 'ConfigSettingsType', 'RunnerType', 'BuildException', 'BuildBackendException', 'TypoWarning', 'check_dependency', 'ProjectBuilder', ) build-0.7.0/src/build/__main__.py000066400000000000000000000277111412073167000166060ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import argparse import contextlib import os import shutil import subprocess import sys import tarfile import tempfile import textwrap import traceback import warnings from typing import Dict, Iterable, Iterator, List, Optional, Sequence, TextIO, Type, Union import build from build import BuildBackendException, BuildException, ConfigSettingsType, ProjectBuilder from build.env import IsolatedEnvBuilder __all__ = ['build', 'main', 'main_parser'] _COLORS = { 'red': '\33[91m', 'green': '\33[92m', 'yellow': '\33[93m', 'bold': '\33[1m', 'dim': '\33[2m', 'underline': '\33[4m', 'reset': '\33[0m', } _NO_COLORS = {color: '' for color in _COLORS} def _init_colors() -> Dict[str, str]: if 'NO_COLOR' in os.environ: if 'FORCE_COLOR' in os.environ: warnings.warn('Both NO_COLOR and FORCE_COLOR environment variables are set, disabling color') return _NO_COLORS elif 'FORCE_COLOR' in os.environ or sys.stdout.isatty(): return _COLORS return _NO_COLORS _STYLES = _init_colors() def _showwarning( message: Union[Warning, str], category: Type[Warning], filename: str, lineno: int, file: Optional[TextIO] = None, line: Optional[str] = None, ) -> None: # pragma: no cover print('{yellow}WARNING{reset} {}'.format(message, **_STYLES)) def _setup_cli() -> None: warnings.showwarning = _showwarning try: import colorama except ModuleNotFoundError: pass else: colorama.init() # fix colors on windows def _error(msg: str, code: int = 1) -> None: # pragma: no cover """ Print an error message and exit. Will color the output when writing to a TTY. :param msg: Error message :param code: Error code """ print('{red}ERROR{reset} {}'.format(msg, **_STYLES)) exit(code) class _ProjectBuilder(ProjectBuilder): @staticmethod def log(message: str) -> None: print('{bold}* {}{reset}'.format(message, **_STYLES)) class _IsolatedEnvBuilder(IsolatedEnvBuilder): @staticmethod def log(message: str) -> None: print('{bold}* {}{reset}'.format(message, **_STYLES)) def _format_dep_chain(dep_chain: Sequence[str]) -> str: return ' -> '.join(dep.partition(';')[0].strip() for dep in dep_chain) def _build_in_isolated_env( builder: ProjectBuilder, outdir: str, distribution: str, config_settings: Optional[ConfigSettingsType] ) -> str: with _IsolatedEnvBuilder() as env: builder.python_executable = env.executable builder.scripts_dir = env.scripts_dir # first install the build dependencies env.install(builder.build_system_requires) # then get the extra required dependencies from the backend (which was installed in the call above :P) env.install(builder.get_requires_for_build(distribution)) return builder.build(distribution, outdir, config_settings or {}) def _build_in_current_env( builder: ProjectBuilder, outdir: str, distribution: str, config_settings: Optional[ConfigSettingsType], skip_dependency_check: bool = False, ) -> str: if not skip_dependency_check: missing = builder.check_dependencies(distribution) if missing: dependencies = ''.join('\n\t' + dep for deps in missing for dep in (deps[0], _format_dep_chain(deps[1:])) if dep) print() _error(f'Missing dependencies:{dependencies}') return builder.build(distribution, outdir, config_settings or {}) def _build( isolation: bool, builder: ProjectBuilder, outdir: str, distribution: str, config_settings: Optional[ConfigSettingsType], skip_dependency_check: bool, ) -> str: if isolation: return _build_in_isolated_env(builder, outdir, distribution, config_settings) else: return _build_in_current_env(builder, outdir, distribution, config_settings, skip_dependency_check) @contextlib.contextmanager def _handle_build_error() -> Iterator[None]: try: yield except BuildException as e: _error(str(e)) except BuildBackendException as e: if isinstance(e.exception, subprocess.CalledProcessError): print() else: if e.exc_info: tb_lines = traceback.format_exception( e.exc_info[0], e.exc_info[1], e.exc_info[2], limit=-1, ) tb = ''.join(tb_lines) else: tb = traceback.format_exc(-1) print('\n{dim}{}{reset}\n'.format(tb.strip('\n'), **_STYLES)) _error(str(e)) def _natural_language_list(elements: Sequence[str]) -> str: if len(elements) == 0: raise IndexError('no elements') elif len(elements) == 1: return elements[0] else: return '{} and {}'.format( ', '.join(elements[:-1]), elements[-1], ) def build_package( srcdir: str, outdir: str, distributions: Sequence[str], config_settings: Optional[ConfigSettingsType] = None, isolation: bool = True, skip_dependency_check: bool = False, ) -> Sequence[str]: """ Run the build process. :param srcdir: Source directory :param outdir: Output directory :param distribution: Distribution to build (sdist or wheel) :param config_settings: Configuration settings to be passed to the backend :param isolation: Isolate the build in a separate environment :param skip_dependency_check: Do not perform the dependency check """ built: List[str] = [] builder = _ProjectBuilder(srcdir) for distribution in distributions: out = _build(isolation, builder, outdir, distribution, config_settings, skip_dependency_check) built.append(os.path.basename(out)) return built def build_package_via_sdist( srcdir: str, outdir: str, distributions: Sequence[str], config_settings: Optional[ConfigSettingsType] = None, isolation: bool = True, skip_dependency_check: bool = False, ) -> Sequence[str]: """ Build a sdist and then the specified distributions from it. :param srcdir: Source directory :param outdir: Output directory :param distribution: Distribution to build (only wheel) :param config_settings: Configuration settings to be passed to the backend :param isolation: Isolate the build in a separate environment :param skip_dependency_check: Do not perform the dependency check """ if 'sdist' in distributions: raise ValueError('Only binary distributions are allowed but sdist was specified') builder = _ProjectBuilder(srcdir) sdist = _build(isolation, builder, outdir, 'sdist', config_settings, skip_dependency_check) sdist_name = os.path.basename(sdist) sdist_out = tempfile.mkdtemp(prefix='build-via-sdist-') built: List[str] = [] # extract sdist with tarfile.open(sdist) as t: t.extractall(sdist_out) try: builder = _ProjectBuilder(os.path.join(sdist_out, sdist_name[: -len('.tar.gz')])) if distributions: builder.log(f'Building {_natural_language_list(distributions)} from sdist') for distribution in distributions: out = _build(isolation, builder, outdir, distribution, config_settings, skip_dependency_check) built.append(os.path.basename(out)) finally: shutil.rmtree(sdist_out, ignore_errors=True) return [sdist_name] + built def main_parser() -> argparse.ArgumentParser: """ Construct the main parser. """ # mypy does not recognize module.__path__ # https://github.com/python/mypy/issues/1422 paths: Iterable[str] = build.__path__ # type: ignore parser = argparse.ArgumentParser( description=textwrap.indent( textwrap.dedent( ''' A simple, correct PEP 517 package builder. By default, a source distribution (sdist) is built from {srcdir} and a binary distribution (wheel) is built from the sdist. This is recommended as it will ensure the sdist can be used to build wheels. Pass -s/--sdist and/or -w/--wheel to build a specific distribution. If you do this, the default behavior will be disabled, and all artifacts will be built from {srcdir} (even if you combine -w/--wheel with -s/--sdist, the wheel will be built from {srcdir}). ''' ).strip(), ' ', ), formatter_class=argparse.RawTextHelpFormatter, ) parser.add_argument( 'srcdir', type=str, nargs='?', default=os.getcwd(), help='source directory (defaults to current directory)', ) parser.add_argument( '--version', '-V', action='version', version=f"build {build.__version__} ({','.join(paths)})", ) parser.add_argument( '--sdist', '-s', action='store_true', help='build a source distribution (disables the default behavior)', ) parser.add_argument( '--wheel', '-w', action='store_true', help='build a wheel (disables the default behavior)', ) parser.add_argument( '--outdir', '-o', type=str, help=f'output directory (defaults to {{srcdir}}{os.sep}dist)', ) parser.add_argument( '--skip-dependency-check', '-x', action='store_true', help='do not check that build dependencies are installed', ) parser.add_argument( '--no-isolation', '-n', action='store_true', help='do not isolate the build in a virtual environment', ) parser.add_argument( '--config-setting', '-C', action='append', help='pass options to the backend. options which begin with a hyphen must be in the form of ' '"--config-setting=--opt(=value)" or "-C--opt(=value)"', ) return parser def main(cli_args: Sequence[str], prog: Optional[str] = None) -> None: # noqa: C901 """ Parse the CLI arguments and invoke the build process. :param cli_args: CLI arguments :param prog: Program name to show in help text """ _setup_cli() parser = main_parser() if prog: parser.prog = prog args = parser.parse_args(cli_args) distributions = [] config_settings = {} if args.config_setting: for arg in args.config_setting: setting, _, value = arg.partition('=') if setting not in config_settings: config_settings[setting] = value else: if not isinstance(config_settings[setting], list): config_settings[setting] = [config_settings[setting]] config_settings[setting].append(value) if args.sdist: distributions.append('sdist') if args.wheel: distributions.append('wheel') # outdir is relative to srcdir only if omitted. outdir = os.path.join(args.srcdir, 'dist') if args.outdir is None else args.outdir if distributions: build_call = build_package else: build_call = build_package_via_sdist distributions = ['wheel'] try: with _handle_build_error(): built = build_call( args.srcdir, outdir, distributions, config_settings, not args.no_isolation, args.skip_dependency_check ) artifact_list = _natural_language_list( ['{underline}{}{reset}{bold}{green}'.format(artifact, **_STYLES) for artifact in built] ) print('{bold}{green}Successfully built {}{reset}'.format(artifact_list, **_STYLES)) except Exception as e: # pragma: no cover tb = traceback.format_exc().strip('\n') print('\n{dim}{}{reset}\n'.format(tb, **_STYLES)) _error(str(e)) def entrypoint() -> None: main(sys.argv[1:]) if __name__ == '__main__': # pragma: no cover main(sys.argv[1:], 'python -m build') build-0.7.0/src/build/env.py000066400000000000000000000251261412073167000156540ustar00rootroot00000000000000""" Creates and manages isolated build environments. """ import abc import functools import logging import os import platform import shutil import subprocess import sys import sysconfig import tempfile from types import TracebackType from typing import Callable, Iterable, List, Optional, Tuple, Type import packaging.requirements import packaging.version import build if sys.version_info < (3, 8): import importlib_metadata as metadata else: from importlib import metadata try: import virtualenv except ModuleNotFoundError: virtualenv = None _logger = logging.getLogger('build.env') class IsolatedEnv(metaclass=abc.ABCMeta): """Abstract base of isolated build environments, as required by the build project.""" @property @abc.abstractmethod def executable(self) -> str: """The executable of the isolated build environment.""" raise NotImplementedError @property @abc.abstractmethod def scripts_dir(self) -> str: """The scripts directory of the isolated build environment.""" raise NotImplementedError @abc.abstractmethod def install(self, requirements: Iterable[str]) -> None: """ Install packages from PEP 508 requirements in the isolated build environment. :param requirements: PEP 508 requirements """ raise NotImplementedError @functools.lru_cache(maxsize=None) def _should_use_virtualenv() -> bool: # virtualenv might be incompatible if it was installed separately # from build. This verifies that virtualenv and all of its # dependencies are installed as specified by build. return virtualenv is not None and not any( packaging.requirements.Requirement(d[1]).name == 'virtualenv' for d in build.check_dependency('build[virtualenv]') if len(d) > 1 ) def _subprocess(cmd: List[str]) -> None: """Invoke subprocess and output stdout and stderr if it fails.""" try: subprocess.check_output(cmd, stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: print(e.output.decode(), end='', file=sys.stderr) raise e class IsolatedEnvBuilder: """Builder object for isolated environments.""" def __init__(self) -> None: self._path: Optional[str] = None def __enter__(self) -> IsolatedEnv: """ Create an isolated build environment. :return: The isolated build environment """ self._path = tempfile.mkdtemp(prefix='build-env-') try: # use virtualenv when available (as it's faster than venv) if _should_use_virtualenv(): self.log('Creating virtualenv isolated environment...') executable, scripts_dir = _create_isolated_env_virtualenv(self._path) else: self.log('Creating venv isolated environment...') executable, scripts_dir = _create_isolated_env_venv(self._path) return _IsolatedEnvVenvPip( path=self._path, python_executable=executable, scripts_dir=scripts_dir, log=self.log, ) except Exception: # cleanup folder if creation fails self.__exit__(*sys.exc_info()) raise def __exit__( self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType] ) -> None: """ Delete the created isolated build environment. :param exc_type: The type of exception raised (if any) :param exc_val: The value of exception raised (if any) :param exc_tb: The traceback of exception raised (if any) """ if self._path is not None and os.path.exists(self._path): # in case the user already deleted skip remove shutil.rmtree(self._path) @staticmethod def log(message: str) -> None: """ Prints message The default implementation uses the logging module but this function can be overwritten by users to have a different implementation. :param msg: Message to output """ if sys.version_info >= (3, 8): _logger.log(logging.INFO, message, stacklevel=2) else: _logger.log(logging.INFO, message) class _IsolatedEnvVenvPip(IsolatedEnv): """ Isolated build environment context manager Non-standard paths injected directly to sys.path will still be passed to the environment. """ def __init__( self, path: str, python_executable: str, scripts_dir: str, log: Callable[[str], None], ) -> None: """ :param path: The path where the environment exists :param python_executable: The python executable within the environment :param log: Log function """ self._path = path self._python_executable = python_executable self._scripts_dir = scripts_dir self._log = log @property def path(self) -> str: """The location of the isolated build environment.""" return self._path @property def executable(self) -> str: """The python executable of the isolated build environment.""" return self._python_executable @property def scripts_dir(self) -> str: return self._scripts_dir def install(self, requirements: Iterable[str]) -> None: """ Install packages from PEP 508 requirements in the isolated build environment. :param requirements: PEP 508 requirement specification to install :note: Passing non-PEP 508 strings will result in undefined behavior, you *should not* rely on it. It is merely an implementation detail, it may change any time without warning. """ if not requirements: return self._log('Installing packages in isolated environment... ({})'.format(', '.join(sorted(requirements)))) # pip does not honour environment markers in command line arguments # but it does for requirements from a file with tempfile.NamedTemporaryFile('w+', prefix='build-reqs-', suffix='.txt', delete=False) as req_file: req_file.write(os.linesep.join(requirements)) try: cmd = [ self.executable, '-Im', 'pip', 'install', '--use-pep517', '--no-warn-script-location', '-r', os.path.abspath(req_file.name), ] _subprocess(cmd) finally: os.unlink(req_file.name) def _create_isolated_env_virtualenv(path: str) -> Tuple[str, str]: """ On Python 2 we use the virtualenv package to provision a virtual environment. :param path: The path where to create the isolated build environment :return: The Python executable and script folder """ cmd = [str(path), '--no-setuptools', '--no-wheel', '--activators', ''] result = virtualenv.cli_run(cmd, setup_logging=False) executable = str(result.creator.exe) script_dir = str(result.creator.script_dir) return executable, script_dir @functools.lru_cache(maxsize=None) def _fs_supports_symlink() -> bool: """Return True if symlinks are supported""" # Using definition used by venv.main() if os.name != 'nt': return True # Windows may support symlinks (setting in Windows 10) with tempfile.NamedTemporaryFile(prefix='build-symlink-') as tmp_file: dest = f'{tmp_file}-b' try: os.symlink(tmp_file.name, dest) os.unlink(dest) return True except (OSError, NotImplementedError, AttributeError): return False def _create_isolated_env_venv(path: str) -> Tuple[str, str]: """ On Python 3 we use the venv package from the standard library. :param path: The path where to create the isolated build environment :return: The Python executable and script folder """ import venv venv.EnvBuilder(with_pip=True, symlinks=_fs_supports_symlink()).create(path) executable, script_dir, purelib = _find_executable_and_scripts(path) # Get the version of pip in the environment pip_distribution = next(iter(metadata.distributions(name='pip', path=[purelib]))) # type: ignore[no-untyped-call] current_pip_version = packaging.version.Version(pip_distribution.version) if platform.system() == 'Darwin' and int(platform.mac_ver()[0].split('.')[0]) >= 11: # macOS 11+ name scheme change requires 20.3. Intel macOS 11.0 can be told to report 10.16 for backwards # compatibility; but that also fixes earlier versions of pip so this is only needed for 11+. is_apple_silicon_python = platform.machine() != 'x86_64' minimum_pip_version = '21.0.1' if is_apple_silicon_python else '20.3.0' else: # PEP-517 and manylinux1 was first implemented in 19.1 minimum_pip_version = '19.1.0' if current_pip_version < packaging.version.Version(minimum_pip_version): _subprocess([executable, '-m', 'pip', 'install', f'pip>={minimum_pip_version}']) # Avoid the setuptools from ensurepip to break the isolation _subprocess([executable, '-m', 'pip', 'uninstall', 'setuptools', '-y']) return executable, script_dir def _find_executable_and_scripts(path: str) -> Tuple[str, str, str]: """ Detect the Python executable and script folder of a virtual environment. :param path: The location of the virtual environment :return: The Python executable, script folder, and purelib folder """ config_vars = sysconfig.get_config_vars().copy() # globally cached, copy before altering it config_vars['base'] = path # The Python that ships with the macOS developer tools varies the # default scheme depending on whether the ``sys.prefix`` is part of a framework. # The framework "osx_framework_library" scheme # can't be used to expand the paths in a venv, which # can happen if build itself is not installed in a venv. # If the Apple-custom "osx_framework_library" scheme is available # we enforce "posix_prefix", the venv scheme, for isolated envs. if 'osx_framework_library' in sysconfig.get_scheme_names(): paths = sysconfig.get_paths(scheme='posix_prefix', vars=config_vars) else: paths = sysconfig.get_paths(vars=config_vars) executable = os.path.join(paths['scripts'], 'python.exe' if os.name == 'nt' else 'python') if not os.path.exists(executable): raise RuntimeError(f'Virtual environment creation failed, executable {executable} missing') return executable, paths['scripts'], paths['purelib'] __all__ = ( 'IsolatedEnvBuilder', 'IsolatedEnv', ) build-0.7.0/src/build/py.typed000066400000000000000000000000001412073167000161710ustar00rootroot00000000000000build-0.7.0/src/build/util.py000066400000000000000000000032441412073167000160360ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import os import pathlib import sys import tempfile from typing import Union import pep517 import build import build.env if sys.version_info >= (3, 8): import importlib.metadata as importlib_metadata else: import importlib_metadata def _project_wheel_metadata(builder: build.ProjectBuilder) -> 'importlib_metadata.PackageMetadata': with tempfile.TemporaryDirectory() as tmpdir: path = pathlib.Path(builder.metadata_path(tmpdir)) # https://github.com/python/importlib_metadata/pull/343 return importlib_metadata.PathDistribution(path).metadata # type: ignore def project_wheel_metadata( srcdir: Union[str, 'os.PathLike[str]'], isolated: bool = True, ) -> 'importlib_metadata.PackageMetadata': """ Return the wheel metadata for a project. Uses the ``prepare_metadata_for_build_wheel`` hook if availablable, otherwise ``build_wheel``. :param srcdir: Project source directory :param isolated: Wether or not to run invoke the backend in the current environment or to create an isolated one and invoke it there. """ builder = build.ProjectBuilder( os.fspath(srcdir), runner=pep517.quiet_subprocess_runner, ) if not isolated: return _project_wheel_metadata(builder) with build.env.IsolatedEnvBuilder() as env: builder.python_executable = env.executable builder.scripts_dir = env.scripts_dir env.install(builder.build_system_requires) env.install(builder.get_requires_for_build('wheel')) return _project_wheel_metadata(builder) __all__ = ('project_wheel_metadata',) build-0.7.0/tests/000077500000000000000000000000001412073167000137605ustar00rootroot00000000000000build-0.7.0/tests/conftest.py000066400000000000000000000140231412073167000161570ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import os import os.path import platform import shutil import stat import subprocess import sys import sysconfig import tempfile import pytest from filelock import FileLock def _build_and_reinstall_build(test_mode): temp = tempfile.mkdtemp() try: subprocess.check_output( [sys.executable, '-m', 'build', f'--{test_mode}', '--no-isolation', '--outdir', temp], ) dist_file = next(d for d in os.listdir(temp) if d.endswith('.whl' if test_mode == 'wheel' else '.tar.gz')) subprocess.check_call( [ sys.executable, '-m', 'pip', 'install', '--upgrade', # ``--upgrade`` will uninstall build prior to installing the ``dist_file`` os.path.join(temp, dist_file), ], ) finally: shutil.rmtree(temp) def _one_time_setup(): test_mode = os.environ.get('TEST_MODE') if not test_mode: return if test_mode == 'path': project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.join(project_root, 'src')) elif test_mode in {'sdist', 'wheel'}: status_marker_file = os.path.join(os.environ['TEST_STATUS_DIR'], 'status-marker') with FileLock(status_marker_file + '.lock'): if not os.path.exists(status_marker_file): _build_and_reinstall_build(test_mode) with open(status_marker_file, 'wb'): pass _one_time_setup() def pytest_addoption(parser): os.environ['PYTHONWARNINGS'] = 'ignore:DEPRECATION::pip._internal.cli.base_command' # for when not run within tox os.environ['PIP_DISABLE_PIP_VERSION_CHECK'] = '1' # do not pollute stderr with upgrade advisory parser.addoption('--run-integration', action='store_true', help='run the integration tests') parser.addoption('--only-integration', action='store_true', help='only run the integration tests') PYPY3_WIN_VENV_BAD = platform.python_implementation() == 'PyPy' and os.name == 'nt' PYPY3_WIN_M = 'https://foss.heptapod.net/pypy/pypy/-/issues/3323 and https://foss.heptapod.net/pypy/pypy/-/issues/3321' def pytest_collection_modifyitems(config, items): skip_int = pytest.mark.skip(reason='integration tests not run (no --run-integration flag)') skip_other = pytest.mark.skip(reason='only integration tests are run (got --only-integration flag)') if config.getoption('--run-integration') and config.getoption('--only-integration'): # pragma: no cover raise pytest.UsageError("--run-integration and --only-integration can't be used together, choose one") if len(items) == 1: # do not require flags if called directly return for item in items: is_integration_file = is_integration(item) if PYPY3_WIN_VENV_BAD and item.get_closest_marker('isolated'): if not (is_integration_file and item.originalname == 'test_build') or ( hasattr(item, 'callspec') and '--no-isolation' not in item.callspec.params.get('args', []) ): item.add_marker(pytest.mark.xfail(reason=PYPY3_WIN_M, strict=True)) if is_integration_file: # pragma: no cover if not config.getoption('--run-integration') and not config.getoption('--only-integration'): item.add_marker(skip_int) elif config.getoption('--only-integration'): # pragma: no cover item.add_marker(skip_other) # run integration tests after unit tests items.sort(key=lambda i: 1 if is_integration(i) else 0) def is_integration(item): return os.path.basename(item.location[0]) == 'test_integration.py' @pytest.fixture(scope='session', autouse=True) def ensure_syconfig_vars_created(): # the config vars are globally cached and may use get_path, make sure they are created sysconfig.get_config_vars() @pytest.fixture def packages_path(): return os.path.realpath(os.path.join(__file__, '..', 'packages')) @pytest.fixture def legacy_path(packages_path): return os.path.join(packages_path, 'legacy') @pytest.fixture def test_flit_path(packages_path): return os.path.join(packages_path, 'test-flit') @pytest.fixture def test_bad_syntax_path(packages_path): return os.path.join(packages_path, 'test-bad-syntax') @pytest.fixture def test_no_backend_path(packages_path): return os.path.join(packages_path, 'test-no-backend') @pytest.fixture def test_no_project_path(packages_path): return os.path.join(packages_path, 'test-no-project') @pytest.fixture def test_no_requires_path(packages_path): return os.path.join(packages_path, 'test-no-requires') @pytest.fixture def test_optional_hooks_path(packages_path): return os.path.join(packages_path, 'test-optional-hooks') @pytest.fixture def test_typo(packages_path): return os.path.join(packages_path, 'test-typo') @pytest.fixture def test_no_prepare_path(packages_path): return os.path.join(packages_path, 'test-no-prepare') @pytest.fixture def test_setuptools_path(packages_path): return os.path.join(packages_path, 'test-setuptools') @pytest.fixture def test_bad_wheel_path(packages_path): return os.path.join(packages_path, 'test-bad-wheel') @pytest.fixture def test_cant_build_via_sdist_path(packages_path): return os.path.join(packages_path, 'test-cant-build-via-sdist') @pytest.fixture def test_invalid_requirements_path(packages_path): return os.path.join(packages_path, 'test-invalid-requirements') @pytest.fixture def test_metadata(packages_path): return os.path.join(packages_path, 'test-metadata') @pytest.fixture def test_no_permission(packages_path): path = os.path.join(packages_path, 'test-no-permission') file = os.path.join(path, 'pyproject.toml') orig_stat = os.stat(file).st_mode os.chmod(file, ~stat.S_IRWXU) yield os.path.join(packages_path, 'test-no-permission') os.chmod(file, orig_stat) @pytest.fixture def tmp_dir(): path = tempfile.mkdtemp(prefix='python-build-test-') yield path shutil.rmtree(path) build-0.7.0/tests/constraints.txt000066400000000000000000000003271412073167000170720ustar00rootroot00000000000000importlib-metadata==0.22;python_version < "3.8" packaging==19.0 pep517==0.9.1 toml==0.10.0 tomli==1.0.0 typing==3.5.3.0;python_version < "3" virtualenv==20.0.35;python_version < "3" setuptools==42.0.0 wheel==0.36.0 build-0.7.0/tests/packages/000077500000000000000000000000001412073167000155365ustar00rootroot00000000000000build-0.7.0/tests/packages/inline/000077500000000000000000000000001412073167000170145ustar00rootroot00000000000000build-0.7.0/tests/packages/inline/build.py000066400000000000000000000032361412073167000204710ustar00rootroot00000000000000import os import sys 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' metadata = f'{dist_info}/METADATA' wheel = f'{dist_info}/WHEEL' entry_points = f'{dist_info}/entry_points.txt' record = f'{dist_info}/RECORD' init = f'{name}/__init__.py' content = { init: f"def do():\n print('greetings from {name}')", metadata: f""" Metadata-Version: 2.1 Name: {pkg_name} Version: {version} Summary: Summary of package Home-page: Does not exists Author: someone Author-email: a@o.com License: MIT Platform: ANY Desc """, wheel: f""" Wheel-Version: 1.0 Generator: {name}-{version} Root-Is-Purelib: true Tag: py3-none-any """, f'{dist_info}/top_level.txt': name, entry_points: '\n[console_scripts]\ndemo-pkg-inline = demo_pkg_inline:do', record: f""" {name}/__init__.py,, {dist_info}/METADATA,, {dist_info}/WHEEL,, {dist_info}/top_level.txt,, {dist_info}/RECORD,, """, } def build_wheel(wheel_directory, metadata_directory=None, config_settings=None): base_name = f'{name}-{version}-py{sys.version_info.major}-none-any.whl' path = os.path.join(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()) print(f'created wheel {path}') return base_name def get_requires_for_build_wheel(config_settings): return [] build-0.7.0/tests/packages/inline/pyproject.toml000066400000000000000000000001121412073167000217220ustar00rootroot00000000000000[build-system] requires = [] build-backend = "build" backend-path = ["."] build-0.7.0/tests/packages/legacy/000077500000000000000000000000001412073167000170025ustar00rootroot00000000000000build-0.7.0/tests/packages/legacy/legacy/000077500000000000000000000000001412073167000202465ustar00rootroot00000000000000build-0.7.0/tests/packages/legacy/legacy/__init__.py000066400000000000000000000001361412073167000223570ustar00rootroot00000000000000# SPDX-License-Identifier: MIT """ legacy - Example legacy package """ __version__ = '1.0.0' build-0.7.0/tests/packages/legacy/setup.py000066400000000000000000000005571412073167000205230ustar00rootroot00000000000000#!/usr/bin/env python from setuptools import setup setup( name='legacy', version='1.0.0', author='Filipe Laíns', author_email='lains@archlinux.org', classifiers=[ 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', ], packages=['legacy'], ) build-0.7.0/tests/packages/test-bad-backend/000077500000000000000000000000001412073167000206265ustar00rootroot00000000000000build-0.7.0/tests/packages/test-bad-backend/pyproject.toml000066400000000000000000000001001412073167000235310ustar00rootroot00000000000000[build-system] requires = [] build-backend = "nonsense_package" build-0.7.0/tests/packages/test-bad-syntax/000077500000000000000000000000001412073167000205655ustar00rootroot00000000000000build-0.7.0/tests/packages/test-bad-syntax/pyproject.toml000066400000000000000000000000531412073167000234770ustar00rootroot00000000000000[build-system] requires = ['bad' 'syntax'] build-0.7.0/tests/packages/test-bad-wheel/000077500000000000000000000000001412073167000203435ustar00rootroot00000000000000build-0.7.0/tests/packages/test-bad-wheel/backend_bad_wheel.py000066400000000000000000000003101412073167000242700ustar00rootroot00000000000000# SPDX-License-Identifier: MIT from setuptools.build_meta import build_sdist # noqa: F401 def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): return 'not a wheel' build-0.7.0/tests/packages/test-bad-wheel/pyproject.toml000066400000000000000000000001541412073167000232570ustar00rootroot00000000000000[build-system] build-backend = 'backend_bad_wheel' backend-path = ['.'] requires = ['setuptools >= 42.0.0'] build-0.7.0/tests/packages/test-bad-wheel/setup.cfg000066400000000000000000000000611412073167000221610ustar00rootroot00000000000000[metadata] name = test_bad_wheel version = 0.0.1 build-0.7.0/tests/packages/test-cant-build-via-sdist/000077500000000000000000000000001412073167000224365ustar00rootroot00000000000000build-0.7.0/tests/packages/test-cant-build-via-sdist/backend_bad_sdist.py000066400000000000000000000015031412073167000264120ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import os.path import tarfile import zipfile def build_sdist(sdist_directory, config_settings=None): name = 'test_cant_build_via_sdist-1.0.0' file = f'{name}.tar.gz' with tarfile.open(os.path.join(sdist_directory, file), 'w') as t: t.add('pyproject.toml', f'{name}/pyproject.toml') t.add('backend_bad_sdist.py', f'{name}/backend_bad_sdist.py') return file def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): if not os.path.isfile('some-file-that-is-needed-for-build.txt'): raise FileNotFoundError('some-file-that-is-needed-for-build.txt is missing!') # pragma: no cover file = 'test_cant_build_via_sdist-1.0.0-py2.py3-none-any.whl' zipfile.ZipFile(os.path.join(wheel_directory, file), 'w').close() return file build-0.7.0/tests/packages/test-cant-build-via-sdist/pyproject.toml000066400000000000000000000001261412073167000253510ustar00rootroot00000000000000[build-system] build-backend = 'backend_bad_sdist' backend-path = ['.'] requires = [] build-0.7.0/tests/packages/test-cant-build-via-sdist/some-file-that-is-needed-for-build.txt000066400000000000000000000000001412073167000315170ustar00rootroot00000000000000build-0.7.0/tests/packages/test-flit/000077500000000000000000000000001412073167000174515ustar00rootroot00000000000000build-0.7.0/tests/packages/test-flit/pyproject.toml000066400000000000000000000005251412073167000223670ustar00rootroot00000000000000[build-system] requires = ['flit_core >=2,<3'] build-backend = 'flit_core.buildapi' [tool.flit.metadata] module = 'test_flit' author = 'Filipe Laíns' author-email = 'lains@archlinux.org' classifiers = [ 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', ] build-0.7.0/tests/packages/test-flit/test_flit/000077500000000000000000000000001412073167000214465ustar00rootroot00000000000000build-0.7.0/tests/packages/test-flit/test_flit/__init__.py000066400000000000000000000001371412073167000235600ustar00rootroot00000000000000# SPDX-License-Identifier: MIT """ test_flit - Example flit package """ __version__ = '1.0.0' build-0.7.0/tests/packages/test-invalid-requirements/000077500000000000000000000000001412073167000226625ustar00rootroot00000000000000build-0.7.0/tests/packages/test-invalid-requirements/pyproject.toml000066400000000000000000000002011412073167000255670ustar00rootroot00000000000000[build-system] requires = ['setuptools >= 42.0.0', 'wheel >= 0.36.0', 'this is invalid'] build-backend = 'setuptools.build_meta' build-0.7.0/tests/packages/test-invalid-requirements/setup.cfg000066400000000000000000000000741412073167000245040ustar00rootroot00000000000000[metadata] name = test_invalid_requirements version = 1.0.0 build-0.7.0/tests/packages/test-metadata/000077500000000000000000000000001412073167000202735ustar00rootroot00000000000000build-0.7.0/tests/packages/test-metadata/backend.py000066400000000000000000000021471412073167000222400ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import pathlib import textwrap def get_requires_for_build_wheel(config_settings=None): return ['tomli'] def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None): import tomli with open('pyproject.toml', 'rb') as f: metadata = tomli.load(f) distinfo = pathlib.Path( metadata_directory, '{}-{}.dist-info'.format( metadata['project']['name'].replace('-', '-'), metadata['project']['version'], ), ) distinfo.mkdir(parents=True, exist_ok=True) distinfo.joinpath('METADATA').write_text( textwrap.dedent( f''' Metadata-Version: 2.2 Name: {metadata['project']['name']} Version: {metadata['project']['version']} Summary: {metadata['project']['description']} ''' ).strip() ) return distinfo.name def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): raise NotImplementedError def build_sdist(sdist_directory, config_settings=None): raise NotImplementedError build-0.7.0/tests/packages/test-metadata/pyproject.toml000066400000000000000000000004121412073167000232040ustar00rootroot00000000000000[build-system] requires = [] build-backend = 'backend' backend-path = ['.'] [project] name = 'test-metadata' version = '1.0.0' description = 'hello!' [tool.black] line-length = 127 skip-string-normalization = true target-version = ['py39', 'py38', 'py37', 'py36'] build-0.7.0/tests/packages/test-no-backend/000077500000000000000000000000001412073167000205145ustar00rootroot00000000000000build-0.7.0/tests/packages/test-no-backend/pyproject.toml000066400000000000000000000000351412073167000234260ustar00rootroot00000000000000[build-system] requires = [] build-0.7.0/tests/packages/test-no-permission/000077500000000000000000000000001412073167000213155ustar00rootroot00000000000000build-0.7.0/tests/packages/test-no-permission/pyproject.toml000066400000000000000000000000001412073167000242170ustar00rootroot00000000000000build-0.7.0/tests/packages/test-no-prepare/000077500000000000000000000000001412073167000205635ustar00rootroot00000000000000build-0.7.0/tests/packages/test-no-prepare/backend_no_prepare.py000066400000000000000000000001511412073167000247330ustar00rootroot00000000000000# SPDX-License-Identifier: MIT from setuptools.build_meta import build_sdist, build_wheel # noqa: F401 build-0.7.0/tests/packages/test-no-prepare/pyproject.toml000066400000000000000000000002001412073167000234670ustar00rootroot00000000000000[build-system] build-backend = 'backend_no_prepare' backend-path = ['.'] requires = ['setuptools >= 42.0.0', 'wheel >= 0.36.0'] build-0.7.0/tests/packages/test-no-prepare/setup.cfg000066400000000000000000000000621412073167000224020ustar00rootroot00000000000000[metadata] name = test_no_prepare version = 1.0.0 build-0.7.0/tests/packages/test-no-project/000077500000000000000000000000001412073167000205735ustar00rootroot00000000000000build-0.7.0/tests/packages/test-no-project/empty.txt000066400000000000000000000000001412073167000224600ustar00rootroot00000000000000build-0.7.0/tests/packages/test-no-requires/000077500000000000000000000000001412073167000207645ustar00rootroot00000000000000build-0.7.0/tests/packages/test-no-requires/pyproject.toml000066400000000000000000000000531412073167000236760ustar00rootroot00000000000000[build-system] build-backend = 'something' build-0.7.0/tests/packages/test-optional-hooks/000077500000000000000000000000001412073167000214615ustar00rootroot00000000000000build-0.7.0/tests/packages/test-optional-hooks/hookless_backend.py000066400000000000000000000000001412073167000253170ustar00rootroot00000000000000build-0.7.0/tests/packages/test-optional-hooks/pyproject.toml000066400000000000000000000001251412073167000243730ustar00rootroot00000000000000[build-system] requires = [] build-backend = 'hookless_backend' backend-path = ['.'] build-0.7.0/tests/packages/test-setuptools/000077500000000000000000000000001412073167000207345ustar00rootroot00000000000000build-0.7.0/tests/packages/test-setuptools/pyproject.toml000066400000000000000000000001561412073167000236520ustar00rootroot00000000000000[build-system] requires = ['setuptools >= 42.0.0', 'wheel >= 0.36.0'] build-backend = 'setuptools.build_meta' build-0.7.0/tests/packages/test-setuptools/setup.cfg000066400000000000000000000001171412073167000225540ustar00rootroot00000000000000[metadata] name = test_setuptools version = 1.0.0 [bdist_wheel] universal = 1 build-0.7.0/tests/packages/test-typo/000077500000000000000000000000001412073167000175065ustar00rootroot00000000000000build-0.7.0/tests/packages/test-typo/pyproject.toml000066400000000000000000000001431412073167000224200ustar00rootroot00000000000000[build_sytem] requires = ['setuptools >= 40.8.0', 'wheel'] build-backend = 'setuptools.build_meta' build-0.7.0/tests/test_env.py000066400000000000000000000137071412073167000161710ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import collections import logging import os import platform import shutil import subprocess import sys import sysconfig import pytest from packaging.version import Version import build.env IS_PYPY3 = platform.python_implementation() == 'PyPy' @pytest.mark.isolated def test_isolation(): subprocess.check_call([sys.executable, '-c', 'import build.env']) with build.env.IsolatedEnvBuilder() as env: with pytest.raises(subprocess.CalledProcessError): debug = 'import sys; import os; print(os.linesep.join(sys.path));' subprocess.check_call([env.executable, '-c', f'{debug} import build.env']) @pytest.mark.isolated def test_isolated_environment_install(mocker): with build.env.IsolatedEnvBuilder() as env: mocker.patch('build.env._subprocess') env.install([]) build.env._subprocess.assert_not_called() env.install(['some', 'requirements']) build.env._subprocess.assert_called() args = build.env._subprocess.call_args[0][0][:-1] assert args == [ env.executable, '-Im', 'pip', 'install', '--use-pep517', '--no-warn-script-location', '-r', ] @pytest.mark.skipif(IS_PYPY3, reason='PyPy3 uses get path to create and provision venv') @pytest.mark.skipif(sys.platform != 'darwin', reason='workaround for Apple Python') def test_can_get_venv_paths_with_conflicting_default_scheme(mocker): mocker.patch.object(build.env, '_should_use_virtualenv', lambda: False) get_scheme_names = mocker.patch('sysconfig.get_scheme_names', return_value=('osx_framework_library',)) with build.env.IsolatedEnvBuilder(): pass assert get_scheme_names.call_count == 1 @pytest.mark.skipif(IS_PYPY3, reason='PyPy3 uses get path to create and provision venv') def test_executable_missing_post_creation(mocker): mocker.patch.object(build.env, '_should_use_virtualenv', lambda: False) original_get_paths = sysconfig.get_paths def _get_paths(vars): # noqa shutil.rmtree(vars['base']) return original_get_paths(vars=vars) get_paths = mocker.patch('sysconfig.get_paths', side_effect=_get_paths) with pytest.raises(RuntimeError, match='Virtual environment creation failed, executable .* missing'): with build.env.IsolatedEnvBuilder(): pass assert get_paths.call_count == 1 def test_isolated_env_abstract(): with pytest.raises(TypeError): build.env.IsolatedEnv() def test_isolated_env_has_executable_still_abstract(): class Env(build.env.IsolatedEnv): # noqa @property def executable(self): raise NotImplementedError with pytest.raises(TypeError): Env() def test_isolated_env_has_install_still_abstract(): class Env(build.env.IsolatedEnv): # noqa def install(self, requirements): raise NotImplementedError with pytest.raises(TypeError): Env() def test_isolated_env_log(mocker, caplog, test_flit_path): mocker.patch('build.env._subprocess') caplog.set_level(logging.DEBUG) builder = build.env.IsolatedEnvBuilder() builder.log('something') with builder as env: env.install(['something']) assert [(record.levelname, record.message) for record in caplog.records] == [ ('INFO', 'something'), ('INFO', 'Creating venv isolated environment...'), ('INFO', 'Installing packages in isolated environment... (something)'), ] if sys.version_info >= (3, 8): # stacklevel assert [(record.lineno) for record in caplog.records] == [107, 103, 194] @pytest.mark.isolated def test_default_pip_is_never_too_old(): with build.env.IsolatedEnvBuilder() as env: version = subprocess.check_output( [env.executable, '-c', 'import pip; print(pip.__version__)'], universal_newlines=True ).strip() assert Version(version) >= Version('19.1') @pytest.mark.isolated @pytest.mark.parametrize('pip_version', ['20.2.0', '20.3.0', '21.0.0', '21.0.1']) @pytest.mark.parametrize('arch', ['x86_64', 'arm64']) def test_pip_needs_upgrade_mac_os_11(mocker, pip_version, arch): SimpleNamespace = collections.namedtuple('SimpleNamespace', 'version') _subprocess = mocker.patch('build.env._subprocess') mocker.patch('platform.system', return_value='Darwin') mocker.patch('platform.machine', return_value=arch) mocker.patch('platform.mac_ver', return_value=('11.0', ('', '', ''), '')) mocker.patch('build.env.metadata.distributions', return_value=(SimpleNamespace(version=pip_version),)) mocker.patch.object(build.env, '_should_use_virtualenv', lambda: False) min_version = Version('20.3' if arch == 'x86_64' else '21.0.1') with build.env.IsolatedEnvBuilder(): if Version(pip_version) < min_version: print(_subprocess.call_args_list) upgrade_call, uninstall_call = _subprocess.call_args_list answer = 'pip>=20.3.0' if arch == 'x86_64' else 'pip>=21.0.1' assert upgrade_call[0][0][1:] == ['-m', 'pip', 'install', answer] assert uninstall_call[0][0][1:] == ['-m', 'pip', 'uninstall', 'setuptools', '-y'] else: (uninstall_call,) = _subprocess.call_args_list assert uninstall_call[0][0][1:] == ['-m', 'pip', 'uninstall', 'setuptools', '-y'] @pytest.mark.isolated @pytest.mark.skipif(IS_PYPY3 and os.name == 'nt', reason='Isolated tests not supported on PyPy3 + Windows') @pytest.mark.parametrize('has_symlink', [True, False] if os.name == 'nt' else [True]) def test_venv_symlink(mocker, has_symlink): if has_symlink: mocker.patch('os.symlink') mocker.patch('os.unlink') else: mocker.patch('os.symlink', side_effect=OSError()) # Cache must be cleared to rerun build.env._fs_supports_symlink.cache_clear() supports_symlink = build.env._fs_supports_symlink() build.env._fs_supports_symlink.cache_clear() assert supports_symlink is has_symlink build-0.7.0/tests/test_integration.py000066400000000000000000000107471412073167000177250ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import os import os.path import platform import re import shutil import subprocess import sys import tarfile import urllib.request import filelock import pytest import build.__main__ IS_WINDOWS = os.name == 'nt' IS_PYPY3 = platform.python_implementation() == 'PyPy' INTEGRATION_SOURCES = { 'dateutil': ('dateutil/dateutil', '2.8.1'), 'pip': ('pypa/pip', '20.2.1'), 'Solaar': ('pwr-Solaar/Solaar', '1.0.3'), 'flit': ('takluyver/flit', '2.3.0'), } _SDIST = re.compile('.*.tar.gz') _WHEEL = re.compile('.*.whl') ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) def get_project(name, tmp_path): dest = tmp_path / name if name == 'build': # our own project is available in-source, just ignore development files def _ignore_folder(base, filenames): ignore = [n for n in filenames if n in excl or any(n.endswith(i) for i in ('_cache', '.egg-info', '.pyc'))] if os.path.basename == ROOT and 'build' in filenames: # ignore build only at root (our module is build too) ignore.append('build') return ignore excl = '.tox', 'dist', '.git', '__pycache__', '.integration-sources', '.github', 'tests', 'docs' shutil.copytree(ROOT, str(dest), ignore=_ignore_folder) return dest # for other projects download from github and cache it tar_store = os.path.join(ROOT, '.integration-sources') try: os.makedirs(tar_store) except OSError: # python 2 has no exist_ok, and checking with exists is not parallel safe pass # just ignore, if the creation failed we will have another failure soon that will notify the user github_org_repo, version = INTEGRATION_SOURCES[name] tar_filename = f'{name}-{version}.tar.gz' tarball = os.path.join(tar_store, tar_filename) with filelock.FileLock(os.path.join(tar_store, f'{tar_filename}.lock')): if not os.path.exists(tarball): url = f'https://github.com/{github_org_repo}/archive/{version}.tar.gz' with urllib.request.urlopen(url) as request, open(tarball, 'wb') as file_handler: shutil.copyfileobj(request, file_handler) with tarfile.open(tarball, 'r:gz') as tar_handler: tar_handler.extractall(str(dest)) return dest / f'{name}-{version}' @pytest.mark.parametrize( 'call', [ None, # via code [sys.executable, '-m', 'build'], # module ['pyproject-build'], # entrypoint ], ids=['code', 'module', 'entrypoint'], ) @pytest.mark.parametrize( 'args', [[], ['-x', '--no-isolation']], ids=['isolated', 'no_isolation'], ) @pytest.mark.parametrize( 'project', [ 'build', 'pip', 'dateutil', 'Solaar', 'flit', ], ) @pytest.mark.isolated def test_build(monkeypatch, project, args, call, tmp_path): if project == 'flit' and '--no-isolation' in args: pytest.xfail("can't build flit without isolation due to missing dependencies") if project == 'Solaar' and IS_WINDOWS and IS_PYPY3: pytest.xfail('Solaar fails building wheels via sdists on Windows on PyPy 3') monkeypatch.chdir(tmp_path) monkeypatch.setenv('SETUPTOOLS_SCM_PRETEND_VERSION', '0+dummy') # for the projects that use setuptools_scm if call and call[0] == 'pyproject-build': exe_name = f"pyproject-build{'.exe' if os.name == 'nt' else ''}" exe = os.path.join(os.path.dirname(sys.executable), exe_name) if os.path.exists(exe): call[0] = exe else: pytest.skip('Running via PYTHONPATH, so the pyproject-build entrypoint is not available') path = get_project(project, tmp_path) pkgs = tmp_path / 'pkgs' args = [str(path), '-o', str(pkgs)] + args if call is None: build.__main__.main(args) else: subprocess.check_call(call + args) pkg_names = os.listdir(str(pkgs)) assert list(filter(_SDIST.match, pkg_names)) assert list(filter(_WHEEL.match, pkg_names)) def test_isolation(tmp_dir, test_flit_path, mocker): try: # if flit is available, we can't properly test the isolation - skip the test in those cases import flit_core # noqa: F401 pytest.xfail('flit_core is available') # pragma: no cover except: # noqa: E722 pass mocker.patch('build.__main__._error') build.__main__.main([test_flit_path, '-o', tmp_dir, '--no-isolation']) build.__main__._error.assert_called_with("Backend 'flit_core.buildapi' is not available.") build-0.7.0/tests/test_main.py000066400000000000000000000316771412073167000163330ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import contextlib import importlib import io import os import re import sys import pytest import build import build.__main__ build_open_owner = 'builtins' cwd = os.getcwd() out = os.path.join(cwd, 'dist') @pytest.mark.parametrize( ('cli_args', 'build_args', 'hook'), [ ( [], [cwd, out, ['wheel'], {}, True, False], 'build_package_via_sdist', ), ( ['-n'], [cwd, out, ['wheel'], {}, False, False], 'build_package_via_sdist', ), ( ['-s'], [cwd, out, ['sdist'], {}, True, False], 'build_package', ), ( ['-w'], [cwd, out, ['wheel'], {}, True, False], 'build_package', ), ( ['-s', '-w'], [cwd, out, ['sdist', 'wheel'], {}, True, False], 'build_package', ), ( ['source'], ['source', os.path.join('source', 'dist'), ['wheel'], {}, True, False], 'build_package_via_sdist', ), ( ['-o', 'out'], [cwd, 'out', ['wheel'], {}, True, False], 'build_package_via_sdist', ), ( ['source', '-o', 'out'], ['source', 'out', ['wheel'], {}, True, False], 'build_package_via_sdist', ), ( ['-x'], [cwd, out, ['wheel'], {}, True, True], 'build_package_via_sdist', ), ( ['-C--flag1', '-C--flag2'], [cwd, out, ['wheel'], {'--flag1': '', '--flag2': ''}, True, False], 'build_package_via_sdist', ), ( ['-C--flag=value'], [cwd, out, ['wheel'], {'--flag': 'value'}, True, False], 'build_package_via_sdist', ), ( ['-C--flag1=value', '-C--flag2=other_value', '-C--flag2=extra_value'], [cwd, out, ['wheel'], {'--flag1': 'value', '--flag2': ['other_value', 'extra_value']}, True, False], 'build_package_via_sdist', ), ], ) def test_parse_args(mocker, cli_args, build_args, hook): mocker.patch('build.__main__.build_package', return_value=['something']) mocker.patch('build.__main__.build_package_via_sdist', return_value=['something']) build.__main__.main(cli_args) if hook == 'build_package': build.__main__.build_package.assert_called_with(*build_args) elif hook == 'build_package_via_sdist': build.__main__.build_package_via_sdist.assert_called_with(*build_args) else: raise ValueError(f'Unknown hook {hook}') # pragma: no cover def test_prog(): out = io.StringIO() with pytest.raises(SystemExit): with contextlib.redirect_stdout(out): build.__main__.main(['--help'], prog='something') assert out.getvalue().startswith('usage: something [-h]') def test_version(capsys): with pytest.raises(SystemExit): build.__main__.main(['--version']) out, err = capsys.readouterr() assert out.startswith(f'build {build.__version__}') @pytest.mark.isolated def test_build_isolated(mocker, test_flit_path): build_cmd = mocker.patch('build.ProjectBuilder.build', return_value='something') required_cmd = mocker.patch( 'build.ProjectBuilder.get_requires_for_build', side_effect=[ ['dep1', 'dep2'], ], ) mocker.patch('build.__main__._error') install = mocker.patch('build.env._IsolatedEnvVenvPip.install') build.__main__.build_package(test_flit_path, '.', ['sdist']) install.assert_any_call({'flit_core >=2,<3'}) required_cmd.assert_called_with('sdist') install.assert_any_call(['dep1', 'dep2']) build_cmd.assert_called_with('sdist', '.', {}) def test_build_no_isolation_check_deps_empty(mocker, test_flit_path): # check_dependencies = [] build_cmd = mocker.patch('build.ProjectBuilder.build', return_value='something') mocker.patch('build.ProjectBuilder.check_dependencies', return_value=[]) build.__main__.build_package(test_flit_path, '.', ['sdist'], isolation=False) build_cmd.assert_called_with('sdist', '.', {}) @pytest.mark.parametrize( ['missing_deps', 'output'], [ ([('foo',)], '\n\tfoo'), ([('foo',), ('bar', 'baz', 'qux')], '\n\tfoo\n\tbar\n\tbaz -> qux'), ], ) def test_build_no_isolation_with_check_deps(mocker, test_flit_path, missing_deps, output): error = mocker.patch('build.__main__._error') build_cmd = mocker.patch('build.ProjectBuilder.build', return_value='something') mocker.patch('build.ProjectBuilder.check_dependencies', return_value=missing_deps) build.__main__.build_package(test_flit_path, '.', ['sdist'], isolation=False) build_cmd.assert_called_with('sdist', '.', {}) error.assert_called_with('Missing dependencies:' + output) @pytest.mark.isolated def test_build_raises_build_exception(mocker, test_flit_path): mocker.patch('build.ProjectBuilder.get_requires_for_build', side_effect=build.BuildException) mocker.patch('build.env._IsolatedEnvVenvPip.install') with pytest.raises(build.BuildException): build.__main__.build_package(test_flit_path, '.', ['sdist']) @pytest.mark.isolated def test_build_raises_build_backend_exception(mocker, test_flit_path): mocker.patch('build.ProjectBuilder.get_requires_for_build', side_effect=build.BuildBackendException(Exception('a'))) mocker.patch('build.env._IsolatedEnvVenvPip.install') msg = f"Backend operation failed: Exception('a'{',' if sys.version_info < (3, 7) else ''})" with pytest.raises(build.BuildBackendException, match=re.escape(msg)): build.__main__.build_package(test_flit_path, '.', ['sdist']) def test_build_package(tmp_dir, test_setuptools_path): build.__main__.build_package(test_setuptools_path, tmp_dir, ['sdist', 'wheel']) assert sorted(os.listdir(tmp_dir)) == [ 'test_setuptools-1.0.0-py2.py3-none-any.whl', 'test_setuptools-1.0.0.tar.gz', ] def test_build_package_via_sdist(tmp_dir, test_setuptools_path): build.__main__.build_package_via_sdist(test_setuptools_path, tmp_dir, ['wheel']) assert sorted(os.listdir(tmp_dir)) == [ 'test_setuptools-1.0.0-py2.py3-none-any.whl', 'test_setuptools-1.0.0.tar.gz', ] def test_build_package_via_sdist_cant_build(tmp_dir, test_cant_build_via_sdist_path): with pytest.raises(build.BuildBackendException): build.__main__.build_package_via_sdist(test_cant_build_via_sdist_path, tmp_dir, ['wheel']) def test_build_package_via_sdist_invalid_distribution(tmp_dir, test_setuptools_path): with pytest.raises(ValueError, match='Only binary distributions are allowed but sdist was specified'): build.__main__.build_package_via_sdist(test_setuptools_path, tmp_dir, ['sdist']) @pytest.mark.parametrize( ('args', 'output'), [ ( [], [ '* Creating venv isolated environment...', '* Installing packages in isolated environment... (setuptools >= 42.0.0, wheel >= 0.36.0)', '* Getting dependencies for sdist...', '* Building sdist...', '* Building wheel from sdist', '* Creating venv isolated environment...', '* Installing packages in isolated environment... (setuptools >= 42.0.0, wheel >= 0.36.0)', '* Getting dependencies for wheel...', '* Installing packages in isolated environment... (wheel)', '* Building wheel...', 'Successfully built test_setuptools-1.0.0.tar.gz and test_setuptools-1.0.0-py2.py3-none-any.whl', ], ), ( ['--no-isolation'], [ '* Getting dependencies for sdist...', '* Building sdist...', '* Building wheel from sdist', '* Getting dependencies for wheel...', '* Building wheel...', 'Successfully built test_setuptools-1.0.0.tar.gz and test_setuptools-1.0.0-py2.py3-none-any.whl', ], ), ( ['--wheel'], [ '* Creating venv isolated environment...', '* Installing packages in isolated environment... (setuptools >= 42.0.0, wheel >= 0.36.0)', '* Getting dependencies for wheel...', '* Installing packages in isolated environment... (wheel)', '* Building wheel...', 'Successfully built test_setuptools-1.0.0-py2.py3-none-any.whl', ], ), ( ['--wheel', '--no-isolation'], [ '* Getting dependencies for wheel...', '* Building wheel...', 'Successfully built test_setuptools-1.0.0-py2.py3-none-any.whl', ], ), ( ['--sdist', '--no-isolation'], ['* Getting dependencies for sdist...', '* Building sdist...', 'Successfully built test_setuptools-1.0.0.tar.gz'], ), ( ['--sdist', '--wheel', '--no-isolation'], [ '* Getting dependencies for sdist...', '* Building sdist...', '* Getting dependencies for wheel...', '* Building wheel...', 'Successfully built test_setuptools-1.0.0.tar.gz and test_setuptools-1.0.0-py2.py3-none-any.whl', ], ), ], ids=[ 'via-sdist-isolation', 'via-sdist-no-isolation', 'wheel-direct-isolation', 'wheel-direct-no-isolation', 'sdist-direct-no-isolation', 'sdist-and-wheel-direct-no-isolation', ], ) @pytest.mark.flaky(reruns=5) def test_output(test_setuptools_path, tmp_dir, capsys, args, output): build.__main__.main([test_setuptools_path, '-o', tmp_dir] + args) stdout, stderr = capsys.readouterr() assert stdout.splitlines() == output @pytest.fixture() def main_reload_styles(): try: yield finally: importlib.reload(build.__main__) @pytest.mark.parametrize( ('color', 'stdout_error', 'stdout_body'), [ ( False, 'ERROR ', [ '* Creating venv isolated environment...', '* Installing packages in isolated environment... (setuptools >= 42.0.0, this is invalid, wheel >= 0.36.0)', '', 'Traceback (most recent call last):', ], ), ( True, '\33[91mERROR\33[0m ', [ '\33[1m* Creating venv isolated environment...\33[0m', '\33[1m* Installing packages in isolated environment... ' '(setuptools >= 42.0.0, this is invalid, wheel >= 0.36.0)\33[0m', '', '\33[2mTraceback (most recent call last):', ], ), ], ids=['no-color', 'color'], ) def test_output_env_subprocess_error( mocker, monkeypatch, main_reload_styles, test_invalid_requirements_path, tmp_dir, capsys, color, stdout_body, stdout_error, ): try: # do not inject hook to have clear output on capsys mocker.patch('colorama.init') except ModuleNotFoundError: # colorama might not be available pass monkeypatch.delenv('NO_COLOR', raising=False) monkeypatch.setenv('FORCE_COLOR' if color else 'NO_COLOR', '') importlib.reload(build.__main__) # reload module to set _STYLES with pytest.raises(SystemExit): build.__main__.main([test_invalid_requirements_path, '-o', tmp_dir]) stdout, stderr = capsys.readouterr() stdout, stderr = stdout.splitlines(), stderr.splitlines() assert stdout[:4] == stdout_body assert stdout[-1].startswith(stdout_error) assert len(stderr) == 1 assert stderr[0].startswith('ERROR: Invalid requirement: ') @pytest.mark.parametrize( ('tty', 'env', 'colors'), [ (False, {}, build.__main__._NO_COLORS), (True, {}, build.__main__._COLORS), (False, {'NO_COLOR': ''}, build.__main__._NO_COLORS), (True, {'NO_COLOR': ''}, build.__main__._NO_COLORS), (False, {'FORCE_COLOR': ''}, build.__main__._COLORS), (True, {'FORCE_COLOR': ''}, build.__main__._COLORS), ], ) def test_colors(mocker, monkeypatch, main_reload_styles, tty, env, colors): mocker.patch('sys.stdout.isatty', return_value=tty) for key, value in env.items(): monkeypatch.setenv(key, value) importlib.reload(build.__main__) # reload module to set _STYLES assert build.__main__._STYLES == colors def test_colors_conflict(monkeypatch, main_reload_styles): with monkeypatch.context() as m: m.setenv('NO_COLOR', '') m.setenv('FORCE_COLOR', '') with pytest.warns( UserWarning, match='Both NO_COLOR and FORCE_COLOR environment variables are set, disabling color', ): importlib.reload(build.__main__) assert build.__main__._STYLES == build.__main__._NO_COLORS build-0.7.0/tests/test_module.py000066400000000000000000000001401412073167000166510ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import build def test_version(): assert build.__version__ build-0.7.0/tests/test_projectbuilder.py000066400000000000000000000472061412073167000204170ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import copy import importlib import logging import os import sys import textwrap import pep517.wrappers import pytest import build if sys.version_info >= (3, 8): # pragma: no cover from importlib import metadata as importlib_metadata else: # pragma: no cover import importlib_metadata import pathlib build_open_owner = 'builtins' DEFAULT_BACKEND = { 'build-backend': 'setuptools.build_meta:__legacy__', 'requires': ['setuptools >= 40.8.0', 'wheel'], } class MockDistribution(importlib_metadata.Distribution): def locate_file(self, path): # pragma: no cover return '' @classmethod def from_name(cls, name): if name == 'extras_dep': return ExtraMockDistribution() elif name == 'requireless_dep': return RequirelessMockDistribution() elif name == 'recursive_dep': return RecursiveMockDistribution() elif name == 'prerelease_dep': return PrereleaseMockDistribution() raise importlib_metadata.PackageNotFoundError class ExtraMockDistribution(MockDistribution): def read_text(self, filename): if filename == 'METADATA': return """ Metadata-Version: 2.2 Name: extras_dep Version: 1.0.0 Provides-Extra: extra_without_associated_deps Provides-Extra: extra_with_unmet_deps Requires-Dist: unmet_dep; extra == 'extra_with_unmet_deps' Provides-Extra: extra_with_met_deps Requires-Dist: extras_dep; extra == 'extra_with_met_deps' Provides-Extra: recursive_extra_with_unmet_deps Requires-Dist: recursive_dep; extra == 'recursive_extra_with_unmet_deps' """.strip() class RequirelessMockDistribution(MockDistribution): def read_text(self, filename): if filename == 'METADATA': return """ Metadata-Version: 2.2 Name: requireless_dep Version: 1.0.0 """.strip() class RecursiveMockDistribution(MockDistribution): def read_text(self, filename): if filename == 'METADATA': return """ Metadata-Version: 2.2 Name: recursive_dep Version: 1.0.0 Requires-Dist: recursive_unmet_dep """.strip() class PrereleaseMockDistribution(MockDistribution): def read_text(self, filename): if filename == 'METADATA': return """ Metadata-Version: 2.2 Name: prerelease_dep Version: 1.0.1a0 """.strip() @pytest.mark.parametrize( ('requirement_string', 'expected'), [ ('extras_dep', None), ('missing_dep', ('missing_dep',)), ('requireless_dep', None), ('extras_dep[undefined_extra]', None), # would the wheel builder filter this out? ('extras_dep[extra_without_associated_deps]', None), ( 'extras_dep[extra_with_unmet_deps]', ('extras_dep[extra_with_unmet_deps]', "unmet_dep; extra == 'extra_with_unmet_deps'"), ), ( 'extras_dep[recursive_extra_with_unmet_deps]', ( 'extras_dep[recursive_extra_with_unmet_deps]', "recursive_dep; extra == 'recursive_extra_with_unmet_deps'", 'recursive_unmet_dep', ), ), ('extras_dep[extra_with_met_deps]', None), ('missing_dep; python_version>"10"', None), ('missing_dep; python_version<="1"', None), ('missing_dep; python_version>="1"', ('missing_dep; python_version>="1"',)), ('extras_dep == 1.0.0', None), ('extras_dep == 2.0.0', ('extras_dep == 2.0.0',)), ('extras_dep[extra_without_associated_deps] == 1.0.0', None), ('extras_dep[extra_without_associated_deps] == 2.0.0', ('extras_dep[extra_without_associated_deps] == 2.0.0',)), ('prerelease_dep >= 1.0.0', None), ], ) def test_check_dependency(monkeypatch, requirement_string, expected): monkeypatch.setattr(importlib_metadata, 'Distribution', MockDistribution) assert next(build.check_dependency(requirement_string), None) == expected def test_bad_project(test_no_project_path): # Passing a nonexistent project directory with pytest.raises(build.BuildException): build.ProjectBuilder(os.path.join(test_no_project_path, 'does-not-exist')) # Passing a file as a project directory with pytest.raises(build.BuildException): build.ProjectBuilder(os.path.join(test_no_project_path, 'empty.txt')) # Passing a project directory with no pyproject.toml or setup.py with pytest.raises(build.BuildException): build.ProjectBuilder(test_no_project_path) def test_init(mocker, test_flit_path, legacy_path, test_no_permission, test_bad_syntax_path): mocker.patch('pep517.wrappers.Pep517HookCaller') # correct flit pyproject.toml builder = build.ProjectBuilder(test_flit_path) pep517.wrappers.Pep517HookCaller.assert_called_with( test_flit_path, 'flit_core.buildapi', backend_path=None, python_executable=sys.executable, runner=builder._runner ) pep517.wrappers.Pep517HookCaller.reset_mock() # custom python builder = build.ProjectBuilder(test_flit_path, python_executable='some-python') pep517.wrappers.Pep517HookCaller.assert_called_with( test_flit_path, 'flit_core.buildapi', backend_path=None, python_executable='some-python', runner=builder._runner ) pep517.wrappers.Pep517HookCaller.reset_mock() # FileNotFoundError builder = build.ProjectBuilder(legacy_path) pep517.wrappers.Pep517HookCaller.assert_called_with( legacy_path, 'setuptools.build_meta:__legacy__', backend_path=None, python_executable=sys.executable, runner=builder._runner, ) # PermissionError if os.name != 'nt': # can't correctly set the permissions required for this with pytest.raises(build.BuildException): build.ProjectBuilder(test_no_permission) # TomlDecodeError with pytest.raises(build.BuildException): build.ProjectBuilder(test_bad_syntax_path) @pytest.mark.parametrize('value', [b'something', 'something_else']) def test_python_executable(test_flit_path, value): builder = build.ProjectBuilder(test_flit_path) builder.python_executable = value assert builder.python_executable == value assert builder._hook.python_executable == value @pytest.mark.parametrize('distribution', ['wheel', 'sdist']) def test_get_requires_for_build_missing_backend(packages_path, distribution): bad_backend_path = os.path.join(packages_path, 'test-bad-backend') builder = build.ProjectBuilder(bad_backend_path) with pytest.raises(build.BuildBackendException): builder.get_requires_for_build(distribution) @pytest.mark.parametrize('distribution', ['wheel', 'sdist']) def test_get_requires_for_build_missing_optional_hooks(test_optional_hooks_path, distribution): builder = build.ProjectBuilder(test_optional_hooks_path) assert builder.get_requires_for_build(distribution) == set() @pytest.mark.parametrize('distribution', ['wheel', 'sdist']) def test_build_missing_backend(packages_path, distribution, tmpdir): bad_backend_path = os.path.join(packages_path, 'test-bad-backend') builder = build.ProjectBuilder(bad_backend_path) with pytest.raises(build.BuildBackendException): builder.build(distribution, str(tmpdir)) def test_check_dependencies(mocker, test_flit_path): mocker.patch('pep517.wrappers.Pep517HookCaller.get_requires_for_build_sdist') mocker.patch('pep517.wrappers.Pep517HookCaller.get_requires_for_build_wheel') builder = build.ProjectBuilder(test_flit_path) side_effects = [ [], ['something'], pep517.wrappers.BackendUnavailable, ] builder._hook.get_requires_for_build_sdist.side_effect = copy.copy(side_effects) builder._hook.get_requires_for_build_wheel.side_effect = copy.copy(side_effects) # requires = [] assert builder.check_dependencies('sdist') == {('flit_core >=2,<3',)} assert builder.check_dependencies('wheel') == {('flit_core >=2,<3',)} # requires = ['something'] assert builder.check_dependencies('sdist') == {('flit_core >=2,<3',), ('something',)} assert builder.check_dependencies('wheel') == {('flit_core >=2,<3',), ('something',)} # BackendUnavailable with pytest.raises(build.BuildBackendException): builder.check_dependencies('sdist') with pytest.raises(build.BuildBackendException): not builder.check_dependencies('wheel') def test_working_directory(tmp_dir): assert os.path.realpath(os.curdir) != os.path.realpath(tmp_dir) with build._working_directory(tmp_dir): assert os.path.realpath(os.curdir) == os.path.realpath(tmp_dir) def test_working_directory_exc_is_not_transformed(mocker, test_flit_path, tmp_dir): mocker.patch('build._working_directory', side_effect=OSError) builder = build.ProjectBuilder(test_flit_path) with pytest.raises(OSError): builder._call_backend('build_sdist', tmp_dir) def test_build(mocker, test_flit_path, tmp_dir): mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True) mocker.patch('build._working_directory', autospec=True) builder = build.ProjectBuilder(test_flit_path) builder._hook.build_sdist.side_effect = ['dist.tar.gz', Exception] builder._hook.build_wheel.side_effect = ['dist.whl', Exception] assert builder.build('sdist', tmp_dir) == os.path.join(tmp_dir, 'dist.tar.gz') builder._hook.build_sdist.assert_called_with(tmp_dir, None) build._working_directory.assert_called_with(test_flit_path) assert builder.build('wheel', tmp_dir) == os.path.join(tmp_dir, 'dist.whl') builder._hook.build_wheel.assert_called_with(tmp_dir, None) build._working_directory.assert_called_with(test_flit_path) with pytest.raises(build.BuildBackendException): build._working_directory.assert_called_with(test_flit_path) builder.build('sdist', tmp_dir) with pytest.raises(build.BuildBackendException): build._working_directory.assert_called_with(test_flit_path) builder.build('wheel', tmp_dir) def test_default_backend(mocker, legacy_path): mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True) builder = build.ProjectBuilder(legacy_path) assert builder._build_system == DEFAULT_BACKEND def test_missing_backend(mocker, test_no_backend_path): mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True) builder = build.ProjectBuilder(test_no_backend_path) assert builder._build_system == {'requires': [], 'build-backend': DEFAULT_BACKEND['build-backend']} def test_missing_requires(mocker, test_no_requires_path): mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True) with pytest.raises(build.BuildException): build.ProjectBuilder(test_no_requires_path) def test_build_system_typo(mocker, test_typo): mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True) with pytest.warns(build.TypoWarning): build.ProjectBuilder(test_typo) def test_missing_outdir(mocker, tmp_dir, test_flit_path): mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True) builder = build.ProjectBuilder(test_flit_path) builder._hook.build_sdist.return_value = 'dist.tar.gz' out = os.path.join(tmp_dir, 'out') builder.build('sdist', out) assert os.path.isdir(out) def test_relative_outdir(mocker, tmp_dir, test_flit_path): mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True) builder = build.ProjectBuilder(test_flit_path) builder._hook.build_sdist.return_value = 'dist.tar.gz' builder.build('sdist', '.') builder._hook.build_sdist.assert_called_with(os.path.abspath('.'), None) def test_build_not_dir_outdir(mocker, tmp_dir, test_flit_path): mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True) builder = build.ProjectBuilder(test_flit_path) builder._hook.build_sdist.return_value = 'dist.tar.gz' out = os.path.join(tmp_dir, 'out') open(out, 'a').close() # create empty file with pytest.raises(build.BuildException): builder.build('sdist', out) @pytest.fixture(scope='session') def demo_pkg_inline(tmp_path_factory): # builds a wheel without any dependencies and with a console script demo-pkg-inline tmp_path = tmp_path_factory.mktemp('demo-pkg-inline') builder = build.ProjectBuilder(srcdir=os.path.join(os.path.dirname(__file__), 'packages', 'inline')) out = tmp_path / 'dist' builder.build('wheel', str(out)) return next(out.iterdir()) @pytest.mark.isolated def test_build_with_dep_on_console_script(tmp_path, demo_pkg_inline, capfd, mocker): """ All command-line scripts provided by the build-required packages must be present in the build environment's PATH. """ # we first install demo pkg inline as build dependency (as this provides a console script we can check) # to validate backend invocations contain the correct path we use an inline backend that will fail, but first # provides the PATH information (and validates shutil.which is able to discover the executable - as PEP states) toml = textwrap.dedent( ''' [build-system] requires = ["demo_pkg_inline"] build-backend = "build" backend-path = ["."] [project] description = "Factory ⸻ A code generator 🏭" authors = [{name = "Łukasz Langa"}] ''' ) code = textwrap.dedent( ''' import os import shutil import sys print("BB " + os.environ["PATH"]) exe_at = shutil.which("demo-pkg-inline") print("BB " + exe_at) ''' ) (tmp_path / 'pyproject.toml').write_text(toml, encoding='UTF-8') (tmp_path / 'build.py').write_text(code) deps = {str(demo_pkg_inline)} # we patch the requires demo_pkg_inline to refer to the wheel -> we don't need index mocker.patch('build.ProjectBuilder.build_system_requires', new_callable=mocker.PropertyMock, return_value=deps) from build.__main__ import main with pytest.raises(SystemExit): main(['--wheel', '--outdir', str(tmp_path / 'dist'), str(tmp_path)]) out, err = capfd.readouterr() lines = [line[3:] for line in out.splitlines() if line.startswith('BB ')] # filter for our markers path_vars = lines[0].split(os.pathsep) which_detected = lines[1] assert which_detected.startswith(path_vars[0]), out def test_prepare(mocker, tmp_dir, test_flit_path): mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True) mocker.patch('build._working_directory', autospec=True) builder = build.ProjectBuilder(test_flit_path) builder._hook.prepare_metadata_for_build_wheel.return_value = 'dist-1.0.dist-info' assert builder.prepare('wheel', tmp_dir) == os.path.join(tmp_dir, 'dist-1.0.dist-info') builder._hook.prepare_metadata_for_build_wheel.assert_called_with(tmp_dir, None, _allow_fallback=False) build._working_directory.assert_called_with(test_flit_path) def test_prepare_no_hook(mocker, tmp_dir, test_flit_path): mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True) builder = build.ProjectBuilder(test_flit_path) failure = pep517.wrappers.HookMissing('prepare_metadata_for_build_wheel') builder._hook.prepare_metadata_for_build_wheel.side_effect = failure assert builder.prepare('wheel', tmp_dir) is None def test_prepare_error(mocker, tmp_dir, test_flit_path): mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True) builder = build.ProjectBuilder(test_flit_path) builder._hook.prepare_metadata_for_build_wheel.side_effect = Exception with pytest.raises(build.BuildBackendException, match='Backend operation failed: Exception'): builder.prepare('wheel', tmp_dir) def test_prepare_not_dir_outdir(mocker, tmp_dir, test_flit_path): mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True) builder = build.ProjectBuilder(test_flit_path) out = os.path.join(tmp_dir, 'out') with open(out, 'w') as f: f.write('Not a directory') with pytest.raises(build.BuildException, match='Build path .* exists and is not a directory'): builder.prepare('wheel', out) def test_no_outdir_single(mocker, tmp_dir, test_flit_path): mocker.patch('pep517.wrappers.Pep517HookCaller.prepare_metadata_for_build_wheel', return_value='') builder = build.ProjectBuilder(test_flit_path) out = os.path.join(tmp_dir, 'out') builder.prepare('wheel', out) assert os.path.isdir(out) def test_no_outdir_multiple(mocker, tmp_dir, test_flit_path): mocker.patch('pep517.wrappers.Pep517HookCaller.prepare_metadata_for_build_wheel', return_value='') builder = build.ProjectBuilder(test_flit_path) out = os.path.join(tmp_dir, 'does', 'not', 'exist') builder.prepare('wheel', out) assert os.path.isdir(out) def test_runner_user_specified(tmp_dir, test_flit_path): def dummy_runner(cmd, cwd=None, env=None): raise RuntimeError('Runner was called') builder = build.ProjectBuilder(test_flit_path, runner=dummy_runner) with pytest.raises(build.BuildBackendException, match='Runner was called'): builder.build('wheel', tmp_dir) def test_metadata_path_no_prepare(tmp_dir, test_no_prepare_path): builder = build.ProjectBuilder(test_no_prepare_path) metadata = importlib_metadata.PathDistribution( pathlib.Path(builder.metadata_path(tmp_dir)), ).metadata assert metadata['name'] == 'test-no-prepare' assert metadata['Version'] == '1.0.0' def test_metadata_path_with_prepare(tmp_dir, test_setuptools_path): builder = build.ProjectBuilder(test_setuptools_path) metadata = importlib_metadata.PathDistribution( pathlib.Path(builder.metadata_path(tmp_dir)), ).metadata assert metadata['name'] == 'test-setuptools' assert metadata['Version'] == '1.0.0' def test_metadata_path_legacy(tmp_dir, legacy_path): builder = build.ProjectBuilder(legacy_path) metadata = importlib_metadata.PathDistribution( pathlib.Path(builder.metadata_path(tmp_dir)), ).metadata assert metadata['name'] == 'legacy' assert metadata['Version'] == '1.0.0' def test_metadata_invalid_wheel(tmp_dir, test_bad_wheel_path): builder = build.ProjectBuilder(test_bad_wheel_path) with pytest.raises(ValueError, match='Invalid wheel'): builder.metadata_path(tmp_dir) @pytest.fixture def mock_tomli_not_available(mocker): loads = mocker.patch('tomli.loads') mocker.patch.dict(sys.modules, {'tomli': None}) importlib.reload(build) try: yield finally: loads.assert_not_called() mocker.stopall() importlib.reload(build) def test_toml_instead_of_tomli(mocker, mock_tomli_not_available, tmp_dir, test_flit_path): mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True) builder = build.ProjectBuilder(test_flit_path) builder._hook.build_sdist.return_value = 'dist.tar.gz' builder.build('sdist', '.') builder._hook.build_sdist.assert_called_with(os.path.abspath('.'), None) def test_log(mocker, caplog, test_flit_path): mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True) mocker.patch('build.ProjectBuilder._call_backend', return_value='some_path') caplog.set_level(logging.DEBUG) builder = build.ProjectBuilder(test_flit_path) builder.get_requires_for_build('sdist') builder.get_requires_for_build('wheel') builder.prepare('wheel', '.') builder.build('sdist', '.') builder.build('wheel', '.') builder.log('something') assert [(record.levelname, record.message) for record in caplog.records] == [ ('INFO', 'Getting dependencies for sdist...'), ('INFO', 'Getting dependencies for wheel...'), ('INFO', 'Getting metadata for wheel...'), ('INFO', 'Building sdist...'), ('INFO', 'Building wheel...'), ('INFO', 'something'), ] if sys.version_info >= (3, 8): # stacklevel assert [(record.lineno) for record in caplog.records] == [305, 305, 338, 368, 368, 562] build-0.7.0/tests/test_util.py000066400000000000000000000017571412073167000163600ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import pytest import build.util @pytest.mark.parametrize('isolated', [False, True]) def test_wheel_metadata(test_setuptools_path, isolated): metadata = build.util.project_wheel_metadata(test_setuptools_path, isolated) assert metadata['name'] == 'test-setuptools' assert metadata['version'] == '1.0.0' def test_wheel_metadata_isolation(test_flit_path): metadata = build.util.project_wheel_metadata(test_flit_path) assert metadata['name'] == 'test_flit' assert metadata['version'] == '1.0.0' with pytest.raises( build.BuildBackendException, match="Backend 'flit_core.buildapi' is not available.", ): build.util.project_wheel_metadata(test_flit_path, isolated=False) def test_with_get_requires(test_metadata): metadata = build.util.project_wheel_metadata(test_metadata) assert metadata['name'] == 'test-metadata' assert str(metadata['version']) == '1.0.0' assert metadata['summary'] == 'hello!' build-0.7.0/tox.ini000066400000000000000000000050541412073167000141350ustar00rootroot00000000000000[tox] envlist = fix type docs {py39, py38, py37, py36, pypy3}{, -path, -sdist, -wheel, -min} isolated_build = true skip_missing_interpreters = true minversion = 3.14 requires = virtualenv>=20.0.34 [testenv] description = run test suite with {basepython} path: via PYTHONPATH sdist: via source distribution wheel: via wheel passenv = LC_ALL PIP_* PYTEST_* setenv = COVERAGE_FILE = {toxworkdir}/.coverage.{envname} TEST_STATUS_DIR = {envtmpdir} path: TEST_MODE = path sdist: TEST_MODE = sdist wheel: TEST_MODE = wheel extras = test commands = pytest -rsx --cov --cov-config setup.cfg \ --cov-report=html:{envdir}/htmlcov --cov-context=test \ --cov-report=xml:{toxworkdir}/coverage.{envname}.xml \ tests {posargs:-n auto} [testenv:fix] description = run static analysis and style checks passenv = HOMEPATH PROGRAMDATA basepython = python3.9 skip_install = true deps = pre-commit>=2 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:type] description = run type check on code base extras = typing commands = mypy --show-error-codes --python-version 3.6 src/build [testenv:{py39, py38, py37, py36, pypy3}-min] description = check minimum versions required of all dependencies skip_install = true commands = pip install .[test] -c tests/constraints.txt pytest -rsx tests {posargs:-n auto} [testenv:docs] description = build documentations basepython = python3.8 extras = docs commands = sphinx-build -n docs {envtmpdir} {posargs:-W} python -c 'print("Documentation available under file://{envtmpdir}/index.html")' [testenv:dev] description = generate a DEV environment usedevelop = true deps = virtualenv>=20.0.34 extras = doc test commands = python -m pip list --format=columns python -c 'import sys; print(sys.executable)' [testenv:coverage] description = combine coverage from test environments passenv = DIFF_AGAINST setenv = COVERAGE_FILE = {toxworkdir}/.coverage skip_install = true deps = coverage>=5.1 diff_cover>=3 parallel_show_output = true commands = coverage combine coverage report --skip-covered --show-missing -i coverage xml -o {toxworkdir}/coverage.xml -i coverage html -d {toxworkdir}/htmlcov -i python -m diff_cover.diff_cover_tool --compare-branch {env:DIFF_AGAINST:origin/main} {toxworkdir}/coverage.xml depends = {py39, py38, py37, py36, pypy3}{, -path, -sdist, -wheel}