├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── changelogithub.config.json ├── docs ├── api.md ├── build_config.md ├── hooks.md ├── index.md ├── metadata.md └── migration.md ├── mkdocs.yml ├── netlify.toml ├── pdm.lock ├── pyproject.toml ├── runtime.txt ├── scripts ├── build.py └── patches │ └── pyproject_metadata.patch ├── src └── pdm │ └── backend │ ├── __init__.py │ ├── _vendor │ ├── __init__.py │ ├── editables │ │ ├── LICENSE.txt │ │ ├── __init__.py │ │ ├── py.typed │ │ └── redirector.py │ ├── packaging │ │ ├── LICENSE │ │ ├── LICENSE.APACHE │ │ ├── LICENSE.BSD │ │ ├── __init__.py │ │ ├── _elffile.py │ │ ├── _manylinux.py │ │ ├── _musllinux.py │ │ ├── _parser.py │ │ ├── _structures.py │ │ ├── _tokenizer.py │ │ ├── markers.py │ │ ├── metadata.py │ │ ├── py.typed │ │ ├── requirements.py │ │ ├── specifiers.py │ │ ├── tags.py │ │ ├── utils.py │ │ └── version.py │ ├── pyproject_metadata │ │ ├── LICENSE │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── errors.py │ │ ├── project_table.py │ │ ├── py.typed │ │ └── pyproject.py │ ├── tomli │ │ ├── LICENSE │ │ ├── __init__.py │ │ ├── _parser.py │ │ ├── _re.py │ │ ├── _types.py │ │ └── py.typed │ ├── tomli_w │ │ ├── LICENSE │ │ ├── __init__.py │ │ ├── _writer.py │ │ └── py.typed │ └── vendor.txt │ ├── base.py │ ├── config.py │ ├── editable.py │ ├── exceptions.py │ ├── hooks │ ├── __init__.py │ ├── base.py │ ├── setuptools.py │ └── version │ │ ├── __init__.py │ │ └── scm.py │ ├── intree.py │ ├── py.typed │ ├── sdist.py │ ├── structures.py │ ├── utils.py │ └── wheel.py └── tests ├── __init__.py ├── conftest.py ├── fixtures ├── hooks │ ├── hook_class.py │ ├── hook_module.py │ └── local_hook.py └── projects │ ├── demo-cextension-in-src │ ├── LICENSE │ ├── pdm.lock │ ├── pdm_build.py │ ├── pyproject.toml │ └── src │ │ └── my_package │ │ ├── __init__.py │ │ └── hellomodule.c │ ├── demo-cextension │ ├── LICENSE │ ├── my_package │ │ ├── __init__.py │ │ └── hellomodule.c │ ├── pdm.lock │ ├── pdm_build.py │ └── pyproject.toml │ ├── demo-combined-extras │ ├── LICENSE │ ├── demo.py │ └── pyproject.toml │ ├── demo-explicit-package-dir │ ├── LICENSE │ ├── README.md │ ├── data_out.json │ ├── foo │ │ └── my_package │ │ │ ├── __init__.py │ │ │ └── data.json │ ├── pyproject.toml │ └── single_module.py │ ├── demo-licenses │ ├── LICENSE │ ├── README.md │ ├── licenses │ │ ├── LICENSE.APACHE.md │ │ └── LICENSE.MIT.md │ ├── pyproject.toml │ └── src │ │ └── foo_module.py │ ├── demo-metadata-test │ ├── LICENSE │ ├── README.md │ └── pyproject.toml │ ├── demo-module │ ├── LICENSE │ ├── README.md │ ├── bar_module.py │ ├── foo_module.py │ └── pyproject.toml │ ├── demo-no-version │ ├── README.md │ ├── anothername.toml │ └── pyproject.toml │ ├── demo-package-include-error │ ├── LICENSE │ ├── README.md │ ├── data_out.json │ ├── my_package │ │ ├── __init__.py │ │ └── data.json │ ├── pdm.lock │ ├── pyproject.toml │ ├── requirements.txt │ ├── requirements_simple.txt │ └── single_module.py │ ├── demo-package-include │ ├── LICENSE │ ├── README.md │ ├── data_out.json │ ├── my_package │ │ ├── __init__.py │ │ └── data.json │ ├── pdm.lock │ ├── pyproject.toml │ ├── requirements.txt │ ├── requirements_simple.txt │ ├── scripts │ │ └── data │ │ │ └── my_script.sh │ └── single_module.py │ ├── demo-package-stubs │ ├── LICENSE │ ├── README.md │ ├── pyproject.toml │ └── src │ │ └── my_package-stubs │ │ ├── __init__.pyi │ │ └── py.typed │ ├── demo-package-with-deep-path │ ├── LICENSE │ ├── README.md │ ├── my_package │ │ ├── __init__.py │ │ └── data │ │ │ ├── data_a.json │ │ │ └── data_inner │ │ │ └── data_b.json │ └── pyproject.toml │ ├── demo-package-with-tests │ ├── LICENSE │ ├── README.md │ ├── my_package │ │ ├── __init__.py │ │ └── data.json │ ├── pdm.lock │ ├── pyproject.toml │ └── tests │ │ └── __init__.py │ ├── demo-package │ ├── LICENSE │ ├── README.md │ ├── data_out.json │ ├── my_package │ │ ├── __init__.py │ │ ├── data.json │ │ └── executable │ ├── pdm.lock │ ├── pyproject.toml │ ├── requirements.txt │ ├── requirements_simple.txt │ └── single_module.py │ ├── demo-pep420-package │ ├── LICENSE │ ├── README.md │ ├── foo │ │ └── my_package │ │ │ ├── __init__.py │ │ │ └── data.json │ └── pyproject.toml │ ├── demo-purelib-with-build │ ├── LICENSE │ ├── my_build.py │ ├── my_package │ │ └── __init__.py │ ├── pdm.lock │ └── pyproject.toml │ ├── demo-reuse-spec │ ├── LICENSES │ │ └── MPL-2.0.txt │ ├── README.md │ ├── pyproject.toml │ └── src │ │ └── foo_module.py │ ├── demo-src-package-include │ ├── LICENSE │ ├── README.md │ ├── data_out.json │ ├── pyproject.toml │ ├── single_module.py │ └── sub │ │ └── my_package │ │ ├── __init__.py │ │ └── data.json │ ├── demo-src-package │ ├── LICENSE │ ├── README.md │ ├── data_out.json │ ├── pyproject.toml │ ├── single_module.py │ └── src │ │ └── my_package │ │ ├── __init__.py │ │ └── data.json │ ├── demo-src-pymodule │ ├── LICENSE │ ├── README.md │ ├── pyproject.toml │ └── src │ │ └── foo_module.py │ └── demo-using-scm │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── foo │ └── __init__.py │ ├── pyproject.toml │ └── version.py ├── pdm └── backend │ └── hooks │ └── version │ └── test_scm.py ├── test_api.py ├── test_file_finder.py ├── test_hooks.py ├── test_metadata.py ├── test_utils.py ├── test_wheel.py └── testutils.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | Testing: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "pypy-3.9"] 15 | os: [ubuntu-latest, windows-latest, macos-latest] 16 | exclude: 17 | - python-version: 3.9 18 | os: macos-latest 19 | include: 20 | - python-version: 3.9 21 | os: macos-13 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: Set up Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@v5 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | allow-prereleases: true 30 | cache: "pip" 31 | - name: Setup PDM 32 | uses: pdm-project/setup-pdm@v4 33 | - name: Install Dependencies 34 | run: pdm sync 35 | - name: Install Mercurial 36 | shell: bash 37 | run: | 38 | case "$RUNNER_OS" in 39 | "Linux") 40 | sudo apt install mercurial 41 | ;; 42 | "Windows") 43 | choco install hg 44 | ;; 45 | "macOS") 46 | brew install mercurial 47 | ;; 48 | "*") 49 | echo "$RUNNER_OS not supported" 50 | exit 1 51 | ;; 52 | esac 53 | 54 | echo "[ui]" >> ~/.hgrc 55 | echo "username = \"John Doe \"" >> ~/.hgrc 56 | - name: Run Tests 57 | run: | 58 | pdm run pytest -vvv tests 59 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | release-pypi: 10 | name: release-pypi 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: 20 21 | 22 | - run: npx changelogithub 23 | continue-on-error: true 24 | env: 25 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 26 | 27 | - uses: actions/setup-python@v5 28 | with: 29 | python-version: "3.11" 30 | 31 | - name: Build artifacts 32 | run: | 33 | pipx run build 34 | 35 | - name: Test Build 36 | run: | 37 | pip install dist/*.whl 38 | pip uninstall -y pdm-backend 39 | pip install dist/*.tar.gz 40 | pip uninstall -y pdm-backend 41 | 42 | - name: Upload to Pypi 43 | run: | 44 | pipx run twine upload --username __token__ --password ${{ secrets.PYPI_TOKEN }} dist/* 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | docs/site 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | .vscode/ 131 | caches/ 132 | .idea/ 133 | __pypackages__ 134 | .pdm-python 135 | .pdm-build/ 136 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | autoupdate_schedule: monthly 3 | 4 | exclude: ^src/pdm/backend/_vendor 5 | repos: 6 | - repo: https://github.com/astral-sh/ruff-pre-commit 7 | rev: 'v0.11.4' 8 | hooks: 9 | - id: ruff 10 | args: [--fix, --exit-non-zero-on-fix, --show-fixes] 11 | - id: ruff-format 12 | 13 | - repo: https://github.com/pre-commit/mirrors-mypy 14 | rev: v1.15.0 15 | hooks: 16 | - id: mypy 17 | exclude: ^(src/pdm/backend/_vendor|tests|scripts) 18 | 19 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Everyone interacting in the PDM project's codebases and issue trackers is expected to 2 | follow the [PyPA Code of Conduct](https://www.pypa.io/en/latest/code-of-conduct/). 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to PDM 2 | 3 | First off, thanks for taking the time to contribute! Contributions include but are not restricted to: 4 | 5 | * Reporting bugs 6 | * Contributing to code 7 | * Writing tests 8 | * Writing documents 9 | 10 | The following is a set of guidelines for contributing. 11 | 12 | ## A recommended flow of contributing to an Open Source project. 13 | 14 | This guideline is for new beginners of OSS. If you are an experienced OSS developer, you can skip 15 | this section. 16 | 17 | 1. First, fork this project to your own namespace using the fork button at the top right of the repository page. 18 | 2. Clone the **upstream** repository to local: 19 | ```bash 20 | $ git clone https://github.com/pdm-project/pdm-backend.git 21 | # Or if you prefer SSH clone: 22 | $ git clone git@github.com:pdm-project/pdm-backend.git 23 | ``` 24 | 3. Add the fork as a new remote: 25 | ```bash 26 | $ git remote add fork https://github.com/yourname/pdm-backend.git 27 | $ git fetch fork 28 | ``` 29 | where `fork` is the remote name of the fork repository. 30 | 31 | **ProTips:** 32 | 33 | 1. Don't modify code on the main branch, the main branch should always keep in track with origin/main. 34 | 35 | To update main branch to date: 36 | 37 | ```bash 38 | $ git pull origin main 39 | # In rare cases that your local main branch diverges from the remote main: 40 | $ git fetch origin && git reset --hard main 41 | ``` 42 | 2. Create a new branch based on the up-to-date main for new patches. 43 | 3. Create a Pull Request from that patch branch. 44 | 45 | ## Local development 46 | 47 | Following [the guide][pdm-install] to install PDM on your machine, then install the development dependencies: 48 | 49 | ```bash 50 | $ pdm sync 51 | ``` 52 | 53 | It will create a virtualenv at `$PWD/.venv` and install all dependencies into it. 54 | 55 | [pdm-install]: https://pdm-project.org/latest/#installation 56 | 57 | ### Run tests 58 | 59 | ```bash 60 | $ pdm run pytest -vv tests 61 | ``` 62 | 63 | The test suite is still simple and requires to be supplied, please help write more test cases. 64 | 65 | ### Code style 66 | 67 | PDM uses `pre-commit` for linting, you need to install `pre-commit` first, then: 68 | 69 | ```bash 70 | $ pre-commit install 71 | $ pre-commit run --all-files 72 | ``` 73 | 74 | PDM uses `ruff` for code style and linting, if you are not following its 75 | suggestions, the CI will fail and your Pull Request will not be merged. 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Frost Ming 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PDM-Backend 2 | 3 | The build backend used by [PDM] that supports latest packaging standards. 4 | 5 | [![PyPI](https://img.shields.io/pypi/v/pdm-backend?label=PyPI)](https://pypi.org/project/pdm-backend) 6 | [![Tests](https://github.com/pdm-project/pdm-backend/actions/workflows/ci.yml/badge.svg)](https://github.com/pdm-project/pdm-backend/actions/workflows/ci.yml) 7 | [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/pdm-project/pdm-backend/main.svg)](https://results.pre-commit.ci/latest/github/pdm-project/pdm-backend/main) 8 | [![pdm-managed](https://img.shields.io/endpoint?url=https%3A%2F%2Fcdn.jsdelivr.net%2Fgh%2Fpdm-project%2F.github%2Fbadge.json)](https://pdm-project.org) 9 | 10 | This is the backend for [PDM] projects that is fully-compatible with [PEP 517] spec, but you can also use it alone. 11 | It reads the metadata of [PEP 621] format and coverts it to [Core metadata]. 12 | 13 | [pep 517]: https://www.python.org/dev/peps/pep-0517/ 14 | [pep 621]: https://www.python.org/dev/peps/pep-0621/ 15 | [Core metadata]: https://packaging.python.org/specifications/core-metadata/ 16 | [PDM]: https://pdm-project.org 17 | 18 | ## Links 19 | 20 | - [Documentation](https://backend.pdm-project.org) 21 | - [Changelog](https://github.com/pdm-project/pdm-backend/releases) 22 | - [PDM Documentation][PDM] 23 | - [PyPI](https://pypi.org/project/pdm-backend) 24 | - [Discord](https://discord.gg/Phn8smztpv) 25 | 26 | > **NOTE** 27 | > This project has been renamed from `pdm-pep517` and the old project lives in the [legacy] branch. 28 | 29 | [legacy]: https://github.com/pdm-project/pdm-backend/tree/legacy 30 | 31 | ## Sponsors 32 | 33 |

34 | 35 | 36 | 37 |

38 | 39 | ## License 40 | 41 | This project is licensed under [MIT license](/LICENSE). 42 | -------------------------------------------------------------------------------- /changelogithub.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "types": { 3 | "feat": { "title": "🚀 Features" }, 4 | "fix": { "title": "🐞 Bug Fixes" }, 5 | "doc": { "title": "📝 Documentation" }, 6 | "chore": { "title": "💻 Chores" } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | ::: pdm.backend.hooks.base 4 | options: 5 | show_root_heading: yes 6 | show_source: false 7 | heading_level: 2 8 | 9 | ::: pdm.backend.config 10 | options: 11 | show_root_heading: yes 12 | show_source: false 13 | heading_level: 2 14 | -------------------------------------------------------------------------------- /docs/hooks.md: -------------------------------------------------------------------------------- 1 | # Build hooks 2 | 3 | It is rather easy to write a build hook, `pdm-backend` now provides 6 hooks for you to customize the every step of the build process, and you don't need to implement them all. Let's learn it with some examples. 4 | 5 | ## Modify the project metadata 6 | 7 | === "pdm_build.py" 8 | 9 | ```python 10 | def pdm_build_initialize(context): 11 | metadata = context.config.metadata 12 | metadata["dependencies"].append("requests") 13 | ``` 14 | 15 | The metadata can be accessed like a dictionary, and any update to the object will take effect immediately. 16 | For sdist builds, the modified `pyproject.toml` will be written into the tarball. 17 | 18 | !!! TIP 19 | As you can see, you don't need to import any specific stuff from `pdm.backend`. Of course, you may need to when you want 20 | to type-annotate your hook function. 21 | 22 | ## Add more files or change the collected files 23 | 24 | There are two ways to achieve this. 25 | 26 | 1. Any files generated under `context.build_dir`, unless excluded by `excludes` setting, will be collected automatically. 27 | 28 | === "pdm_build.py" 29 | 30 | ```{ .python .annotate } 31 | def pdm_build_initialize(context): 32 | context.ensure_build_dir() # make sure it is created. 33 | with open(os.path.join(context.build_dir, "COPYING.md"), "w") as f: # (1)! 34 | f.write("This is a generated COPYING file.") 35 | ``` 36 | 37 | 1. The file will be packed as `COPYING.md` inside the artifact. 38 | 39 | Or, you can generate it anywhere and include the path explicitly. 40 | 41 | === "pdm_build.py" 42 | 43 | ```{ .python .annotate } 44 | def pdm_build_update_files(context, files): 45 | extra_file_path = Path.home() / ".config/myconfig.yaml" 46 | files["myconfig.yaml"] = extra_file_path # (1)! 47 | ``` 48 | 49 | 1. The file path inside the artifact will be `myconfig.yaml`. 50 | 51 | ## Call setup() function to build extensions 52 | 53 | === "pyproject.toml" 54 | 55 | ```toml 56 | [tool.pdm.build] 57 | run-setuptools = true 58 | ``` 59 | 60 | === "pdm_build.py" 61 | 62 | ```python 63 | from setuptools import Extension 64 | 65 | ext_modules = [Extension("my_package.hello", ["my_package/hellomodule.c"])] 66 | 67 | def pdm_build_update_setup_kwargs(context, setup_kwargs): 68 | setup_kwargs.update(ext_modules=ext_modules) 69 | ``` 70 | 71 | ## Enable the hook for a specific build target 72 | 73 | Sometimes you only want to activate the hook for a specific hook, you can define the `pdm_build_hook_enabled()` hook: 74 | 75 | === "pdm_build.py" 76 | 77 | ```python 78 | def pdm_build_hook_enabled(context): 79 | # Only enable for sdist builds 80 | return context.target == "sdist" 81 | ``` 82 | 83 | You can also look at the `context` object inside a specific hook to determine it should be called. 84 | 85 | ## Change the build directory 86 | 87 | By default, build files will be written to `.pdm-build` directory under the project root. You can change it by setting `context.build_dir`. 88 | However, since the build clean will be run before `pdm_build_initialize`, this must be done in the `pdm_build_clean` hook: 89 | 90 | === "pdm_build.py" 91 | 92 | ```python 93 | def pdm_build_clean(context): 94 | context.build_dir = Path(make_temp_dir()) 95 | ``` 96 | 97 | ## Build hooks flow 98 | 99 | The hooks are called in the following order: 100 | 101 | ```mermaid 102 | flowchart TD 103 | A{{pdm_build_hook_enabled?}} --> pdm_build_clean 104 | pdm_build_clean --> pdm_build_initialize 105 | pdm_build_initialize --> B{{run-setuptools?}} 106 | B --> |yes|C[pdm_build_update_setup_kwargs] 107 | B --> |no|D[pdm_build_update_files] 108 | C --> D 109 | D --> pdm_build_finalize 110 | ``` 111 | 112 | Read the [API reference](./api.md) for more details. 113 | 114 | ## Distribute the hook as a plugin 115 | 116 | If you want to share your hook with others, you can change the hook script file into a Python package and upload it to PyPI. 117 | 118 | File structure: 119 | 120 | ``` 121 | pdm-build-mypyc 122 | ├── mypyc_build.py 123 | ├── LICENSE 124 | ├── README.md 125 | └── pyproject.toml 126 | ``` 127 | 128 | === "pyproject.toml" 129 | 130 | ```toml 131 | [build-system] 132 | requires = ["pdm-backend"] 133 | build-backend = "pdm.backend" 134 | 135 | [project] 136 | name = "pdm-build-mypyc" 137 | version = "0.1.0" 138 | description = "A pdm build hook to compile Python code with mypyc" 139 | authors = [{name = "...", email = "..."}] 140 | license = "MIT" 141 | readme = "README.md" 142 | 143 | [project.entry-points."pdm.build.hook"] 144 | mypyc = "mypyc_build:MypycBuildHook" 145 | ``` 146 | 147 | === "mypyc_build.py" 148 | 149 | ```python 150 | class MypycBuildHook: 151 | def pdm_build_hook_enabled(self, context): 152 | return context.target == "wheel" 153 | 154 | def pdm_build_initialize(self, context): 155 | context.ensure_build_dir() 156 | mypyc_build(context.build_dir) 157 | ``` 158 | 159 | The plugin must be distributed with an entry point under `pdm.build.hook` group. The entry point value can be any of the following: 160 | 161 | - A module containing hook functions 162 | - A class object which implements the hook functions as methods 163 | - An instance of the above class object 164 | 165 | After it is published, another user can enable this hook by including the package name in `build-system.requires`: 166 | 167 | ```toml 168 | [build-system] 169 | requires = ["pdm-backend", "pdm-build-mypyc"] 170 | build-backend = "pdm.backend" 171 | ``` 172 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # PDM-Backend 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/pdm-backend?label=PyPI)](https://pypi.org/project/pdm-backend) 4 | [![Tests](https://github.com/pdm-project/pdm-backend/actions/workflows/ci.yml/badge.svg)](https://github.com/pdm-project/pdm-backend/actions/workflows/ci.yml) 5 | [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/pdm-project/pdm-backend/main.svg)](https://results.pre-commit.ci/latest/github/pdm-project/pdm-backend/main) 6 | [![pdm-managed](https://img.shields.io/badge/pdm-managed-blueviolet)][PDM] 7 | 8 | PDM-Backend is a build backend that supports the latest packaging standards, which includes: 9 | 10 | - [PEP 517] Build backend API 11 | - [PEP 621] Project metadata 12 | - [PEP 660] Editable build backend 13 | 14 | [PEP 517]: https://www.python.org/dev/peps/pep-0517/ 15 | [PEP 621]: https://www.python.org/dev/peps/pep-0621/ 16 | [PEP 660]: https://www.python.org/dev/peps/pep-0660/ 17 | 18 | ## Quick start 19 | 20 | To use it as PEP 517 build backend, edit your `pyproject.toml` as below: 21 | 22 | ```toml 23 | [build-system] 24 | requires = ["pdm-backend"] 25 | build-backend = "pdm.backend" 26 | ``` 27 | 28 | It is recommended to use [PDM] to manage your project, which will automatically generate the above configuration for you. 29 | 30 | [PDM]: https://pdm-project.org 31 | 32 | Write the project metadata in `pyproject.toml` in [PEP 621] format: 33 | 34 | ```toml 35 | [project] 36 | name = "my-project" 37 | version = "0.1.0" 38 | description = "A project built with PDM-Backend" 39 | authors = [{name = "John Doe", email="me@johndoe.org"}] 40 | dependencies = ["requests"] 41 | requires-python = ">=3.9" 42 | readme = "README.md" 43 | license = "MIT" 44 | ``` 45 | 46 | Then run the build command to build the project as wheel and sdist: 47 | 48 | === "PDM" 49 | 50 | ```bash 51 | pdm build 52 | ``` 53 | 54 | === "build" 55 | 56 | ```bash 57 | python -m build 58 | # Or 59 | pyproject-build 60 | ``` 61 | 62 | Read the corresponding documentation sections for more details: 63 | 64 | - [Metadata](./metadata.md) for how to write the project metadata 65 | - [Build configuration](./build_config.md) for customizing the build process. 66 | 67 | ## Migrate from `pdm-pep517` 68 | 69 | `pdm-backend` is the successor of `pdm-pep517`. If you are using the latter for your project, read the [migration guide](./migration.md) to migrate to `pdm-backend`. 70 | 71 | ## Sponsors 72 | 73 | Thanks to all the individuals and organizations who sponsor PDM project! 74 | 75 |

76 | 77 | 78 | 79 |

80 | -------------------------------------------------------------------------------- /docs/metadata.md: -------------------------------------------------------------------------------- 1 | # Project metadata 2 | 3 | The project metadata is stored in the `project` table in `pyproject.toml`, which is based on [PEP 621](https://peps.python.org/pep-0621/). 4 | 5 | On top of that, we also support some additional features. 6 | 7 | ## Dynamic project version 8 | 9 | `pdm-backend` can determine the version of the project dynamically. To do this, you need to leave the `version` field out from your `pyproject.toml` and add `version` to the list of `project.dynamic`: 10 | 11 | ```diff 12 | [project] 13 | ... 14 | - version = "0.1.0" remove this line 15 | + dynamic = ["version"] 16 | ``` 17 | 18 | Then in `[tool.pdm.version]` table, specify how to get the version info. There are three ways supported: 19 | 20 | ### Read from a static string in the given file path 21 | 22 | ```toml 23 | [tool.pdm.version] 24 | source = "file" 25 | path = "mypackage/__init__.py" 26 | ``` 27 | 28 | In this way, the file MUST contain a line like: 29 | 30 | ```python 31 | __version__ = "0.1.0" # Single quotes and double quotes are both OK, comments are allowed. 32 | ``` 33 | 34 | ### Read from SCM tag, supporting `git` and `hg` 35 | 36 | ```toml 37 | [tool.pdm.version] 38 | source = "scm" 39 | ``` 40 | 41 | When building from a source tree where SCM is not available, you can use the env var `PDM_BUILD_SCM_VERSION` to pretend the version is set. 42 | 43 | ```bash 44 | PDM_BUILD_SCM_VERSION=0.1.0 python -m build 45 | ``` 46 | 47 | +++ 2.2.0 48 | 49 | Alternatively, you can specify a default version in the configuration: 50 | 51 | ```toml 52 | [tool.pdm.version] 53 | fallback_version = "0.0.0" 54 | ``` 55 | 56 | To control which scm tags are used to generate the version, you can use two 57 | fields: `tag_filter` and `tag_regex`. 58 | 59 | ```toml 60 | [tool.pdm.version] 61 | source = "scm" 62 | tag_filter = "test/*" 63 | tag_regex = '^test/(?:\D*)?(?P([1-9][0-9]*!)?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*((a|b|c|rc)(0|[1-9][0-9]*))?(\.post(0|[1-9][0-9]*))?(\.dev(0|[1-9][0-9]*))?$)$' 64 | ``` 65 | 66 | `tag_filter` filters the set of tags which are considered as candidates to 67 | capture your project's version. For `git` repositories, this field is a glob 68 | matched against the tag. For `hg` repositories, it is a regular expression used 69 | with the `latesttag` function. 70 | 71 | `tag_regex` configures how you extract a version from a tag. It is applied after 72 | `tag_filter` extracts candidate tags to extract the version from that tag. It is 73 | a python style regular expression. 74 | 75 | +++ 2.2.0 76 | 77 | To customize the format of the version string, specify the `version_format` option with a format function: 78 | 79 | ```toml 80 | [tool.pdm.version] 81 | source = "scm" 82 | version_format = "mypackage.version:format_version" 83 | ``` 84 | 85 | ```python 86 | # mypackage/version.py 87 | from pdm.backend.hooks.version import SCMVersion 88 | 89 | def format_version(version: SCMVersion) -> str: 90 | if version.distance is None: 91 | return str(version.version) 92 | else: 93 | return f"{version.version}.post{version.distance}" 94 | ``` 95 | 96 | +++ 2.4.0 97 | 98 | From 2.4.0, the `version_format` function **can** take `context` as the second argument. 99 | 100 | ### Get with a specific function 101 | 102 | ```toml 103 | [tool.pdm.version] 104 | source = "call" 105 | getter = "mypackage.version:get_version" 106 | ``` 107 | 108 | You can also supply it with literal arguments: 109 | 110 | ```toml 111 | getter = "mypackage.version:get_version('dev')" 112 | ``` 113 | 114 | !!! note 115 | 116 | The module that the version getter function is in 117 | [must be importable *before* installation](https://github.com/pdm-project/pdm/issues/3385#issuecomment-2632705361). 118 | If you are using a `src` layout, you must specify the function like this: 119 | 120 | ```toml 121 | getter = "src.mypackage.version:get_version" 122 | ``` 123 | 124 | Or put it in a package outside the target package, e.g. in a `scripts/` directory in the repository root. 125 | 126 | ```toml 127 | getter = "scripts.version:get_version" 128 | ``` 129 | 130 | ### Writing dynamic version to file 131 | 132 | You can instruct `pdm-backend` to write back the dynamic version to a file. It is supported for all sources but `file`. 133 | 134 | ```toml 135 | [tool.pdm.version] 136 | source = "scm" 137 | write_to = "foo/version.txt" 138 | ``` 139 | 140 | By default, `pdm-backend` will just write the SCM version itself. 141 | You can provide a template as a Python-formatted string to create a syntactically correct Python assignment: 142 | 143 | ```toml 144 | [tool.pdm.version] 145 | source = "scm" 146 | write_to = "foo/_version.py" 147 | write_template = "__version__ = '{}'" 148 | ``` 149 | 150 | !!! note 151 | The path in `write_to` is relative to the root of the wheel file, hence the `package-dir` part should be stripped. 152 | 153 | !!! note 154 | `pdm-backend` will rewrite the whole file each time, so you can't have additional contents in that file. 155 | 156 | ## Variables expansion 157 | 158 | ### Environment variables 159 | 160 | You can refer to environment variables in form of `${VAR}` in the dependency strings, both work for `dependencies` and `optional-dependencies`: 161 | 162 | ```toml 163 | [project] 164 | dependencies = [ 165 | "foo @ https://${USERNAME}:${PASSWORD}/mypypi.org/packages/foo-0.1.0-py3-none-any.whl" 166 | ] 167 | ``` 168 | 169 | When you build the project, the variables will be expanded with the current values of the environment variables. 170 | 171 | ### Relative paths 172 | 173 | You can add a dependency with the relative paths to the project root. To do this, `pdm-backend` provides a special variable `${PROJECT_ROOT}` 174 | to refer to the project root, and the dependency must be defined with the `file://` URL: 175 | 176 | ```toml 177 | [project] 178 | dependencies = [ 179 | "foo @ file:///${PROJECT_ROOT}/foo-0.1.0-py3-none-any.whl" 180 | ] 181 | ``` 182 | 183 | To refer to a package beyond the project root: 184 | 185 | ```toml 186 | [project] 187 | dependencies = [ 188 | "foo @ file:///${PROJECT_ROOT}/../packages/foo-0.1.0-py3-none-any.whl" 189 | ] 190 | ``` 191 | 192 | !!! note 193 | The triple slashes `///` is required for the compatibility of Windows and POSIX systems. 194 | 195 | !!! note 196 | The relative paths will be expanded into the absolute paths on the local machine. So it makes no sense to include them in a distribution, since others who install the package will not have the same paths. 197 | -------------------------------------------------------------------------------- /docs/migration.md: -------------------------------------------------------------------------------- 1 | # Migrate from pdm-pep517 2 | 3 | It is quite easy to migrate from the [pdm-pep517] backend, there are only a few difference between the configurations of the two backends. 4 | 5 | ## `tool.pdm.build` table 6 | 7 | `pdm-pep517` has renamed the `tool.pdm` table to `tool.pdm.build` since 1.0.0. If you are still storing the [build configurations](./build_config.md) directly under `tool.pdm` table, move them under `tool.pdm.build` now. The old table is no longer supported. 8 | 9 | === "Legacy" 10 | 11 | ```toml 12 | [tool.pdm] 13 | includes = ["src", "data/*.json"] 14 | package-dir = "src" 15 | ``` 16 | 17 | === "New" 18 | 19 | ```toml 20 | [tool.pdm.build] 21 | includes = ["src", "data/*.json"] 22 | package-dir = "src" 23 | ``` 24 | 25 | ## `setup-script` 26 | 27 | In `pdm-pep517` you are allowed to call a custom `build` function during the build to add user-generated contents, which is specified by `tool.pdm.build.setup-script` option. However, this option has been dropped in `pdm-backend`, use a custom hook instead, and the custom script can be loaded automatically with the name `pdm_build.py`. 28 | 29 | === "Legacy" 30 | 31 | ```toml 32 | # pyproject.toml 33 | [tool.pdm.build] 34 | run-setuptools = false 35 | setup-script = "build.py" 36 | ``` 37 | ```python 38 | # build.py 39 | def build(src, dst): 40 | # add more files to the dst directory 41 | ... 42 | ``` 43 | 44 | === "New" 45 | 46 | ```toml 47 | # pyproject.toml 48 | [tool.pdm.build] 49 | # Either key is not necessary anymore. 50 | ``` 51 | ```python 52 | # pdm_build.py 53 | def pdm_build_update_files(context, files): 54 | # add more files to the dst directory 55 | new_file = do_create_files() 56 | files["new_file"] = new_file 57 | ``` 58 | 59 | And if `run-setuptools` is `true`, `pdm-pep517` will instead generate a `setup.py` file and call the specified script to update the arguments passed to `setup()` function. In `pdm-backend`, this can be also done via custom hook: 60 | 61 | === "Legacy" 62 | 63 | ```toml 64 | # pyproject.toml 65 | [tool.pdm.build] 66 | run-setuptools = true 67 | setup-script = "build.py" 68 | ``` 69 | ```python 70 | # build.py 71 | def build(setup_kwargs): 72 | # modify the setup_kwargs 73 | setup_kwargs['extensions'] = [Extension(...)] 74 | ``` 75 | 76 | === "New" 77 | 78 | ```toml 79 | # pyproject.toml 80 | [tool.pdm.build] 81 | run-setuptools = true 82 | ``` 83 | ```python 84 | # pdm_build.py 85 | def pdm_build_update_setup_kwargs(context, setup_kwargs): 86 | # modify the setup_kwargs 87 | setup_kwargs['extensions'] = [Extension(...)] 88 | ``` 89 | 90 | 91 | That's all, no more changes are required to be made and your project keeps working as before. 92 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: PDM-Backend 2 | 3 | repo_url: https://github.com/pdm-project/pdm-backend 4 | edit_uri: edit/main/docs 5 | 6 | theme: 7 | name: material 8 | palette: 9 | - scheme: default 10 | primary: deep purple 11 | accent: teal 12 | toggle: 13 | icon: material/weather-sunny 14 | name: Switch to dark mode 15 | - scheme: slate 16 | primary: deep purple 17 | accent: teal 18 | toggle: 19 | icon: material/weather-night 20 | name: Switch to light mode 21 | font: 22 | text: Open Sans 23 | code: Fira Code 24 | logo: https://cdn.jsdelivr.net/gh/pdm-project/pdm@main/docs/assets/logo.svg 25 | favicon: https://cdn.jsdelivr.net/gh/pdm-project/pdm@main/docs/assets/logo.svg 26 | 27 | plugins: 28 | - search 29 | - mkdocstrings: 30 | handlers: 31 | python: 32 | options: 33 | docstring_style: google 34 | - "mkdocs-version-annotations": 35 | version_added_admonition: "tip" 36 | 37 | nav: 38 | - Home: index.md 39 | - metadata.md 40 | - build_config.md 41 | - hooks.md 42 | - api.md 43 | - migration.md 44 | 45 | markdown_extensions: 46 | - pymdownx.highlight: 47 | linenums: true 48 | - pymdownx.tabbed: 49 | alternate_style: true 50 | - pymdownx.details 51 | - admonition 52 | - tables 53 | - toc: 54 | permalink: "#" 55 | - pymdownx.superfences: 56 | custom_fences: 57 | - name: mermaid 58 | class: mermaid 59 | format: !!python/name:pymdownx.superfences.fence_code_format 60 | 61 | copyright: Copyright © 2019 Frost Ming 62 | 63 | extra: 64 | version: 65 | provider: mike 66 | social: 67 | - icon: fontawesome/brands/github 68 | link: https://github.com/pdm-project/pdm-backend 69 | - icon: fontawesome/brands/twitter 70 | link: https://twitter.com/pdm_project 71 | - icon: fontawesome/brands/discord 72 | link: https://discord.gg/Phn8smztpv 73 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "site" 3 | command = """ 4 | pip3 install -U mkdocs "mkdocstrings[python]" mkdocs-material mkdocs-version-annotations 5 | pip3 install . 6 | mkdocs build 7 | """ 8 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | # PEP 621 project metadata 3 | # https://www.python.org/dev/peps/pep-0621 4 | name = "pdm-backend" 5 | description = "The build backend used by PDM that supports latest packaging standards" 6 | authors = [ 7 | { name = "Frost Ming", email = "me@frostming.com" } 8 | ] 9 | license = {text = "MIT"} 10 | requires-python = ">=3.9" 11 | readme = "README.md" 12 | keywords = ["packaging", "PEP 517", "build"] 13 | dynamic = ["version"] 14 | 15 | classifiers = [ 16 | "Development Status :: 5 - Production/Stable", 17 | "Topic :: Software Development :: Build Tools", 18 | "Programming Language :: Python :: 3", 19 | "Programming Language :: Python :: 3.9", 20 | "Programming Language :: Python :: 3.10", 21 | "Programming Language :: Python :: 3.11", 22 | "Programming Language :: Python :: 3.12", 23 | "Programming Language :: Python :: 3.13", 24 | ] 25 | 26 | dependencies = [ 27 | "importlib-metadata>=3.6; python_version < \"3.10\"" 28 | ] 29 | 30 | [project.urls] 31 | Homepage = "https://github.com/pdm-project/pdm-backend" 32 | Repository = "https://github.com/pdm-project/pdm-backend" 33 | Documentation = "https://backend.pdm-project.org" 34 | 35 | [build-system] 36 | requires = [] 37 | build-backend = "pdm.backend.intree" 38 | backend-path = ["src"] 39 | 40 | [tool.ruff] 41 | src = ["src"] 42 | target-version = "py38" 43 | exclude = ["tests/fixtures"] 44 | 45 | [tool.ruff.lint] 46 | extend-select = [ 47 | "I", # isort 48 | "C4", # flake8-comprehensions 49 | "W", # pycodestyle 50 | "YTT", # flake8-2020 51 | "UP", # pyupgrade 52 | "FA", # flake8-annotations 53 | ] 54 | 55 | [tool.ruff.lint.mccabe] 56 | max-complexity = 10 57 | 58 | [tool.ruff.lint.isort] 59 | known-first-party = ["pdm.backend"] 60 | 61 | [tool.vendoring] 62 | destination = "src/pdm/backend/_vendor/" 63 | requirements = "src/pdm/backend/_vendor/vendor.txt" 64 | namespace = "pdm.backend._vendor" 65 | patches-dir = "scripts/patches" 66 | protected-files = ["__init__.py", "README.md", "vendor.txt"] 67 | 68 | [tool.vendoring.transformations] 69 | substitute = [ 70 | {match = "import packaging", replace = "import pdm.backend._vendor.packaging"}, 71 | ] 72 | drop = [ 73 | "bin/", 74 | "*.so", 75 | "typing.*", 76 | "*/tests/", 77 | "**/test_*.py", 78 | "**/*_test.py" 79 | ] 80 | 81 | [tool.pdm.version] 82 | source = "scm" 83 | 84 | [tool.pdm.build] 85 | includes = ["src"] 86 | package-dir = "src" 87 | source-includes = ["tests"] 88 | 89 | [tool.pdm.dev-dependencies] 90 | test = [ 91 | "pytest", 92 | "pytest-cov", 93 | "pytest-gitconfig", 94 | "pytest-xdist", 95 | "setuptools", 96 | ] 97 | 98 | dev = [ 99 | "editables>=0.3", 100 | "pre-commit>=2.21.0", 101 | "vendoring>=1.2.0; python_version ~= \"3.8\"", 102 | ] 103 | docs = [ 104 | "mkdocs>=1.4.2", 105 | "mkdocstrings[python]>=0.19.0", 106 | "mkdocs-material>=8.5.10", 107 | "mkdocs-version-annotations>=1.0.0", 108 | ] 109 | 110 | [tool.pdm.scripts] 111 | build = "python scripts/build.py" 112 | docs = "mkdocs serve" 113 | test = "pytest" 114 | 115 | [tool.coverage.report] 116 | exclude_lines = [ 117 | "pragma: no cover", 118 | "def __repr__", 119 | "if self.debug", 120 | "raise AssertionError", 121 | "raise NotImplementedError", 122 | "if __name__ == .__main__.:", 123 | "if TYPE_CHECKING:", 124 | ] 125 | ignore_errors = true 126 | 127 | [tool.mypy] 128 | ignore_missing_imports = true 129 | disallow_incomplete_defs = true 130 | disallow_untyped_calls = true 131 | disallow_untyped_defs = true 132 | disallow_untyped_decorators = true 133 | explicit_package_bases = true 134 | namespace_packages = true 135 | 136 | [[tool.mypy.overrides]] 137 | module = "pdm.backend._vendor.*" 138 | ignore_errors = true 139 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | 3.10 2 | -------------------------------------------------------------------------------- /scripts/build.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is a simple script to call pdm-pep517's backend apis to make release artifacts. 3 | """ 4 | 5 | import argparse 6 | import logging 7 | import os 8 | 9 | import pdm.backend as api 10 | 11 | logger = logging.getLogger("pdm.backend") 12 | handler = logging.StreamHandler() 13 | handler.setLevel(logging.DEBUG) 14 | logger.addHandler(handler) 15 | logger.setLevel(logging.DEBUG) 16 | 17 | 18 | def main() -> None: 19 | parser = argparse.ArgumentParser() 20 | parser.add_argument("--no-wheel", action="store_false", dest="wheel") 21 | parser.add_argument("--no-sdist", action="store_false", dest="sdist") 22 | parser.add_argument("--no-editable", action="store_false", dest="editable") 23 | parser.add_argument("path", nargs="?", default=".") 24 | args = parser.parse_args() 25 | os.chdir(args.path) 26 | if args.sdist: 27 | api.build_sdist("dist") 28 | if args.wheel: 29 | api.build_wheel("dist") 30 | if args.editable: 31 | api.build_editable("dist") 32 | 33 | 34 | if __name__ == "__main__": 35 | main() 36 | -------------------------------------------------------------------------------- /scripts/patches/pyproject_metadata.patch: -------------------------------------------------------------------------------- 1 | diff --git a/src/pdm/backend/_vendor/pyproject_metadata/__init__.py b/src/pdm/backend/_vendor/pyproject_metadata/__init__.py 2 | index 39ae6e4..283264b 100644 3 | --- a/src/pdm/backend/_vendor/pyproject_metadata/__init__.py 4 | +++ b/src/pdm/backend/_vendor/pyproject_metadata/__init__.py 5 | @@ -63,6 +63,7 @@ if typing.TYPE_CHECKING: 6 | 7 | from .project_table import Dynamic, PyProjectTable 8 | 9 | +import pdm.backend._vendor.packaging as packaging 10 | import packaging.markers 11 | import packaging.specifiers 12 | import packaging.utils 13 | diff --git a/src/pdm/backend/_vendor/pyproject_metadata/pyproject.py b/src/pdm/backend/_vendor/pyproject_metadata/pyproject.py 14 | index d1822e1..a85f9a1 100644 15 | --- a/src/pdm/backend/_vendor/pyproject_metadata/pyproject.py 16 | +++ b/src/pdm/backend/_vendor/pyproject_metadata/pyproject.py 17 | @@ -13,6 +13,7 @@ import pathlib 18 | import re 19 | import typing 20 | 21 | +import pdm.backend._vendor.packaging as packaging 22 | import packaging.requirements 23 | 24 | from .errors import ErrorCollector 25 | -------------------------------------------------------------------------------- /src/pdm/backend/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | PEP-517 compliant pyproject build-system API 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | from pathlib import Path 8 | from typing import Any, Mapping 9 | 10 | 11 | def get_requires_for_build_wheel( 12 | config_settings: Mapping[str, Any] | None = None, 13 | ) -> list[str]: 14 | """ 15 | Returns an additional list of requirements for building, as PEP508 strings, 16 | above and beyond those specified in the pyproject.toml file. 17 | 18 | When C-extension build is needed, setuptools should be required, otherwise 19 | just return an empty list. 20 | """ 21 | from pdm.backend.wheel import WheelBuilder 22 | 23 | with WheelBuilder(Path.cwd(), config_settings) as builder: 24 | if builder.config.build_config.run_setuptools: 25 | return ["setuptools>=40.8.0"] 26 | return [] 27 | 28 | 29 | def get_requires_for_build_sdist( 30 | config_settings: Mapping[str, Any] | None = None, 31 | ) -> list[str]: 32 | """There isn't any requirement for building a sdist at this point.""" 33 | return [] 34 | 35 | 36 | def prepare_metadata_for_build_wheel( 37 | metadata_directory: str, config_settings: Mapping[str, Any] | None = None 38 | ) -> str: 39 | """Prepare the metadata, places it in metadata_directory""" 40 | from pdm.backend.wheel import WheelBuilder 41 | 42 | with WheelBuilder(Path.cwd(), config_settings) as builder: 43 | return builder.prepare_metadata(metadata_directory).name 44 | 45 | 46 | def build_wheel( 47 | wheel_directory: str, 48 | config_settings: Mapping[str, Any] | None = None, 49 | metadata_directory: str | None = None, 50 | ) -> str: 51 | """Builds a wheel, places it in wheel_directory""" 52 | from pdm.backend.wheel import WheelBuilder 53 | 54 | with WheelBuilder(Path.cwd(), config_settings) as builder: 55 | return builder.build( 56 | wheel_directory, metadata_directory=metadata_directory 57 | ).name 58 | 59 | 60 | def build_sdist( 61 | sdist_directory: str, config_settings: Mapping[str, Any] | None = None 62 | ) -> str: 63 | """Builds an sdist, places it in sdist_directory""" 64 | from pdm.backend.sdist import SdistBuilder 65 | 66 | with SdistBuilder(Path.cwd(), config_settings) as builder: 67 | return builder.build(sdist_directory).name 68 | 69 | 70 | def get_requires_for_build_editable( 71 | config_settings: Mapping[str, Any] | None = None, 72 | ) -> list[str]: 73 | """ 74 | Returns an additional list of requirements for building, as PEP508 strings, 75 | above and beyond those specified in the pyproject.toml file. 76 | 77 | When C-extension build is needed, setuptools should be required, otherwise 78 | just return an empty list. 79 | """ 80 | return get_requires_for_build_wheel(config_settings) 81 | 82 | 83 | def prepare_metadata_for_build_editable( 84 | metadata_directory: str, config_settings: Mapping[str, Any] | None = None 85 | ) -> str: 86 | """Prepare the metadata, places it in metadata_directory""" 87 | from pdm.backend.editable import EditableBuilder 88 | 89 | with EditableBuilder(Path.cwd(), config_settings) as builder: 90 | return builder.prepare_metadata(metadata_directory).name 91 | 92 | 93 | def build_editable( 94 | wheel_directory: str, 95 | config_settings: Mapping[str, Any] | None = None, 96 | metadata_directory: str | None = None, 97 | ) -> str: 98 | from pdm.backend.editable import EditableBuilder 99 | 100 | with EditableBuilder(Path.cwd(), config_settings) as builder: 101 | return builder.build( 102 | wheel_directory, metadata_directory=metadata_directory 103 | ).name 104 | -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm-backend/6c339b6a2f8a9e454e9c240cca374aa0d9148ec1/src/pdm/backend/_vendor/__init__.py -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/editables/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Paul Moore 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/editables/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from pathlib import Path 4 | from typing import Dict, Iterable, List, Tuple, Union 5 | 6 | __all__ = ( 7 | "EditableProject", 8 | "__version__", 9 | ) 10 | 11 | __version__ = "0.5" 12 | 13 | 14 | # Check if a project name is valid, based on PEP 426: 15 | # https://peps.python.org/pep-0426/#name 16 | def is_valid(name: str) -> bool: 17 | return ( 18 | re.match(r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", name, re.IGNORECASE) 19 | is not None 20 | ) 21 | 22 | 23 | # Slightly modified version of the normalisation from PEP 503: 24 | # https://peps.python.org/pep-0503/#normalized-names 25 | # This version uses underscore, so that the result is more 26 | # likely to be a valid import name 27 | def normalize(name: str) -> str: 28 | return re.sub(r"[-_.]+", "_", name).lower() 29 | 30 | 31 | class EditableException(Exception): 32 | pass 33 | 34 | 35 | class EditableProject: 36 | def __init__(self, project_name: str, project_dir: Union[str, os.PathLike]) -> None: 37 | if not is_valid(project_name): 38 | raise ValueError(f"Project name {project_name} is not valid") 39 | self.project_name = normalize(project_name) 40 | self.bootstrap = f"_editable_impl_{self.project_name}" 41 | self.project_dir = Path(project_dir) 42 | self.redirections: Dict[str, str] = {} 43 | self.path_entries: List[Path] = [] 44 | self.subpackages: Dict[str, Path] = {} 45 | 46 | def make_absolute(self, path: Union[str, os.PathLike]) -> Path: 47 | return (self.project_dir / path).resolve() 48 | 49 | def map(self, name: str, target: Union[str, os.PathLike]) -> None: 50 | if "." in name: 51 | raise EditableException( 52 | f"Cannot map {name} as it is not a top-level package" 53 | ) 54 | abs_target = self.make_absolute(target) 55 | if abs_target.is_dir(): 56 | abs_target = abs_target / "__init__.py" 57 | if abs_target.is_file(): 58 | self.redirections[name] = str(abs_target) 59 | else: 60 | raise EditableException(f"{target} is not a valid Python package or module") 61 | 62 | def add_to_path(self, dirname: Union[str, os.PathLike]) -> None: 63 | self.path_entries.append(self.make_absolute(dirname)) 64 | 65 | def add_to_subpackage(self, package: str, dirname: Union[str, os.PathLike]) -> None: 66 | self.subpackages[package] = self.make_absolute(dirname) 67 | 68 | def files(self) -> Iterable[Tuple[str, str]]: 69 | yield f"{self.project_name}.pth", self.pth_file() 70 | if self.subpackages: 71 | for package, location in self.subpackages.items(): 72 | yield self.package_redirection(package, location) 73 | if self.redirections: 74 | yield f"{self.bootstrap}.py", self.bootstrap_file() 75 | 76 | def dependencies(self) -> List[str]: 77 | deps = [] 78 | if self.redirections: 79 | deps.append("editables") 80 | return deps 81 | 82 | def pth_file(self) -> str: 83 | lines = [] 84 | if self.redirections: 85 | lines.append(f"import {self.bootstrap}") 86 | for entry in self.path_entries: 87 | lines.append(str(entry)) 88 | return "\n".join(lines) 89 | 90 | def package_redirection(self, package: str, location: Path) -> Tuple[str, str]: 91 | init_py = package.replace(".", "/") + "/__init__.py" 92 | content = f"__path__ = [{str(location)!r}]" 93 | return init_py, content 94 | 95 | def bootstrap_file(self) -> str: 96 | bootstrap = [ 97 | "from editables.redirector import RedirectingFinder as F", 98 | "F.install()", 99 | ] 100 | for name, path in self.redirections.items(): 101 | bootstrap.append(f"F.map_module({name!r}, {path!r})") 102 | return "\n".join(bootstrap) 103 | -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/editables/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm-backend/6c339b6a2f8a9e454e9c240cca374aa0d9148ec1/src/pdm/backend/_vendor/editables/py.typed -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/editables/redirector.py: -------------------------------------------------------------------------------- 1 | import importlib.abc 2 | import importlib.machinery 3 | import importlib.util 4 | import sys 5 | from types import ModuleType 6 | from typing import Dict, Optional, Sequence, Union 7 | 8 | ModulePath = Optional[Sequence[Union[bytes, str]]] 9 | 10 | 11 | class RedirectingFinder(importlib.abc.MetaPathFinder): 12 | _redirections: Dict[str, str] = {} 13 | 14 | @classmethod 15 | def map_module(cls, name: str, path: str) -> None: 16 | cls._redirections[name] = path 17 | 18 | @classmethod 19 | def find_spec( 20 | cls, fullname: str, path: ModulePath = None, target: Optional[ModuleType] = None 21 | ) -> Optional[importlib.machinery.ModuleSpec]: 22 | if "." in fullname: 23 | return None 24 | if path is not None: 25 | return None 26 | try: 27 | redir = cls._redirections[fullname] 28 | except KeyError: 29 | return None 30 | spec = importlib.util.spec_from_file_location(fullname, redir) 31 | return spec 32 | 33 | @classmethod 34 | def install(cls) -> None: 35 | for f in sys.meta_path: 36 | if f == cls: 37 | break 38 | else: 39 | sys.meta_path.append(cls) 40 | 41 | @classmethod 42 | def invalidate_caches(cls) -> None: 43 | # importlib.invalidate_caches calls finders' invalidate_caches methods, 44 | # and since we install this meta path finder as a class rather than an instance, 45 | # we have to override the inherited invalidate_caches method (using self) 46 | # as a classmethod instead 47 | pass 48 | -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/packaging/LICENSE: -------------------------------------------------------------------------------- 1 | This software is made available under the terms of *either* of the licenses 2 | found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made 3 | under the terms of *both* these licenses. 4 | -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/packaging/LICENSE.BSD: -------------------------------------------------------------------------------- 1 | Copyright (c) Donald Stufft and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/packaging/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is dual licensed under the terms of the Apache License, Version 2 | # 2.0, and the BSD License. See the LICENSE file in the root of this repository 3 | # for complete details. 4 | 5 | __title__ = "packaging" 6 | __summary__ = "Core utilities for Python packages" 7 | __uri__ = "https://github.com/pypa/packaging" 8 | 9 | __version__ = "24.1" 10 | 11 | __author__ = "Donald Stufft and individual contributors" 12 | __email__ = "donald@stufft.io" 13 | 14 | __license__ = "BSD-2-Clause or Apache-2.0" 15 | __copyright__ = "2014 %s" % __author__ 16 | -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/packaging/_elffile.py: -------------------------------------------------------------------------------- 1 | """ 2 | ELF file parser. 3 | 4 | This provides a class ``ELFFile`` that parses an ELF executable in a similar 5 | interface to ``ZipFile``. Only the read interface is implemented. 6 | 7 | Based on: https://gist.github.com/lyssdod/f51579ae8d93c8657a5564aefc2ffbca 8 | ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html 9 | """ 10 | 11 | from __future__ import annotations 12 | 13 | import enum 14 | import os 15 | import struct 16 | from typing import IO 17 | 18 | 19 | class ELFInvalid(ValueError): 20 | pass 21 | 22 | 23 | class EIClass(enum.IntEnum): 24 | C32 = 1 25 | C64 = 2 26 | 27 | 28 | class EIData(enum.IntEnum): 29 | Lsb = 1 30 | Msb = 2 31 | 32 | 33 | class EMachine(enum.IntEnum): 34 | I386 = 3 35 | S390 = 22 36 | Arm = 40 37 | X8664 = 62 38 | AArc64 = 183 39 | 40 | 41 | class ELFFile: 42 | """ 43 | Representation of an ELF executable. 44 | """ 45 | 46 | def __init__(self, f: IO[bytes]) -> None: 47 | self._f = f 48 | 49 | try: 50 | ident = self._read("16B") 51 | except struct.error: 52 | raise ELFInvalid("unable to parse identification") 53 | magic = bytes(ident[:4]) 54 | if magic != b"\x7fELF": 55 | raise ELFInvalid(f"invalid magic: {magic!r}") 56 | 57 | self.capacity = ident[4] # Format for program header (bitness). 58 | self.encoding = ident[5] # Data structure encoding (endianness). 59 | 60 | try: 61 | # e_fmt: Format for program header. 62 | # p_fmt: Format for section header. 63 | # p_idx: Indexes to find p_type, p_offset, and p_filesz. 64 | e_fmt, self._p_fmt, self._p_idx = { 65 | (1, 1): ("HHIIIIIHHH", ">IIIIIIII", (0, 1, 4)), # 32-bit MSB. 67 | (2, 1): ("HHIQQQIHHH", ">IIQQQQQQ", (0, 2, 5)), # 64-bit MSB. 69 | }[(self.capacity, self.encoding)] 70 | except KeyError: 71 | raise ELFInvalid( 72 | f"unrecognized capacity ({self.capacity}) or " 73 | f"encoding ({self.encoding})" 74 | ) 75 | 76 | try: 77 | ( 78 | _, 79 | self.machine, # Architecture type. 80 | _, 81 | _, 82 | self._e_phoff, # Offset of program header. 83 | _, 84 | self.flags, # Processor-specific flags. 85 | _, 86 | self._e_phentsize, # Size of section. 87 | self._e_phnum, # Number of sections. 88 | ) = self._read(e_fmt) 89 | except struct.error as e: 90 | raise ELFInvalid("unable to parse machine and section information") from e 91 | 92 | def _read(self, fmt: str) -> tuple[int, ...]: 93 | return struct.unpack(fmt, self._f.read(struct.calcsize(fmt))) 94 | 95 | @property 96 | def interpreter(self) -> str | None: 97 | """ 98 | The path recorded in the ``PT_INTERP`` section header. 99 | """ 100 | for index in range(self._e_phnum): 101 | self._f.seek(self._e_phoff + self._e_phentsize * index) 102 | try: 103 | data = self._read(self._p_fmt) 104 | except struct.error: 105 | continue 106 | if data[self._p_idx[0]] != 3: # Not PT_INTERP. 107 | continue 108 | self._f.seek(data[self._p_idx[1]]) 109 | return os.fsdecode(self._f.read(data[self._p_idx[2]])).strip("\0") 110 | return None 111 | -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/packaging/_musllinux.py: -------------------------------------------------------------------------------- 1 | """PEP 656 support. 2 | 3 | This module implements logic to detect if the currently running Python is 4 | linked against musl, and what musl version is used. 5 | """ 6 | 7 | from __future__ import annotations 8 | 9 | import functools 10 | import re 11 | import subprocess 12 | import sys 13 | from typing import Iterator, NamedTuple, Sequence 14 | 15 | from ._elffile import ELFFile 16 | 17 | 18 | class _MuslVersion(NamedTuple): 19 | major: int 20 | minor: int 21 | 22 | 23 | def _parse_musl_version(output: str) -> _MuslVersion | None: 24 | lines = [n for n in (n.strip() for n in output.splitlines()) if n] 25 | if len(lines) < 2 or lines[0][:4] != "musl": 26 | return None 27 | m = re.match(r"Version (\d+)\.(\d+)", lines[1]) 28 | if not m: 29 | return None 30 | return _MuslVersion(major=int(m.group(1)), minor=int(m.group(2))) 31 | 32 | 33 | @functools.lru_cache 34 | def _get_musl_version(executable: str) -> _MuslVersion | None: 35 | """Detect currently-running musl runtime version. 36 | 37 | This is done by checking the specified executable's dynamic linking 38 | information, and invoking the loader to parse its output for a version 39 | string. If the loader is musl, the output would be something like:: 40 | 41 | musl libc (x86_64) 42 | Version 1.2.2 43 | Dynamic Program Loader 44 | """ 45 | try: 46 | with open(executable, "rb") as f: 47 | ld = ELFFile(f).interpreter 48 | except (OSError, TypeError, ValueError): 49 | return None 50 | if ld is None or "musl" not in ld: 51 | return None 52 | proc = subprocess.run([ld], stderr=subprocess.PIPE, text=True) 53 | return _parse_musl_version(proc.stderr) 54 | 55 | 56 | def platform_tags(archs: Sequence[str]) -> Iterator[str]: 57 | """Generate musllinux tags compatible to the current platform. 58 | 59 | :param archs: Sequence of compatible architectures. 60 | The first one shall be the closest to the actual architecture and be the part of 61 | platform tag after the ``linux_`` prefix, e.g. ``x86_64``. 62 | The ``linux_`` prefix is assumed as a prerequisite for the current platform to 63 | be musllinux-compatible. 64 | 65 | :returns: An iterator of compatible musllinux tags. 66 | """ 67 | sys_musl = _get_musl_version(sys.executable) 68 | if sys_musl is None: # Python not dynamically linked against musl. 69 | return 70 | for arch in archs: 71 | for minor in range(sys_musl.minor, -1, -1): 72 | yield f"musllinux_{sys_musl.major}_{minor}_{arch}" 73 | 74 | 75 | if __name__ == "__main__": # pragma: no cover 76 | import sysconfig 77 | 78 | plat = sysconfig.get_platform() 79 | assert plat.startswith("linux-"), "not linux" 80 | 81 | print("plat:", plat) 82 | print("musl:", _get_musl_version(sys.executable)) 83 | print("tags:", end=" ") 84 | for t in platform_tags(re.sub(r"[.-]", "_", plat.split("-", 1)[-1])): 85 | print(t, end="\n ") 86 | -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/packaging/_structures.py: -------------------------------------------------------------------------------- 1 | # This file is dual licensed under the terms of the Apache License, Version 2 | # 2.0, and the BSD License. See the LICENSE file in the root of this repository 3 | # for complete details. 4 | 5 | 6 | class InfinityType: 7 | def __repr__(self) -> str: 8 | return "Infinity" 9 | 10 | def __hash__(self) -> int: 11 | return hash(repr(self)) 12 | 13 | def __lt__(self, other: object) -> bool: 14 | return False 15 | 16 | def __le__(self, other: object) -> bool: 17 | return False 18 | 19 | def __eq__(self, other: object) -> bool: 20 | return isinstance(other, self.__class__) 21 | 22 | def __gt__(self, other: object) -> bool: 23 | return True 24 | 25 | def __ge__(self, other: object) -> bool: 26 | return True 27 | 28 | def __neg__(self: object) -> "NegativeInfinityType": 29 | return NegativeInfinity 30 | 31 | 32 | Infinity = InfinityType() 33 | 34 | 35 | class NegativeInfinityType: 36 | def __repr__(self) -> str: 37 | return "-Infinity" 38 | 39 | def __hash__(self) -> int: 40 | return hash(repr(self)) 41 | 42 | def __lt__(self, other: object) -> bool: 43 | return True 44 | 45 | def __le__(self, other: object) -> bool: 46 | return True 47 | 48 | def __eq__(self, other: object) -> bool: 49 | return isinstance(other, self.__class__) 50 | 51 | def __gt__(self, other: object) -> bool: 52 | return False 53 | 54 | def __ge__(self, other: object) -> bool: 55 | return False 56 | 57 | def __neg__(self: object) -> InfinityType: 58 | return Infinity 59 | 60 | 61 | NegativeInfinity = NegativeInfinityType() 62 | -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/packaging/_tokenizer.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import contextlib 4 | import re 5 | from dataclasses import dataclass 6 | from typing import Iterator, NoReturn 7 | 8 | from .specifiers import Specifier 9 | 10 | 11 | @dataclass 12 | class Token: 13 | name: str 14 | text: str 15 | position: int 16 | 17 | 18 | class ParserSyntaxError(Exception): 19 | """The provided source text could not be parsed correctly.""" 20 | 21 | def __init__( 22 | self, 23 | message: str, 24 | *, 25 | source: str, 26 | span: tuple[int, int], 27 | ) -> None: 28 | self.span = span 29 | self.message = message 30 | self.source = source 31 | 32 | super().__init__() 33 | 34 | def __str__(self) -> str: 35 | marker = " " * self.span[0] + "~" * (self.span[1] - self.span[0]) + "^" 36 | return "\n ".join([self.message, self.source, marker]) 37 | 38 | 39 | DEFAULT_RULES: dict[str, str | re.Pattern[str]] = { 40 | "LEFT_PARENTHESIS": r"\(", 41 | "RIGHT_PARENTHESIS": r"\)", 42 | "LEFT_BRACKET": r"\[", 43 | "RIGHT_BRACKET": r"\]", 44 | "SEMICOLON": r";", 45 | "COMMA": r",", 46 | "QUOTED_STRING": re.compile( 47 | r""" 48 | ( 49 | ('[^']*') 50 | | 51 | ("[^"]*") 52 | ) 53 | """, 54 | re.VERBOSE, 55 | ), 56 | "OP": r"(===|==|~=|!=|<=|>=|<|>)", 57 | "BOOLOP": r"\b(or|and)\b", 58 | "IN": r"\bin\b", 59 | "NOT": r"\bnot\b", 60 | "VARIABLE": re.compile( 61 | r""" 62 | \b( 63 | python_version 64 | |python_full_version 65 | |os[._]name 66 | |sys[._]platform 67 | |platform_(release|system) 68 | |platform[._](version|machine|python_implementation) 69 | |python_implementation 70 | |implementation_(name|version) 71 | |extra 72 | )\b 73 | """, 74 | re.VERBOSE, 75 | ), 76 | "SPECIFIER": re.compile( 77 | Specifier._operator_regex_str + Specifier._version_regex_str, 78 | re.VERBOSE | re.IGNORECASE, 79 | ), 80 | "AT": r"\@", 81 | "URL": r"[^ \t]+", 82 | "IDENTIFIER": r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b", 83 | "VERSION_PREFIX_TRAIL": r"\.\*", 84 | "VERSION_LOCAL_LABEL_TRAIL": r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*", 85 | "WS": r"[ \t]+", 86 | "END": r"$", 87 | } 88 | 89 | 90 | class Tokenizer: 91 | """Context-sensitive token parsing. 92 | 93 | Provides methods to examine the input stream to check whether the next token 94 | matches. 95 | """ 96 | 97 | def __init__( 98 | self, 99 | source: str, 100 | *, 101 | rules: dict[str, str | re.Pattern[str]], 102 | ) -> None: 103 | self.source = source 104 | self.rules: dict[str, re.Pattern[str]] = { 105 | name: re.compile(pattern) for name, pattern in rules.items() 106 | } 107 | self.next_token: Token | None = None 108 | self.position = 0 109 | 110 | def consume(self, name: str) -> None: 111 | """Move beyond provided token name, if at current position.""" 112 | if self.check(name): 113 | self.read() 114 | 115 | def check(self, name: str, *, peek: bool = False) -> bool: 116 | """Check whether the next token has the provided name. 117 | 118 | By default, if the check succeeds, the token *must* be read before 119 | another check. If `peek` is set to `True`, the token is not loaded and 120 | would need to be checked again. 121 | """ 122 | assert ( 123 | self.next_token is None 124 | ), f"Cannot check for {name!r}, already have {self.next_token!r}" 125 | assert name in self.rules, f"Unknown token name: {name!r}" 126 | 127 | expression = self.rules[name] 128 | 129 | match = expression.match(self.source, self.position) 130 | if match is None: 131 | return False 132 | if not peek: 133 | self.next_token = Token(name, match[0], self.position) 134 | return True 135 | 136 | def expect(self, name: str, *, expected: str) -> Token: 137 | """Expect a certain token name next, failing with a syntax error otherwise. 138 | 139 | The token is *not* read. 140 | """ 141 | if not self.check(name): 142 | raise self.raise_syntax_error(f"Expected {expected}") 143 | return self.read() 144 | 145 | def read(self) -> Token: 146 | """Consume the next token and return it.""" 147 | token = self.next_token 148 | assert token is not None 149 | 150 | self.position += len(token.text) 151 | self.next_token = None 152 | 153 | return token 154 | 155 | def raise_syntax_error( 156 | self, 157 | message: str, 158 | *, 159 | span_start: int | None = None, 160 | span_end: int | None = None, 161 | ) -> NoReturn: 162 | """Raise ParserSyntaxError at the given position.""" 163 | span = ( 164 | self.position if span_start is None else span_start, 165 | self.position if span_end is None else span_end, 166 | ) 167 | raise ParserSyntaxError( 168 | message, 169 | source=self.source, 170 | span=span, 171 | ) 172 | 173 | @contextlib.contextmanager 174 | def enclosing_tokens( 175 | self, open_token: str, close_token: str, *, around: str 176 | ) -> Iterator[None]: 177 | if self.check(open_token): 178 | open_position = self.position 179 | self.read() 180 | else: 181 | open_position = None 182 | 183 | yield 184 | 185 | if open_position is None: 186 | return 187 | 188 | if not self.check(close_token): 189 | self.raise_syntax_error( 190 | f"Expected matching {close_token} for {open_token}, after {around}", 191 | span_start=open_position, 192 | ) 193 | 194 | self.read() 195 | -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/packaging/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm-backend/6c339b6a2f8a9e454e9c240cca374aa0d9148ec1/src/pdm/backend/_vendor/packaging/py.typed -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/packaging/requirements.py: -------------------------------------------------------------------------------- 1 | # This file is dual licensed under the terms of the Apache License, Version 2 | # 2.0, and the BSD License. See the LICENSE file in the root of this repository 3 | # for complete details. 4 | from __future__ import annotations 5 | 6 | from typing import Any, Iterator 7 | 8 | from ._parser import parse_requirement as _parse_requirement 9 | from ._tokenizer import ParserSyntaxError 10 | from .markers import Marker, _normalize_extra_values 11 | from .specifiers import SpecifierSet 12 | from .utils import canonicalize_name 13 | 14 | 15 | class InvalidRequirement(ValueError): 16 | """ 17 | An invalid requirement was found, users should refer to PEP 508. 18 | """ 19 | 20 | 21 | class Requirement: 22 | """Parse a requirement. 23 | 24 | Parse a given requirement string into its parts, such as name, specifier, 25 | URL, and extras. Raises InvalidRequirement on a badly-formed requirement 26 | string. 27 | """ 28 | 29 | # TODO: Can we test whether something is contained within a requirement? 30 | # If so how do we do that? Do we need to test against the _name_ of 31 | # the thing as well as the version? What about the markers? 32 | # TODO: Can we normalize the name and extra name? 33 | 34 | def __init__(self, requirement_string: str) -> None: 35 | try: 36 | parsed = _parse_requirement(requirement_string) 37 | except ParserSyntaxError as e: 38 | raise InvalidRequirement(str(e)) from e 39 | 40 | self.name: str = parsed.name 41 | self.url: str | None = parsed.url or None 42 | self.extras: set[str] = set(parsed.extras or []) 43 | self.specifier: SpecifierSet = SpecifierSet(parsed.specifier) 44 | self.marker: Marker | None = None 45 | if parsed.marker is not None: 46 | self.marker = Marker.__new__(Marker) 47 | self.marker._markers = _normalize_extra_values(parsed.marker) 48 | 49 | def _iter_parts(self, name: str) -> Iterator[str]: 50 | yield name 51 | 52 | if self.extras: 53 | formatted_extras = ",".join(sorted(self.extras)) 54 | yield f"[{formatted_extras}]" 55 | 56 | if self.specifier: 57 | yield str(self.specifier) 58 | 59 | if self.url: 60 | yield f"@ {self.url}" 61 | if self.marker: 62 | yield " " 63 | 64 | if self.marker: 65 | yield f"; {self.marker}" 66 | 67 | def __str__(self) -> str: 68 | return "".join(self._iter_parts(self.name)) 69 | 70 | def __repr__(self) -> str: 71 | return f"" 72 | 73 | def __hash__(self) -> int: 74 | return hash( 75 | ( 76 | self.__class__.__name__, 77 | *self._iter_parts(canonicalize_name(self.name)), 78 | ) 79 | ) 80 | 81 | def __eq__(self, other: Any) -> bool: 82 | if not isinstance(other, Requirement): 83 | return NotImplemented 84 | 85 | return ( 86 | canonicalize_name(self.name) == canonicalize_name(other.name) 87 | and self.extras == other.extras 88 | and self.specifier == other.specifier 89 | and self.url == other.url 90 | and self.marker == other.marker 91 | ) 92 | -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/packaging/utils.py: -------------------------------------------------------------------------------- 1 | # This file is dual licensed under the terms of the Apache License, Version 2 | # 2.0, and the BSD License. See the LICENSE file in the root of this repository 3 | # for complete details. 4 | 5 | from __future__ import annotations 6 | 7 | import re 8 | from typing import NewType, Tuple, Union, cast 9 | 10 | from .tags import Tag, parse_tag 11 | from .version import InvalidVersion, Version 12 | 13 | BuildTag = Union[Tuple[()], Tuple[int, str]] 14 | NormalizedName = NewType("NormalizedName", str) 15 | 16 | 17 | class InvalidName(ValueError): 18 | """ 19 | An invalid distribution name; users should refer to the packaging user guide. 20 | """ 21 | 22 | 23 | class InvalidWheelFilename(ValueError): 24 | """ 25 | An invalid wheel filename was found, users should refer to PEP 427. 26 | """ 27 | 28 | 29 | class InvalidSdistFilename(ValueError): 30 | """ 31 | An invalid sdist filename was found, users should refer to the packaging user guide. 32 | """ 33 | 34 | 35 | # Core metadata spec for `Name` 36 | _validate_regex = re.compile( 37 | r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.IGNORECASE 38 | ) 39 | _canonicalize_regex = re.compile(r"[-_.]+") 40 | _normalized_regex = re.compile(r"^([a-z0-9]|[a-z0-9]([a-z0-9-](?!--))*[a-z0-9])$") 41 | # PEP 427: The build number must start with a digit. 42 | _build_tag_regex = re.compile(r"(\d+)(.*)") 43 | 44 | 45 | def canonicalize_name(name: str, *, validate: bool = False) -> NormalizedName: 46 | if validate and not _validate_regex.match(name): 47 | raise InvalidName(f"name is invalid: {name!r}") 48 | # This is taken from PEP 503. 49 | value = _canonicalize_regex.sub("-", name).lower() 50 | return cast(NormalizedName, value) 51 | 52 | 53 | def is_normalized_name(name: str) -> bool: 54 | return _normalized_regex.match(name) is not None 55 | 56 | 57 | def canonicalize_version( 58 | version: Version | str, *, strip_trailing_zero: bool = True 59 | ) -> str: 60 | """ 61 | This is very similar to Version.__str__, but has one subtle difference 62 | with the way it handles the release segment. 63 | """ 64 | if isinstance(version, str): 65 | try: 66 | parsed = Version(version) 67 | except InvalidVersion: 68 | # Legacy versions cannot be normalized 69 | return version 70 | else: 71 | parsed = version 72 | 73 | parts = [] 74 | 75 | # Epoch 76 | if parsed.epoch != 0: 77 | parts.append(f"{parsed.epoch}!") 78 | 79 | # Release segment 80 | release_segment = ".".join(str(x) for x in parsed.release) 81 | if strip_trailing_zero: 82 | # NB: This strips trailing '.0's to normalize 83 | release_segment = re.sub(r"(\.0)+$", "", release_segment) 84 | parts.append(release_segment) 85 | 86 | # Pre-release 87 | if parsed.pre is not None: 88 | parts.append("".join(str(x) for x in parsed.pre)) 89 | 90 | # Post-release 91 | if parsed.post is not None: 92 | parts.append(f".post{parsed.post}") 93 | 94 | # Development release 95 | if parsed.dev is not None: 96 | parts.append(f".dev{parsed.dev}") 97 | 98 | # Local version segment 99 | if parsed.local is not None: 100 | parts.append(f"+{parsed.local}") 101 | 102 | return "".join(parts) 103 | 104 | 105 | def parse_wheel_filename( 106 | filename: str, 107 | ) -> tuple[NormalizedName, Version, BuildTag, frozenset[Tag]]: 108 | if not filename.endswith(".whl"): 109 | raise InvalidWheelFilename( 110 | f"Invalid wheel filename (extension must be '.whl'): {filename}" 111 | ) 112 | 113 | filename = filename[:-4] 114 | dashes = filename.count("-") 115 | if dashes not in (4, 5): 116 | raise InvalidWheelFilename( 117 | f"Invalid wheel filename (wrong number of parts): {filename}" 118 | ) 119 | 120 | parts = filename.split("-", dashes - 2) 121 | name_part = parts[0] 122 | # See PEP 427 for the rules on escaping the project name. 123 | if "__" in name_part or re.match(r"^[\w\d._]*$", name_part, re.UNICODE) is None: 124 | raise InvalidWheelFilename(f"Invalid project name: {filename}") 125 | name = canonicalize_name(name_part) 126 | 127 | try: 128 | version = Version(parts[1]) 129 | except InvalidVersion as e: 130 | raise InvalidWheelFilename( 131 | f"Invalid wheel filename (invalid version): {filename}" 132 | ) from e 133 | 134 | if dashes == 5: 135 | build_part = parts[2] 136 | build_match = _build_tag_regex.match(build_part) 137 | if build_match is None: 138 | raise InvalidWheelFilename( 139 | f"Invalid build number: {build_part} in '{filename}'" 140 | ) 141 | build = cast(BuildTag, (int(build_match.group(1)), build_match.group(2))) 142 | else: 143 | build = () 144 | tags = parse_tag(parts[-1]) 145 | return (name, version, build, tags) 146 | 147 | 148 | def parse_sdist_filename(filename: str) -> tuple[NormalizedName, Version]: 149 | if filename.endswith(".tar.gz"): 150 | file_stem = filename[: -len(".tar.gz")] 151 | elif filename.endswith(".zip"): 152 | file_stem = filename[: -len(".zip")] 153 | else: 154 | raise InvalidSdistFilename( 155 | f"Invalid sdist filename (extension must be '.tar.gz' or '.zip'):" 156 | f" {filename}" 157 | ) 158 | 159 | # We are requiring a PEP 440 version, which cannot contain dashes, 160 | # so we split on the last dash. 161 | name_part, sep, version_part = file_stem.rpartition("-") 162 | if not sep: 163 | raise InvalidSdistFilename(f"Invalid sdist filename: {filename}") 164 | 165 | name = canonicalize_name(name_part) 166 | 167 | try: 168 | version = Version(version_part) 169 | except InvalidVersion as e: 170 | raise InvalidSdistFilename( 171 | f"Invalid sdist filename (invalid version): {filename}" 172 | ) from e 173 | 174 | return (name, version) 175 | -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/pyproject_metadata/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2019 Filipe Laíns 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice (including the next 11 | paragraph) shall be included in all copies or substantial portions of the 12 | Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/pyproject_metadata/constants.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | """ 4 | Constants for the pyproject_metadata package, collected here to make them easy 5 | to update. These should be considered mostly private. 6 | """ 7 | 8 | from __future__ import annotations 9 | 10 | __all__ = [ 11 | "KNOWN_BUILD_SYSTEM_FIELDS", 12 | "KNOWN_METADATA_FIELDS", 13 | "KNOWN_METADATA_VERSIONS", 14 | "KNOWN_METADATA_VERSIONS", 15 | "KNOWN_MULTIUSE", 16 | "KNOWN_PROJECT_FIELDS", 17 | "KNOWN_TOPLEVEL_FIELDS", 18 | "PRE_SPDX_METADATA_VERSIONS", 19 | "PROJECT_TO_METADATA", 20 | ] 21 | 22 | 23 | def __dir__() -> list[str]: 24 | return __all__ 25 | 26 | 27 | KNOWN_METADATA_VERSIONS = {"2.1", "2.2", "2.3", "2.4"} 28 | PRE_SPDX_METADATA_VERSIONS = {"2.1", "2.2", "2.3"} 29 | 30 | PROJECT_TO_METADATA = { 31 | "authors": frozenset(["Author", "Author-Email"]), 32 | "classifiers": frozenset(["Classifier"]), 33 | "dependencies": frozenset(["Requires-Dist"]), 34 | "description": frozenset(["Summary"]), 35 | "dynamic": frozenset(), 36 | "entry-points": frozenset(), 37 | "gui-scripts": frozenset(), 38 | "keywords": frozenset(["Keywords"]), 39 | "license": frozenset(["License", "License-Expression"]), 40 | "license-files": frozenset(["License-File"]), 41 | "maintainers": frozenset(["Maintainer", "Maintainer-Email"]), 42 | "name": frozenset(["Name"]), 43 | "optional-dependencies": frozenset(["Provides-Extra", "Requires-Dist"]), 44 | "readme": frozenset(["Description", "Description-Content-Type"]), 45 | "requires-python": frozenset(["Requires-Python"]), 46 | "scripts": frozenset(), 47 | "urls": frozenset(["Project-URL"]), 48 | "version": frozenset(["Version"]), 49 | } 50 | 51 | KNOWN_TOPLEVEL_FIELDS = {"build-system", "project", "tool", "dependency-groups"} 52 | KNOWN_BUILD_SYSTEM_FIELDS = {"backend-path", "build-backend", "requires"} 53 | KNOWN_PROJECT_FIELDS = set(PROJECT_TO_METADATA) 54 | 55 | KNOWN_METADATA_FIELDS = { 56 | "author", 57 | "author-email", 58 | "classifier", 59 | "description", 60 | "description-content-type", 61 | "download-url", # Not specified via pyproject standards, deprecated by PEP 753 62 | "dynamic", # Can't be in dynamic 63 | "home-page", # Not specified via pyproject standards, deprecated by PEP 753 64 | "keywords", 65 | "license", 66 | "license-expression", 67 | "license-file", 68 | "maintainer", 69 | "maintainer-email", 70 | "metadata-version", 71 | "name", # Can't be in dynamic 72 | "obsoletes", # Deprecated 73 | "obsoletes-dist", # Rarely used 74 | "platform", # Not specified via pyproject standards 75 | "project-url", 76 | "provides", # Deprecated 77 | "provides-dist", # Rarely used 78 | "provides-extra", 79 | "requires", # Deprecated 80 | "requires-dist", 81 | "requires-external", # Not specified via pyproject standards 82 | "requires-python", 83 | "summary", 84 | "supported-platform", # Not specified via pyproject standards 85 | "version", # Can't be in dynamic 86 | } 87 | 88 | KNOWN_MULTIUSE = { 89 | "dynamic", 90 | "platform", 91 | "provides-extra", 92 | "supported-platform", 93 | "license-file", 94 | "classifier", 95 | "requires-dist", 96 | "requires-external", 97 | "project-url", 98 | "provides-dist", 99 | "obsoletes-dist", 100 | "requires", # Deprecated 101 | "obsoletes", # Deprecated 102 | "provides", # Deprecated 103 | } 104 | -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/pyproject_metadata/errors.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | """ 4 | This module defines exceptions and error handling utilities. It is the 5 | recommend path to access ``ConfiguratonError``, ``ConfigurationWarning``, and 6 | ``ExceptionGroup``. For backward compatibility, ``ConfigurationError`` is 7 | re-exported in the top-level package. 8 | """ 9 | 10 | from __future__ import annotations 11 | 12 | import builtins 13 | import contextlib 14 | import dataclasses 15 | import sys 16 | import typing 17 | import warnings 18 | 19 | __all__ = [ 20 | "ConfigurationError", 21 | "ConfigurationWarning", 22 | "ExceptionGroup", 23 | ] 24 | 25 | 26 | def __dir__() -> list[str]: 27 | return __all__ 28 | 29 | 30 | class ConfigurationError(Exception): 31 | """Error in the backend metadata. Has an optional key attribute, which will be non-None 32 | if the error is related to a single key in the pyproject.toml file.""" 33 | 34 | def __init__(self, msg: str, *, key: str | None = None): 35 | super().__init__(msg) 36 | self._key = key 37 | 38 | @property 39 | def key(self) -> str | None: # pragma: no cover 40 | return self._key 41 | 42 | 43 | class ConfigurationWarning(UserWarning): 44 | """Warnings about backend metadata.""" 45 | 46 | 47 | if sys.version_info >= (3, 11): 48 | ExceptionGroup = builtins.ExceptionGroup 49 | else: 50 | 51 | class ExceptionGroup(Exception): 52 | """A minimal implementation of `ExceptionGroup` from Python 3.11. 53 | 54 | Users can replace this with a more complete implementation, such as from 55 | the exceptiongroup backport package, if better error messages and 56 | integration with tooling is desired and the addition of a dependency is 57 | acceptable. 58 | """ 59 | 60 | message: str 61 | exceptions: list[Exception] 62 | 63 | def __init__(self, message: str, exceptions: list[Exception]) -> None: 64 | self.message = message 65 | self.exceptions = exceptions 66 | 67 | def __repr__(self) -> str: 68 | return f"{self.__class__.__name__}({self.message!r}, {self.exceptions!r})" 69 | 70 | 71 | @dataclasses.dataclass 72 | class ErrorCollector: 73 | """ 74 | Collect errors and raise them as a group at the end (if collect_errors is True), 75 | otherwise raise them immediately. 76 | """ 77 | 78 | collect_errors: bool 79 | errors: list[Exception] = dataclasses.field(default_factory=list) 80 | 81 | def config_error( 82 | self, 83 | msg: str, 84 | *, 85 | key: str | None = None, 86 | got: typing.Any = None, 87 | got_type: type[typing.Any] | None = None, 88 | warn: bool = False, 89 | **kwargs: typing.Any, 90 | ) -> None: 91 | """Raise a configuration error, or add it to the error list.""" 92 | msg = msg.format(key=f'"{key}"', **kwargs) 93 | if got is not None: 94 | msg = f"{msg} (got {got!r})" 95 | if got_type is not None: 96 | msg = f"{msg} (got {got_type.__name__})" 97 | 98 | if warn: 99 | warnings.warn(msg, ConfigurationWarning, stacklevel=3) 100 | elif self.collect_errors: 101 | self.errors.append(ConfigurationError(msg, key=key)) 102 | else: 103 | raise ConfigurationError(msg, key=key) 104 | 105 | def finalize(self, msg: str) -> None: 106 | """Raise a group exception if there are any errors.""" 107 | if self.errors: 108 | raise ExceptionGroup(msg, self.errors) 109 | 110 | @contextlib.contextmanager 111 | def collect(self) -> typing.Generator[None, None, None]: 112 | """Support nesting; add any grouped errors to the error list.""" 113 | if self.collect_errors: 114 | try: 115 | yield 116 | except ExceptionGroup as error: 117 | self.errors.extend(error.exceptions) 118 | else: 119 | yield 120 | -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/pyproject_metadata/project_table.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | """ 4 | This module contains type definitions for the tables used in the 5 | ``pyproject.toml``. You should either import this at type-check time only, or 6 | make sure ``typing_extensions`` is available for Python 3.10 and below. 7 | 8 | Documentation notice: the fields with hyphens are not shown due to a sphinx-autodoc bug. 9 | """ 10 | 11 | from __future__ import annotations 12 | 13 | import sys 14 | import typing 15 | from typing import Any, Dict, List, Union 16 | 17 | if sys.version_info < (3, 11): 18 | from typing_extensions import Required 19 | else: 20 | from typing import Required 21 | 22 | if sys.version_info < (3, 8): 23 | from typing_extensions import Literal, TypedDict 24 | else: 25 | from typing import Literal, TypedDict 26 | 27 | 28 | __all__ = [ 29 | "BuildSystemTable", 30 | "ContactTable", 31 | "Dynamic", 32 | "IncludeGroupTable", 33 | "LicenseTable", 34 | "ProjectTable", 35 | "PyProjectTable", 36 | "ReadmeTable", 37 | ] 38 | 39 | 40 | def __dir__() -> list[str]: 41 | return __all__ 42 | 43 | 44 | class ContactTable(TypedDict, total=False): 45 | name: str 46 | email: str 47 | 48 | 49 | class LicenseTable(TypedDict, total=False): 50 | text: str 51 | file: str 52 | 53 | 54 | ReadmeTable = TypedDict( 55 | "ReadmeTable", {"file": str, "text": str, "content-type": str}, total=False 56 | ) 57 | 58 | Dynamic = Literal[ 59 | "authors", 60 | "classifiers", 61 | "dependencies", 62 | "description", 63 | "dynamic", 64 | "entry-points", 65 | "gui-scripts", 66 | "keywords", 67 | "license", 68 | "maintainers", 69 | "optional-dependencies", 70 | "readme", 71 | "requires-python", 72 | "scripts", 73 | "urls", 74 | "version", 75 | ] 76 | 77 | ProjectTable = TypedDict( 78 | "ProjectTable", 79 | { 80 | "name": Required[str], 81 | "version": str, 82 | "description": str, 83 | "license": Union[LicenseTable, str], 84 | "license-files": List[str], 85 | "readme": Union[str, ReadmeTable], 86 | "requires-python": str, 87 | "dependencies": List[str], 88 | "optional-dependencies": Dict[str, List[str]], 89 | "entry-points": Dict[str, Dict[str, str]], 90 | "authors": List[ContactTable], 91 | "maintainers": List[ContactTable], 92 | "urls": Dict[str, str], 93 | "classifiers": List[str], 94 | "keywords": List[str], 95 | "scripts": Dict[str, str], 96 | "gui-scripts": Dict[str, str], 97 | "dynamic": List[Dynamic], 98 | }, 99 | total=False, 100 | ) 101 | 102 | BuildSystemTable = TypedDict( 103 | "BuildSystemTable", 104 | { 105 | "build-backend": str, 106 | "requires": List[str], 107 | "backend-path": List[str], 108 | }, 109 | total=False, 110 | ) 111 | 112 | # total=False here because this could be 113 | # extended in the future 114 | IncludeGroupTable = TypedDict( 115 | "IncludeGroupTable", 116 | {"include-group": str}, 117 | total=False, 118 | ) 119 | 120 | PyProjectTable = TypedDict( 121 | "PyProjectTable", 122 | { 123 | "build-system": BuildSystemTable, 124 | "project": ProjectTable, 125 | "tool": Dict[str, Any], 126 | "dependency-groups": Dict[str, List[Union[str, IncludeGroupTable]]], 127 | }, 128 | total=False, 129 | ) 130 | 131 | # Tests for type checking 132 | if typing.TYPE_CHECKING: 133 | PyProjectTable( 134 | { 135 | "build-system": BuildSystemTable( 136 | {"build-backend": "one", "requires": ["two"]} 137 | ), 138 | "project": ProjectTable( 139 | { 140 | "name": "one", 141 | "version": "0.1.0", 142 | } 143 | ), 144 | "tool": {"thing": object()}, 145 | "dependency-groups": { 146 | "one": [ 147 | "one", 148 | IncludeGroupTable({"include-group": "two"}), 149 | ] 150 | }, 151 | } 152 | ) 153 | -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/pyproject_metadata/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm-backend/6c339b6a2f8a9e454e9c240cca374aa0d9148ec1/src/pdm/backend/_vendor/pyproject_metadata/py.typed -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/tomli/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Taneli Hukkinen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/tomli/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | # SPDX-FileCopyrightText: 2021 Taneli Hukkinen 3 | # Licensed to PSF under a Contributor Agreement. 4 | 5 | __all__ = ("loads", "load", "TOMLDecodeError") 6 | __version__ = "2.0.1" # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT 7 | 8 | from ._parser import TOMLDecodeError, load, loads 9 | 10 | # Pretend this exception was created here. 11 | TOMLDecodeError.__module__ = __name__ 12 | -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/tomli/_re.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | # SPDX-FileCopyrightText: 2021 Taneli Hukkinen 3 | # Licensed to PSF under a Contributor Agreement. 4 | 5 | from __future__ import annotations 6 | 7 | from datetime import date, datetime, time, timedelta, timezone, tzinfo 8 | from functools import lru_cache 9 | import re 10 | from typing import Any 11 | 12 | from ._types import ParseFloat 13 | 14 | # E.g. 15 | # - 00:32:00.999999 16 | # - 00:32:00 17 | _TIME_RE_STR = r"([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(?:\.([0-9]{1,6})[0-9]*)?" 18 | 19 | RE_NUMBER = re.compile( 20 | r""" 21 | 0 22 | (?: 23 | x[0-9A-Fa-f](?:_?[0-9A-Fa-f])* # hex 24 | | 25 | b[01](?:_?[01])* # bin 26 | | 27 | o[0-7](?:_?[0-7])* # oct 28 | ) 29 | | 30 | [+-]?(?:0|[1-9](?:_?[0-9])*) # dec, integer part 31 | (?P 32 | (?:\.[0-9](?:_?[0-9])*)? # optional fractional part 33 | (?:[eE][+-]?[0-9](?:_?[0-9])*)? # optional exponent part 34 | ) 35 | """, 36 | flags=re.VERBOSE, 37 | ) 38 | RE_LOCALTIME = re.compile(_TIME_RE_STR) 39 | RE_DATETIME = re.compile( 40 | rf""" 41 | ([0-9]{{4}})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) # date, e.g. 1988-10-27 42 | (?: 43 | [Tt ] 44 | {_TIME_RE_STR} 45 | (?:([Zz])|([+-])([01][0-9]|2[0-3]):([0-5][0-9]))? # optional time offset 46 | )? 47 | """, 48 | flags=re.VERBOSE, 49 | ) 50 | 51 | 52 | def match_to_datetime(match: re.Match) -> datetime | date: 53 | """Convert a `RE_DATETIME` match to `datetime.datetime` or `datetime.date`. 54 | 55 | Raises ValueError if the match does not correspond to a valid date 56 | or datetime. 57 | """ 58 | ( 59 | year_str, 60 | month_str, 61 | day_str, 62 | hour_str, 63 | minute_str, 64 | sec_str, 65 | micros_str, 66 | zulu_time, 67 | offset_sign_str, 68 | offset_hour_str, 69 | offset_minute_str, 70 | ) = match.groups() 71 | year, month, day = int(year_str), int(month_str), int(day_str) 72 | if hour_str is None: 73 | return date(year, month, day) 74 | hour, minute, sec = int(hour_str), int(minute_str), int(sec_str) 75 | micros = int(micros_str.ljust(6, "0")) if micros_str else 0 76 | if offset_sign_str: 77 | tz: tzinfo | None = cached_tz( 78 | offset_hour_str, offset_minute_str, offset_sign_str 79 | ) 80 | elif zulu_time: 81 | tz = timezone.utc 82 | else: # local date-time 83 | tz = None 84 | return datetime(year, month, day, hour, minute, sec, micros, tzinfo=tz) 85 | 86 | 87 | @lru_cache(maxsize=None) 88 | def cached_tz(hour_str: str, minute_str: str, sign_str: str) -> timezone: 89 | sign = 1 if sign_str == "+" else -1 90 | return timezone( 91 | timedelta( 92 | hours=sign * int(hour_str), 93 | minutes=sign * int(minute_str), 94 | ) 95 | ) 96 | 97 | 98 | def match_to_localtime(match: re.Match) -> time: 99 | hour_str, minute_str, sec_str, micros_str = match.groups() 100 | micros = int(micros_str.ljust(6, "0")) if micros_str else 0 101 | return time(int(hour_str), int(minute_str), int(sec_str), micros) 102 | 103 | 104 | def match_to_number(match: re.Match, parse_float: ParseFloat) -> Any: 105 | if match.group("floatpart"): 106 | return parse_float(match.group()) 107 | return int(match.group(), 0) 108 | -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/tomli/_types.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | # SPDX-FileCopyrightText: 2021 Taneli Hukkinen 3 | # Licensed to PSF under a Contributor Agreement. 4 | 5 | from typing import Any, Callable, Tuple 6 | 7 | # Type annotations 8 | ParseFloat = Callable[[str], Any] 9 | Key = Tuple[str, ...] 10 | Pos = int 11 | -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/tomli/py.typed: -------------------------------------------------------------------------------- 1 | # Marker file for PEP 561 2 | -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/tomli_w/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Taneli Hukkinen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/tomli_w/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ("dumps", "dump") 2 | __version__ = "1.0.0" # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT 3 | 4 | from pdm.backend._vendor.tomli_w._writer import dump, dumps 5 | -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/tomli_w/_writer.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Generator, Mapping 4 | from datetime import date, datetime, time 5 | from decimal import Decimal 6 | import string 7 | from types import MappingProxyType 8 | from typing import Any, BinaryIO, NamedTuple 9 | 10 | ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127)) 11 | ILLEGAL_BASIC_STR_CHARS = frozenset('"\\') | ASCII_CTRL - frozenset("\t") 12 | BARE_KEY_CHARS = frozenset(string.ascii_letters + string.digits + "-_") 13 | ARRAY_TYPES = (list, tuple) 14 | ARRAY_INDENT = " " * 4 15 | MAX_LINE_LENGTH = 100 16 | 17 | COMPACT_ESCAPES = MappingProxyType( 18 | { 19 | "\u0008": "\\b", # backspace 20 | "\u000A": "\\n", # linefeed 21 | "\u000C": "\\f", # form feed 22 | "\u000D": "\\r", # carriage return 23 | "\u0022": '\\"', # quote 24 | "\u005C": "\\\\", # backslash 25 | } 26 | ) 27 | 28 | 29 | def dump( 30 | __obj: dict[str, Any], __fp: BinaryIO, *, multiline_strings: bool = False 31 | ) -> None: 32 | ctx = Context(multiline_strings, {}) 33 | for chunk in gen_table_chunks(__obj, ctx, name=""): 34 | __fp.write(chunk.encode()) 35 | 36 | 37 | def dumps(__obj: dict[str, Any], *, multiline_strings: bool = False) -> str: 38 | ctx = Context(multiline_strings, {}) 39 | return "".join(gen_table_chunks(__obj, ctx, name="")) 40 | 41 | 42 | class Context(NamedTuple): 43 | allow_multiline: bool 44 | # cache rendered inline tables (mapping from object id to rendered inline table) 45 | inline_table_cache: dict[int, str] 46 | 47 | 48 | def gen_table_chunks( 49 | table: Mapping[str, Any], 50 | ctx: Context, 51 | *, 52 | name: str, 53 | inside_aot: bool = False, 54 | ) -> Generator[str, None, None]: 55 | yielded = False 56 | literals = [] 57 | tables: list[tuple[str, Any, bool]] = [] # => [(key, value, inside_aot)] 58 | for k, v in table.items(): 59 | if isinstance(v, dict): 60 | tables.append((k, v, False)) 61 | elif is_aot(v) and not all(is_suitable_inline_table(t, ctx) for t in v): 62 | tables.extend((k, t, True) for t in v) 63 | else: 64 | literals.append((k, v)) 65 | 66 | if inside_aot or name and (literals or not tables): 67 | yielded = True 68 | yield f"[[{name}]]\n" if inside_aot else f"[{name}]\n" 69 | 70 | if literals: 71 | yielded = True 72 | for k, v in literals: 73 | yield f"{format_key_part(k)} = {format_literal(v, ctx)}\n" 74 | 75 | for k, v, in_aot in tables: 76 | if yielded: 77 | yield "\n" 78 | else: 79 | yielded = True 80 | key_part = format_key_part(k) 81 | display_name = f"{name}.{key_part}" if name else key_part 82 | yield from gen_table_chunks(v, ctx, name=display_name, inside_aot=in_aot) 83 | 84 | 85 | def format_literal(obj: object, ctx: Context, *, nest_level: int = 0) -> str: 86 | if isinstance(obj, bool): 87 | return "true" if obj else "false" 88 | if isinstance(obj, (int, float, date, datetime)): 89 | return str(obj) 90 | if isinstance(obj, Decimal): 91 | return format_decimal(obj) 92 | if isinstance(obj, time): 93 | if obj.tzinfo: 94 | raise ValueError("TOML does not support offset times") 95 | return str(obj) 96 | if isinstance(obj, str): 97 | return format_string(obj, allow_multiline=ctx.allow_multiline) 98 | if isinstance(obj, ARRAY_TYPES): 99 | return format_inline_array(obj, ctx, nest_level) 100 | if isinstance(obj, dict): 101 | return format_inline_table(obj, ctx) 102 | raise TypeError(f"Object of type {type(obj)} is not TOML serializable") 103 | 104 | 105 | def format_decimal(obj: Decimal) -> str: 106 | if obj.is_nan(): 107 | return "nan" 108 | if obj == Decimal("inf"): 109 | return "inf" 110 | if obj == Decimal("-inf"): 111 | return "-inf" 112 | return str(obj) 113 | 114 | 115 | def format_inline_table(obj: dict, ctx: Context) -> str: 116 | # check cache first 117 | obj_id = id(obj) 118 | if obj_id in ctx.inline_table_cache: 119 | return ctx.inline_table_cache[obj_id] 120 | 121 | if not obj: 122 | rendered = "{}" 123 | else: 124 | rendered = ( 125 | "{ " 126 | + ", ".join( 127 | f"{format_key_part(k)} = {format_literal(v, ctx)}" 128 | for k, v in obj.items() 129 | ) 130 | + " }" 131 | ) 132 | ctx.inline_table_cache[obj_id] = rendered 133 | return rendered 134 | 135 | 136 | def format_inline_array(obj: tuple | list, ctx: Context, nest_level: int) -> str: 137 | if not obj: 138 | return "[]" 139 | item_indent = ARRAY_INDENT * (1 + nest_level) 140 | closing_bracket_indent = ARRAY_INDENT * nest_level 141 | return ( 142 | "[\n" 143 | + ",\n".join( 144 | item_indent + format_literal(item, ctx, nest_level=nest_level + 1) 145 | for item in obj 146 | ) 147 | + f",\n{closing_bracket_indent}]" 148 | ) 149 | 150 | 151 | def format_key_part(part: str) -> str: 152 | if part and BARE_KEY_CHARS.issuperset(part): 153 | return part 154 | return format_string(part, allow_multiline=False) 155 | 156 | 157 | def format_string(s: str, *, allow_multiline: bool) -> str: 158 | do_multiline = allow_multiline and "\n" in s 159 | if do_multiline: 160 | result = '"""\n' 161 | s = s.replace("\r\n", "\n") 162 | else: 163 | result = '"' 164 | 165 | pos = seq_start = 0 166 | while True: 167 | try: 168 | char = s[pos] 169 | except IndexError: 170 | result += s[seq_start:pos] 171 | if do_multiline: 172 | return result + '"""' 173 | return result + '"' 174 | if char in ILLEGAL_BASIC_STR_CHARS: 175 | result += s[seq_start:pos] 176 | if char in COMPACT_ESCAPES: 177 | if do_multiline and char == "\n": 178 | result += "\n" 179 | else: 180 | result += COMPACT_ESCAPES[char] 181 | else: 182 | result += "\\u" + hex(ord(char))[2:].rjust(4, "0") 183 | seq_start = pos + 1 184 | pos += 1 185 | 186 | 187 | def is_aot(obj: Any) -> bool: 188 | """Decides if an object behaves as an array of tables (i.e. a nonempty list 189 | of dicts).""" 190 | return bool( 191 | isinstance(obj, ARRAY_TYPES) and obj and all(isinstance(v, dict) for v in obj) 192 | ) 193 | 194 | 195 | def is_suitable_inline_table(obj: dict, ctx: Context) -> bool: 196 | """Use heuristics to decide if the inline-style representation is a good 197 | choice for a given table.""" 198 | rendered_inline = f"{ARRAY_INDENT}{format_inline_table(obj, ctx)}," 199 | return len(rendered_inline) <= MAX_LINE_LENGTH and "\n" not in rendered_inline 200 | -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/tomli_w/py.typed: -------------------------------------------------------------------------------- 1 | # Marker file for PEP 561 2 | -------------------------------------------------------------------------------- /src/pdm/backend/_vendor/vendor.txt: -------------------------------------------------------------------------------- 1 | packaging==24.1 2 | tomli==2.0.1 3 | tomli_w==1.0.0 4 | pyproject-metadata==0.9.0 5 | editables==0.5 6 | -------------------------------------------------------------------------------- /src/pdm/backend/editable.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import warnings 5 | from pathlib import Path 6 | 7 | from pdm.backend._vendor.editables import EditableProject 8 | from pdm.backend._vendor.packaging.utils import canonicalize_name 9 | from pdm.backend.exceptions import ConfigError, PDMWarning 10 | from pdm.backend.hooks.base import Context 11 | from pdm.backend.utils import to_filename 12 | from pdm.backend.wheel import WheelBuilder 13 | 14 | 15 | def is_subpath(path: str, parent: str) -> bool: 16 | return os.path.normcase(path).startswith(os.path.normcase(parent)) 17 | 18 | 19 | class EditableBuildHook: 20 | def pdm_build_initialize(self, context: Context) -> None: 21 | editables = self._prepare_editable(context) 22 | context.config.metadata.setdefault("dependencies", []).extend( 23 | editables.dependencies() 24 | ) 25 | context.editables = editables 26 | 27 | def pdm_build_update_files(self, context: Context, files: dict[str, Path]) -> None: 28 | packages: list[str] = context.config.convert_package_paths()["packages"] 29 | proxied = {p.replace(".", "/") for p in packages} 30 | for relpath in list(files): 31 | if os.path.splitext(relpath)[1] in (".py", ".pyc", ".pyo"): 32 | # All .py[cod] files are proxied 33 | del files[relpath] 34 | elif any(is_subpath(relpath, p) for p in proxied): 35 | # also exclude data files in proxied packages 36 | del files[relpath] 37 | editables: EditableProject = context.editables 38 | context.ensure_build_dir() 39 | for name, content in editables.files(): 40 | with open(os.path.join(context.build_dir, name), "w", newline="") as f: 41 | f.write(content) 42 | files[name] = context.build_dir.joinpath(name) 43 | 44 | def _prepare_editable(self, context: Context) -> EditableProject: 45 | config = context.config 46 | try: 47 | editables = EditableProject( 48 | to_filename(canonicalize_name(config.metadata["name"])), 49 | context.root.as_posix(), 50 | ) 51 | except ValueError as e: 52 | raise ConfigError(str(e)) from None 53 | package_paths = config.convert_package_paths() 54 | build_config = config.build_config 55 | package_dir = build_config.package_dir 56 | if build_config.editable_backend == "editables": 57 | for package in package_paths.get("packages", []): 58 | if "." in package: 59 | continue 60 | editables.map(package, os.path.join(package_dir, package)) 61 | 62 | for module in package_paths.get("py_modules", []): 63 | if "." in module: 64 | continue 65 | 66 | patterns: tuple[str, ...] = (f"{module}.py",) 67 | if os.name == "nt": 68 | patterns += (f"{module}.*.pyd",) 69 | else: 70 | patterns += (f"{module}.*.so",) 71 | for pattern in patterns: 72 | path = next(Path(package_dir).glob(pattern), None) 73 | if path: 74 | editables.map(module, path.as_posix()) 75 | break 76 | 77 | if not editables.redirections: 78 | # For implicit namespace packages, modules cannot be mapped. 79 | # Fallback to .pth method in this case. 80 | if build_config.editable_backend == "editables": 81 | warnings.warn( 82 | "editables backend is not available for namespace packages, " 83 | "fallback to path entries", 84 | PDMWarning, 85 | ) 86 | editables.add_to_path(package_dir) 87 | return editables 88 | 89 | 90 | class EditableBuilder(WheelBuilder): 91 | target = "editable" 92 | hooks = WheelBuilder.hooks + [EditableBuildHook()] 93 | -------------------------------------------------------------------------------- /src/pdm/backend/exceptions.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | class BuildError(RuntimeError): 5 | pass 6 | 7 | 8 | class ConfigError(ValueError): 9 | pass 10 | 11 | 12 | class PDMWarning(UserWarning): 13 | pass 14 | 15 | 16 | class ValidationError(ConfigError): 17 | def __init__(self, summary: str, key: str | None = None) -> None: 18 | super().__init__(summary) 19 | self.key = key 20 | 21 | def __str__(self) -> str: 22 | prefix = f"{self.key}: " if self.key else "" 23 | return f"{prefix}{self.args[0]}" 24 | -------------------------------------------------------------------------------- /src/pdm/backend/hooks/__init__.py: -------------------------------------------------------------------------------- 1 | from pdm.backend.hooks.base import BuildHookInterface, Context 2 | 3 | __all__ = ["Context", "BuildHookInterface"] 4 | -------------------------------------------------------------------------------- /src/pdm/backend/hooks/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import dataclasses 4 | from pathlib import Path 5 | from typing import TYPE_CHECKING, Any, Iterable 6 | 7 | from pdm.backend.config import Config 8 | 9 | if TYPE_CHECKING: 10 | from typing import Protocol 11 | 12 | from pdm.backend.base import Builder, Target 13 | else: 14 | Protocol = object 15 | 16 | 17 | @dataclasses.dataclass() 18 | class Context: 19 | """The context object for the build hook, 20 | which contains useful information about the building process. 21 | Custom hooks can also change the values of attributes or 22 | assign arbitrary attributes to this object. 23 | 24 | Attributes: 25 | build_dir: The build directory for storing files generated during the build 26 | dist_dir: The directory to store the built artifacts 27 | kwargs: The extra args passed to the build method 28 | builder: The builder associated with this build context 29 | """ 30 | 31 | build_dir: Path 32 | dist_dir: Path 33 | kwargs: dict[str, Any] 34 | builder: Builder 35 | 36 | @property 37 | def config(self) -> Config: 38 | """The parsed pyproject.toml as a Config object""" 39 | return self.builder.config 40 | 41 | @property 42 | def root(self) -> Path: 43 | """The project root directory""" 44 | return self.builder.location 45 | 46 | @property 47 | def target(self) -> Target: 48 | """The target to build, one of 'sdist', 'wheel', 'editable'""" 49 | return self.builder.target 50 | 51 | @property 52 | def config_settings(self) -> dict[str, str]: 53 | """The config settings passed to the hook""" 54 | return self.builder.config_settings 55 | 56 | def ensure_build_dir(self) -> Path: 57 | """Return the build dir and create if it doesn't exist""" 58 | if not self.build_dir.exists(): 59 | self.build_dir.mkdir(mode=0o700, parents=True, exist_ok=True) 60 | (self.build_dir / ".gitignore").write_text("*\n") 61 | return self.build_dir 62 | 63 | def expand_paths(self, path: str) -> Iterable[Path]: 64 | def path_filter(p: Path) -> bool: 65 | return p.is_file() or p.is_symlink() 66 | 67 | plib_path = Path(path) 68 | if plib_path.parts and plib_path.parts[0] == "${BUILD_DIR}": 69 | return filter( 70 | path_filter, self.build_dir.glob(Path(*plib_path.parts[1:]).as_posix()) 71 | ) 72 | 73 | return filter(path_filter, self.root.glob(path)) 74 | 75 | 76 | class BuildHookInterface(Protocol): 77 | """The interface definition for build hooks. 78 | 79 | Custom hooks can implement part of the methods to provide corresponding abilities. 80 | """ 81 | 82 | def pdm_build_hook_enabled(self, context: Context) -> bool: 83 | """Return True if the hook is enabled for the current build and context 84 | 85 | Parameters: 86 | context: The context for this build 87 | """ 88 | ... 89 | 90 | def pdm_build_clean(self, context: Context) -> None: 91 | """An optional clean step which will be called before the build starts 92 | 93 | Parameters: 94 | context: The context for this build 95 | """ 96 | ... 97 | 98 | def pdm_build_initialize(self, context: Context) -> None: 99 | """This hook will be called before the build starts, 100 | any updates to the context object will be seen by the following processes. 101 | It is recommended to modify the metadata in this hook. 102 | 103 | Parameters: 104 | context: The context for this build 105 | """ 106 | ... 107 | 108 | def pdm_build_update_files(self, context: Context, files: dict[str, Path]) -> None: 109 | """Passed in the current file mapping of {relpath: path} 110 | for hooks to update. 111 | 112 | Parameters: 113 | context: The context for this build 114 | files: The file mapping to be included in the build artifact, where the key 115 | is the relpath inside the artifact(wheel or tarball) and the value is 116 | the local path to the file. 117 | """ 118 | ... 119 | 120 | def pdm_build_finalize(self, context: Context, artifact: Path) -> None: 121 | """This hook will be called after the build is done, 122 | the artifact is the path to the built artifact. 123 | 124 | Parameters: 125 | context: The context for this build 126 | artifact: The path to the built artifact 127 | """ 128 | ... 129 | 130 | def pdm_build_update_setup_kwargs( 131 | self, context: Context, kwargs: dict[str, Any] 132 | ) -> None: 133 | """Passed in the setup kwargs for hooks to update. 134 | 135 | Parameters: 136 | context: The context for this build 137 | kwargs: The arguments to be passed to the setup() function 138 | 139 | Note: 140 | This hook will be called in the subprocess of running setup.py. 141 | Any changes made to the context won't be written back. 142 | """ 143 | ... 144 | -------------------------------------------------------------------------------- /src/pdm/backend/hooks/setuptools.py: -------------------------------------------------------------------------------- 1 | """A built-in hook to generate setup.py and run the script""" 2 | 3 | from __future__ import annotations 4 | 5 | import atexit 6 | import pickle 7 | import shutil 8 | import subprocess 9 | import sys 10 | import tempfile 11 | from pathlib import Path 12 | from typing import cast 13 | 14 | from pdm.backend.exceptions import BuildError 15 | from pdm.backend.hooks.base import Context 16 | 17 | # A minimal template of setup.py, which is used to build extensions 18 | SETUP_FORMAT = """\ 19 | # -*- coding: utf-8 -*- 20 | from setuptools import setup 21 | 22 | {before} 23 | setup_kwargs = {{ 24 | 'name': {name!r}, 25 | 'version': {version!r}, 26 | 'description': {description!r}, 27 | 'url': {url!r}, 28 | {extra} 29 | }} 30 | {after} 31 | 32 | setup(**setup_kwargs) 33 | """ 34 | 35 | HOOK_TEMPLATE = """\ 36 | import pickle 37 | 38 | context_dump = {context_dump!r} 39 | context = pickle.loads(context_dump) 40 | builder = context.builder 41 | builder.call_hook("pdm_build_update_setup_kwargs", context, setup_kwargs) 42 | """ 43 | 44 | 45 | def _format_list(data: list[str], indent: int = 4) -> str: 46 | result = ["["] 47 | for row in data: 48 | result.append(" " * indent + repr(row) + ",") 49 | result.append(" " * (indent - 4) + "]") 50 | return "\n".join(result) 51 | 52 | 53 | def _format_dict_list(data: dict[str, list[str]], indent: int = 4) -> str: 54 | result = ["{"] 55 | for key, value in data.items(): 56 | result.append( 57 | " " * indent + repr(key) + ": " + _format_list(value, indent + 4) + "," 58 | ) 59 | result.append(" " * (indent - 4) + "}") 60 | return "\n".join(result) 61 | 62 | 63 | def _recursive_copy_files(src: Path, dest: Path) -> None: 64 | if src.is_file(): 65 | shutil.copy2(src, dest) 66 | else: 67 | dest.mkdir(exist_ok=True) 68 | for child in src.iterdir(): 69 | _recursive_copy_files(child, dest / child.name) 70 | 71 | 72 | class SetuptoolsBuildHook: 73 | """A build hook to run setuptools build command.""" 74 | 75 | def pdm_build_hook_enabled(self, context: Context) -> bool: 76 | return context.target != "sdist" and context.config.build_config.run_setuptools 77 | 78 | def pdm_build_update_files(self, context: Context, files: dict[str, Path]) -> None: 79 | if context.target == "editable": 80 | self._build_inplace(context) 81 | else: 82 | self._build_lib(context) 83 | 84 | def _build_lib(self, context: Context) -> None: 85 | build_dir = context.ensure_build_dir() 86 | setup_py = self.ensure_setup_py(context) 87 | with tempfile.TemporaryDirectory(prefix="pdm-build-") as temp_dir: 88 | build_args = [sys.executable, str(setup_py), "build", "-b", temp_dir] 89 | try: 90 | subprocess.check_call(build_args) 91 | except subprocess.CalledProcessError as e: 92 | raise BuildError(f"Error occurs when running {build_args}:\n{e}") 93 | lib_dir = next(Path(temp_dir).glob("lib.*"), None) 94 | if not lib_dir: 95 | return 96 | # copy the files under temp_dir/lib.* to context.build_dir 97 | _recursive_copy_files(lib_dir, build_dir) 98 | 99 | def _build_inplace(self, context: Context) -> None: 100 | setup_py = self.ensure_setup_py(context) 101 | build_args = [sys.executable, str(setup_py), "build_ext", "--inplace"] 102 | try: 103 | subprocess.check_call(build_args) 104 | except subprocess.CalledProcessError as e: 105 | raise BuildError(f"Error occurs when running {build_args}:\n{e}") 106 | 107 | def ensure_setup_py(self, context: Context, clean: bool = True) -> Path: 108 | """Ensures the requirement has a setup.py ready.""" 109 | # XXX: Currently only handle PDM project, and do nothing if not. 110 | 111 | setup_py_path = context.root.joinpath("setup.py") 112 | if setup_py_path.is_file(): 113 | return setup_py_path 114 | 115 | setup_py_path.write_text(self.format_setup_py(context), encoding="utf-8") 116 | 117 | # Clean this temp file when process exits 118 | def cleanup() -> None: 119 | try: 120 | setup_py_path.unlink() 121 | except OSError: 122 | pass 123 | 124 | if clean: 125 | atexit.register(cleanup) 126 | return setup_py_path 127 | 128 | def format_setup_py(self, context: Context) -> str: 129 | before, extra, after = [], [], [] 130 | meta = context.config.validate() 131 | kwargs = { 132 | "name": meta.name, 133 | "version": str(meta.version or "0.0.0"), 134 | "description": meta.description or "UNKNOWN", 135 | "url": meta.urls.get("homepage", ""), 136 | } 137 | 138 | # Run the pdm_build_update_setup_kwargs hook to update the kwargs 139 | after.append(HOOK_TEMPLATE.format(context_dump=pickle.dumps(context))) 140 | 141 | package_paths = context.config.convert_package_paths() 142 | if package_paths["packages"]: 143 | extra.append( 144 | " 'packages': {},\n".format( 145 | _format_list(cast("list[str]", package_paths["packages"]), 8) 146 | ) 147 | ) 148 | if package_paths["package_dir"]: 149 | extra.append( 150 | " 'package_dir': {!r},\n".format(package_paths["package_dir"]) 151 | ) 152 | if package_paths["package_data"]: 153 | extra.append( 154 | " 'package_data': {!r},\n".format(package_paths["package_data"]) 155 | ) 156 | if package_paths["exclude_package_data"]: 157 | extra.append( 158 | " 'exclude_package_data': {!r},\n".format( 159 | package_paths["exclude_package_data"] 160 | ) 161 | ) 162 | 163 | if meta.dependencies: 164 | before.append( 165 | f"INSTALL_REQUIRES = {_format_list([str(d) for d in meta.dependencies])}\n" 166 | ) 167 | extra.append(" 'install_requires': INSTALL_REQUIRES,\n") 168 | if meta.optional_dependencies: 169 | extras_require = { 170 | k: [str(d) for d in v] for k, v in meta.optional_dependencies.items() 171 | } 172 | before.append(f"EXTRAS_REQUIRE = {_format_dict_list(extras_require)}\n") 173 | extra.append(" 'extras_require': EXTRAS_REQUIRE,\n") 174 | if meta.requires_python is not None: 175 | extra.append(f" 'python_requires': '{meta.requires_python}',\n") 176 | entry_points = meta.entrypoints.copy() 177 | entry_points.update( 178 | {"console_scripts": meta.scripts, "gui_scripts": meta.gui_scripts} 179 | ) 180 | if entry_points: 181 | entry_points_list = { 182 | group: [f"{k} = {v}" for k, v in values.items()] 183 | for group, values in entry_points.items() 184 | } 185 | before.append(f"ENTRY_POINTS = {_format_dict_list(entry_points_list)}\n") 186 | extra.append(" 'entry_points': ENTRY_POINTS,\n") 187 | return SETUP_FORMAT.format( 188 | before="".join(before), after="".join(after), extra="".join(extra), **kwargs 189 | ) 190 | -------------------------------------------------------------------------------- /src/pdm/backend/hooks/version/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import inspect 4 | import os 5 | import re 6 | import warnings 7 | from pathlib import Path 8 | from typing import Callable 9 | 10 | from pdm.backend._vendor.packaging.version import Version 11 | from pdm.backend.exceptions import ConfigError, PDMWarning, ValidationError 12 | from pdm.backend.hooks.base import Context 13 | from pdm.backend.hooks.version.scm import SCMVersion as SCMVersion 14 | from pdm.backend.hooks.version.scm import ( 15 | default_version_formatter, 16 | get_version_from_scm, 17 | ) 18 | from pdm.backend.utils import evaluate_module_attribute 19 | 20 | 21 | class DynamicVersionBuildHook: 22 | """Dynamic version implementation. 23 | 24 | Currently supports `file` and `scm` sources. 25 | """ 26 | 27 | supported_sources = ("file", "scm", "call") 28 | 29 | def pdm_build_initialize(self, context: Context) -> None: 30 | version_config = ( 31 | context.config.data.get("tool", {}).get("pdm", {}).get("version", {}) 32 | ) 33 | metadata = context.config.metadata 34 | if not version_config or "version" in metadata: 35 | if metadata.get("version") is None: 36 | metadata["version"] = "0.0.0" 37 | try: 38 | metadata["dynamic"].remove("version") 39 | except (ValueError, KeyError): 40 | pass 41 | return 42 | if "version" not in metadata.get("dynamic", []): 43 | raise ValidationError( 44 | "missing 'version' in project.dynamic", 45 | "The 'version' field must be present in project.dynamic to " 46 | "resolve it dynamically", 47 | ) 48 | source: str = version_config.get("source") 49 | if not source: 50 | raise ConfigError("tool.pdm.version.source is required") 51 | if source not in self.supported_sources: 52 | warnings.warn( 53 | f"Invalid version source {source}, must be one of " 54 | f"{', '.join(self.supported_sources)}", 55 | PDMWarning, 56 | ) 57 | return 58 | options = {k: v for k, v in version_config.items() if k != "source"} 59 | metadata["version"] = str( 60 | getattr(self, f"resolve_version_from_{source}")(context, **options) 61 | ) 62 | metadata["dynamic"].remove("version") 63 | 64 | def resolve_version_from_file(self, context: Context, path: str) -> Version: 65 | """Resolve version from a file.""" 66 | version_source = context.root / path 67 | with open(version_source, encoding="utf-8") as fp: 68 | match = re.search( 69 | r"^__version__\s*=\s*[\"'](.+?)[\"']\s*(?:#.*)?$", fp.read(), re.M 70 | ) 71 | if not match: 72 | raise ConfigError( 73 | f"Couldn't find version in file {version_source!r}, " 74 | "it should appear as `__version__ = 'a.b.c'`.", 75 | ) 76 | return Version(match.group(1)) 77 | 78 | def resolve_version_from_scm( 79 | self, 80 | context: Context, 81 | write_to: str | None = None, 82 | write_template: str = "{}\n", 83 | tag_regex: str | None = None, 84 | tag_filter: str | None = None, 85 | version_format: str | None = None, 86 | fallback_version: str | None = None, 87 | ) -> Version: 88 | if os.environ.get("PDM_BUILD_SCM_VERSION"): 89 | version = os.environ["PDM_BUILD_SCM_VERSION"] 90 | else: 91 | version_formatter: ( 92 | Callable[[SCMVersion, Context], str] | Callable[[SCMVersion], str] 93 | ) 94 | if version_format is not None: 95 | version_formatter, _ = evaluate_module_attribute( 96 | version_format, context.root 97 | ) 98 | else: 99 | version_formatter = default_version_formatter 100 | scm_version = get_version_from_scm( 101 | context.root, tag_regex=tag_regex, tag_filter=tag_filter 102 | ) 103 | if scm_version is None: 104 | if fallback_version is not None: 105 | version = fallback_version 106 | else: 107 | raise ConfigError( 108 | "Cannot find the version from SCM or SCM isn't detected. \n" 109 | "You can still specify the version via environment variable " 110 | "`PDM_BUILD_SCM_VERSION`, or specify `fallback_version` config." 111 | ) 112 | else: 113 | params = inspect.signature(version_formatter).parameters 114 | if len(params) > 1: 115 | version = version_formatter(scm_version, context) # type: ignore[call-arg] 116 | else: 117 | version = version_formatter(scm_version) # type: ignore[call-arg] 118 | try: 119 | parsed_version = Version(version) 120 | except ValueError: 121 | if fallback_version is not None: 122 | return Version(fallback_version) 123 | raise ConfigError( 124 | f"Invalid version {version}, it must comply with PEP 440. \n" 125 | "You can still specify the version via environment variable " 126 | "`PDM_BUILD_SCM_VERSION`, or specify `fallback_version` config." 127 | ) 128 | self._write_version(context, version, write_to, write_template) 129 | return parsed_version 130 | 131 | def _write_version( 132 | self, 133 | context: Context, 134 | version: str, 135 | write_to: str | None = None, 136 | write_template: str = "{}\n", 137 | ) -> None: 138 | """Write the resolved version to the file.""" 139 | if write_to is not None: 140 | if context.target == "sdist" and context.config.build_config.package_dir: 141 | write_to = os.path.join( 142 | context.config.build_config.package_dir, write_to 143 | ) 144 | if context.target == "editable": 145 | target = Path(context.config.build_config.package_dir or ".") / write_to 146 | else: 147 | target = context.build_dir / write_to 148 | if not target.parent.exists(): 149 | target.parent.mkdir(0o700, parents=True) 150 | with open(target, "w", encoding="utf-8", newline="") as fp: 151 | fp.write(write_template.format(version)) 152 | 153 | def resolve_version_from_call( 154 | self, 155 | context: Context, 156 | getter: str, 157 | write_to: str | None = None, 158 | write_template: str = "{}\n", 159 | fallback_version: str | None = None, 160 | ) -> Version: 161 | version_getter, args = evaluate_module_attribute(getter, context.root) 162 | version = version_getter(*args) 163 | if version is None: 164 | if fallback_version is not None: 165 | return Version(fallback_version) 166 | else: 167 | raise ConfigError( 168 | "Cannot get version from the call, you can specify fallback version via `fallback_version` config." 169 | ) 170 | parsed_version = Version(version) 171 | self._write_version(context, version, write_to, write_template) 172 | return parsed_version 173 | -------------------------------------------------------------------------------- /src/pdm/backend/intree.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from typing import Any, Mapping 5 | 6 | from pdm.backend import build_editable as build_editable 7 | from pdm.backend import build_sdist as build_sdist 8 | from pdm.backend import build_wheel as build_wheel 9 | from pdm.backend import ( 10 | prepare_metadata_for_build_editable as prepare_metadata_for_build_editable, 11 | ) 12 | from pdm.backend import ( 13 | prepare_metadata_for_build_wheel as prepare_metadata_for_build_wheel, 14 | ) 15 | 16 | if sys.version_info >= (3, 11): 17 | import tomllib 18 | else: 19 | import pdm.backend._vendor.tomli as tomllib 20 | 21 | 22 | def get_requires_for_build_wheel( 23 | config_settings: Mapping[str, Any] | None = None, 24 | ) -> list[str]: 25 | with open("pyproject.toml", "rb") as fp: 26 | config = tomllib.load(fp) 27 | return config["project"].get("dependencies", []) 28 | 29 | 30 | get_requires_for_build_sdist = get_requires_for_build_wheel 31 | 32 | 33 | def get_requires_for_build_editable( 34 | config_settings: Mapping[str, Any] | None = None, 35 | ) -> list[str]: 36 | return get_requires_for_build_wheel(config_settings) + ["editables"] 37 | -------------------------------------------------------------------------------- /src/pdm/backend/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm-backend/6c339b6a2f8a9e454e9c240cca374aa0d9148ec1/src/pdm/backend/py.typed -------------------------------------------------------------------------------- /src/pdm/backend/sdist.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import tarfile 5 | from copy import copy 6 | from io import BytesIO 7 | from pathlib import Path 8 | from posixpath import join as pjoin 9 | from typing import Iterable 10 | 11 | from pdm.backend._vendor.packaging.utils import canonicalize_name 12 | from pdm.backend.base import Builder 13 | from pdm.backend.hooks import Context 14 | from pdm.backend.utils import normalize_file_permissions, safe_version, to_filename 15 | 16 | 17 | def clean_tarinfo(tar_info: tarfile.TarInfo) -> tarfile.TarInfo: 18 | """ 19 | Clean metadata from a TarInfo object to make it more reproducible. 20 | 21 | - Set uid & gid to 0 22 | - Set uname and gname to "" 23 | - Normalise permissions to 644 or 755 24 | - Set mtime if not None 25 | """ 26 | ti = copy(tar_info) 27 | ti.uid = 0 28 | ti.gid = 0 29 | ti.uname = "" 30 | ti.gname = "" 31 | ti.mode = normalize_file_permissions(ti.mode) 32 | 33 | if "SOURCE_DATE_EPOCH" in os.environ: 34 | ti.mtime = int(os.environ["SOURCE_DATE_EPOCH"]) 35 | 36 | return ti 37 | 38 | 39 | class SdistBuilder(Builder): 40 | """This build should be performed for PDM project only.""" 41 | 42 | target = "sdist" 43 | 44 | def get_files(self, context: Context) -> Iterable[tuple[str, Path]]: 45 | collected = dict(super().get_files(context)) 46 | context.ensure_build_dir() 47 | context.config.write_to(context.build_dir / "pyproject.toml") 48 | collected["pyproject.toml"] = context.build_dir / "pyproject.toml" 49 | metadata = self.config.validate() 50 | 51 | def gen_additional_files() -> Iterable[str]: 52 | if local_hook := self.config.build_config.custom_hook: 53 | yield local_hook 54 | if metadata.readme and metadata.readme.file: 55 | yield metadata.readme.file.relative_to(self.location).as_posix() 56 | yield from self.find_license_files(metadata) 57 | 58 | root = self.location 59 | for file in gen_additional_files(): 60 | if file in collected: 61 | continue 62 | if root.joinpath(file).exists(): 63 | collected[file] = root / file 64 | return collected.items() 65 | 66 | def build_artifact( 67 | self, context: Context, files: Iterable[tuple[str, Path]] 68 | ) -> Path: 69 | version = to_filename(safe_version(context.config.metadata["version"])) 70 | name = to_filename(canonicalize_name(context.config.metadata["name"])) 71 | dist_info = f"{name}-{version}" 72 | 73 | target = context.dist_dir / f"{dist_info}.tar.gz" 74 | 75 | with tarfile.open(target, mode="w:gz", format=tarfile.PAX_FORMAT) as tar: 76 | for relpath, path in files: 77 | tar_info = tar.gettarinfo(path, pjoin(dist_info, relpath)) 78 | tar_info = clean_tarinfo(tar_info) 79 | if tar_info.isreg(): 80 | with path.open("rb") as f: 81 | tar.addfile(tar_info, f) 82 | else: 83 | tar.addfile(tar_info) 84 | self._show_add_file(relpath, path) 85 | 86 | pkg_info = str(self.config.validate().as_rfc822()).encode("utf-8") 87 | tar_info = tarfile.TarInfo(pjoin(dist_info, "PKG-INFO")) 88 | tar_info.size = len(pkg_info) 89 | tar_info = clean_tarinfo(tar_info) 90 | tar.addfile(tar_info, BytesIO(pkg_info)) 91 | self._show_add_file("PKG-INFO", Path("PKG-INFO")) 92 | 93 | return target 94 | -------------------------------------------------------------------------------- /src/pdm/backend/structures.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | from pathlib import Path 5 | from typing import Any, Iterator, MutableMapping 6 | 7 | 8 | class Table(MutableMapping[str, Any]): 9 | def __init__(self, data: dict[str, Any]) -> None: 10 | self.__data = data 11 | 12 | def __len__(self) -> int: 13 | return len(self.__data) 14 | 15 | def __iter__(self) -> Iterator[str]: 16 | return iter(self.__data) 17 | 18 | def __getitem__(self, __key: str) -> Any: 19 | return self.__data[__key] 20 | 21 | def __setitem__(self, __key: str, __value: Any) -> None: 22 | self.__data[__key] = __value 23 | 24 | def __delitem__(self, __key: str) -> None: 25 | del self.__data[__key] 26 | 27 | 28 | class FileMap(MutableMapping[str, Path]): 29 | def __init__(self) -> None: 30 | self.__data: dict[str, Path] = {} 31 | 32 | def __normalize_path(self, path: str) -> str: 33 | path = os.path.normpath(path) 34 | if os.sep == "\\": 35 | path = path.replace("\\", "/") 36 | return path 37 | 38 | def __len__(self) -> int: 39 | return len(self.__data) 40 | 41 | def __iter__(self) -> Iterator[str]: 42 | return iter(self.__data) 43 | 44 | def __getitem__(self, __key: str) -> Path: 45 | return self.__data[self.__normalize_path(__key)] 46 | 47 | def __setitem__(self, __key: str, __value: Path) -> None: 48 | self.__data[self.__normalize_path(__key)] = __value 49 | 50 | def __delitem__(self, __key: str) -> None: 51 | del self.__data[__key] 52 | -------------------------------------------------------------------------------- /src/pdm/backend/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | import contextlib 5 | import functools 6 | import importlib.util 7 | import os 8 | import re 9 | import sys 10 | import types 11 | import urllib.parse 12 | from fnmatch import fnmatchcase 13 | from pathlib import Path 14 | from typing import Any, Callable, Generator, Iterable, Match 15 | 16 | from pdm.backend._vendor.packaging.markers import Marker 17 | from pdm.backend._vendor.packaging.requirements import Requirement 18 | from pdm.backend._vendor.packaging.version import InvalidVersion, Version 19 | from pdm.backend.exceptions import ConfigError 20 | 21 | 22 | def safe_version(version: str) -> str: 23 | """ 24 | Convert an arbitrary string to a standard version string 25 | """ 26 | try: 27 | # normalize the version 28 | return str(Version(version)) 29 | except InvalidVersion: 30 | version = version.replace(" ", ".") 31 | return re.sub("[^A-Za-z0-9.]+", "-", version) 32 | 33 | 34 | def to_filename(name: str) -> str: 35 | """Convert a project or version name to its filename-escaped form 36 | 37 | Any '-' characters are currently replaced with '_'. 38 | """ 39 | return name.replace("-", "_") 40 | 41 | 42 | def is_python_package(fullpath: str) -> bool: 43 | if not os.path.isdir(fullpath): 44 | return False 45 | if os.path.basename(fullpath.rstrip("/")) in ("__pycache__", "__pypackages__"): 46 | return False 47 | if os.path.isfile(os.path.join(fullpath, "__init__.py")): 48 | return True 49 | # stubs 50 | return os.path.basename(fullpath).endswith("-stubs") and os.path.isfile( 51 | os.path.join(fullpath, "__init__.pyi") 52 | ) 53 | 54 | 55 | def merge_marker(requirement: Requirement, marker: str) -> None: 56 | """Merge the target marker str with the requirement markers""" 57 | if not requirement.marker: 58 | requirement.marker = Marker(marker) 59 | return 60 | old_marker = requirement.marker 61 | if "or" in old_marker._markers: 62 | new_marker = Marker(f"({old_marker}) and {marker}") 63 | else: 64 | new_marker = Marker(f"{old_marker} and {marker}") 65 | requirement.marker = new_marker 66 | 67 | 68 | def find_packages_iter( 69 | where: str = ".", 70 | exclude: Iterable[str] = (), 71 | include: Iterable[str] = ("*",), 72 | src: str = ".", 73 | ) -> Iterable[str]: 74 | """ 75 | All the packages found in 'where' that pass the 'include' filter, but 76 | not the 'exclude' filter. 77 | """ 78 | 79 | def _build_filter(patterns: Iterable[str]) -> Callable[[str], bool]: 80 | """ 81 | Given a list of patterns, return a callable that will be true only if 82 | the input matches at least one of the patterns. 83 | """ 84 | return lambda name: any(fnmatchcase(name, pat=pat) for pat in patterns) 85 | 86 | fexclude, finclude = _build_filter(exclude), _build_filter(include) 87 | for root, dirs, _files in os.walk(where, followlinks=True): 88 | # Copy dirs to iterate over it, then empty dirs. 89 | all_dirs = dirs[:] 90 | dirs[:] = [] 91 | 92 | for dir in all_dirs: 93 | full_path = os.path.join(root, dir) 94 | rel_path = os.path.relpath(full_path, src) 95 | package = rel_path.replace(os.path.sep, ".") 96 | # Skip directory trees that are not valid packages 97 | if "." in dir: 98 | continue 99 | 100 | # Should this package be included? 101 | if ( 102 | os.path.isfile(os.path.join(full_path, "__init__.py")) 103 | and finclude(package) 104 | and not fexclude(package) 105 | ): 106 | yield package 107 | 108 | # Keep searching subdirectories, as there may be more packages 109 | # down there, even if the parent was excluded. 110 | dirs.append(dir) 111 | 112 | 113 | def normalize_path(filename: str | Path) -> str: 114 | """Normalize a file/dir name for comparison purposes""" 115 | filename = os.path.abspath(filename) if sys.platform == "cygwin" else filename 116 | return os.path.normcase(os.path.realpath(os.path.normpath(filename))) 117 | 118 | 119 | def is_relative_path(target: Path, other: Path) -> bool: 120 | try: 121 | target.relative_to(other) 122 | except ValueError: 123 | return False 124 | else: 125 | return True 126 | 127 | 128 | def expand_vars(line: str, root: str) -> str: 129 | """Expand environment variables in a string.""" 130 | if "$" not in line: 131 | return line 132 | 133 | if "://" in line: 134 | quote: Callable[[str], str] = urllib.parse.quote 135 | else: 136 | quote = str 137 | line = line.replace("file:///${PROJECT_ROOT}", Path(root).as_uri()) 138 | 139 | def replace_func(match: Match[str]) -> str: 140 | rv = os.getenv(match.group(1)) 141 | if rv is None: 142 | return match.group(0) 143 | return quote(rv) 144 | 145 | return re.sub(r"\$\{(.+?)\}", replace_func, line) 146 | 147 | 148 | def import_module_at_path( 149 | src_path: str | Path, module_name: str = "_local", context: Path | None = None 150 | ) -> types.ModuleType: 151 | """Import a module from a given path.""" 152 | spec = importlib.util.spec_from_file_location(module_name, src_path) 153 | if spec is None: 154 | raise ValueError(f"Could not import module {module_name} from {src_path}") 155 | if context is not None: 156 | sys.path.insert(0, str(context.absolute())) 157 | module = importlib.util.module_from_spec(spec) 158 | spec.loader.exec_module(module) # type: ignore 159 | if context is not None: 160 | sys.path.pop(0) 161 | return module 162 | 163 | 164 | def normalize_file_permissions(st_mode: int) -> int: 165 | """ 166 | Normalizes the permission bits in the st_mode field from stat to 644/755 167 | Popular VCSs only track whether a file is executable or not. The exact 168 | permissions can vary on systems with different umasks. Normalising 169 | to 644 (non executable) or 755 (executable) makes builds more reproducible. 170 | """ 171 | # Set 644 permissions, leaving higher bits of st_mode unchanged 172 | new_mode = (st_mode | 0o644) & ~0o133 173 | if st_mode & 0o100: 174 | new_mode |= 0o111 # Executable: 644 -> 755 175 | 176 | return new_mode 177 | 178 | 179 | @contextlib.contextmanager 180 | def patch_sys_path(path: str | Path) -> Generator[None]: 181 | old_path = sys.path[:] 182 | sys.path.insert(0, str(path)) 183 | try: 184 | yield 185 | finally: 186 | sys.path[:] = old_path 187 | 188 | 189 | _attr_regex = re.compile(r"([\w.]+):([\w.]+)\s*(\([^)]+\))?") 190 | 191 | 192 | def evaluate_module_attribute( 193 | expression: str, context: Path | None = None 194 | ) -> tuple[Any, tuple[Any, ...]]: 195 | """Evaluate the value of an expression like ':' 196 | 197 | Returns: 198 | the object and the calling arguments if any 199 | """ 200 | if context is None: 201 | cm = contextlib.nullcontext() 202 | else: 203 | cm = patch_sys_path(context) # type: ignore[assignment] 204 | 205 | matched = _attr_regex.match(expression) 206 | if matched is None: 207 | raise ConfigError( 208 | "Invalid expression, must be in the format of `module:attribute`." 209 | ) 210 | with cm: 211 | module = importlib.import_module(matched.group(1)) 212 | attrs = matched.group(2).split(".") 213 | obj: Any = functools.reduce(getattr, attrs, module) 214 | args_group = matched.group(3) 215 | if args_group: 216 | # make tuple 217 | args_group = args_group.strip()[:-1] + ",)" 218 | args = ast.literal_eval(args_group) 219 | 220 | else: 221 | args = () 222 | return obj, args 223 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | FIXTURES = Path(__file__).parent / "fixtures" 4 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import subprocess 3 | from pathlib import Path 4 | 5 | import pytest 6 | 7 | from tests import FIXTURES 8 | 9 | 10 | @pytest.fixture 11 | def fixture_project(tmp_path: Path, name: str, monkeypatch: pytest.MonkeyPatch) -> Path: 12 | project = FIXTURES / "projects" / name 13 | shutil.copytree(project, tmp_path / name) 14 | monkeypatch.chdir(tmp_path / name) 15 | return tmp_path / name 16 | 17 | 18 | @pytest.fixture 19 | def dist(tmp_path: Path) -> Path: 20 | return tmp_path / "dist" 21 | 22 | 23 | @pytest.fixture 24 | def scm(fixture_project: Path) -> None: 25 | subprocess.check_call(["git", "init"]) 26 | subprocess.check_call(["git", "add", "."]) 27 | subprocess.check_call(["git", "commit", "-m", "initial commit"]) 28 | subprocess.check_call(["git", "tag", "-a", "0.1.0", "-m", "version 0.1.0"]) 29 | -------------------------------------------------------------------------------- /tests/fixtures/hooks/hook_class.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from pathlib import Path 5 | from typing import Any 6 | 7 | from pdm.backend.hooks.base import Context 8 | 9 | logger = logging.getLogger("hooks") 10 | 11 | 12 | class BuildHook: 13 | def __init__(self, name: str = "2") -> None: 14 | self.name = name 15 | 16 | def pdm_build_clean(self, context: Context) -> None: 17 | logger.info("Hook%s build clean called", self.name) 18 | 19 | def pdm_build_initialize(self, context: Context) -> None: 20 | logger.info("Hook%s build initialize called", self.name) 21 | 22 | def pdm_build_update_files(self, context: Context, files: dict[str, Path]) -> None: 23 | logger.info("Hook%s build update files called", self.name) 24 | 25 | def pdm_build_finalize(self, context: Context, artifact: Path) -> None: 26 | logger.info("Hook%s build finalize called", self.name) 27 | 28 | 29 | # Hook instance 30 | hook3 = BuildHook("3") 31 | -------------------------------------------------------------------------------- /tests/fixtures/hooks/hook_module.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from pathlib import Path 5 | 6 | from pdm.backend.hooks.base import Context 7 | 8 | logger = logging.getLogger("hooks") 9 | 10 | 11 | def pdm_build_clean(context: Context) -> None: 12 | logger.info("Hook1 build clean called") 13 | 14 | 15 | def pdm_build_initialize(context: Context) -> None: 16 | logger.info("Hook1 build initialize called") 17 | 18 | 19 | def pdm_build_update_files(context: Context, files: dict[str, Path]) -> None: 20 | logger.info("Hook1 build update files called") 21 | 22 | 23 | def pdm_build_finalize(context: Context, artifact: Path) -> None: 24 | logger.info("Hook1 build finalize called") 25 | -------------------------------------------------------------------------------- /tests/fixtures/hooks/local_hook.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from pathlib import Path 5 | 6 | from pdm.backend.hooks.base import Context 7 | 8 | logger = logging.getLogger("hooks") 9 | 10 | 11 | def pdm_build_clean(context: Context) -> None: 12 | logger.info("Hook4 build clean called") 13 | 14 | 15 | def pdm_build_initialize(context: Context) -> None: 16 | logger.info("Hook4 build initialize called") 17 | 18 | 19 | def pdm_build_update_files(context: Context, files: dict[str, Path]) -> None: 20 | logger.info("Hook4 build update files called") 21 | 22 | 23 | def pdm_build_finalize(context: Context, artifact: Path) -> None: 24 | logger.info("Hook4 build finalize called") 25 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-cextension-in-src/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-cextension-in-src/pdm.lock: -------------------------------------------------------------------------------- 1 | 2 | [metadata] 3 | lock_version = "2" 4 | content_hash = "sha256:8c4af1482660b6fba86228da00d17cedd7e023605b6e7a2b78668ad60e205a25" 5 | 6 | [metadata.files] 7 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-cextension-in-src/pdm_build.py: -------------------------------------------------------------------------------- 1 | # build.py 2 | from setuptools import Extension 3 | 4 | ext_modules = [Extension("my_package.hello", ["src/my_package/hellomodule.c"])] 5 | 6 | 7 | def pdm_build_update_setup_kwargs(context, setup_kwargs): 8 | setup_kwargs.update(ext_modules=ext_modules) 9 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-cextension-in-src/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pdm-backend"] 3 | build-backend = "pdm.backend" 4 | 5 | [project] 6 | authors = [ 7 | {name = "frostming", email = "mianghong@gmail.com"}, 8 | ] 9 | dynamic = ["version"] 10 | requires-python = ">=3.5" 11 | license = { text = "MIT" } 12 | dependencies = [] 13 | description = "" 14 | name = "demo-package" 15 | 16 | [project.optional-dependencies] 17 | 18 | [tool.pdm] 19 | version = {source = "file", path = "src/my_package/__init__.py" } 20 | 21 | [tool.pdm.build] 22 | source-includes = ["**/*.c"] 23 | run-setuptools = true 24 | 25 | [[tool.pdm.source]] 26 | url = "https://test.pypi.org/simple" 27 | verify_ssl = true 28 | name = "testpypi" 29 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-cextension-in-src/src/my_package/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-cextension-in-src/src/my_package/hellomodule.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | static PyObject* helloworld(PyObject* self, PyObject* args) 4 | { 5 | printf("Hello World\n"); 6 | return Py_None; 7 | } 8 | 9 | static PyMethodDef myMethods[] = { 10 | { "helloworld", helloworld, METH_NOARGS, "Prints Hello World" }, 11 | { NULL, NULL, 0, NULL } 12 | }; 13 | 14 | static struct PyModuleDef hellomodule = { 15 | PyModuleDef_HEAD_INIT, 16 | "hello", 17 | "Test Module", 18 | -1, 19 | myMethods 20 | }; 21 | 22 | PyMODINIT_FUNC PyInit_hello(void) 23 | { 24 | return PyModule_Create(&hellomodule); 25 | } 26 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-cextension/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-cextension/my_package/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-cextension/my_package/hellomodule.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | static PyObject* helloworld(PyObject* self, PyObject* args) 4 | { 5 | printf("Hello World\n"); 6 | return Py_None; 7 | } 8 | 9 | static PyMethodDef myMethods[] = { 10 | { "helloworld", helloworld, METH_NOARGS, "Prints Hello World" }, 11 | { NULL, NULL, 0, NULL } 12 | }; 13 | 14 | static struct PyModuleDef hellomodule = { 15 | PyModuleDef_HEAD_INIT, 16 | "hello", 17 | "Test Module", 18 | -1, 19 | myMethods 20 | }; 21 | 22 | PyMODINIT_FUNC PyInit_hello(void) 23 | { 24 | return PyModule_Create(&hellomodule); 25 | } 26 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-cextension/pdm.lock: -------------------------------------------------------------------------------- 1 | 2 | [metadata] 3 | lock_version = "2" 4 | content_hash = "sha256:8c4af1482660b6fba86228da00d17cedd7e023605b6e7a2b78668ad60e205a25" 5 | 6 | [metadata.files] 7 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-cextension/pdm_build.py: -------------------------------------------------------------------------------- 1 | # build.py 2 | from setuptools import Extension 3 | 4 | ext_modules = [Extension("my_package.hello", ["my_package/hellomodule.c"])] 5 | 6 | 7 | def pdm_build_update_setup_kwargs(context, setup_kwargs): 8 | setup_kwargs.update(ext_modules=ext_modules) 9 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-cextension/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pdm-backend"] 3 | build-backend = "pdm.backend" 4 | 5 | [project] 6 | authors = [ 7 | {name = "frostming", email = "mianghong@gmail.com"}, 8 | ] 9 | dynamic = ["version"] 10 | requires-python = ">=3.5" 11 | license = { text = "MIT" } 12 | dependencies = [] 13 | description = "" 14 | name = "demo-package" 15 | 16 | [project.optional-dependencies] 17 | 18 | [tool.pdm] 19 | version = {source = "file", path = "my_package/__init__.py" } 20 | 21 | [tool.pdm.build] 22 | source-includes = ["**/*.c"] 23 | run-setuptools = true 24 | 25 | [[tool.pdm.source]] 26 | url = "https://test.pypi.org/simple" 27 | verify_ssl = true 28 | name = "testpypi" 29 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-combined-extras/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-combined-extras/demo.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | print(os.name) 4 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-combined-extras/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pdm-backend"] 3 | build-backend = "pdm.backend" 4 | 5 | [project] 6 | authors = [{name = "frostming", email = "mianghong@gmail.com"}] 7 | description = "" 8 | license = "MIT" 9 | name = "demo-package-extra" 10 | requires-python = ">=3.5" 11 | version = "0.1.0" 12 | 13 | dependencies = ["urllib3"] 14 | 15 | [project.optional-dependencies] 16 | be = ["idna"] 17 | te = ["chardet"] 18 | all = ["idna", "chardet"] 19 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-explicit-package-dir/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-explicit-package-dir/README.md: -------------------------------------------------------------------------------- 1 | # This is a demo module 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-explicit-package-dir/data_out.json: -------------------------------------------------------------------------------- 1 | {"name": "foo"} 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-explicit-package-dir/foo/my_package/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-explicit-package-dir/foo/my_package/data.json: -------------------------------------------------------------------------------- 1 | {"name": "demo-module"} 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-explicit-package-dir/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pdm-backend"] 3 | build-backend = "pdm.backend" 4 | 5 | [project] 6 | authors = [ 7 | {name = "frostming", email = "mianghong@gmail.com"}, 8 | ] 9 | dynamic = ["version"] 10 | requires-python = ">=3.5" 11 | license = "MIT" 12 | dependencies = [] 13 | description = "" 14 | name = "demo-package" 15 | 16 | [project.optional-dependencies] 17 | 18 | [tool.pdm] 19 | version = {source = "file", path = "foo/my_package/__init__.py" } 20 | 21 | [tool.pdm.build] 22 | package-dir = "foo" 23 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-explicit-package-dir/single_module.py: -------------------------------------------------------------------------------- 1 | print("hello world") 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-licenses/LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm-backend/6c339b6a2f8a9e454e9c240cca374aa0d9148ec1/tests/fixtures/projects/demo-licenses/LICENSE -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-licenses/README.md: -------------------------------------------------------------------------------- 1 | # This is a demo module 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-licenses/licenses/LICENSE.APACHE.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm-backend/6c339b6a2f8a9e454e9c240cca374aa0d9148ec1/tests/fixtures/projects/demo-licenses/licenses/LICENSE.APACHE.md -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-licenses/licenses/LICENSE.MIT.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm-backend/6c339b6a2f8a9e454e9c240cca374aa0d9148ec1/tests/fixtures/projects/demo-licenses/licenses/LICENSE.MIT.md -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-licenses/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pdm-backend"] 3 | build-backend = "pdm.backend" 4 | 5 | [project] 6 | authors = [ 7 | {name = "frostming", email = "mianghong@gmail.com"}, 8 | ] 9 | dynamic = ["version"] 10 | requires-python = ">=3.5" 11 | license-files = ["LICENSE", "licenses/LICENSE*"] 12 | dependencies = [] 13 | description = "" 14 | name = "demo-module" 15 | readme = "README.md" 16 | 17 | [project.optional-dependencies] 18 | 19 | [tool.pdm] 20 | version = {source = "file", path = "src/foo_module.py" } 21 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-licenses/src/foo_module.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | foo = "hello" 3 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-metadata-test/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-metadata-test/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm-backend/6c339b6a2f8a9e454e9c240cca374aa0d9148ec1/tests/fixtures/projects/demo-metadata-test/README.md -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-metadata-test/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pdm-backend"] 3 | build-backend = "pdm.backend" 4 | 5 | [project] 6 | authors = [ 7 | {name = "Corporation, Inc.", email = "corporation@example.com"}, 8 | {name = "Example", email = "example@example.com"}, 9 | ] 10 | name = "demo-metadata-test" 11 | requires-python = ">=3.9" 12 | license = "MIT" 13 | dependencies = [] 14 | description = "" 15 | readme = "README.md" 16 | version = "3.2.1" 17 | 18 | [project.optional-dependencies] 19 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-module/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-module/README.md: -------------------------------------------------------------------------------- 1 | # This is a demo module 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-module/bar_module.py: -------------------------------------------------------------------------------- 1 | bar = "Hello" 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-module/foo_module.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" # don't edit this line 2 | foo = "hello" 3 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-module/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pdm-backend"] 3 | build-backend = "pdm.backend" 4 | 5 | [project] 6 | authors = [ 7 | {name = "frostming", email = "mianghong@gmail.com"}, 8 | ] 9 | dynamic = ["version"] 10 | requires-python = ">=3.5" 11 | license = "MIT" 12 | dependencies = [] 13 | description = "" 14 | name = "demo-module" 15 | readme = "README.md" 16 | 17 | [project.optional-dependencies] 18 | 19 | [tool.pdm] 20 | version = {source = "file", path = "foo_module.py" } 21 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-no-version/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm-backend/6c339b6a2f8a9e454e9c240cca374aa0d9148ec1/tests/fixtures/projects/demo-no-version/README.md -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-no-version/anothername.toml: -------------------------------------------------------------------------------- 1 | pyproject.toml -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-no-version/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pdm-backend"] 3 | build-backend = "pdm.backend" 4 | 5 | [project] 6 | authors = [ 7 | {name = "frostming", email = "mianghong@gmail.com"}, 8 | ] 9 | name = "demo" 10 | requires-python = ">=3.5" 11 | license = "MIT" 12 | dependencies = [] 13 | description = "" 14 | readme = "README.md" 15 | dynamic = ["version"] 16 | 17 | [project.optional-dependencies] 18 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-include-error/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-include-error/README.md: -------------------------------------------------------------------------------- 1 | # This is a demo module 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-include-error/data_out.json: -------------------------------------------------------------------------------- 1 | {"name": "foo"} 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-include-error/my_package/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-include-error/my_package/data.json: -------------------------------------------------------------------------------- 1 | {"name": "demo-module"} 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-include-error/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pdm-backend"] 3 | build-backend = "pdm.backend" 4 | 5 | [project] 6 | authors = [ 7 | {name = "frostming", email = "mianghong@gmail.com"}, 8 | ] 9 | dynamic = ["version"] 10 | requires-python = ">=3.5" 11 | license = "MIT" 12 | dependencies = ["flask"] 13 | description = "" 14 | name = "demo-package" 15 | readme = "README.md" 16 | 17 | [project.optional-dependencies] 18 | 19 | [tool.pdm] 20 | version = {source = "file", path = "my_package/__init__.py" } 21 | 22 | [tool.pdm.build] 23 | includes = [ 24 | "my_package/", 25 | "single_module.py", 26 | "data_out.json", 27 | ] 28 | excludes = [ 29 | "my_package/*.json" 30 | ] 31 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-include-error/requirements.txt: -------------------------------------------------------------------------------- 1 | click==7.1.2; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" \ 2 | --hash=sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc \ 3 | --hash=sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a 4 | flask==1.1.2; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" \ 5 | --hash=sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557 \ 6 | --hash=sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060 7 | itsdangerous==1.1.0; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" \ 8 | --hash=sha256:72a1252c0b2cc2bcc351acf2cfe2ec0159d8578c54767d5c2aa67fd869346e55 \ 9 | --hash=sha256:ac4c9f590d59c36b7d2953f97fda415f2461280e5279650aafe1e593f129c4f7 10 | Jinja2==2.11.2; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" \ 11 | --hash=sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035 \ 12 | --hash=sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0 13 | MarkupSafe==1.1.1; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" \ 14 | --hash=sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161 \ 15 | --hash=sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7 \ 16 | --hash=sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183 \ 17 | --hash=sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b \ 18 | --hash=sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e \ 19 | --hash=sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f \ 20 | --hash=sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1 \ 21 | --hash=sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5 \ 22 | --hash=sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1 \ 23 | --hash=sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735 \ 24 | --hash=sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21 \ 25 | --hash=sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235 \ 26 | --hash=sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b \ 27 | --hash=sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f \ 28 | --hash=sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905 \ 29 | --hash=sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1 \ 30 | --hash=sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d \ 31 | --hash=sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff \ 32 | --hash=sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473 \ 33 | --hash=sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e \ 34 | --hash=sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66 \ 35 | --hash=sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5 \ 36 | --hash=sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d \ 37 | --hash=sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e \ 38 | --hash=sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6 \ 39 | --hash=sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2 \ 40 | --hash=sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c \ 41 | --hash=sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15 \ 42 | --hash=sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2 \ 43 | --hash=sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42 \ 44 | --hash=sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b \ 45 | --hash=sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be \ 46 | --hash=sha256:30323461f382a59bcb940be0adc5e0d6be5dfc6bd1c2c5cbe2d13b96414e1619 47 | Werkzeug==1.0.1; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" \ 48 | --hash=sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43 \ 49 | --hash=sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c 50 | --extra-index-url https://test.pypi.org/simple 51 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-include-error/requirements_simple.txt: -------------------------------------------------------------------------------- 1 | click==7.1.2; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" 2 | flask==1.1.2; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" 3 | itsdangerous==1.1.0; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" 4 | Jinja2==2.11.2; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" 5 | MarkupSafe==1.1.1; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" 6 | Werkzeug==1.0.1; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" 7 | --extra-index-url https://test.pypi.org/simple 8 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-include-error/single_module.py: -------------------------------------------------------------------------------- 1 | print("hello world") 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-include/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-include/README.md: -------------------------------------------------------------------------------- 1 | # This is a demo module 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-include/data_out.json: -------------------------------------------------------------------------------- 1 | {"name": "foo"} 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-include/my_package/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-include/my_package/data.json: -------------------------------------------------------------------------------- 1 | {"name": "demo-module"} 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-include/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pdm-backend"] 3 | build-backend = "pdm.backend" 4 | 5 | [project] 6 | authors = [ 7 | {name = "frostming", email = "mianghong@gmail.com"}, 8 | ] 9 | dynamic = ["version"] 10 | requires-python = ">=3.5" 11 | license = "MIT" 12 | dependencies = ["flask"] 13 | description = "" 14 | name = "demo-package" 15 | readme = "README.md" 16 | 17 | [project.optional-dependencies] 18 | 19 | [tool.pdm] 20 | version = {source = "file", path = "my_package/__init__.py" } 21 | 22 | [tool.pdm.build] 23 | includes = [ 24 | "my_package/", 25 | "requirements.txt", 26 | "data_out.json", 27 | ] 28 | excludes = [ 29 | "my_package/*.json" 30 | ] 31 | source-includes = ["scripts/"] 32 | 33 | [tool.pdm.build.wheel-data] 34 | scripts = ["scripts/**/*"] 35 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-include/requirements.txt: -------------------------------------------------------------------------------- 1 | click==7.1.2; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" \ 2 | --hash=sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc \ 3 | --hash=sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a 4 | flask==1.1.2; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" \ 5 | --hash=sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557 \ 6 | --hash=sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060 7 | itsdangerous==1.1.0; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" \ 8 | --hash=sha256:72a1252c0b2cc2bcc351acf2cfe2ec0159d8578c54767d5c2aa67fd869346e55 \ 9 | --hash=sha256:ac4c9f590d59c36b7d2953f97fda415f2461280e5279650aafe1e593f129c4f7 10 | Jinja2==2.11.2; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" \ 11 | --hash=sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035 \ 12 | --hash=sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0 13 | MarkupSafe==1.1.1; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" \ 14 | --hash=sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161 \ 15 | --hash=sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7 \ 16 | --hash=sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183 \ 17 | --hash=sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b \ 18 | --hash=sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e \ 19 | --hash=sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f \ 20 | --hash=sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1 \ 21 | --hash=sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5 \ 22 | --hash=sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1 \ 23 | --hash=sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735 \ 24 | --hash=sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21 \ 25 | --hash=sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235 \ 26 | --hash=sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b \ 27 | --hash=sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f \ 28 | --hash=sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905 \ 29 | --hash=sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1 \ 30 | --hash=sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d \ 31 | --hash=sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff \ 32 | --hash=sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473 \ 33 | --hash=sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e \ 34 | --hash=sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66 \ 35 | --hash=sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5 \ 36 | --hash=sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d \ 37 | --hash=sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e \ 38 | --hash=sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6 \ 39 | --hash=sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2 \ 40 | --hash=sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c \ 41 | --hash=sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15 \ 42 | --hash=sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2 \ 43 | --hash=sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42 \ 44 | --hash=sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b \ 45 | --hash=sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be \ 46 | --hash=sha256:30323461f382a59bcb940be0adc5e0d6be5dfc6bd1c2c5cbe2d13b96414e1619 47 | Werkzeug==1.0.1; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" \ 48 | --hash=sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43 \ 49 | --hash=sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c 50 | --extra-index-url https://test.pypi.org/simple 51 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-include/requirements_simple.txt: -------------------------------------------------------------------------------- 1 | click==7.1.2; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" 2 | flask==1.1.2; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" 3 | itsdangerous==1.1.0; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" 4 | Jinja2==2.11.2; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" 5 | MarkupSafe==1.1.1; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" 6 | Werkzeug==1.0.1; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" 7 | --extra-index-url https://test.pypi.org/simple 8 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-include/scripts/data/my_script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Hello world" 4 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-include/single_module.py: -------------------------------------------------------------------------------- 1 | print("hello world") 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-stubs/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-stubs/README.md: -------------------------------------------------------------------------------- 1 | # This is a demo module for a stub package 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-stubs/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pdm-backend"] 3 | build-backend = "pdm.backend" 4 | 5 | [project] 6 | authors = [ 7 | {name = "frostming", email = "mianghong@gmail.com"}, 8 | ] 9 | version = '0.0.0' 10 | requires-python = ">=2.7" 11 | license = "MIT" 12 | dependencies = ["flask"] 13 | description = "" 14 | name = "demo-package-stubs" 15 | readme = "README.md" 16 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-stubs/src/my_package-stubs/__init__.pyi: -------------------------------------------------------------------------------- 1 | def hello_world() -> None: ... 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-stubs/src/my_package-stubs/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm-backend/6c339b6a2f8a9e454e9c240cca374aa0d9148ec1/tests/fixtures/projects/demo-package-stubs/src/my_package-stubs/py.typed -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-with-deep-path/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-with-deep-path/README.md: -------------------------------------------------------------------------------- 1 | # This is a demo module 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-with-deep-path/my_package/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-with-deep-path/my_package/data/data_a.json: -------------------------------------------------------------------------------- 1 | {"name": "demo-module"} 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-with-deep-path/my_package/data/data_inner/data_b.json: -------------------------------------------------------------------------------- 1 | {"name": "foo"} 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-with-deep-path/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pdm-backend"] 3 | build-backend = "pdm.backend" 4 | 5 | [project] 6 | authors = [ 7 | {name = "frostming", email = "mianghong@gmail.com"}, 8 | ] 9 | dynamic = ["version"] 10 | requires-python = ">=3.5" 11 | license = "MIT" 12 | dependencies = ["flask"] 13 | description = "" 14 | name = "demo-package" 15 | readme = "README.md" 16 | 17 | [project.optional-dependencies] 18 | 19 | [tool.pdm] 20 | version = {source = "file", path = "my_package/__init__.py" } 21 | 22 | [tool.pdm.build] 23 | source-includes = ["my_package/**/*.json"] 24 | 25 | [[tool.pdm.source]] 26 | url = "https://test.pypi.org/simple" 27 | verify_ssl = true 28 | name = "testpypi" 29 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-with-tests/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-with-tests/README.md: -------------------------------------------------------------------------------- 1 | # This is a demo module 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-with-tests/my_package/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-with-tests/my_package/data.json: -------------------------------------------------------------------------------- 1 | {"name": "demo-module"} 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-with-tests/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pdm-backend"] 3 | build-backend = "pdm.backend" 4 | 5 | [project] 6 | authors = [ 7 | {name = "frostming", email = "mianghong@gmail.com"}, 8 | ] 9 | dynamic = ["version"] 10 | requires-python = ">=3.5" 11 | license = "MIT" 12 | dependencies = ["flask"] 13 | description = "" 14 | name = "demo-package" 15 | readme = "README.md" 16 | 17 | [project.optional-dependencies] 18 | 19 | [tool.pdm] 20 | version = {source = "file", path = "my_package/__init__.py" } 21 | 22 | [[tool.pdm.source]] 23 | url = "https://test.pypi.org/simple" 24 | verify_ssl = true 25 | name = "testpypi" 26 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-with-tests/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm-backend/6c339b6a2f8a9e454e9c240cca374aa0d9148ec1/tests/fixtures/projects/demo-package-with-tests/tests/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package/README.md: -------------------------------------------------------------------------------- 1 | # This is a demo module 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package/data_out.json: -------------------------------------------------------------------------------- 1 | {"name": "foo"} 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package/my_package/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package/my_package/data.json: -------------------------------------------------------------------------------- 1 | {"name": "demo-module"} 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package/my_package/executable: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm-backend/6c339b6a2f8a9e454e9c240cca374aa0d9148ec1/tests/fixtures/projects/demo-package/my_package/executable -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package/pdm.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "click" 3 | sections = ["default"] 4 | version = "6.7" 5 | summary = "A simple wrapper around optparse for powerful command line utilities." 6 | 7 | [[package]] 8 | name = "flask" 9 | sections = ["default"] 10 | version = "1.0.3" 11 | summary = "A simple framework for building complex web applications." 12 | dependencies = [ 13 | "Werkzeug>=0.14", 14 | "Jinja2>=2.10", 15 | "itsdangerous>=0.24", 16 | "click>=5.1", 17 | ] 18 | 19 | [[package]] 20 | name = "itsdangerous" 21 | sections = ["default"] 22 | version = "0.24" 23 | summary = "Various helpers to pass trusted data to untrusted environments and back." 24 | 25 | [[package]] 26 | name = "Jinja2" 27 | sections = ["default"] 28 | version = "2.10.3" 29 | summary = "A very fast and expressive template engine." 30 | dependencies = [ 31 | "MarkupSafe>=0.23", 32 | ] 33 | 34 | [[package]] 35 | name = "MarkupSafe" 36 | sections = ["default"] 37 | version = "1.0" 38 | summary = "UNKNOWN" 39 | 40 | [[package]] 41 | name = "Werkzeug" 42 | sections = ["default"] 43 | version = "0.14.1" 44 | summary = "The comprehensive WSGI web application library." 45 | 46 | [metadata] 47 | lock_version = "2" 48 | content_hash = "sha256:09889bcdf876ae1bc905934125e58d5328b16ca3e758f87d8785b490d43d3dad" 49 | 50 | [metadata.files] 51 | "click 6.7" = [ 52 | {file = "click-6.7-py2.py3-none-any.whl", hash = "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d"}, 53 | {file = "click-6.7.tar.gz", hash = "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"}, 54 | ] 55 | "flask 1.0.3" = [ 56 | {file = "Flask-1.0.3-py2.py3-none-any.whl", hash = "sha256:e7d32475d1de5facaa55e3958bc4ec66d3762076b074296aa50ef8fdc5b9df61"}, 57 | {file = "Flask-1.0.3.tar.gz", hash = "sha256:ad7c6d841e64296b962296c2c2dabc6543752985727af86a975072dea984b6f3"}, 58 | ] 59 | "itsdangerous 0.24" = [ 60 | {file = "itsdangerous-0.24.tar.gz", hash = "sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"}, 61 | ] 62 | "jinja2 2.10.3" = [ 63 | {file = "Jinja2-2.10.3-py2.py3-none-any.whl", hash = "sha256:f72f23bf65cbdbb2d69ce857a97cdba3c9de069a929fcbadb8d738a1db5a94d1"}, 64 | {file = "Jinja2-2.10.3.tar.gz", hash = "sha256:7a4ac21fce4afedbbc6ea2de5de4f312df670350708104bd7562aaa452baef1f"}, 65 | ] 66 | "markupsafe 1.0" = [ 67 | {file = "MarkupSafe-1.0.tar.gz", hash = "sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"}, 68 | ] 69 | "werkzeug 0.14.1" = [ 70 | {file = "Werkzeug-0.14.1-py2.py3-none-any.whl", hash = "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"}, 71 | {file = "Werkzeug-0.14.1.tar.gz", hash = "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c"}, 72 | ] 73 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pdm-backend"] 3 | build-backend = "pdm.backend" 4 | 5 | [project] 6 | authors = [ 7 | {name = "frostming", email = "mianghong@gmail.com"}, 8 | ] 9 | dynamic = ["version"] 10 | requires-python = ">=2.7" 11 | license = "MIT" 12 | dependencies = ["flask"] 13 | description = "" 14 | name = "demo-package" 15 | readme = "README.md" 16 | 17 | [project.optional-dependencies] 18 | 19 | [tool.pdm] 20 | version = {source = "file", path = "my_package/__init__.py" } 21 | 22 | [tool.pdm.build] 23 | source-includes = ["data_out.json"] 24 | editable-backend = "editables" 25 | 26 | [[tool.pdm.source]] 27 | url = "https://test.pypi.org/simple" 28 | verify_ssl = true 29 | name = "testpypi" 30 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package/requirements.txt: -------------------------------------------------------------------------------- 1 | click==7.1.2; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" \ 2 | --hash=sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc \ 3 | --hash=sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a 4 | flask==1.1.2; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" \ 5 | --hash=sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557 \ 6 | --hash=sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060 7 | itsdangerous==1.1.0; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" \ 8 | --hash=sha256:72a1252c0b2cc2bcc351acf2cfe2ec0159d8578c54767d5c2aa67fd869346e55 \ 9 | --hash=sha256:ac4c9f590d59c36b7d2953f97fda415f2461280e5279650aafe1e593f129c4f7 10 | Jinja2==2.11.2; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" \ 11 | --hash=sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035 \ 12 | --hash=sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0 13 | MarkupSafe==1.1.1; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" \ 14 | --hash=sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161 \ 15 | --hash=sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7 \ 16 | --hash=sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183 \ 17 | --hash=sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b \ 18 | --hash=sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e \ 19 | --hash=sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f \ 20 | --hash=sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1 \ 21 | --hash=sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5 \ 22 | --hash=sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1 \ 23 | --hash=sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735 \ 24 | --hash=sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21 \ 25 | --hash=sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235 \ 26 | --hash=sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b \ 27 | --hash=sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f \ 28 | --hash=sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905 \ 29 | --hash=sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1 \ 30 | --hash=sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d \ 31 | --hash=sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff \ 32 | --hash=sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473 \ 33 | --hash=sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e \ 34 | --hash=sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66 \ 35 | --hash=sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5 \ 36 | --hash=sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d \ 37 | --hash=sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e \ 38 | --hash=sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6 \ 39 | --hash=sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2 \ 40 | --hash=sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c \ 41 | --hash=sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15 \ 42 | --hash=sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2 \ 43 | --hash=sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42 \ 44 | --hash=sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b \ 45 | --hash=sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be \ 46 | --hash=sha256:30323461f382a59bcb940be0adc5e0d6be5dfc6bd1c2c5cbe2d13b96414e1619 47 | Werkzeug==1.0.1; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" \ 48 | --hash=sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43 \ 49 | --hash=sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c 50 | --extra-index-url https://test.pypi.org/simple 51 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package/requirements_simple.txt: -------------------------------------------------------------------------------- 1 | click==7.1.2; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" 2 | flask==1.1.2; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" 3 | itsdangerous==1.1.0; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" 4 | Jinja2==2.11.2; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" 5 | MarkupSafe==1.1.1; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" 6 | Werkzeug==1.0.1; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4" 7 | --extra-index-url https://test.pypi.org/simple 8 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package/single_module.py: -------------------------------------------------------------------------------- 1 | print("hello world") 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-pep420-package/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-pep420-package/README.md: -------------------------------------------------------------------------------- 1 | # This is a demo module 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-pep420-package/foo/my_package/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-pep420-package/foo/my_package/data.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-pep420-package/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pdm-backend"] 3 | build-backend = "pdm.backend" 4 | 5 | [project] 6 | authors = [ 7 | {name = "frostming", email = "mianghong@gmail.com"}, 8 | ] 9 | dynamic = ["version"] 10 | requires-python = ">=3.5" 11 | license = "MIT" 12 | dependencies = [] 13 | description = "" 14 | name = "demo-package" 15 | 16 | [project.optional-dependencies] 17 | 18 | [tool.pdm] 19 | version = {source = "file", path = "foo/my_package/__init__.py" } 20 | 21 | [tool.pdm.build] 22 | includes = ["foo/"] 23 | editable-backend = "editables" 24 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-purelib-with-build/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-purelib-with-build/my_build.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def pdm_build_initialize(context): 5 | context.ensure_build_dir() 6 | os.makedirs(os.path.join(context.build_dir, "my_package")) 7 | with open(os.path.join(context.build_dir, "my_package/version.txt"), "w") as f: 8 | f.write(context.config.metadata["version"]) 9 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-purelib-with-build/my_package/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-purelib-with-build/pdm.lock: -------------------------------------------------------------------------------- 1 | 2 | [metadata] 3 | lock_version = "2" 4 | content_hash = "sha256:8c4af1482660b6fba86228da00d17cedd7e023605b6e7a2b78668ad60e205a25" 5 | 6 | [metadata.files] 7 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-purelib-with-build/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pdm-backend"] 3 | build-backend = "pdm.backend" 4 | 5 | [project] 6 | authors = [ 7 | {name = "frostming", email = "mianghong@gmail.com"}, 8 | ] 9 | dynamic = ["version"] 10 | requires-python = ">=3.5" 11 | license = "MIT" 12 | dependencies = [] 13 | description = "" 14 | name = "demo-package" 15 | 16 | [project.optional-dependencies] 17 | 18 | [tool.pdm] 19 | version = {source = "file", path = "my_package/__init__.py" } 20 | 21 | [tool.pdm.build] 22 | custom-hook = "my_build.py" 23 | is-purelib = true 24 | 25 | [[tool.pdm.source]] 26 | url = "https://test.pypi.org/simple" 27 | verify_ssl = true 28 | name = "testpypi" 29 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-reuse-spec/LICENSES/MPL-2.0.txt: -------------------------------------------------------------------------------- 1 | This is the license text. 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-reuse-spec/README.md: -------------------------------------------------------------------------------- 1 | # This is a demo module 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-reuse-spec/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pdm-backend"] 3 | build-backend = "pdm.backend" 4 | 5 | [project] 6 | authors = [ 7 | {name = "CharString", email = "chris.wesseling@pm.me"}, 8 | ] 9 | dynamic = ["version"] 10 | requires-python = ">=3.5" 11 | license = "MPL-2.0" 12 | dependencies = [] 13 | description = "" 14 | name = "demo-module" 15 | readme = "README.md" 16 | 17 | [project.optional-dependencies] 18 | 19 | [tool.pdm] 20 | version = {source = "file", path = "src/foo_module.py" } 21 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-reuse-spec/src/foo_module.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | foo = "hello" 3 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-src-package-include/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-src-package-include/README.md: -------------------------------------------------------------------------------- 1 | # This is a demo module 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-src-package-include/data_out.json: -------------------------------------------------------------------------------- 1 | {"name": "foo"} 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-src-package-include/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pdm-backend"] 3 | build-backend = "pdm.backend" 4 | 5 | [project] 6 | authors = [ 7 | {name = "frostming", email = "mianghong@gmail.com"}, 8 | ] 9 | dynamic = ["version"] 10 | requires-python = ">=3.5" 11 | license = "MIT" 12 | dependencies = [] 13 | description = "" 14 | name = "demo-package" 15 | 16 | [project.optional-dependencies] 17 | 18 | [tool.pdm] 19 | version = {source = "file", path = "sub/my_package/__init__.py" } 20 | 21 | [tool.pdm.build] 22 | includes = ["sub", "data_out.json"] 23 | package-dir = "sub" 24 | editable-backend = "editables" 25 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-src-package-include/single_module.py: -------------------------------------------------------------------------------- 1 | print("hello world") 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-src-package-include/sub/my_package/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-src-package-include/sub/my_package/data.json: -------------------------------------------------------------------------------- 1 | {"name": "demo-module"} 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-src-package/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-src-package/README.md: -------------------------------------------------------------------------------- 1 | # This is a demo module 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-src-package/data_out.json: -------------------------------------------------------------------------------- 1 | {"name": "foo"} 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-src-package/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pdm-backend"] 3 | build-backend = "pdm.backend" 4 | 5 | [project] 6 | authors = [ 7 | {name = "frostming", email = "mianghong@gmail.com"}, 8 | ] 9 | dynamic = ["version"] 10 | requires-python = ">=3.5" 11 | license = "MIT" 12 | dependencies = [] 13 | description = "" 14 | name = "demo-package" 15 | 16 | [project.optional-dependencies] 17 | 18 | [tool.pdm] 19 | version = {source = "file", path = "src/my_package/__init__.py" } 20 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-src-package/single_module.py: -------------------------------------------------------------------------------- 1 | print("hello world") 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-src-package/src/my_package/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-src-package/src/my_package/data.json: -------------------------------------------------------------------------------- 1 | {"name": "demo-module"} 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-src-pymodule/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-src-pymodule/README.md: -------------------------------------------------------------------------------- 1 | # This is a demo module 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-src-pymodule/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pdm-backend"] 3 | build-backend = "pdm.backend" 4 | 5 | [project] 6 | authors = [ 7 | {name = "frostming", email = "mianghong@gmail.com"}, 8 | ] 9 | dynamic = ["version"] 10 | requires-python = ">=3.5" 11 | license = "MIT" 12 | dependencies = [] 13 | description = "" 14 | name = "demo-module" 15 | readme = "README.md" 16 | 17 | [project.optional-dependencies] 18 | 19 | [tool.pdm] 20 | version = {source = "file", path = "src/foo_module.py" } 21 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-src-pymodule/src/foo_module.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | foo = "hello" 3 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-using-scm/.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | __pycache__/ 3 | build/ 4 | dist/ 5 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-using-scm/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-using-scm/README.md: -------------------------------------------------------------------------------- 1 | # This is a demo module 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-using-scm/foo/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | foo = "hello" 3 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-using-scm/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pdm-backend"] 3 | build-backend = "pdm.backend" 4 | 5 | [project] 6 | authors = [ 7 | {name = "frostming", email = "mianghong@gmail.com"}, 8 | ] 9 | dynamic = ["version"] 10 | requires-python = ">=3.5" 11 | license = "MIT" 12 | dependencies = [] 13 | description = "" 14 | name = "foo" 15 | readme = "README.md" 16 | 17 | [project.optional-dependencies] 18 | 19 | [tool.pdm] 20 | version = {source = "scm" } 21 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-using-scm/version.py: -------------------------------------------------------------------------------- 1 | from pdm.backend.hooks.version import SCMVersion 2 | 3 | 4 | def format_version(version: SCMVersion) -> str: 5 | return f"{version.version}rc{version.distance or 0}" 6 | -------------------------------------------------------------------------------- /tests/test_file_finder.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | import pytest 6 | 7 | from pdm.backend.base import Builder, is_same_or_descendant_path 8 | from pdm.backend.exceptions import ValidationError 9 | from pdm.backend.sdist import SdistBuilder 10 | from pdm.backend.wheel import WheelBuilder 11 | from tests import FIXTURES 12 | 13 | 14 | @pytest.mark.parametrize("builder_cls", (WheelBuilder, SdistBuilder)) 15 | def test_auto_include_tests_for_sdist( 16 | builder_cls: type[Builder], tmp_path: Path 17 | ) -> None: 18 | with builder_cls(FIXTURES / "projects/demo-package-with-tests") as builder: 19 | context = builder.build_context(tmp_path) 20 | builder.clean(context) 21 | builder.initialize(context) 22 | files = dict(builder.get_files(context)) 23 | 24 | sdist_only_files = ("tests/__init__.py", "pyproject.toml") 25 | include_files = ("my_package/__init__.py",) 26 | for file in include_files: 27 | assert file in files 28 | 29 | for file in sdist_only_files: 30 | if isinstance(builder, SdistBuilder): 31 | assert file in files 32 | else: 33 | assert file not in files 34 | 35 | 36 | @pytest.mark.parametrize( 37 | "target,path,expect", 38 | [ 39 | ("a/b", "a", True), 40 | ("a/b/c", "a/b/c", True), 41 | ("b/c", "a", False), 42 | ("a", "a/b", False), 43 | ("a", "b/c", False), 44 | ], 45 | ) 46 | def test_is_same_or_descendant_path(target, path, expect) -> None: 47 | assert is_same_or_descendant_path(target, path) == expect 48 | 49 | 50 | @pytest.mark.parametrize("builder_cls", (WheelBuilder, SdistBuilder)) 51 | def test_recursive_glob_patterns_in_includes( 52 | builder_cls: type[Builder], tmp_path: Path 53 | ) -> None: 54 | with builder_cls(FIXTURES / "projects/demo-package-with-deep-path") as builder: 55 | context = builder.build_context(tmp_path) 56 | builder.clean(context) 57 | builder.initialize(context) 58 | files = dict(builder.get_files(context)) 59 | 60 | data_files = ( 61 | "my_package/data/data_a.json", 62 | "my_package/data/data_inner/data_b.json", 63 | ) 64 | 65 | assert "my_package/__init__.py" in files 66 | 67 | for file in data_files: 68 | if isinstance(builder, WheelBuilder): 69 | assert file not in files 70 | else: 71 | assert file in files 72 | 73 | 74 | @pytest.mark.parametrize( 75 | ["includes", "excludes", "data_a_exist", "data_b_exist"], 76 | [ 77 | (["**/*.json"], ["my_package/data/*.json"], False, True), 78 | (["my_package/data/data_a.json"], ["my_package/data/*.json"], True, False), 79 | ( 80 | ["my_package/", "my_package/data/data_a.json"], 81 | ["my_package/data/data_a.json"], 82 | False, 83 | True, 84 | ), 85 | (["my_package/data/*"], ["my_package/data/"], True, False), 86 | (["**/data/*.json"], ["my_package/data/*.json"], False, False), 87 | ], 88 | ) 89 | def test_merge_includes_and_excludes( 90 | includes, excludes, data_a_exist: bool, data_b_exist: bool, tmp_path: Path 91 | ) -> None: 92 | builder = WheelBuilder(FIXTURES / "projects/demo-package-with-deep-path") 93 | data_a = "my_package/data/data_a.json" 94 | data_b = "my_package/data/data_inner/data_b.json" 95 | 96 | with builder: 97 | context = builder.build_context(tmp_path) 98 | builder.clean(context) 99 | builder.initialize(context) 100 | builder.config.build_config["includes"] = includes 101 | builder.config.build_config["excludes"] = excludes 102 | builder.config.build_config["source-includes"] = [] 103 | include_files = dict(builder.get_files(context)) 104 | assert (data_a in include_files) == data_a_exist 105 | assert (data_b in include_files) == data_b_exist 106 | 107 | 108 | def test_license_file_matching() -> None: 109 | builder = WheelBuilder(FIXTURES / "projects/demo-licenses") 110 | builder.config.metadata["license-files"] = ["LICENSE"] 111 | with builder: 112 | license_files = builder.find_license_files(builder.config.validate()) 113 | assert license_files == ["LICENSE"] 114 | 115 | 116 | def test_license_file_glob_matching() -> None: 117 | builder = WheelBuilder(FIXTURES / "projects/demo-licenses") 118 | with builder: 119 | license_files = sorted(builder.find_license_files(builder.config.validate())) 120 | assert license_files == [ 121 | "LICENSE", 122 | "licenses/LICENSE.APACHE.md", 123 | "licenses/LICENSE.MIT.md", 124 | ] 125 | 126 | 127 | def test_default_license_files() -> None: 128 | builder = WheelBuilder(FIXTURES / "projects/demo-licenses") 129 | del builder.config.metadata["license-files"] 130 | with builder: 131 | license_files = builder.find_license_files(builder.config.validate()) 132 | assert license_files == ["LICENSE"] 133 | 134 | 135 | def test_license_file_paths_no_matching() -> None: 136 | builder = WheelBuilder(FIXTURES / "projects/demo-licenses") 137 | builder.config.metadata["license-files"] = ["LICENSE.md"] 138 | with pytest.raises(ValidationError, match=".*must match at least one file"): 139 | builder.config.validate() 140 | 141 | 142 | def test_license_file_explicit_empty() -> None: 143 | builder = WheelBuilder(FIXTURES / "projects/demo-licenses") 144 | builder.config.metadata["license-files"] = [] 145 | with builder: 146 | license_files = list(builder.find_license_files(builder.config.validate())) 147 | assert not license_files 148 | 149 | 150 | def test_collect_build_files_with_src_layout(tmp_path) -> None: 151 | builder = WheelBuilder(FIXTURES / "projects/demo-src-package") 152 | with builder: 153 | context = builder.build_context(tmp_path) 154 | builder.clean(context) 155 | builder.initialize(context) 156 | build_dir = context.ensure_build_dir() 157 | (build_dir / "my_package").mkdir() 158 | (build_dir / "my_package" / "hello.py").write_text("print('hello')\n") 159 | files = dict(builder.get_files(context)) 160 | assert "my_package/hello.py" in files 161 | -------------------------------------------------------------------------------- /tests/test_hooks.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | from unittest.mock import Mock 3 | 4 | import pytest 5 | 6 | from pdm.backend.wheel import WheelBuilder 7 | from tests import FIXTURES 8 | from tests.fixtures.hooks import hook_class, hook_module 9 | 10 | 11 | def make_entry_point(hook): 12 | ep = Mock() 13 | ep.load.return_value = hook 14 | return ep 15 | 16 | 17 | @pytest.fixture() 18 | def project_with_hooks(monkeypatch, tmp_path): 19 | hooks = [ 20 | make_entry_point(hook_module), 21 | make_entry_point(hook_class.hook3), 22 | make_entry_point(hook_class.BuildHook), 23 | ] 24 | monkeypatch.setattr("pdm.backend.base.entry_points", Mock(return_value=hooks)) 25 | project = FIXTURES / "projects/demo-purelib-with-build" 26 | shutil.copytree(project, tmp_path / project.name) 27 | shutil.copy2( 28 | FIXTURES / "hooks/local_hook.py", tmp_path / project.name / "my_build.py" 29 | ) 30 | return tmp_path / project.name 31 | 32 | 33 | def test_load_hooks(project_with_hooks, caplog: pytest.LogCaptureFixture): 34 | builder = WheelBuilder(project_with_hooks) 35 | hooks = builder._hooks 36 | assert len(hooks) == 6 37 | assert hooks[:4] == WheelBuilder.hooks + [hook_module, hook_class.hook3] 38 | assert isinstance(hooks[4], hook_class.BuildHook) 39 | 40 | caplog.set_level("INFO", logger="hooks") 41 | with builder: 42 | builder.build(str(project_with_hooks / "dist")) 43 | 44 | messages = [record.message for record in caplog.records if record.name == "hooks"] 45 | for num in range(1, 5): 46 | assert f"Hook{num} build clean called" in messages 47 | assert f"Hook{num} build initialize called" in messages 48 | assert f"Hook{num} build update files called" in messages 49 | assert f"Hook{num} build finalize called" in messages 50 | -------------------------------------------------------------------------------- /tests/test_metadata.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pdm.backend.config import Config 4 | from tests import FIXTURES 5 | 6 | 7 | def test_parse_module(monkeypatch: pytest.MonkeyPatch) -> None: 8 | metadata = Config.from_pyproject(FIXTURES / "projects/demo-module") 9 | monkeypatch.chdir(metadata.root) 10 | paths = metadata.convert_package_paths() 11 | assert sorted(paths["py_modules"]) == ["bar_module", "foo_module"] 12 | assert paths["packages"] == [] 13 | assert paths["package_dir"] == {} 14 | 15 | 16 | def test_parse_package(monkeypatch: pytest.MonkeyPatch) -> None: 17 | metadata = Config.from_pyproject(FIXTURES / "projects/demo-package-include") 18 | monkeypatch.chdir(metadata.root) 19 | paths = metadata.convert_package_paths() 20 | assert paths["py_modules"] == [] 21 | assert paths["packages"] == ["my_package"] 22 | assert paths["package_dir"] == {} 23 | assert paths["package_data"] == {"": ["*"]} 24 | 25 | 26 | def test_parse_error_package(monkeypatch: pytest.MonkeyPatch) -> None: 27 | metadata = Config.from_pyproject(FIXTURES / "projects/demo-package-include-error") 28 | monkeypatch.chdir(metadata.root) 29 | with pytest.raises(ValueError): 30 | metadata.convert_package_paths() 31 | 32 | 33 | def test_parse_src_package(monkeypatch: pytest.MonkeyPatch) -> None: 34 | metadata = Config.from_pyproject(FIXTURES / "projects/demo-src-package") 35 | monkeypatch.chdir(metadata.root) 36 | paths = metadata.convert_package_paths() 37 | assert paths["packages"] == ["my_package"] 38 | assert paths["py_modules"] == [] 39 | assert paths["package_dir"] == {"": "src"} 40 | 41 | 42 | def test_parse_pep420_namespace_package(monkeypatch: pytest.MonkeyPatch) -> None: 43 | metadata = Config.from_pyproject(FIXTURES / "projects/demo-pep420-package") 44 | monkeypatch.chdir(metadata.root) 45 | paths = metadata.convert_package_paths() 46 | assert paths["package_dir"] == {} 47 | assert paths["packages"] == ["foo.my_package"] 48 | assert paths["py_modules"] == [] 49 | 50 | 51 | def test_explicit_package_dir(monkeypatch: pytest.MonkeyPatch) -> None: 52 | metadata = Config.from_pyproject(FIXTURES / "projects/demo-explicit-package-dir") 53 | monkeypatch.chdir(metadata.root) 54 | paths = metadata.convert_package_paths() 55 | assert paths["packages"] == ["my_package"] 56 | assert paths["py_modules"] == [] 57 | assert paths["package_dir"] == {"": "foo"} 58 | 59 | 60 | def test_implicit_namespace_package(monkeypatch: pytest.MonkeyPatch) -> None: 61 | metadata = Config.from_pyproject(FIXTURES / "projects/demo-pep420-package") 62 | monkeypatch.chdir(metadata.root) 63 | paths = metadata.convert_package_paths() 64 | assert paths["packages"] == ["foo.my_package"] 65 | assert paths["py_modules"] == [] 66 | assert paths["package_dir"] == {} 67 | 68 | 69 | def test_src_dir_containing_modules(monkeypatch: pytest.MonkeyPatch) -> None: 70 | metadata = Config.from_pyproject(FIXTURES / "projects/demo-src-pymodule") 71 | monkeypatch.chdir(metadata.root) 72 | paths = metadata.convert_package_paths() 73 | assert paths["package_dir"] == {"": "src"} 74 | assert not paths["packages"] 75 | assert paths["py_modules"] == ["foo_module"] 76 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from pdm.backend.utils import expand_vars 6 | 7 | is_nt = os.name == "nt" 8 | 9 | 10 | @pytest.mark.skipif(is_nt, reason="Posix path") 11 | def test_expand_vars_posix(monkeypatch): 12 | monkeypatch.setenv("FOO", "foo=a") 13 | monkeypatch.setenv("BAR", "bar") 14 | root = "/abc/def" 15 | 16 | line = "file:///${PROJECT_ROOT}/${FOO}:${BAR}:${BAZ}" 17 | assert expand_vars(line, root) == "file:///abc/def/foo%3Da:bar:${BAZ}" 18 | 19 | 20 | @pytest.mark.skipif(not is_nt, reason="Windows path") 21 | def test_expand_vars_win(monkeypatch): 22 | monkeypatch.setenv("FOO", "foo=a") 23 | monkeypatch.setenv("BAR", "bar") 24 | root = "C:/abc/def" 25 | 26 | line = "file:///${PROJECT_ROOT}/${FOO}:${BAR}:${BAZ}" 27 | assert expand_vars(line, root) == "file:///C:/abc/def/foo%3Da:bar:${BAZ}" 28 | -------------------------------------------------------------------------------- /tests/test_wheel.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | 4 | import pytest 5 | 6 | from pdm.backend import wheel 7 | from tests import FIXTURES 8 | 9 | 10 | @pytest.mark.skipif(hasattr(sys, "pypy_version_info"), reason="invalid on PyPy3") 11 | @pytest.mark.parametrize( 12 | "python_tag, py_limited_api, plat_name, tag", 13 | [ 14 | ("cp36", "abi3", "win_amd64", "cp36-abi3-win_amd64"), 15 | ("py3", "none", "win_amd64", "py3-none-win_amd64"), 16 | ], 17 | ) 18 | def test_override_tags_in_wheel_filename( 19 | python_tag: str, py_limited_api: str, plat_name: str, tag: str 20 | ) -> None: 21 | project = FIXTURES / "projects/demo-cextension" 22 | with wheel.WheelBuilder( 23 | project, 24 | config_settings={ 25 | "--python-tag": python_tag, 26 | "--py-limited-api": py_limited_api, 27 | "--plat-name": plat_name, 28 | }, 29 | ) as builder: 30 | assert builder.tag == tag 31 | 32 | 33 | def test_dist_info_name_with_no_version(tmp_path: Path) -> None: 34 | project = FIXTURES / "projects/demo-no-version" 35 | with wheel.WheelBuilder(project) as builder: 36 | builder.initialize(builder.build_context(tmp_path)) 37 | assert builder.dist_info_name == "demo-0.0.0.dist-info" 38 | assert builder.tag == "py3-none-any" 39 | -------------------------------------------------------------------------------- /tests/testutils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import tarfile 4 | import zipfile 5 | from pathlib import Path 6 | 7 | 8 | def get_tarball_names(path: Path) -> list[str]: 9 | with tarfile.open(path, "r:gz") as tar: 10 | return tar.getnames() 11 | 12 | 13 | def get_wheel_names(path: Path) -> list[str]: 14 | with zipfile.ZipFile(path) as zf: 15 | return zf.namelist() 16 | --------------------------------------------------------------------------------