Import Upstream version 1.0.3

This commit is contained in:
luoyaoming 2024-05-07 10:51:59 +08:00
parent ec2c33e439
commit 04eff854ff
45 changed files with 1298 additions and 1351 deletions

17
.github/workflows/cd.yml vendored Normal file
View File

@ -0,0 +1,17 @@
name: CD
on:
workflow_dispatch:
pull_request:
branches:
- main
release:
types:
- published
jobs:
dist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hynek/build-and-inspect-python-package@v1

View File

@ -0,0 +1,59 @@
name: change detection
on:
workflow_call:
outputs:
run-docs:
description: Whether or not build the docs
value: ${{ jobs.change-detection.outputs.run-docs || false }}
run-tests:
description: Whether or not run the tests
value: ${{ jobs.change-detection.outputs.run-tests || false }}
jobs:
change-detection:
name: Identify source changes
runs-on: ubuntu-latest
timeout-minutes: 1
outputs:
run-docs: ${{ steps.docs-changes.outputs.run-docs || false }}
run-tests: ${{ steps.tests-changes.outputs.run-tests || false }}
steps:
- uses: actions/checkout@v4
- name: Get a list of the changed runtime-related files
if: github.event_name == 'pull_request'
id: changed-testable-files
uses: Ana06/get-changed-files@v2.2.0
with:
filter: |
src/**
tests/**
tox.ini
pyproject.toml
.github/workflows/test.yml
.github/workflows/reusable-type.yml
.github/workflows/reusable-pytest.yml
- name: Set a flag for running the tests
if: >-
github.event_name != 'pull_request'
|| steps.changed-testable-files.outputs.added_modified_renamed != ''
id: tests-changes
run: >-
echo "run-tests=true" >> "${GITHUB_OUTPUT}"
- name: Get a list of the changed documentation-related files
if: github.event_name == 'pull_request'
id: changed-docs-files
uses: Ana06/get-changed-files@v2.2.0
with:
filter: |
docs/**
CHANGELOG.rst
README.md
.github/workflows/test.yml
.github/workflows/reusable-check.yml
- name: Set a flag for building the docs
if: >-
github.event_name != 'pull_request'
|| steps.changed-docs-files.outputs.added_modified_renamed != ''
id: docs-changes
run: >-
echo "run-docs=true" >> "${GITHUB_OUTPUT}"

View File

@ -1,13 +1,6 @@
name: check
on:
push:
branches:
- main
pull_request:
branches:
- main
schedule:
- cron: "0 8 * * *"
workflow_call:
jobs:
docs:
@ -16,12 +9,12 @@ jobs:
PY_COLORS: 1
TOX_PARALLEL_NO_SPINNER: 1
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Python 3.9
- name: Setup Python 3.10
uses: actions/setup-python@v4
with:
python-version: 3.9
python-version: "3.10"
- name: Install tox
run: python -m pip install tox

102
.github/workflows/reusable-pytest.yml vendored Normal file
View File

@ -0,0 +1,102 @@
name: pytest
on:
workflow_call:
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:
- "pypy-3.7"
- "pypy-3.8"
- "pypy-3.9"
- "3.12"
- "3.11"
- "3.10"
- "3.9"
- "3.8"
- "3.7"
tox-target:
- "tox"
- "min"
continue-on-error: >- # jobs not required in branch protection
${{
(
startsWith(matrix.py, 'pypy-')
&& (!endsWith(matrix.py, '-3.7') || matrix.os == 'windows')
)
&& true
|| false
}}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup python for test ${{ matrix.py }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.py }}
allow-prereleases: true
- name: Pick environment to run
run: |
import platform
import os
import sys
if platform.python_implementation() == "PyPy":
base = f"pypy{sys.version_info.major}{sys.version_info.minor}"
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@v4
with:
python-version: 3.9
- name: Install tox
run: python -m pip install tox
- name: Run test suite via tox
if: matrix.tox-target == 'tox'
run: |
tox -vv --notest -e ${{env.BASE}}
tox -e ${{env.BASE}} --skip-pkg-install
- name: Run minimum version test
if: matrix.tox-target == 'min'
run: tox -e ${{env.BASE}}-${{ matrix.tox-target }}
- name: Run path test
if: matrix.tox-target == 'tox' && matrix.py == '3.10'
run: tox -e path
- name: Combine coverage files
if: always()
run: tox -e coverage
- uses: codecov/codecov-action@v3
if: always()
env:
PYTHON: ${{ matrix.python }}
with:
file: ./.tox/coverage.xml
flags: tests
env_vars: PYTHON
name: ${{ matrix.py }} - ${{ matrix.os }}

26
.github/workflows/reusable-type.yml vendored Normal file
View File

@ -0,0 +1,26 @@
name: type
on:
workflow_call:
jobs:
type:
runs-on: ubuntu-latest
env:
PY_COLORS: 1
TOX_PARALLEL_NO_SPINNER: 1
steps:
- uses: actions/checkout@v4
- name: Setup Python 3.9
uses: actions/setup-python@v4
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

View File

@ -3,15 +3,9 @@ on:
push:
branches:
- main
paths-ignore:
- "docs/**"
- "*.md"
pull_request:
branches:
- main
paths-ignore:
- "docs/**"
- "*.md"
schedule:
- cron: "0 8 * * *"
workflow_dispatch:
@ -21,111 +15,54 @@ concurrency:
cancel-in-progress: true
jobs:
change-detection:
uses: ./.github/workflows/reusable-change-detection.yml
check-docs:
needs: change-detection
if: fromJSON(needs.change-detection.outputs.run-docs)
uses: ./.github/workflows/reusable-docs.yml
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:
- "pypy-3.7"
- "pypy-3.8"
- "pypy-3.9"
- "3.11"
- "3.10"
- "3.9"
- "3.8"
- "3.7"
- "3.6"
tox-target:
- "tox"
- "min"
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup python for test ${{ matrix.py }}
uses: actions/setup-python@v4
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}{sys.version_info.minor}"
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@v4
with:
python-version: 3.9
- name: Install tox
run: python -m pip install tox
- name: Run test suite via tox
if: matrix.tox-target == 'tox'
run: |
tox -vv --notest -e ${{env.BASE}}
tox -e ${{env.BASE}} --skip-pkg-install
- name: Run minimum version test
if: matrix.tox-target == 'min'
run: tox -e ${{env.BASE}}-${{ matrix.tox-target }}
- name: Run path test
if: matrix.tox-target == 'tox' && matrix.py == '3.10'
run: tox -e path
- name: Combine coverage files
if: always()
run: tox -e coverage
- uses: codecov/codecov-action@v3
if: always()
env:
PYTHON: ${{ matrix.python }}
with:
file: ./.tox/coverage.xml
flags: tests
env_vars: PYTHON
name: ${{ matrix.py }} - ${{ matrix.os }}
needs: change-detection
if: fromJSON(needs.change-detection.outputs.run-tests)
uses: ./.github/workflows/reusable-pytest.yml
type:
needs: change-detection
if: fromJSON(needs.change-detection.outputs.run-tests)
uses: ./.github/workflows/reusable-type.yml
# https://github.com/marketplace/actions/alls-green#why
required-checks-pass: # This job does nothing and is only used for the branch protection
if: always()
needs:
- change-detection # transitive
- check-docs
- pytest
- type
runs-on: ubuntu-latest
env:
PY_COLORS: 1
TOX_PARALLEL_NO_SPINNER: 1
steps:
- uses: actions/checkout@v3
- name: Setup Python 3.9
uses: actions/setup-python@v4
- name: Decide whether the needed jobs succeeded or failed
uses: re-actors/alls-green@release/v1
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
allowed-skips: >-
${{
fromJSON(needs.change-detection.outputs.run-docs)
&& ''
|| '
check-docs,
'
}}
${{
fromJSON(needs.change-detection.outputs.run-tests)
&& ''
|| '
pytest,
type,
'
}}
jobs: ${{ toJSON(needs) }}

View File

@ -4,7 +4,7 @@ ci:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
rev: v4.4.0
hooks:
- id: check-ast
- id: check-builtin-literals
@ -17,52 +17,40 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- id: double-quote-string-fixer
- repo: https://github.com/asottile/pyupgrade
rev: v3.1.0
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.14
hooks:
- id: pyupgrade
args: ["--py36-plus"]
- repo: https://github.com/psf/black
rev: 22.10.0
- id: validate-pyproject
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.7.0
hooks:
- id: black
- repo: https://github.com/asottile/blacken-docs
rev: v1.12.1
rev: 1.16.0
hooks:
- id: blacken-docs
additional_dependencies: [black==22.6]
additional_dependencies: [black==23.7.0]
- repo: https://github.com/pre-commit/mirrors-prettier
rev: "v3.0.0-alpha.2"
rev: "v3.0.3"
hooks:
- id: prettier
- repo: https://github.com/PyCQA/isort
rev: 5.10.1
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.287
hooks:
- id: isort
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v2.1.0
hooks:
- id: setup-cfg-fmt
args: [--include-version-classifiers, --max-py-version=3.11]
- repo: https://github.com/PyCQA/flake8
rev: "5.0.4"
hooks:
- id: flake8
additional_dependencies: ["flake8-bugbear==22.7.1"]
language_version: python3.9
- id: ruff
args: [--fix, --format, grouped, --show-fixes]
- repo: https://github.com/codespell-project/codespell
rev: "v2.2.2"
rev: "v2.2.5"
hooks:
- id: codespell
args: ["-L", "sur"]
- repo: https://github.com/pre-commit/pygrep-hooks
rev: "v1.9.0"
rev: "v1.10.0"
hooks:
- id: python-check-blanket-noqa
- id: python-check-blanket-type-ignore
- id: python-no-log-warn
- id: python-no-eval
- id: python-use-type-annotations
- id: rst-backticks
- id: rst-directive-colons
- id: rst-inline-touching-normal
- repo: https://github.com/tox-dev/tox-ini-fmt
rev: "1.3.1"
hooks:
- id: tox-ini-fmt

View File

@ -2,257 +2,224 @@
Changelog
+++++++++
1.0.3 (2023-09-06)
==================
- Avoid CPython 3.8.17, 3.9.17, 3.10.12, and 3.11.4 tarfile symlink bug
triggered by adding ``data_filter`` in 1.0.0.
(PR :pr:`675`, fixes issue :issue:`674`)
1.0.0 (2023-09-01)
==================
- Removed the ``toml`` library fallback; ``toml`` can no longer be used
as a substitute for ``tomli``
(PR :pr:`567`)
- Added ``runner`` parameter to ``util.project_wheel_metadata``
(PR :pr:`566`, fixes issue :issue:`553`)
- Modified ``ProjectBuilder`` constructor signature, added alternative
``ProjectBuilder.from_env`` constructor, redefined ``env.IsolatedEnv``
interface, and exposed ``env.DefaultIsolatedEnv``, replacing
``env.IsolatedEnvBuilder``. The aim has been to shift responsibility for
modifying the environment from the project builder to the ``IsolatedEnv``
entirely and to ensure that the builder will be initialised from an
``IsolatedEnv`` in a consistent manner. Mutating the project builder is no
longer supported.
(PR :pr:`537`)
- ``virtualenv`` is no longer imported when using ``-n``, for faster builds
(PR :pr:`636`, fixes issue :issue:`510`)
- The SDist now contains the repository contents, including tests. Flit-core
3.8+ required.
(PR :pr:`657`, :pr:`661`, fixes issue :issue:`656`)
- The minimum version of ``importlib-metadata`` has been increased to 4.6 and
Python 3.10 due to a bug in the standard library version with URL
requirements in extras. This is still not required for 3.8 when bootstrapping
(as long as you don't have URL requirements in extras).
(PR :pr:`631`, fixes issue :issue:`630`)
- Docs now built with Sphinx 7
(PR :pr:`660`)
- Tests now contain a ``network`` marker
(PR :pr:`649`, fixes issue :issue:`648`)
- Config-settings are now passed to ``get_requires*`` hooks, fixing a long
standing bug. If this affects your setuptools build, you can use
``-C--build-option=<cmd> -C--build-option=<option>`` to workaround an issue
with Setuptools not allowing unrecognised build options when running this
hook.
(PR :pr:`627`, fixes issue :issue:`#264`)
- Test on Python 3.12 betas/RCs
(PR :pr:`624`)
- Filter out malicious files when extracting tar archives when Python supports it
(PR :pr:`609`)
- Specify encoding, fixing issues when ``PYTHONWARNDEFAULTENCODING`` is set.
(PR :pr:`587`, fixes issue :issue:`577`)
- Ruff is now used for linting.
0.10.0 (2023-01-11)
===================
- Replace ``pep517`` dependency with ``pyproject_hooks``,
into which ``pep517`` has been renamed
(PR :pr:`539`, Fixes :issue:`529`)
- Change build backend from ``setuptools`` to ``flit``
(PR :pr:`470`, Fixes :issue:`394`)
- Dropped support for Python 3.6 (PR :pr:`532`)
0.9.0 (2022-10-27)
==================
- Hide a Python 3.11.0 unavoidable warning with venv (`PR #527`_)
- Hide a Python 3.11.0 unavoidable warning with venv (PR :pr:`527`)
- Fix infinite recursion error in ``check_dependency`` with circular
dependencies (`PR #512`_, Fixes `#511`_)
- Only import colorama on Windows (`PR #494`_, Fixes `#493`_)
- Flush output more often to reduce interleaved output (`PR #494`_)
- Small API cleanup, like better ``__all__`` and srcdir being read only. (`PR #477`_)
- Only use ``importlib_metadata`` when needed (`PR #401`_)
- Clarify in printout when build dependencies are being installed (`PR #514`_)
dependencies (PR :pr:`512`, Fixes :issue:`511`)
- Only import colorama on Windows (PR :pr:`494`, Fixes :issue:`493`)
- Flush output more often to reduce interleaved output (PR :pr:`494`)
- Small API cleanup, like better ``_all__`` and srcdir being read only. (PR :pr:`477`)
- Only use ``importlib_metadata`` when needed (PR :pr:`401`)
- Clarify in printout when build dependencies are being installed (PR :pr:`514`)
.. _PR #401: https://github.com/pypa/build/pull/401
.. _PR #477: https://github.com/pypa/build/pull/477
.. _PR #494: https://github.com/pypa/build/pull/494
.. _PR #512: https://github.com/pypa/build/pull/512
.. _PR #514: https://github.com/pypa/build/pull/514
.. _PR #527: https://github.com/pypa/build/pull/527
.. _#493: https://github.com/pypa/build/issues/493
.. _#511: https://github.com/pypa/build/issues/511
0.8.0 (2022-05-22)
==================
- Accept ``os.PathLike[str]`` in addition to ``str`` for paths in public
API (`PR #392`_, Fixes `#372`_)
API (PR :pr:`392`, Fixes :issue:`372`)
- Add schema validation for ``build-system`` table to check conformity
with PEP 517 and PEP 518 (`PR #365`_, Fixes `#364`_)
- Better support for Python 3.11 (sysconfig schemes `PR #434`_, `PR #463`_, tomllib `PR #443`_, warnings `PR #420`_)
- Improved error printouts (`PR #442`_)
- Avoid importing packaging unless needed (`PR #395`_, Fixes `#393`_)
with PEP 517 and PEP 518 (PR :pr:`365`, Fixes :issue:`364`)
- Better support for Python 3.11 (sysconfig schemes PR :pr:`434`, PR :pr:`463`, tomllib PR :pr:`443`, warnings PR :pr:`420`)
- Improved error printouts (PR :pr:`442`)
- Avoid importing packaging unless needed (PR :pr:`395`, Fixes :issue:`393`)
Breaking Changes
----------------
- Failure to create a virtual environment in the ``build.env`` module now raises
``build.FailedProcessError`` (`PR #442`_)
``build.FailedProcessError`` (PR :pr:`442`)
.. _PR #365: https://github.com/pypa/build/pull/365
.. _PR #392: https://github.com/pypa/build/pull/392
.. _PR #395: https://github.com/pypa/build/pull/395
.. _PR #420: https://github.com/pypa/build/pull/420
.. _PR #434: https://github.com/pypa/build/pull/434
.. _PR #442: https://github.com/pypa/build/pull/442
.. _PR #443: https://github.com/pypa/build/pull/443
.. _PR #463: https://github.com/pypa/build/pull/463
.. _#364: https://github.com/pypa/build/issues/364
.. _#372: https://github.com/pypa/build/issues/372
.. _#393: https://github.com/pypa/build/pull/393
0.7.0 (2021-09-16)
==================
- Add ``build.util`` module with an high-level utility API (`PR #340`_)
- Add ``build.util`` module with an high-level utility API (PR :pr:`340`)
.. _PR #340: https://github.com/pypa/build/pull/340
0.6.0.post1 (2021-08-05)
========================
- Fix compatibility 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
- Fix compatibility with Python 3.6 and 3.7 (PR :pr:`339`, Fixes :issue:`338`)
0.6.0 (2021-08-02)
==================
- Improved output (`PR #333`_, Fixes `#142`_)
- The CLI now honors `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`_)
- Improved output (PR :pr:`333`, Fixes :issue:`142`)
- The CLI now honors ``NO_COLOR`` (PR :pr:`333`)
- The CLI can now be forced to colorize the output by setting the ``FORCE_COLOR`` environment variable (PR :pr:`335`)
- Added logging to ``build`` and ``build.env`` (PR :pr:`333`)
- Switch to a TOML v1 compliant parser (PR :pr:`336`, Fixes :issue:`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 (2021-06-22)
==================
- 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
- Fix invoking the backend on an inexistent output directory with multiple levels (PR :pr:`318`, Fixes :issue:`316`)
- When building wheels via sdists, use an isolated temporary directory (PR :pr:`321`, Fixes :issue:`320`)
0.5.0 (2021-06-19)
==================
- 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`_)
- Add ``ProjectBuilder.metadata_path`` helper (PR :pr:`303`, Fixes :issue:`301`)
- Added a ``build.__main__.build_package_via_sdist`` method (PR :pr:`304`)
- Use appropriate installation scheme for Apple Python venvs (PR :pr:`314`, Fixes :issue:`310`)
Breaking Changes
----------------
- Binary distributions are now built via the sdist by default in the CLI (`PR #304`_, Fixes `#257`_)
- Binary distributions are now built via the sdist by default in the CLI (PR :pr:`304`, Fixes :issue:`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
- As a side-effect of PR :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 :pr:`312`)
0.4.0 (2021-05-23)
==================
- 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
- Validate that the supplied source directory is valid (PR :pr:`260`, Fixes :issue:`259`)
- Set and test minimum versions of build's runtime dependencies (PR :pr:`267`, Fixes :issue:`263`)
- Use symlinks on creating venv's when available (PR :pr:`274`, Fixes :issue:`271`)
- Error sooner if pip upgrade is required and fails (PR :pr:`288`, Fixes :issue:`256`)
- Add a ``runner`` argument to ``ProjectBuilder`` (PR :pr:`290`, Fixes :issue:`289`)
- Hide irrelevant ``pep517`` error traceback and improve error messages (PR :pr:`296`)
- Try to use ``colorama`` to fix colors on Windows (PR :pr:`300`)
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-dependencies`` 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
- As a side-effect of PR :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-dependencies`` option has been renamed to ``--skip-dependency-check`` (PR :pr:`297`)
- The ``skip_dependencies`` argument of ``build.__main__.build_package`` has been renamed to ``skip_dependency_check`` (PR :pr:`297`)
- ``build.ConfigSettings`` has been renamed to ``build.ConfigSettingsType`` (PR :pr:`298`)
- ``build.ProjectBuilder.build_dependencies`` to ``build.ProjectBuilder.build_system_requires`` (PR :pr:`284`, Fixes :issue:`182`)
- ``build.ProjectBuilder.get_dependencies`` to ``build.ProjectBuilder.get_requires_for_build`` (PR :pr:`284`, Fixes :issue:`182`)
0.3.1 (2021-03-09)
==================
- 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
- Support direct usage from pipx run in 0.16.1.0+ (PR :pr:`247`)
- Use UTF-8 encoding when reading pyproject.toml (PR :pr:`251`, Fixes :issue:`250`)
0.3.0 (2021-02-19)
==================
- 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
- 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 :pr:`229`, PR :pr:`230`, Fixes :issue:`228`)
- Build dependencies in isolation, instead of in the build environment (PR :pr:`232`, Fixes :issue:`231`)
- Fallback on venv if virtualenv is too old (PR :pr:`241`)
- Add metadata preparation hook (PR :pr:`217`, Fixes :issue:`130`)
0.2.1 (2021-02-09)
==================
- 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
- Fix error from unrecognised pip flag on Python 3.6.0 to 3.6.5 (PR :pr:`227`, Fixes :issue:`226`)
0.2.0 (2021-02-07)
==================
- 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`_)
- Check dependencies recursively (PR :pr:`183`, Fixes :issue:`25`)
- Build wheel and sdist distributions in separate environments, as they may have different dependencies (PR :pr:`195`, Fixes :issue:`194`)
- Add support for pre-releases in ``check_dependency`` (PR :pr:`204`, Fixes :issue:`191`)
- Fixes console scripts not being available during build (PR :pr:`221`, Fixes :issue:`214`)
- Do not add the default backend requirements to ``requires`` when no backend is specified (PR :pr:`177`, Fixes :issue:`107`)
- Return the sdist name in ``ProjectBuild.build`` (PR :pr:`197`)
- Improve documentation (PR :pr:`178`, PR :pr:`203`)
- Add changelog (PR :pr:`219`, Fixes :issue:`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
- Move ``config_settings`` argument to the hook calls (PR :pr:`218`, Fixes :issue:`216`)

View File

@ -1,15 +1,14 @@
# 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)
[![Discord](https://img.shields.io/discord/803025117553754132?label=Discord%20chat%20%23build)](https://discord.gg/pypa)
A simple, correct PEP 517 build frontend.
A simple, correct Python build frontend.
See the [documentation](https://pypa-build.readthedocs.io/en/latest/) for more information.

5
debian/changelog vendored
View File

@ -1,5 +0,0 @@
python-build (0.9.0-ok1) yangtze; urgency=medium
* Build for openkylin.
-- sufang <sufang@kylinos.cn> Mon, 30 Jan 2023 15:19:05 +0800

34
debian/control vendored
View File

@ -1,34 +0,0 @@
Source: python-build
Section: python
Priority: optional
Maintainer: OpenKylin Developers <packaging@lists.openkylin.top>
Build-Depends: debhelper-compat (= 13),
dh-python,
python3-setuptools,
python3-all,
python3-packaging,
python3-pep517,
python3-toml,
Standards-Version: 4.6.2
Homepage: https://github.com/pypa/build
Vcs-Browser: https://gitee.com/openkylin/python-build
Vcs-Git: https://gitee.com/openkylin/python-build.git
Testsuite: autopkgtest-pkg-python
Rules-Requires-Root: no
Package: python3-build
Architecture: all
Depends: ${python3:Depends},
${misc:Depends},
python3-packaging,
python3-pep517,
python3-toml,
python3-wheel,
Suggests: python3-pip,
python3-venv,
Description: Simple, correct PEP517 package builder (Python 3)
python-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.
.
This package installs the library for Python 3.

114
debian/copyright vendored
View File

@ -1,114 +0,0 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: python-build
Source: <url://example.com>
#
# Please double check copyright with the licensecheck(1) command.
Files: .dockerignore
.github/CODEOWNERS
.github/dependabot.yml
.github/workflows/check.yml
.github/workflows/test.yml
.gitignore
.pre-commit-config.yaml
.readthedocs.yml
CHANGELOG.rst
README.md
codecov.yml
docs/api.rst
docs/differences.rst
docs/index.rst
docs/installation.rst
docs/mission.rst
docs/test_suite.rst
pyproject.toml
setup.cfg
setup.py
src/build/__init__.py
src/build/__main__.py
src/build/env.py
src/build/py.typed
src/build/util.py
tests/conftest.py
tests/constraints.txt
tests/packages/inline/pyproject.toml
tests/packages/legacy/legacy/__init__.py
tests/packages/legacy/setup.py
tests/packages/test-bad-backend/pyproject.toml
tests/packages/test-bad-syntax/pyproject.toml
tests/packages/test-bad-wheel/backend_bad_wheel.py
tests/packages/test-bad-wheel/pyproject.toml
tests/packages/test-bad-wheel/setup.cfg
tests/packages/test-cant-build-via-sdist/backend_bad_sdist.py
tests/packages/test-cant-build-via-sdist/pyproject.toml
tests/packages/test-cant-build-via-sdist/some-file-that-is-needed-for-build.txt
tests/packages/test-flit/pyproject.toml
tests/packages/test-flit/test_flit/__init__.py
tests/packages/test-invalid-requirements/pyproject.toml
tests/packages/test-invalid-requirements/setup.cfg
tests/packages/test-metadata/backend.py
tests/packages/test-metadata/pyproject.toml
tests/packages/test-no-backend/pyproject.toml
tests/packages/test-no-permission/pyproject.toml
tests/packages/test-no-prepare/backend_no_prepare.py
tests/packages/test-no-prepare/pyproject.toml
tests/packages/test-no-prepare/setup.cfg
tests/packages/test-no-project/empty.txt
tests/packages/test-no-requires/pyproject.toml
tests/packages/test-optional-hooks/hookless_backend.py
tests/packages/test-optional-hooks/pyproject.toml
tests/packages/test-setuptools/pyproject.toml
tests/packages/test-setuptools/setup.cfg
tests/packages/test-typo/pyproject.toml
tests/test_env.py
tests/test_integration.py
tests/test_main.py
tests/test_module.py
tests/test_projectbuilder.py
tests/test_self_packaging.py
tests/test_util.py
tox.ini
Copyright: __NO_COPYRIGHT_NOR_LICENSE__
License: __NO_COPYRIGHT_NOR_LICENSE__
Files: tests/packages/inline/build.py
Copyright: __NO_COPYRIGHT__ in: tests/packages/inline/build.py
License: __UNKNOWN__
Desc
.
Wheel-Version: 1.0
Generator: {name}-{version}
Root-Is-Purelib: true
Tag: py3-none-any
Files: docs/conf.py
Copyright: __NO_COPYRIGHT__ in: docs/conf.py
License: __UNKNOWN__
# The short X.Y version
#----------------------------------------------------------------------------
# Files marked as NO_LICENSE_TEXT_FOUND may be covered by the following
# license/copyright files.
#----------------------------------------------------------------------------
# License file: LICENSE
Copyright © 2019 Filipe Laíns <filipe.lains@gmail.com>
.
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.

View File

@ -1 +0,0 @@
# You must remove unused comment lines for the released package.

20
debian/rules vendored
View File

@ -1,20 +0,0 @@
#!/usr/bin/make -f
export PYBUILD_NAME=build
export SETUPTOOLS_USE_DISTUTILS=stdlib
%:
dh $@ --with python3 --buildsystem=pybuild
# Unfortunately, python-build's testsuite relies heavily on "pip
# install" and other network-related operations, which are not not
# allowed during build time.
#
# Although not all tests are affected by this, it is becoming more and
# more difficult to maintain a list of tests that should be disabled
# because of this issue. For this reason, and in order to keep the
# maintenance burden low, we decided to disable running these tests
# during build time.
#
# TODO: Create a dep8 test that runs the entire upstream testsuite.
override_dh_auto_test:

View File

@ -1 +0,0 @@
3.0 (native)

View File

@ -1,5 +0,0 @@
---
Bug-Database: https://github.com/pypa/build/issues
Bug-Submit: https://github.com/pypa/build/issues/new
Repository: https://github.com/pypa/build.git
Repository-Browse: https://github.com/pypa/build

4
debian/watch vendored
View File

@ -1,4 +0,0 @@
# Compulsory line, this is a version 4 file
version=4
https://github.com/pypa/build/tags .*/archive/refs/tags/v?((?:\d+\.?)*)\.tar\.gz

View File

@ -1 +0,0 @@
../CHANGELOG.rst

1
docs/changelog.rst Normal file
View File

@ -0,0 +1 @@
.. include:: ../CHANGELOG.rst

View File

@ -35,6 +35,7 @@ extensions = [
'sphinx.ext.intersphinx',
'sphinx_autodoc_typehints',
'sphinx_argparse_cli',
'sphinx_issues',
]
intersphinx_mapping = {
@ -65,3 +66,11 @@ html_title = f'build {version}'
# html_static_path = ['_static']
autoclass_content = 'both'
nitpick_ignore = [
# https://github.com/python/importlib_metadata/issues/316
('py:class', 'importlib.metadata._meta.PackageMetadata'),
]
issues_github_path = 'pypa/build'

View File

@ -4,9 +4,10 @@
build
*****
A simple, correct :pep:`517` build frontend.
A simple, correct Python packaging build frontend.
build will invoke the :pep:`517` hooks to build a distribution package.
build manages ``pyproject.toml``-based builds, invoking
build-backend hooks as appropriate to build a distribution package.
It is a simple build tool and does not perform any dependency management.
.. sphinx_argparse_cli::
@ -43,6 +44,7 @@ environment, but this behavior can be disabled with ``--no-isolation``.
:hidden:
test_suite
release
.. toctree::
:caption: Project Links

View File

@ -2,20 +2,18 @@
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:
``build`` can be installed via `pip`_ or an equivalent:
.. code-block:: sh
$ pip install build
You can also check out the latest `git tag`_, download a tarball_ from GitHub, or
manually fetch the artifacts from the `project page`_ on PyPI. The git tags are
recommended for redistribution and are PGP-signed with one of the following keys:
- |3DCE51D60930EBA47858BA4146F633CBB0EB4BF2|_ *(Filipe Laíns)*
.. tip::
If you prefer, or are already using virtualenv_ in your workflow, you can
install ``build`` with the optional ``virtualenv`` dependency:
@ -33,13 +31,9 @@ of the following keys:
Bootstrapping
=============
This package can build itself with only the ``toml`` and ``pep517``
dependencies. The ``--skip-dependency-check`` flag should be used in this
case.
On Python 3.10 and older, we have a dependency on tomli_, but toml_ can be
used instead, which may make bootstrapping easier.
This package can build itself only with the ``tomli`` (can be omitted in Python 3.11+)
and ``pyproject-hooks`` dependencies.
The ``--skip-dependency-check`` flag should be used in this case.
Compatibility
=============
@ -47,13 +41,11 @@ 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)
- 3.10
- 3.11
- PyPy3
@ -70,7 +62,6 @@ versions:
.. _project page: https://pypi.org/project/build/
.. _tomli: https://github.com/hukkin/tomli
.. _toml: https://github.com/uiri/toml
.. |3DCE51D60930EBA47858BA4146F633CBB0EB4BF2| replace:: ``3DCE51D60930EBA47858BA4146F633CBB0EB4BF2``

27
docs/release.rst Normal file
View File

@ -0,0 +1,27 @@
***************
Release Process
***************
As this project is critical to the Python ecosystem's supply chain security, all
releases are PGP signed with one of the keys listed in the :doc:`installation page <installation>`.
Before releasing please make sure your PGP key is listed there, and preferably
signed by one of the other key holders. If your key is not signed by one of the
other key holders, please make sure the PR that added your key to the
:doc:`installation page <installation>` was approved by at least one other maintainer.
After that is done, you may release the project by following these steps:
#. Bump the versions in ``pyproject.toml`` and ``src/build/__init__.py``
#. Update ``CHANGELOG.rst`` with the new version and current date
#. Make a release commit with the changes made above
- The commit message should follow the ``release X.Y.Z`` format
#. Make a signed tag (``git tag -s X.Y.Z``)
- The tag title should follow the ``build X.Y.Z`` format
- The tag body should be a plaintext version of the changelog for the current
release
#. Push the commit and tag to the repository (``git push`` and ``git push --tags``)
#. Build the Python artifacts (``python -m build``)
#. Sign and push the artifacts to PyPI (``twine upload -s dist/*``)
If you have any questions, please look at previous releases and/or ping the
other maintainers.

View File

@ -1,6 +1,86 @@
[build-system]
requires = ["setuptools >=42.0"]
build-backend = "setuptools.build_meta"
requires = ["flit-core >= 3.8"]
build-backend = "flit_core.buildapi"
[project]
name = "build"
version = "1.0.3"
description = "A simple, correct Python build frontend"
readme = "README.md"
requires-python = ">= 3.7"
license.file = "LICENSE"
authors = [
{ name = "Filipe Laíns", email = "lains@riseup.net" },
{ name = "Bernát Gábor", email = "gaborjbernat@gmail.com" },
{ name = "layday", email = "layday@protonmail.com" },
{ name = "Henry Schreiner", email = "henryschreineriii@gmail.com" },
]
classifiers = [
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
urls.homepage = "https://github.com/pypa/build"
urls.changelog = "https://pypa-build.readthedocs.io/en/stable/changelog.html"
dependencies = [
"packaging >= 19.0",
"pyproject_hooks",
# not actually a runtime dependency, only supplied as there is not "recommended dependency" support
'colorama; os_name == "nt"',
'importlib-metadata >= 4.6; python_version < "3.10"', # Not required for 3.8+, but fixes a stdlib bug
'tomli >= 1.1.0; python_version < "3.11"',
]
[project.optional-dependencies]
docs = [
"furo >= 2023.08.17",
"sphinx ~= 7.0",
"sphinx-argparse-cli >= 1.5",
"sphinx-autodoc-typehints >= 1.10",
"sphinx-issues >= 3.0.0",
]
test = [
"filelock >= 3",
"pytest >= 6.2.4",
"pytest-cov >= 2.12",
"pytest-mock >= 2",
"pytest-rerunfailures >= 9.1",
"pytest-xdist >= 1.34",
"wheel >= 0.36.0",
'setuptools >= 42.0.0; python_version < "3.10"',
'setuptools >= 56.0.0; python_version == "3.10"',
'setuptools >= 56.0.0; python_version == "3.11"',
'setuptools >= 67.8.0; python_version >= "3.12"',
]
typing = [
"importlib-metadata >= 5.1",
"mypy ~= 1.5.0",
"tomli",
"typing-extensions >= 3.7.4.3",
]
virtualenv = [
"virtualenv >= 20.0.35",
]
[project.scripts]
pyproject-build = "build.__main__:entrypoint"
[project.entry-points."pipx.run"]
build = "build.__main__:entrypoint"
[tool.flit.sdist]
include = ["tests/", ".gitignore", "CHANGELOG.rst", "docs/", ".dockerignore", "tox.ini"]
exclude = ["**/__pycache__", "docs/_build", "**/*.egg-info", "tests/packages/*/build"]
[tool.coverage.run]
source = [
@ -12,6 +92,7 @@ source = [
exclude_lines = [
'\#\s*pragma: no cover',
'^\s*raise NotImplementedError\b',
"if typing.TYPE_CHECKING:",
]
[tool.coverage.paths]
@ -35,25 +116,26 @@ norecursedirs = "tests/integration/*"
markers = [
"isolated",
"pypy3323bug",
"network",
]
filterwarnings = [
"error",
"ignore:path is deprecated.:DeprecationWarning",
"ignore:The --rsyncdir command line argument and rsyncdirs config variable are deprecated.:DeprecationWarning",
"default:Python 3.14 will, by default, filter extracted tar archives:DeprecationWarning",
]
[tool.mypy]
files = "src"
python_version = "3.6"
python_version = "3.7"
strict = true
show_error_codes = true
enable_error_code = ["ignore-without-code", "truthy-bool", "redundant-expr"]
[[tool.mypy.overrides]]
module = [
"colorama", # Optional dependency
"pep517.*", # Untyped
"pyproject_hooks.*", # Untyped
"virtualenv", # Optional dependency
]
ignore_missing_imports = true
@ -61,12 +143,36 @@ ignore_missing_imports = true
[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
[tool.ruff]
line-length = 127
exclude = ["tests/packages/test-bad-syntax"]
select = [
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"C9", # mccabe
"E", # pycodestyle
"F", # pyflakes
"I", # isort
"PGH", # pygrep-hooks
"RUF", # ruff
"UP", # pyupgrade
"W", # pycodestyle
"YTT", # flake8-2020
"TRY", # tryceratops
"EM", # flake8-errmsg
]
src = ["src"]
[tool.ruff.mccabe]
max-complexity = 10
[tool.ruff.isort]
lines-between-types = 1
lines-after-imports = 2
known-first-party = ["build"]
[tool.check-wheel-contents]
ignore = [
"W005", # We _are_ build
]

View File

@ -1,74 +0,0 @@
[metadata]
name = build
version = 0.9.0
description = A simple, correct PEP 517 build frontend
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 :: 3.10
Programming Language :: Python :: 3.11
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
colorama;os_name == "nt" # not actually a runtime dependency, only supplied as there is not "recommended dependency" support
importlib-metadata>=0.22;python_version < "3.8"
tomli>=1.0.0;python_version < "3.11" # toml can be used instead -- in case it makes bootstrapping easier
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>=2021.08.31
sphinx~=4.0
sphinx-argparse-cli>=1.5
sphinx-autodoc-typehints>=1.10
test =
filelock>=3
pytest>=6.2.4
pytest-cov>=2.12
pytest-mock>=2
pytest-rerunfailures>=9.1
pytest-xdist>=1.34
toml>=0.10.0
wheel>=0.36.0
setuptools>=42.0.0;python_version < "3.10"
setuptools>=56.0.0;python_version >= "3.10"
typing =
importlib-metadata>=4.6.4
mypy==0.950
typing-extensions>=3.7.4.3;python_version < "3.8"
virtualenv =
virtualenv>=20.0.35
[options.package_data]
build =
py.typed

View File

@ -1,5 +0,0 @@
#!/usr/bin/env python
from setuptools import setup
setup()

View File

@ -4,67 +4,47 @@
build - A simple, correct PEP 517 build frontend
"""
__version__ = '0.9.0'
from __future__ import annotations
__version__ = '1.0.3'
import contextlib
import difflib
import logging
import os
import re
import subprocess
import sys
import textwrap
import types
import warnings
import zipfile
from collections import OrderedDict
from typing import (
AbstractSet,
Any,
Callable,
Dict,
Iterator,
List,
Mapping,
MutableMapping,
Optional,
Sequence,
Set,
Tuple,
Type,
Union,
from collections.abc import Iterator
from typing import Any, Callable, Mapping, Optional, Sequence, TypeVar, Union
import pyproject_hooks
from . import env
from ._exceptions import (
BuildBackendException,
BuildException,
BuildSystemTableValidationError,
FailedProcessError,
TypoWarning,
)
from ._util import check_dependency, parse_wheel_filename
import pep517.wrappers
TOMLDecodeError: Type[Exception]
toml_loads: Callable[[str], MutableMapping[str, Any]]
if sys.version_info >= (3, 11):
from tomllib import TOMLDecodeError
from tomllib import loads as toml_loads
import tomllib
else:
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[import,no-redef]
from toml import loads as toml_loads # type: ignore[no-redef]
import tomli as tomllib
RunnerType = Callable[[Sequence[str], Optional[str], Optional[Mapping[str, str]]], None]
ConfigSettingsType = Mapping[str, Union[str, Sequence[str]]]
PathType = Union[str, 'os.PathLike[str]']
_ExcInfoType = Union[Tuple[Type[BaseException], BaseException, types.TracebackType], Tuple[None, None, None]]
_WHEEL_NAME_REGEX = re.compile(
r'(?P<distribution>.+)-(?P<version>.+)'
r'(-(?P<build_tag>.+))?-(?P<python_tag>.+)'
r'-(?P<abi_tag>.+)-(?P<platform_tag>.+)\.whl'
)
_TProjectBuilder = TypeVar('_TProjectBuilder', bound='ProjectBuilder')
_DEFAULT_BACKEND = {
@ -76,148 +56,42 @@ _DEFAULT_BACKEND = {
_logger = logging.getLogger(__name__)
class BuildException(Exception):
"""
Exception raised by :class:`ProjectBuilder`
"""
class BuildBackendException(Exception):
"""
Exception raised when a backend operation fails
"""
def __init__(
self, exception: Exception, description: Optional[str] = None, exc_info: _ExcInfoType = (None, None, None)
) -> None:
super().__init__()
self.exception = exception
self.exc_info = exc_info
self._description = description
def __str__(self) -> str:
if self._description:
return self._description
return f'Backend operation failed: {self.exception!r}'
class BuildSystemTableValidationError(BuildException):
"""
Exception raised when the ``[build-system]`` table in pyproject.toml is invalid.
"""
def __str__(self) -> str:
return f'Failed to validate `build-system` in pyproject.toml: {self.args[0]}'
class FailedProcessError(Exception):
"""
Exception raised when an setup or prepration operation fails.
"""
def __init__(self, exception: subprocess.CalledProcessError, description: str) -> None:
super().__init__()
self.exception = exception
self._description = description
def __str__(self) -> str:
cmd = ' '.join(self.exception.cmd)
description = f"{self._description}\n Command '{cmd}' failed with return code {self.exception.returncode}"
for stream_name in ('stdout', 'stderr'):
stream = getattr(self.exception, stream_name)
if stream:
description += f'\n {stream_name}:\n'
description += textwrap.indent(stream.decode(), ' ')
return description
class TypoWarning(Warning):
"""
Warning raised when a possible typo is found
"""
@contextlib.contextmanager
def _working_directory(path: str) -> Iterator[None]:
current = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(current)
def _validate_source_directory(srcdir: PathType) -> 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)
normalised_req_string = str(req)
# ``Requirement`` doesn't implement ``__eq__`` so we cannot compare reqs for
# equality directly but the string representation is stable.
if normalised_req_string in ancestral_req_strings:
# cyclical dependency, already checked.
return
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 + (normalised_req_string,)
else:
if req.specifier and not req.specifier.contains(dist.version, prereleases=True):
# the installed version is incompatible.
yield ancestral_req_strings + (normalised_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 + (normalised_req_string,), req.extras)
def _find_typo(dictionary: Mapping[str, str], expected: str) -> None:
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,
stacklevel=2,
)
def _parse_build_system_table(pyproject_toml: Mapping[str, Any]) -> Dict[str, Any]:
def _validate_source_directory(source_dir: PathType) -> None:
if not os.path.isdir(source_dir):
msg = f'Source {source_dir} is not a directory'
raise BuildException(msg)
pyproject_toml = os.path.join(source_dir, 'pyproject.toml')
setup_py = os.path.join(source_dir, 'setup.py')
if not os.path.exists(pyproject_toml) and not os.path.exists(setup_py):
msg = f'Source {source_dir} does not appear to be a Python project: no pyproject.toml or setup.py'
raise BuildException(msg)
def _read_pyproject_toml(path: PathType) -> Mapping[str, Any]:
try:
with open(path, 'rb') as f:
return tomllib.loads(f.read().decode())
except FileNotFoundError:
return {}
except PermissionError as e:
msg = f"{e.strerror}: '{e.filename}' "
raise BuildException(msg) from None
except tomllib.TOMLDecodeError as e:
msg = f'Failed to parse {path}: {e} '
raise BuildException(msg) from None
def _parse_build_system_table(pyproject_toml: Mapping[str, Any]) -> Mapping[str, Any]:
# If pyproject.toml is missing (per PEP 517) or [build-system] is missing
# (per PEP 518), use default values
if 'build-system' not in pyproject_toml:
@ -229,11 +103,13 @@ def _parse_build_system_table(pyproject_toml: Mapping[str, Any]) -> Dict[str, An
# If [build-system] is present, it must have a ``requires`` field (per PEP 518)
if 'requires' not in build_system_table:
_find_typo(build_system_table, 'requires')
raise BuildSystemTableValidationError('`requires` is a required property')
msg = '`requires` is a required property'
raise BuildSystemTableValidationError(msg)
elif not isinstance(build_system_table['requires'], list) or not all(
isinstance(i, str) for i in build_system_table['requires']
):
raise BuildSystemTableValidationError('`requires` must be an array of strings')
msg = '`requires` must be an array of strings'
raise BuildSystemTableValidationError(msg)
if 'build-backend' not in build_system_table:
_find_typo(build_system_table, 'build-backend')
@ -241,21 +117,31 @@ def _parse_build_system_table(pyproject_toml: Mapping[str, Any]) -> Dict[str, An
# but leave ``requires`` intact to emulate pip
build_system_table['build-backend'] = _DEFAULT_BACKEND['build-backend']
elif not isinstance(build_system_table['build-backend'], str):
raise BuildSystemTableValidationError('`build-backend` must be a string')
msg = '`build-backend` must be a string'
raise BuildSystemTableValidationError(msg)
if 'backend-path' in build_system_table and (
not isinstance(build_system_table['backend-path'], list)
or not all(isinstance(i, str) for i in build_system_table['backend-path'])
):
raise BuildSystemTableValidationError('`backend-path` must be an array of strings')
msg = '`backend-path` must be an array of strings'
raise BuildSystemTableValidationError(msg)
unknown_props = build_system_table.keys() - {'requires', 'build-backend', 'backend-path'}
if unknown_props:
raise BuildSystemTableValidationError(f'Unknown properties: {", ".join(unknown_props)}')
msg = f'Unknown properties: {", ".join(unknown_props)}'
raise BuildSystemTableValidationError(msg)
return build_system_table
def _wrap_subprocess_runner(runner: RunnerType, env: env.IsolatedEnv) -> RunnerType:
def _invoke_wrapped_runner(cmd: Sequence[str], cwd: str | None, extra_environ: Mapping[str, str] | None) -> None:
runner(cmd, cwd, {**(env.make_extra_environ() or {}), **(extra_environ or {})})
return _invoke_wrapped_runner
class ProjectBuilder:
"""
The PEP 517 consumer API.
@ -263,100 +149,73 @@ class ProjectBuilder:
def __init__(
self,
srcdir: PathType,
source_dir: PathType,
python_executable: str = sys.executable,
scripts_dir: Optional[str] = None,
runner: RunnerType = pep517.wrappers.default_subprocess_runner,
runner: RunnerType = pyproject_hooks.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 source_dir: The source directory
:param python_executable: The python executable where the backend lives
:param runner: An alternative runner for backend subprocesses
:param runner: Runner for backend subprocesses
The 'runner', if provided, must accept the following arguments:
The ``runner``, if provided, must accept the following arguments:
- cmd: a list of strings representing the command and arguments to
- ``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
- ``cwd``: a string representing the working directory that must be
used for the subprocess. Corresponds to the provided source_dir.
- ``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)
self._source_dir: str = os.path.abspath(source_dir)
_validate_source_directory(source_dir)
spec_file = os.path.join(srcdir, 'pyproject.toml')
self._python_executable = python_executable
self._runner = runner
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}' ") # noqa: B904 # use raise from
except TOMLDecodeError as e:
raise BuildException(f'Failed to parse {spec_file}: {e} ') # noqa: B904 # use raise from
pyproject_toml_path = os.path.join(source_dir, 'pyproject.toml')
self._build_system = _parse_build_system_table(_read_pyproject_toml(pyproject_toml_path))
self._build_system = _parse_build_system_table(spec)
self._backend = self._build_system['build-backend']
self._scripts_dir = scripts_dir
self._hook_runner = runner
self._hook = pep517.wrappers.Pep517HookCaller(
self.srcdir,
self._hook = pyproject_hooks.BuildBackendHookCaller(
self._source_dir,
self._backend,
backend_path=self._build_system.get('backend-path'),
python_executable=python_executable,
python_executable=self._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)
@classmethod
def from_isolated_env(
cls: type[_TProjectBuilder],
env: env.IsolatedEnv,
source_dir: PathType,
runner: RunnerType = pyproject_hooks.default_subprocess_runner,
) -> _TProjectBuilder:
return cls(
source_dir=source_dir,
python_executable=env.python_executable,
runner=_wrap_subprocess_runner(runner, env),
)
@property
def srcdir(self) -> str:
def source_dir(self) -> str:
"""Project source directory."""
return self._srcdir
return self._source_dir
@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
return self._python_executable
@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]:
def build_system_requires(self) -> set[str]:
"""
The dependencies defined in the ``pyproject.toml``'s
``build-system.requires`` field or the default build dependencies
@ -364,7 +223,7 @@ class ProjectBuilder:
"""
return set(self._build_system['requires'])
def get_requires_for_build(self, distribution: str, config_settings: Optional[ConfigSettingsType] = None) -> Set[str]:
def get_requires_for_build(self, distribution: str, config_settings: ConfigSettingsType | None = None) -> set[str]:
"""
Return the dependencies defined by the backend in addition to
:attr:`build_system_requires` for a given distribution.
@ -380,9 +239,7 @@ class ProjectBuilder:
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, ...]]:
def check_dependencies(self, distribution: str, config_settings: ConfigSettingsType | None = 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
@ -396,8 +253,8 @@ class ProjectBuilder:
return {u for d in dependencies for u in check_dependency(d)}
def prepare(
self, distribution: str, output_directory: PathType, config_settings: Optional[ConfigSettingsType] = None
) -> Optional[str]:
self, distribution: str, output_directory: PathType, config_settings: ConfigSettingsType | None = None
) -> str | None:
"""
Prepare metadata for a distribution.
@ -415,7 +272,7 @@ class ProjectBuilder:
_allow_fallback=False,
)
except BuildBackendException as exception:
if isinstance(exception.exception, pep517.wrappers.HookMissing):
if isinstance(exception.exception, pyproject_hooks.HookMissing):
return None
raise
@ -423,8 +280,8 @@ class ProjectBuilder:
self,
distribution: str,
output_directory: PathType,
config_settings: Optional[ConfigSettingsType] = None,
metadata_directory: Optional[str] = None,
config_settings: ConfigSettingsType | None = None,
metadata_directory: str | None = None,
) -> str:
"""
Build a distribution.
@ -457,9 +314,10 @@ class ProjectBuilder:
# fallback to build_wheel hook
wheel = self.build('wheel', output_directory)
match = _WHEEL_NAME_REGEX.match(os.path.basename(wheel))
match = parse_wheel_filename(os.path.basename(wheel))
if not match:
raise ValueError('Invalid wheel')
msg = 'Invalid wheel'
raise ValueError(msg)
distinfo = f"{match['distribution']}-{match['version']}.dist-info"
member_prefix = f'{distinfo}/'
with zipfile.ZipFile(wheel) as w:
@ -470,7 +328,7 @@ class ProjectBuilder:
return os.path.join(output_directory, distinfo)
def _call_backend(
self, hook_name: str, outdir: PathType, config_settings: Optional[ConfigSettingsType] = None, **kwargs: Any
self, hook_name: str, outdir: PathType, config_settings: ConfigSettingsType | None = None, **kwargs: Any
) -> str:
outdir = os.path.abspath(outdir)
@ -478,7 +336,8 @@ class ProjectBuilder:
if os.path.exists(outdir):
if not os.path.isdir(outdir):
raise BuildException(f"Build path '{outdir}' exists and is not a directory")
msg = f"Build path '{outdir}' exists and is not a directory"
raise BuildException(msg)
else:
os.makedirs(outdir)
@ -489,21 +348,18 @@ class ProjectBuilder:
@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( # noqa: B904 # use raise from
exception,
f"Backend '{self._backend}' is not available.",
sys.exc_info(),
)
except subprocess.CalledProcessError as exception:
raise BuildBackendException( # noqa: B904 # use raise from
exception, f'Backend subprocess exited when trying to invoke {hook}'
)
except Exception as exception:
raise BuildBackendException(exception, exc_info=sys.exc_info()) # noqa: B904 # use raise from
try:
yield
except pyproject_hooks.BackendUnavailable as exception:
raise BuildBackendException(
exception,
f"Backend '{self._backend}' is not available.",
sys.exc_info(),
) from None
except subprocess.CalledProcessError as exception:
raise BuildBackendException(exception, f'Backend subprocess exited when trying to invoke {hook}') from None
except Exception as exception:
raise BuildBackendException(exception, exc_info=sys.exc_info()) from None
@staticmethod
def log(message: str) -> None:
@ -535,5 +391,5 @@ __all__ = [
]
def __dir__() -> List[str]:
def __dir__() -> list[str]:
return __all__

View File

@ -1,5 +1,6 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
import argparse
import contextlib
@ -8,18 +9,20 @@ import platform
import shutil
import subprocess
import sys
import tarfile
import tempfile
import textwrap
import traceback
import warnings
from typing import Dict, Iterator, List, NoReturn, Optional, Sequence, TextIO, Type, Union
from collections.abc import Iterator, Sequence
from functools import partial
from typing import NoReturn, TextIO
import build
from build import BuildBackendException, BuildException, ConfigSettingsType, FailedProcessError, PathType, ProjectBuilder
from build.env import IsolatedEnvBuilder
from . import ConfigSettingsType, PathType, ProjectBuilder
from ._exceptions import BuildBackendException, BuildException, FailedProcessError
from .env import DefaultIsolatedEnv
_COLORS = {
@ -34,10 +37,10 @@ _COLORS = {
_NO_COLORS = {color: '' for color in _COLORS}
def _init_colors() -> Dict[str, str]:
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')
warnings.warn('Both NO_COLOR and FORCE_COLOR environment variables are set, disabling color', stacklevel=2)
return _NO_COLORS
elif 'FORCE_COLOR' in os.environ or sys.stdout.isatty():
return _COLORS
@ -52,12 +55,12 @@ def _cprint(fmt: str = '', msg: str = '') -> None:
def _showwarning(
message: Union[Warning, str],
category: Type[Warning],
message: Warning | str,
category: type[Warning],
filename: str,
lineno: int,
file: Optional[TextIO] = None,
line: Optional[str] = None,
file: TextIO | None = None,
line: str | None = None,
) -> None: # pragma: no cover
_cprint('{yellow}WARNING{reset} {}', str(message))
@ -91,7 +94,7 @@ class _ProjectBuilder(ProjectBuilder):
_cprint('{bold}* {}{reset}', message)
class _IsolatedEnvBuilder(IsolatedEnvBuilder):
class _DefaultIsolatedEnv(DefaultIsolatedEnv):
@staticmethod
def log(message: str) -> None:
_cprint('{bold}* {}{reset}', message)
@ -102,27 +105,28 @@ def _format_dep_chain(dep_chain: Sequence[str]) -> str:
def _build_in_isolated_env(
builder: ProjectBuilder, outdir: PathType, distribution: str, config_settings: Optional[ConfigSettingsType]
srcdir: PathType, outdir: PathType, distribution: str, config_settings: ConfigSettingsType | None
) -> str:
with _IsolatedEnvBuilder() as env:
builder.python_executable = env.executable
builder.scripts_dir = env.scripts_dir
with _DefaultIsolatedEnv() as env:
builder = _ProjectBuilder.from_isolated_env(env, srcdir)
# 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))
env.install(builder.get_requires_for_build(distribution, config_settings or {}))
return builder.build(distribution, outdir, config_settings or {})
def _build_in_current_env(
builder: ProjectBuilder,
srcdir: PathType,
outdir: PathType,
distribution: str,
config_settings: Optional[ConfigSettingsType],
config_settings: ConfigSettingsType | None,
skip_dependency_check: bool = False,
) -> str:
builder = _ProjectBuilder(srcdir)
if not skip_dependency_check:
missing = builder.check_dependencies(distribution)
missing = builder.check_dependencies(distribution, config_settings or {})
if missing:
dependencies = ''.join('\n\t' + dep for deps in missing for dep in (deps[0], _format_dep_chain(deps[1:])) if dep)
_cprint()
@ -133,16 +137,16 @@ def _build_in_current_env(
def _build(
isolation: bool,
builder: ProjectBuilder,
srcdir: PathType,
outdir: PathType,
distribution: str,
config_settings: Optional[ConfigSettingsType],
config_settings: ConfigSettingsType | None,
skip_dependency_check: bool,
) -> str:
if isolation:
return _build_in_isolated_env(builder, outdir, distribution, config_settings)
return _build_in_isolated_env(srcdir, outdir, distribution, config_settings)
else:
return _build_in_current_env(builder, outdir, distribution, config_settings, skip_dependency_check)
return _build_in_current_env(srcdir, outdir, distribution, config_settings, skip_dependency_check)
@contextlib.contextmanager
@ -172,7 +176,8 @@ def _handle_build_error() -> Iterator[None]:
def _natural_language_list(elements: Sequence[str]) -> str:
if len(elements) == 0:
raise IndexError('no elements')
msg = 'no elements'
raise IndexError(msg)
elif len(elements) == 1:
return elements[0]
else:
@ -186,7 +191,7 @@ def build_package(
srcdir: PathType,
outdir: PathType,
distributions: Sequence[str],
config_settings: Optional[ConfigSettingsType] = None,
config_settings: ConfigSettingsType | None = None,
isolation: bool = True,
skip_dependency_check: bool = False,
) -> Sequence[str]:
@ -200,10 +205,9 @@ def build_package(
: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)
built: list[str] = []
for distribution in distributions:
out = _build(isolation, builder, outdir, distribution, config_settings, skip_dependency_check)
out = _build(isolation, srcdir, outdir, distribution, config_settings, skip_dependency_check)
built.append(os.path.basename(out))
return built
@ -212,7 +216,7 @@ def build_package_via_sdist(
srcdir: PathType,
outdir: PathType,
distributions: Sequence[str],
config_settings: Optional[ConfigSettingsType] = None,
config_settings: ConfigSettingsType | None = None,
isolation: bool = True,
skip_dependency_check: bool = False,
) -> Sequence[str]:
@ -226,28 +230,30 @@ def build_package_via_sdist(
: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')
from ._util import TarFile
builder = _ProjectBuilder(srcdir)
sdist = _build(isolation, builder, outdir, 'sdist', config_settings, skip_dependency_check)
if 'sdist' in distributions:
msg = 'Only binary distributions are allowed but sdist was specified'
raise ValueError(msg)
sdist = _build(isolation, srcdir, 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
built: list[str] = []
if distributions:
# extract sdist
with TarFile.open(sdist) as t:
t.extractall(sdist_out)
try:
_ProjectBuilder.log(f'Building {_natural_language_list(distributions)} from sdist')
srcdir = os.path.join(sdist_out, sdist_name[: -len('.tar.gz')])
for distribution in distributions:
out = _build(isolation, srcdir, 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:
@ -258,7 +264,7 @@ def main_parser() -> argparse.ArgumentParser:
description=textwrap.indent(
textwrap.dedent(
'''
A simple, correct PEP 517 build frontend.
A simple, correct Python build frontend.
By default, a source distribution (sdist) is built from {srcdir}
and a binary distribution (wheel) is built from the sdist.
@ -273,7 +279,12 @@ def main_parser() -> argparse.ArgumentParser:
).strip(),
' ',
),
formatter_class=argparse.RawTextHelpFormatter,
formatter_class=partial(
argparse.RawDescriptionHelpFormatter,
# Prevent argparse from taking up the entire width of the terminal window
# which impedes readability.
width=min(shutil.get_terminal_size().columns - 2, 127),
),
)
parser.add_argument(
'srcdir',
@ -305,6 +316,7 @@ def main_parser() -> argparse.ArgumentParser:
'-o',
type=str,
help=f'output directory (defaults to {{srcdir}}{os.sep}dist)',
metavar='PATH',
)
parser.add_argument(
'--skip-dependency-check',
@ -316,19 +328,22 @@ def main_parser() -> argparse.ArgumentParser:
'--no-isolation',
'-n',
action='store_true',
help='do not isolate the build in a virtual environment',
help='disable building the project in an isolated virtual environment. '
'Build dependencies must be installed separately when this option is used',
)
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)"',
help='settings to pass to the backend. Multiple settings can be provided. '
'Settings beginning with a hyphen will erroneously be interpreted as options to build if separated '
'by a space character; use ``--config-setting=--my-setting -C--my-other-setting``',
metavar='KEY[=VALUE]',
)
return parser
def main(cli_args: Sequence[str], prog: Optional[str] = None) -> None: # noqa: C901
def main(cli_args: Sequence[str], prog: str | None = None) -> None:
"""
Parse the CLI arguments and invoke the build process.

70
src/build/_exceptions.py Normal file
View File

@ -0,0 +1,70 @@
from __future__ import annotations
import subprocess
import textwrap
import types
class BuildException(Exception):
"""
Exception raised by :class:`build.ProjectBuilder`.
"""
class BuildBackendException(Exception):
"""
Exception raised when a backend operation fails.
"""
def __init__(
self,
exception: Exception,
description: str | None = None,
exc_info: tuple[type[BaseException], BaseException, types.TracebackType]
| tuple[None, None, None] = (None, None, None),
) -> None:
super().__init__()
self.exception = exception
self.exc_info = exc_info
self._description = description
def __str__(self) -> str:
if self._description:
return self._description
return f'Backend operation failed: {self.exception!r}'
class BuildSystemTableValidationError(BuildException):
"""
Exception raised when the ``[build-system]`` table in pyproject.toml is invalid.
"""
def __str__(self) -> str:
return f'Failed to validate `build-system` in pyproject.toml: {self.args[0]}'
class FailedProcessError(Exception):
"""
Exception raised when a setup or preparation operation fails.
"""
def __init__(self, exception: subprocess.CalledProcessError, description: str) -> None:
super().__init__()
self.exception = exception
self._description = description
def __str__(self) -> str:
cmd = ' '.join(self.exception.cmd)
description = f"{self._description}\n Command '{cmd}' failed with return code {self.exception.returncode}"
for stream_name in ('stdout', 'stderr'):
stream = getattr(self.exception, stream_name)
if stream:
description += f'\n {stream_name}:\n'
description += textwrap.indent(stream.decode(), ' ')
return description
class TypoWarning(Warning):
"""
Warning raised when a possible typo is found.
"""

14
src/build/_importlib.py Normal file
View File

@ -0,0 +1,14 @@
import sys
if sys.version_info < (3, 8):
import importlib_metadata as metadata
elif sys.version_info < (3, 9, 10) or (3, 10, 0) <= sys.version_info < (3, 10, 2):
try:
import importlib_metadata as metadata
except ModuleNotFoundError:
from importlib import metadata
else:
from importlib import metadata
__all__ = ['metadata']

88
src/build/_util.py Normal file
View File

@ -0,0 +1,88 @@
from __future__ import annotations
import re
import sys
import tarfile
import typing
from collections.abc import Iterator, Set
_WHEEL_FILENAME_REGEX = re.compile(
r'(?P<distribution>.+)-(?P<version>.+)'
r'(-(?P<build_tag>.+))?-(?P<python_tag>.+)'
r'-(?P<abi_tag>.+)-(?P<platform_tag>.+)\.whl'
)
def check_dependency(
req_string: str, ancestral_req_strings: tuple[str, ...] = (), parent_extras: Set[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
from ._importlib import metadata
req = packaging.requirements.Requirement(req_string)
normalised_req_string = str(req)
# ``Requirement`` doesn't implement ``__eq__`` so we cannot compare reqs for
# equality directly but the string representation is stable.
if normalised_req_string in ancestral_req_strings:
# cyclical dependency, already checked.
return
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 = metadata.distribution(req.name)
except metadata.PackageNotFoundError:
# dependency is not installed in the environment.
yield (*ancestral_req_strings, normalised_req_string)
else:
if req.specifier and not req.specifier.contains(dist.version, prereleases=True):
# the installed version is incompatible.
yield (*ancestral_req_strings, normalised_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, normalised_req_string), req.extras)
def parse_wheel_filename(filename: str) -> re.Match[str] | None:
return _WHEEL_FILENAME_REGEX.match(filename)
if typing.TYPE_CHECKING:
TarFile = tarfile.TarFile
else:
# Per https://peps.python.org/pep-0706/, the "data" filter will become
# the default in Python 3.14. The first series of releases with the filter
# had a broken filter that could not process symlinks correctly.
if (
(3, 8, 18) <= sys.version_info < (3, 9)
or (3, 9, 18) <= sys.version_info < (3, 10)
or (3, 10, 13) <= sys.version_info < (3, 11)
or (3, 11, 5) <= sys.version_info < (3, 12)
or (3, 12) <= sys.version_info < (3, 14)
):
class TarFile(tarfile.TarFile):
extraction_filter = staticmethod(tarfile.data_filter)
else:
TarFile = tarfile.TarFile

View File

@ -1,8 +1,8 @@
"""
Creates and manages isolated build environments.
"""
from __future__ import annotations
import abc
import functools
import importlib.util
import logging
import os
import platform
@ -11,46 +11,37 @@ import subprocess
import sys
import sysconfig
import tempfile
import typing
import warnings
from types import TracebackType
from typing import Callable, Collection, List, Optional, Tuple, Type
from collections.abc import Collection, Mapping
import build
from ._exceptions import FailedProcessError
from ._util import check_dependency
try:
import virtualenv
except ModuleNotFoundError:
virtualenv = None
if sys.version_info >= (3, 8):
from typing import Protocol
elif typing.TYPE_CHECKING:
from typing_extensions import Protocol
else:
Protocol = abc.ABC
_logger = logging.getLogger(__name__)
class IsolatedEnv(metaclass=abc.ABCMeta):
"""Abstract base of isolated build environments, as required by the build project."""
class IsolatedEnv(Protocol):
"""Isolated build environment ABC."""
@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
def python_executable(self) -> str:
"""The Python executable of the isolated environment."""
@abc.abstractmethod
def install(self, requirements: Collection[str]) -> None:
"""
Install packages from PEP 508 requirements in the isolated build environment.
:param requirements: PEP 508 requirements
"""
raise NotImplementedError
def make_extra_environ(self) -> Mapping[str, str] | None:
"""Generate additional env vars specific to the isolated environment."""
@functools.lru_cache(maxsize=None)
@ -60,71 +51,99 @@ 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(
return importlib.util.find_spec('virtualenv') is not None and not any(
packaging.requirements.Requirement(d[1]).name == 'virtualenv'
for d in build.check_dependency('build[virtualenv]')
for d in check_dependency('build[virtualenv]')
if len(d) > 1
)
def _subprocess(cmd: List[str]) -> None:
def _subprocess(cmd: list[str]) -> None:
"""Invoke subprocess and output stdout and stderr if it fails."""
try:
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
print(e.output.decode(), end='', file=sys.stderr)
raise e
raise
class IsolatedEnvBuilder:
"""Builder object for isolated environments."""
class DefaultIsolatedEnv(IsolatedEnv):
"""An isolated environment which combines venv and virtualenv with pip."""
def __init__(self) -> None:
self._path: Optional[str] = None
def __enter__(self) -> IsolatedEnv:
"""
Create an isolated build environment.
:return: The isolated build environment
"""
# Call ``realpath`` to prevent spurious warning from being emitted
# that the venv location has changed on Windows. The username is
# DOS-encoded in the output of tempfile - the location is the same
# but the representation of it is different, which confuses venv.
# Ref: https://bugs.python.org/issue46171
self._path = os.path.realpath(tempfile.mkdtemp(prefix='build-env-'))
def __enter__(self) -> DefaultIsolatedEnv:
try:
self._path = tempfile.mkdtemp(prefix='build-env-')
# 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)
self._python_executable, self._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,
)
# Call ``realpath`` to prevent spurious warning from being emitted
# that the venv location has changed on Windows. The username is
# DOS-encoded in the output of tempfile - the location is the same
# but the representation of it is different, which confuses venv.
# Ref: https://bugs.python.org/issue46171
self._path = os.path.realpath(tempfile.mkdtemp(prefix='build-env-'))
self._python_executable, self._scripts_dir = _create_isolated_env_venv(self._path)
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.
return self
: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
def __exit__(self, *args: object) -> None:
if os.path.exists(self._path): # in case the user already deleted skip remove
shutil.rmtree(self._path)
@property
def path(self) -> str:
"""The location of the isolated build environment."""
return self._path
@property
def python_executable(self) -> str:
"""The python executable of the isolated build environment."""
return self._python_executable
def make_extra_environ(self) -> dict[str, str]:
path = os.environ.get('PATH')
return {'PATH': os.pathsep.join([self._scripts_dir, path]) if path is not None else self._scripts_dir}
def install(self, requirements: Collection[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(f'Installing packages in isolated environment... ({", ".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, encoding='utf-8') as req_file:
req_file.write(os.linesep.join(requirements))
try:
cmd = [
self.python_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)
@staticmethod
def log(message: str) -> None:
"""
@ -141,85 +160,15 @@ class IsolatedEnvBuilder:
_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: Collection[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]:
def _create_isolated_env_virtualenv(path: str) -> tuple[str, str]:
"""
We optionally can 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
"""
import virtualenv
cmd = [str(path), '--no-setuptools', '--no-wheel', '--activators', '']
result = virtualenv.cli_run(cmd, setup_logging=False)
executable = str(result.creator.exe)
@ -240,12 +189,12 @@ def _fs_supports_symlink() -> bool:
try:
os.symlink(tmp_file.name, dest)
os.unlink(dest)
return True
except (OSError, NotImplementedError, AttributeError):
return False
return True
def _create_isolated_env_venv(path: str) -> Tuple[str, str]:
def _create_isolated_env_venv(path: str) -> tuple[str, str]:
"""
On Python 3 we use the venv package from the standard library.
@ -268,12 +217,12 @@ def _create_isolated_env_venv(path: str) -> Tuple[str, str]:
warnings.filterwarnings('ignore', 'check_home argument is deprecated and ignored.', DeprecationWarning)
venv.EnvBuilder(with_pip=True, symlinks=symlinks).create(path)
except subprocess.CalledProcessError as exc:
raise build.FailedProcessError(exc, 'Failed to create venv. Maybe try installing virtualenv.') from None
raise FailedProcessError(exc, 'Failed to create venv. Maybe try installing virtualenv.') from None
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]
pip_distribution = next(iter(metadata.distributions(name='pip', path=[purelib])))
current_pip_version = packaging.version.Version(pip_distribution.version)
if platform.system() == 'Darwin' and int(platform.mac_ver()[0].split('.')[0]) >= 11:
@ -293,7 +242,7 @@ def _create_isolated_env_venv(path: str) -> Tuple[str, str]:
return executable, script_dir
def _find_executable_and_scripts(path: str) -> Tuple[str, str, str]:
def _find_executable_and_scripts(path: str) -> tuple[str, str, str]:
"""
Detect the Python executable and script folder of a virtual environment.
@ -329,12 +278,13 @@ def _find_executable_and_scripts(path: str) -> Tuple[str, str, str]:
paths = sysconfig.get_paths(vars=config_vars)
executable = os.path.join(paths['scripts'], 'python.exe' if sys.platform.startswith('win') else 'python')
if not os.path.exists(executable):
raise RuntimeError(f'Virtual environment creation failed, executable {executable} missing')
msg = f'Virtual environment creation failed, executable {executable} missing'
raise RuntimeError(msg)
return executable, paths['scripts'], paths['purelib']
__all__ = [
'IsolatedEnvBuilder',
'IsolatedEnv',
'DefaultIsolatedEnv',
]

View File

@ -1,57 +1,57 @@
# SPDX-License-Identifier: MIT
import os
from __future__ import annotations
import pathlib
import sys
import tempfile
import pep517
import pyproject_hooks
import build
import build.env
from . import PathType, ProjectBuilder, RunnerType
from ._importlib import metadata
from .env import DefaultIsolatedEnv
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':
def _project_wheel_metadata(builder: ProjectBuilder) -> 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[arg-type]
return metadata.PathDistribution(path).metadata
def project_wheel_metadata(
srcdir: build.PathType,
source_dir: PathType,
isolated: bool = True,
) -> 'importlib_metadata.PackageMetadata':
*,
runner: RunnerType = pyproject_hooks.quiet_subprocess_runner,
) -> metadata.PackageMetadata:
"""
Return the wheel metadata for a project.
Uses the ``prepare_metadata_for_build_wheel`` hook if available,
otherwise ``build_wheel``.
:param srcdir: Project source directory
:param source_dir: Project source directory
:param isolated: Whether or not to run invoke the backend in the current
environment or to create an isolated one and invoke it
there.
:param runner: An alternative runner for backend subprocesses
"""
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'))
if isolated:
with DefaultIsolatedEnv() as env:
builder = ProjectBuilder.from_isolated_env(
env,
source_dir,
runner=runner,
)
env.install(builder.build_system_requires)
env.install(builder.get_requires_for_build('wheel'))
return _project_wheel_metadata(builder)
else:
builder = ProjectBuilder(
source_dir,
runner=runner,
)
return _project_wheel_metadata(builder)

View File

@ -1,5 +1,6 @@
# SPDX-License-Identifier: MIT
import contextlib
import os
import os.path
import shutil
@ -13,6 +14,12 @@ import pytest
import build.env
if sys.version_info < (3, 8):
import importlib_metadata as metadata
else:
from importlib import metadata
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
@ -31,7 +38,8 @@ def pytest_collection_modifyitems(config, items):
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")
msg = "--run-integration and --only-integration can't be used together, choose one"
raise pytest.UsageError(msg)
if len(items) == 1: # do not require flags if called directly
return
@ -109,3 +117,25 @@ def tmp_dir():
@pytest.fixture(autouse=True)
def force_venv(mocker):
mocker.patch.object(build.env, '_should_use_virtualenv', lambda: False)
def pytest_report_header() -> str:
interesting_packages = [
'build',
'colorama',
'filelock',
'packaging',
'pip',
'pyproject_hooks',
'setuptools',
'tomli',
'virtualenv',
'wheel',
]
valid = []
for package in interesting_packages:
# Old versions of importlib_metadata made this FileNotFoundError
with contextlib.suppress(ModuleNotFoundError, FileNotFoundError):
valid.append(f'{package}=={metadata.version(package)}')
reqs = ' '.join(valid)
return f'installed packages of interest: {reqs}'

View File

@ -1,9 +1,10 @@
importlib-metadata==0.22
importlib-metadata==4.6
packaging==19.0
pep517==0.9.1
pyproject_hooks==1.0
setuptools==42.0.0; python_version < "3.10"
setuptools==56.0.0; python_version >= "3.10"
toml==0.10.0
tomli==1.0.0
setuptools==56.0.0; python_version == "3.10"
setuptools==56.0.0; python_version == "3.11"
setuptools==67.8.0; python_version >= "3.12"
tomli==1.1.0
virtualenv==20.0.35
wheel==0.36.0

View File

@ -1,6 +1,6 @@
# SPDX-License-Identifier: MIT
from setuptools.build_meta import build_sdist # noqa: F401
from setuptools.build_meta import build_sdist as build_sdist
def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):

View File

@ -16,7 +16,8 @@ def build_sdist(sdist_directory, config_settings=None):
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!')
msg = 'some-file-that-is-needed-for-build.txt is missing!'
raise FileNotFoundError(msg)
# 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()

View File

@ -1,3 +1,4 @@
# SPDX-License-Identifier: MIT
from setuptools.build_meta import build_sdist, build_wheel # noqa: F401
from setuptools.build_meta import build_sdist as build_sdist
from setuptools.build_meta import build_wheel as build_wheel

View File

@ -1,6 +1,5 @@
# SPDX-License-Identifier: MIT
import collections
import inspect
import logging
import platform
import subprocess
@ -20,15 +19,15 @@ 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 build.env.DefaultIsolatedEnv() 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'])
subprocess.check_call([env.python_executable, '-c', f'{debug} import build.env'])
@pytest.mark.isolated
def test_isolated_environment_install(mocker):
with build.env.IsolatedEnvBuilder() as env:
with build.env.DefaultIsolatedEnv() as env:
mocker.patch('build.env._subprocess')
env.install([])
@ -38,7 +37,7 @@ def test_isolated_environment_install(mocker):
build.env._subprocess.assert_called()
args = build.env._subprocess.call_args[0][0][:-1]
assert args == [
env.executable,
env.python_executable,
'-Im',
'pip',
'install',
@ -52,7 +51,7 @@ def test_isolated_environment_install(mocker):
@pytest.mark.skipif(sys.platform != 'darwin', reason='workaround for Apple Python')
def test_can_get_venv_paths_with_conflicting_default_scheme(mocker):
get_scheme_names = mocker.patch('sysconfig.get_scheme_names', return_value=('osx_framework_library',))
with build.env.IsolatedEnvBuilder():
with build.env.DefaultIsolatedEnv():
pass
assert get_scheme_names.call_count == 1
@ -62,7 +61,7 @@ def test_can_get_venv_paths_with_posix_local_default_scheme(mocker):
get_paths = mocker.spy(sysconfig, 'get_paths')
# We should never call this, but we patch it to ensure failure if we do
get_default_scheme = mocker.patch('sysconfig.get_default_scheme', return_value='posix_local')
with build.env.IsolatedEnvBuilder():
with build.env.DefaultIsolatedEnv():
pass
get_paths.assert_called_once_with(scheme='posix_prefix', vars=mocker.ANY)
assert get_default_scheme.call_count == 0
@ -71,7 +70,7 @@ def test_can_get_venv_paths_with_posix_local_default_scheme(mocker):
def test_executable_missing_post_creation(mocker):
venv_create = mocker.patch('venv.EnvBuilder.create')
with pytest.raises(RuntimeError, match='Virtual environment creation failed, executable .* missing'):
with build.env.IsolatedEnvBuilder():
with build.env.DefaultIsolatedEnv():
pass
assert venv_create.call_count == 1
@ -105,8 +104,7 @@ def test_isolated_env_log(mocker, caplog, package_test_flit):
mocker.patch('build.env._subprocess')
caplog.set_level(logging.DEBUG)
builder = build.env.IsolatedEnvBuilder()
frameinfo = inspect.getframeinfo(inspect.currentframe())
builder = build.env.DefaultIsolatedEnv()
builder.log('something') # line number 106
with builder as env:
env.install(['something'])
@ -116,19 +114,15 @@ def test_isolated_env_log(mocker, caplog, package_test_flit):
('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] == [
frameinfo.lineno + 1,
frameinfo.lineno - 6,
frameinfo.lineno + 85,
]
@pytest.mark.isolated
def test_default_pip_is_never_too_old():
with build.env.IsolatedEnvBuilder() as env:
with build.env.DefaultIsolatedEnv() as env:
version = subprocess.check_output(
[env.executable, '-c', 'import pip; print(pip.__version__)'], universal_newlines=True
[env.python_executable, '-c', 'import pip; print(pip.__version__)'],
text=True,
encoding='utf-8',
).strip()
assert Version(version) >= Version('19.1')
@ -147,7 +141,7 @@ def test_pip_needs_upgrade_mac_os_11(mocker, pip_version, arch):
mocker.patch(metadata_name + '.distributions', return_value=(SimpleNamespace(version=pip_version),))
min_version = Version('20.3' if arch == 'x86_64' else '21.0.1')
with build.env.IsolatedEnvBuilder():
with build.env.DefaultIsolatedEnv():
if Version(pip_version) < min_version:
print(_subprocess.call_args_list)
upgrade_call, uninstall_call = _subprocess.call_args_list

View File

@ -1,5 +1,6 @@
# SPDX-License-Identifier: MIT
import importlib.util
import os
import os.path
import platform
@ -93,8 +94,8 @@ def get_project(name, tmp_path):
)
@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 in {'build', 'flit'} and '--no-isolation' in args:
pytest.xfail(f"can't build {project} 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')
@ -110,7 +111,7 @@ def test_build(monkeypatch, project, args, call, tmp_path):
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
args = [str(path), '-o', str(pkgs), *args]
if call is None:
build.__main__.main(args)
@ -123,11 +124,7 @@ def test_build(monkeypatch, project, args, call, tmp_path):
def test_isolation(tmp_dir, package_test_flit, mocker):
try:
import flit_core # noqa: F401
except ModuleNotFoundError:
pass
else:
if importlib.util.find_spec('flit_core'):
pytest.xfail('flit_core is available -- we want it missing!') # pragma: no cover
mocker.patch('build.__main__._error')

View File

@ -20,6 +20,8 @@ build_open_owner = 'builtins'
cwd = os.getcwd()
out = os.path.join(cwd, 'dist')
ANSI_STRIP = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
@pytest.mark.parametrize(
('cli_args', 'build_args', 'hook'),
@ -96,8 +98,9 @@ def test_parse_args(mocker, cli_args, build_args, hook):
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
else: # pragma: no cover
msg = f'Unknown hook {hook}'
raise ValueError(msg)
def test_prog():
@ -127,13 +130,13 @@ def test_build_isolated(mocker, package_test_flit):
],
)
mocker.patch('build.__main__._error')
install = mocker.patch('build.env._IsolatedEnvVenvPip.install')
install = mocker.patch('build.env.DefaultIsolatedEnv.install')
build.__main__.build_package(package_test_flit, '.', ['sdist'])
install.assert_any_call({'flit_core >=2,<3'})
required_cmd.assert_called_with('sdist')
required_cmd.assert_called_with('sdist', {})
install.assert_any_call(['dep1', 'dep2'])
build_cmd.assert_called_with('sdist', '.', {})
@ -170,7 +173,7 @@ def test_build_no_isolation_with_check_deps(mocker, package_test_flit, missing_d
@pytest.mark.isolated
def test_build_raises_build_exception(mocker, package_test_flit):
mocker.patch('build.ProjectBuilder.get_requires_for_build', side_effect=build.BuildException)
mocker.patch('build.env._IsolatedEnvVenvPip.install')
mocker.patch('build.env.DefaultIsolatedEnv.install')
with pytest.raises(build.BuildException):
build.__main__.build_package(package_test_flit, '.', ['sdist'])
@ -179,13 +182,14 @@ def test_build_raises_build_exception(mocker, package_test_flit):
@pytest.mark.isolated
def test_build_raises_build_backend_exception(mocker, package_test_flit):
mocker.patch('build.ProjectBuilder.get_requires_for_build', side_effect=build.BuildBackendException(Exception('a')))
mocker.patch('build.env._IsolatedEnvVenvPip.install')
mocker.patch('build.env.DefaultIsolatedEnv.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(package_test_flit, '.', ['sdist'])
@pytest.mark.network
@pytest.mark.pypy3323bug
def test_build_package(tmp_dir, package_test_setuptools):
build.__main__.build_package(package_test_setuptools, tmp_dir, ['sdist', 'wheel'])
@ -196,6 +200,7 @@ def test_build_package(tmp_dir, package_test_setuptools):
]
@pytest.mark.network
@pytest.mark.pypy3323bug
def test_build_package_via_sdist(tmp_dir, package_test_setuptools):
build.__main__.build_package_via_sdist(package_test_setuptools, tmp_dir, ['wheel'])
@ -221,7 +226,7 @@ def test_build_package_via_sdist_invalid_distribution(tmp_dir, package_test_setu
@pytest.mark.parametrize(
('args', 'output'),
[
(
pytest.param(
[],
[
'* Creating venv isolated environment...',
@ -236,8 +241,10 @@ def test_build_package_via_sdist_invalid_distribution(tmp_dir, package_test_setu
'* Building wheel...',
'Successfully built test_setuptools-1.0.0.tar.gz and test_setuptools-1.0.0-py2.py3-none-any.whl',
],
id='via-sdist-isolation',
marks=[pytest.mark.network, pytest.mark.isolated],
),
(
pytest.param(
['--no-isolation'],
[
'* Getting build dependencies for sdist...',
@ -247,8 +254,9 @@ def test_build_package_via_sdist_invalid_distribution(tmp_dir, package_test_setu
'* Building wheel...',
'Successfully built test_setuptools-1.0.0.tar.gz and test_setuptools-1.0.0-py2.py3-none-any.whl',
],
id='via-sdist-no-isolation',
),
(
pytest.param(
['--wheel'],
[
'* Creating venv isolated environment...',
@ -258,24 +266,28 @@ def test_build_package_via_sdist_invalid_distribution(tmp_dir, package_test_setu
'* Building wheel...',
'Successfully built test_setuptools-1.0.0-py2.py3-none-any.whl',
],
id='wheel-direct-isolation',
marks=[pytest.mark.network, pytest.mark.isolated],
),
(
pytest.param(
['--wheel', '--no-isolation'],
[
'* Getting build dependencies for wheel...',
'* Building wheel...',
'Successfully built test_setuptools-1.0.0-py2.py3-none-any.whl',
],
id='wheel-direct-no-isolation',
),
(
pytest.param(
['--sdist', '--no-isolation'],
[
'* Getting build dependencies for sdist...',
'* Building sdist...',
'Successfully built test_setuptools-1.0.0.tar.gz',
],
id='sdist-direct-no-isolation',
),
(
pytest.param(
['--sdist', '--wheel', '--no-isolation'],
[
'* Getting build dependencies for sdist...',
@ -284,20 +296,13 @@ def test_build_package_via_sdist_invalid_distribution(tmp_dir, package_test_setu
'* Building wheel...',
'Successfully built test_setuptools-1.0.0.tar.gz and test_setuptools-1.0.0-py2.py3-none-any.whl',
],
id='sdist-and-wheel-direct-no-isolation',
),
],
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(package_test_setuptools, tmp_dir, capsys, args, output):
build.__main__.main([package_test_setuptools, '-o', tmp_dir] + args)
build.__main__.main([package_test_setuptools, '-o', tmp_dir, *args])
stdout, stderr = capsys.readouterr()
assert stdout.splitlines() == output
@ -368,8 +373,10 @@ def test_output_env_subprocess_error(
assert stdout[:4] == stdout_body
assert stdout[-1].startswith(stdout_error)
assert len(stderr) == 1
assert stderr[0].startswith('ERROR: Invalid requirement: ')
# Newer versions of pip also color stderr - strip them if present
cleaned_stderr = ANSI_STRIP.sub('', '\n'.join(stderr)).strip()
assert len(cleaned_stderr.splitlines()) == 1
assert cleaned_stderr.startswith('ERROR: Invalid requirement: ')
@pytest.mark.parametrize(

View File

@ -0,0 +1,11 @@
import pytest
from build.__main__ import _natural_language_list
def test_natural_language_list():
assert _natural_language_list(['one']) == 'one'
assert _natural_language_list(['one', 'two']) == 'one and two'
assert _natural_language_list(['one', 'two', 'three']) == 'one, two and three'
with pytest.raises(IndexError, match='no elements'):
_natural_language_list([])

View File

@ -2,24 +2,18 @@
import copy
import importlib
import logging
import os
import pathlib
import sys
import textwrap
import pep517.wrappers
import pyproject_hooks
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
from build import _importlib
build_open_owner = 'builtins'
@ -31,7 +25,7 @@ DEFAULT_BACKEND = {
}
class MockDistribution(importlib_metadata.Distribution):
class MockDistribution(_importlib.metadata.Distribution):
def locate_file(self, path): # pragma: no cover
return ''
@ -49,7 +43,7 @@ class MockDistribution(importlib_metadata.Distribution):
return CircularMockDistribution()
elif name == 'nested_circular_dep':
return NestedCircularMockDistribution()
raise importlib_metadata.PackageNotFoundError
raise _importlib.metadata.PackageNotFoundError
class ExtraMockDistribution(MockDistribution):
@ -60,13 +54,13 @@ class ExtraMockDistribution(MockDistribution):
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'
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()
@ -142,33 +136,33 @@ class NestedCircularMockDistribution(MockDistribution):
('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-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[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]',
(
'extras_dep[recursive_extra_with_unmet_deps]',
'recursive_dep; extra == "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),
('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',)),
('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),
('circular_dep', None),
],
)
def test_check_dependency(monkeypatch, requirement_string, expected):
monkeypatch.setattr(importlib_metadata, 'Distribution', MockDistribution)
monkeypatch.setattr(_importlib.metadata, 'Distribution', MockDistribution)
assert next(build.check_dependency(requirement_string), None) == expected
@ -185,25 +179,25 @@ def test_bad_project(package_test_no_project):
def test_init(mocker, package_test_flit, package_legacy, test_no_permission, package_test_bad_syntax):
mocker.patch('pep517.wrappers.Pep517HookCaller')
mocker.patch('pyproject_hooks.BuildBackendHookCaller')
# correct flit pyproject.toml
builder = build.ProjectBuilder(package_test_flit)
pep517.wrappers.Pep517HookCaller.assert_called_with(
pyproject_hooks.BuildBackendHookCaller.assert_called_with(
package_test_flit, 'flit_core.buildapi', backend_path=None, python_executable=sys.executable, runner=builder._runner
)
pep517.wrappers.Pep517HookCaller.reset_mock()
pyproject_hooks.BuildBackendHookCaller.reset_mock()
# custom python
builder = build.ProjectBuilder(package_test_flit, python_executable='some-python')
pep517.wrappers.Pep517HookCaller.assert_called_with(
pyproject_hooks.BuildBackendHookCaller.assert_called_with(
package_test_flit, 'flit_core.buildapi', backend_path=None, python_executable='some-python', runner=builder._runner
)
pep517.wrappers.Pep517HookCaller.reset_mock()
pyproject_hooks.BuildBackendHookCaller.reset_mock()
# FileNotFoundError
builder = build.ProjectBuilder(package_legacy)
pep517.wrappers.Pep517HookCaller.assert_called_with(
pyproject_hooks.BuildBackendHookCaller.assert_called_with(
package_legacy,
'setuptools.build_meta:__legacy__',
backend_path=None,
@ -221,13 +215,11 @@ def test_init(mocker, package_test_flit, package_legacy, test_no_permission, pac
build.ProjectBuilder(package_test_bad_syntax)
@pytest.mark.parametrize('value', [b'something', 'something_else'])
def test_python_executable(package_test_flit, value):
builder = build.ProjectBuilder(package_test_flit)
builder.python_executable = value
assert builder.python_executable == value
assert builder._hook.python_executable == value
def test_init_makes_source_dir_absolute(package_test_flit):
rel_dir = os.path.relpath(package_test_flit, os.getcwd())
assert not os.path.isabs(rel_dir)
builder = build.ProjectBuilder(rel_dir)
assert os.path.isabs(builder.source_dir)
@pytest.mark.parametrize('distribution', ['wheel', 'sdist'])
@ -256,15 +248,15 @@ def test_build_missing_backend(packages_path, distribution, tmpdir):
def test_check_dependencies(mocker, package_test_flit):
mocker.patch('pep517.wrappers.Pep517HookCaller.get_requires_for_build_sdist')
mocker.patch('pep517.wrappers.Pep517HookCaller.get_requires_for_build_wheel')
mocker.patch('pyproject_hooks.BuildBackendHookCaller.get_requires_for_build_sdist')
mocker.patch('pyproject_hooks.BuildBackendHookCaller.get_requires_for_build_wheel')
builder = build.ProjectBuilder(package_test_flit)
side_effects = [
[],
['something'],
pep517.wrappers.BackendUnavailable,
pyproject_hooks.BackendUnavailable,
]
builder._hook.get_requires_for_build_sdist.side_effect = copy.copy(side_effects)
@ -285,23 +277,8 @@ def test_check_dependencies(mocker, package_test_flit):
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, package_test_flit, tmp_dir):
mocker.patch('build._working_directory', side_effect=OSError)
builder = build.ProjectBuilder(package_test_flit)
with pytest.raises(OSError):
builder._call_backend('build_sdist', tmp_dir)
def test_build(mocker, package_test_flit, tmp_dir):
mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
mocker.patch('build._working_directory', autospec=True)
mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True)
builder = build.ProjectBuilder(package_test_flit)
@ -310,23 +287,19 @@ def test_build(mocker, package_test_flit, tmp_dir):
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(package_test_flit)
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(package_test_flit)
with pytest.raises(build.BuildBackendException):
build._working_directory.assert_called_with(package_test_flit)
builder.build('sdist', tmp_dir)
with pytest.raises(build.BuildBackendException):
build._working_directory.assert_called_with(package_test_flit)
builder.build('wheel', tmp_dir)
def test_default_backend(mocker, package_legacy):
mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True)
builder = build.ProjectBuilder(package_legacy)
@ -334,7 +307,7 @@ def test_default_backend(mocker, package_legacy):
def test_missing_backend(mocker, package_test_no_backend):
mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True)
builder = build.ProjectBuilder(package_test_no_backend)
@ -342,21 +315,21 @@ def test_missing_backend(mocker, package_test_no_backend):
def test_missing_requires(mocker, package_test_no_requires):
mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True)
with pytest.raises(build.BuildException):
build.ProjectBuilder(package_test_no_requires)
def test_build_system_typo(mocker, package_test_typo):
mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True)
with pytest.warns(build.TypoWarning):
build.ProjectBuilder(package_test_typo)
def test_missing_outdir(mocker, tmp_dir, package_test_flit):
mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True)
builder = build.ProjectBuilder(package_test_flit)
builder._hook.build_sdist.return_value = 'dist.tar.gz'
@ -368,7 +341,7 @@ def test_missing_outdir(mocker, tmp_dir, package_test_flit):
def test_relative_outdir(mocker, tmp_dir, package_test_flit):
mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True)
builder = build.ProjectBuilder(package_test_flit)
builder._hook.build_sdist.return_value = 'dist.tar.gz'
@ -379,13 +352,13 @@ def test_relative_outdir(mocker, tmp_dir, package_test_flit):
def test_build_not_dir_outdir(mocker, tmp_dir, package_test_flit):
mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True)
builder = build.ProjectBuilder(package_test_flit)
builder._hook.build_sdist.return_value = 'dist.tar.gz'
out = os.path.join(tmp_dir, 'out')
open(out, 'a').close() # create empty file
open(out, 'a', encoding='utf-8').close() # create empty file
with pytest.raises(build.BuildException):
builder.build('sdist', out)
@ -395,7 +368,7 @@ def test_build_not_dir_outdir(mocker, tmp_dir, package_test_flit):
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'))
builder = build.ProjectBuilder(source_dir=os.path.join(os.path.dirname(__file__), 'packages', 'inline'))
out = tmp_path / 'dist'
builder.build('wheel', str(out))
return next(out.iterdir())
@ -432,7 +405,7 @@ def test_build_with_dep_on_console_script(tmp_path, demo_pkg_inline, capfd, mock
'''
)
(tmp_path / 'pyproject.toml').write_text(toml, encoding='UTF-8')
(tmp_path / 'build.py').write_text(code)
(tmp_path / 'build.py').write_text(code, encoding='utf-8')
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)
@ -449,29 +422,27 @@ def test_build_with_dep_on_console_script(tmp_path, demo_pkg_inline, capfd, mock
def test_prepare(mocker, tmp_dir, package_test_flit):
mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
mocker.patch('build._working_directory', autospec=True)
mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True)
builder = build.ProjectBuilder(package_test_flit)
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(package_test_flit)
def test_prepare_no_hook(mocker, tmp_dir, package_test_flit):
mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True)
builder = build.ProjectBuilder(package_test_flit)
failure = pep517.wrappers.HookMissing('prepare_metadata_for_build_wheel')
failure = pyproject_hooks.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, package_test_flit):
mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True)
builder = build.ProjectBuilder(package_test_flit)
builder._hook.prepare_metadata_for_build_wheel.side_effect = Exception
@ -481,19 +452,19 @@ def test_prepare_error(mocker, tmp_dir, package_test_flit):
def test_prepare_not_dir_outdir(mocker, tmp_dir, package_test_flit):
mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True)
builder = build.ProjectBuilder(package_test_flit)
out = os.path.join(tmp_dir, 'out')
with open(out, 'w') as f:
with open(out, 'w', encoding='utf-8') 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, package_test_flit):
mocker.patch('pep517.wrappers.Pep517HookCaller.prepare_metadata_for_build_wheel', return_value='')
mocker.patch('pyproject_hooks.BuildBackendHookCaller.prepare_metadata_for_build_wheel', return_value='')
builder = build.ProjectBuilder(package_test_flit)
@ -504,7 +475,7 @@ def test_no_outdir_single(mocker, tmp_dir, package_test_flit):
def test_no_outdir_multiple(mocker, tmp_dir, package_test_flit):
mocker.patch('pep517.wrappers.Pep517HookCaller.prepare_metadata_for_build_wheel', return_value='')
mocker.patch('pyproject_hooks.BuildBackendHookCaller.prepare_metadata_for_build_wheel', return_value='')
builder = build.ProjectBuilder(package_test_flit)
@ -515,8 +486,9 @@ def test_no_outdir_multiple(mocker, tmp_dir, package_test_flit):
def test_runner_user_specified(tmp_dir, package_test_flit):
def dummy_runner(cmd, cwd=None, env=None):
raise RuntimeError('Runner was called')
def dummy_runner(cmd, cwd=None, extra_environ=None):
msg = 'Runner was called'
raise RuntimeError(msg)
builder = build.ProjectBuilder(package_test_flit, runner=dummy_runner)
with pytest.raises(build.BuildBackendException, match='Runner was called'):
@ -526,7 +498,7 @@ def test_runner_user_specified(tmp_dir, package_test_flit):
def test_metadata_path_no_prepare(tmp_dir, package_test_no_prepare):
builder = build.ProjectBuilder(package_test_no_prepare)
metadata = importlib_metadata.PathDistribution(
metadata = _importlib.metadata.PathDistribution(
pathlib.Path(builder.metadata_path(tmp_dir)),
).metadata
@ -537,7 +509,7 @@ def test_metadata_path_no_prepare(tmp_dir, package_test_no_prepare):
def test_metadata_path_with_prepare(tmp_dir, package_test_setuptools):
builder = build.ProjectBuilder(package_test_setuptools)
metadata = importlib_metadata.PathDistribution(
metadata = _importlib.metadata.PathDistribution(
pathlib.Path(builder.metadata_path(tmp_dir)),
).metadata
@ -548,7 +520,7 @@ def test_metadata_path_with_prepare(tmp_dir, package_test_setuptools):
def test_metadata_path_legacy(tmp_dir, package_legacy):
builder = build.ProjectBuilder(package_legacy)
metadata = importlib_metadata.PathDistribution(
metadata = _importlib.metadata.PathDistribution(
pathlib.Path(builder.metadata_path(tmp_dir)),
).metadata
@ -563,33 +535,8 @@ def test_metadata_invalid_wheel(tmp_dir, package_test_bad_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)
@pytest.mark.skipif(sys.version_info >= (3, 11), reason='No need to test old toml support on 3.11+')
def test_toml_instead_of_tomli(mocker, mock_tomli_not_available, tmp_dir, package_test_flit):
mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
builder = build.ProjectBuilder(package_test_flit)
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, package_test_flit):
mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True)
mocker.patch('build.ProjectBuilder._call_backend', return_value='some_path')
caplog.set_level(logging.DEBUG)
@ -609,8 +556,6 @@ def test_log(mocker, caplog, package_test_flit):
('INFO', 'Building wheel...'),
('INFO', 'something'),
]
if sys.version_info >= (3, 8): # stacklevel
assert caplog.records[-1].lineno == 602
@pytest.mark.parametrize(

View File

@ -5,7 +5,7 @@ import sys
import tarfile
import zipfile
from pathlib import Path
from pathlib import Path, PurePosixPath
import pytest
@ -13,32 +13,41 @@ import pytest
DIR = Path(__file__).parent.resolve()
MAIN_DIR = DIR.parent
sdist_files = {
'.dockerignore',
'.gitignore',
'CHANGELOG.rst',
'LICENSE',
'PKG-INFO',
'README.md',
'docs/conf.py',
'pyproject.toml',
'setup.cfg',
'setup.py',
'src',
'src/build',
'src/build.egg-info',
'src/build.egg-info/PKG-INFO',
'src/build.egg-info/SOURCES.txt',
'src/build.egg-info/dependency_links.txt',
'src/build.egg-info/entry_points.txt',
'src/build.egg-info/requires.txt',
'src/build.egg-info/top_level.txt',
'src/build/__init__.py',
'src/build/__main__.py',
'src/build/env.py',
'src/build/py.typed',
'src/build/util.py',
'tests/constraints.txt',
'tests/packages/test-cant-build-via-sdist/some-file-that-is-needed-for-build.txt',
'tests/packages/test-no-project/empty.txt',
'tox.ini',
}
sdist_patterns = {
'docs/*.rst',
'src/build/*.py',
'tests/*.py',
'tests/packages/*/*.py',
'tests/packages/*/*/*.py',
'tests/packages/*/pyproject.toml',
'tests/packages/*/setup.*',
}
sdist_files |= {str(PurePosixPath(p.relative_to(MAIN_DIR))) for path in sdist_patterns for p in MAIN_DIR.glob(path)}
wheel_files = {
'build/__init__.py',
'build/__main__.py',
'build/_exceptions.py',
'build/_importlib.py',
'build/_util.py',
'build/env.py',
'build/py.typed',
'build/util.py',
@ -47,12 +56,11 @@ wheel_files = {
'dist-info/RECORD',
'dist-info/WHEEL',
'dist-info/entry_points.txt',
'dist-info/top_level.txt',
}
@pytest.mark.network
def test_build_sdist(monkeypatch, tmpdir):
monkeypatch.chdir(MAIN_DIR)
subprocess.run(
@ -65,19 +73,19 @@ def test_build_sdist(monkeypatch, tmpdir):
str(tmpdir),
],
check=True,
).stdout
)
(sdist,) = tmpdir.visit('*.tar.gz')
with tarfile.open(str(sdist), 'r:gz') as tar:
simpler = {n.split('/', 1)[-1] for n in tar.getnames()[1:]}
simpler = {n.split('/', 1)[-1] for n in tar.getnames()}
assert simpler == sdist_files
@pytest.mark.network
@pytest.mark.parametrize('args', ((), ('--wheel',)), ids=('from_sdist', 'direct'))
def test_build_wheel(monkeypatch, tmpdir, args):
monkeypatch.chdir(MAIN_DIR)
subprocess.run(

View File

@ -1,12 +1,14 @@
# SPDX-License-Identifier: MIT
import importlib.util
import pytest
import build.util
@pytest.mark.pypy3323bug
@pytest.mark.parametrize('isolated', [False, True])
@pytest.mark.parametrize('isolated', [False, pytest.param(True, marks=[pytest.mark.network, pytest.mark.isolated])])
def test_wheel_metadata(package_test_setuptools, isolated):
metadata = build.util.project_wheel_metadata(package_test_setuptools, isolated)
@ -14,13 +16,10 @@ def test_wheel_metadata(package_test_setuptools, isolated):
assert metadata['version'] == '1.0.0'
@pytest.mark.network
@pytest.mark.pypy3323bug
def test_wheel_metadata_isolation(package_test_flit):
try:
import flit_core # noqa: F401
except ModuleNotFoundError:
pass
else:
if importlib.util.find_spec('flit_core'):
pytest.xfail('flit_core is available -- we want it missing!') # pragma: no cover
metadata = build.util.project_wheel_metadata(package_test_flit)
@ -35,6 +34,7 @@ def test_wheel_metadata_isolation(package_test_flit):
build.util.project_wheel_metadata(package_test_flit, isolated=False)
@pytest.mark.network
@pytest.mark.pypy3323bug
def test_with_get_requires(package_test_metadata):
metadata = build.util.project_wheel_metadata(package_test_metadata)

83
tox.ini
View File

@ -1,30 +1,30 @@
[tox]
envlist =
requires =
tox>=4.2
virtualenv>=20.0.34
env_list =
fix
type
docs
path
{py311, py310, py39, py38, py37, py36, pypy37, pypy38, pypy39}{, -min}
isolated_build = true
{py312, py311, py310, py39, py38, py37, pypy39, pypy38, pypy37}{, -min}
skip_missing_interpreters = true
minversion = 3.14
requires =
virtualenv>=20.0.34
[testenv]
description =
run test suite with {basepython}
passenv =
extras =
test
pass_env =
LC_ALL
PIP_*
PYTEST_*
TERM
setenv =
set_env =
COVERAGE_FILE = {toxworkdir}/.coverage.{envname}
TEST_STATUS_DIR = {envtmpdir}
PYPY3323BUG = 1
extras =
test
PYTHONWARNDEFAULTENCODING = 1
TEST_STATUS_DIR = {envtmpdir}
commands =
pytest -ra --cov --cov-config pyproject.toml \
--cov-report=html:{envdir}/htmlcov --cov-context=test \
@ -32,49 +32,52 @@ commands =
[testenv:fix]
description = run static analysis and style checks
passenv =
HOMEPATH
PROGRAMDATA
basepython = python3.9
base_python = python3.9
skip_install = true
deps =
pre-commit>=2
pass_env =
HOMEPATH
PROGRAMDATA
commands =
pre-commit run --all-files --show-diff-on-failure
python -c 'print("hint: run {envdir}/bin/pre-commit install to add checks as pre-commit hook")'
[testenv:path]
description = verify build can run from source (bootstrap)
setenv =
PYTHONPATH = {toxinidir}/src
COVERAGE_FILE = {toxworkdir}/.coverage.{envname}
commands_pre =
python -E -m pip uninstall -y build colorama
[testenv:type]
description = run type check on code base
extras = typing
extras =
typing
set_env =
PYTHONWARNDEFAULTENCODING =
commands =
mypy
[testenv:{py311, py310, py39, py38, py37, py36, pypy37, pypy38, pypy39}-min]
description = check minimum versions required of all dependencies
skip_install = true
commands_pre =
pip install .[test] -c tests/constraints.txt
[testenv:docs]
description = build documentations
basepython = python3.8
base_python = python3.10
extras =
docs
commands =
sphinx-build -n docs {envtmpdir} {posargs:-W}
python -c 'print("Documentation available under file://{envtmpdir}/index.html")'
[testenv:path]
description = verify build can run from source (bootstrap)
set_env =
COVERAGE_FILE = {toxworkdir}/.coverage.{envname}
PYTHONPATH = {toxinidir}/src
commands_pre =
python -E -m pip uninstall -y build colorama
[testenv:{py312, py311, py310, py39, py38, py37, pypy37, pypy38, pypy39}-min]
description = check minimum versions required of all dependencies
skip_install = true
commands_pre =
pip install .[test] -c tests/constraints.txt
[testenv:dev]
description = generate a DEV environment
usedevelop = true
package = editable
deps =
virtualenv>=20.0.34
extras =
@ -86,24 +89,20 @@ commands =
[testenv:coverage]
description = combine coverage from test environments
passenv =
DIFF_AGAINST
setenv =
skip_install = true
deps =
coverage[toml]>=5.1
diff_cover>=3
parallel_show_output = true
pass_env =
DIFF_AGAINST
set_env =
commands =
coverage combine {toxworkdir}
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 = {py311, py310, py39, py38, py37, py36, pypy37, pypy38, pypy39}{,-min}, path
[flake8]
max-line-length = 127
max-complexity = 10
extend-ignore = E203
extend-select = B9
depends =
path
{py312, py311, py310, py39, py38, py37, pypy39, pypy38, pypy37}{, -min}