├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .pre-commit-hooks.yaml ├── .readthedocs.yaml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── README_zh.md ├── SECURITY.md ├── chatbot ├── .gitignore ├── app.py ├── pdm.lock ├── pyproject.toml └── requirements.txt ├── codecov.yml ├── docs ├── assets │ ├── extra.css │ ├── extra.js │ ├── logo.svg │ └── logo_big.png ├── dev │ ├── benchmark.md │ ├── changelog.md │ ├── contributing.md │ ├── fixtures.md │ └── write.md ├── index.md ├── overrides │ └── main.html ├── reference │ ├── api.md │ ├── build.md │ ├── cli.md │ ├── configuration.md │ └── pep621.md └── usage │ ├── advanced.md │ ├── config.md │ ├── dependency.md │ ├── hooks.md │ ├── lock-targets.md │ ├── lockfile.md │ ├── pep582.md │ ├── project.md │ ├── publish.md │ ├── scripts.md │ ├── template.md │ ├── uv.md │ └── venv.md ├── install-pdm.py ├── install-pdm.py.sha256 ├── mkdocs.yml ├── news ├── .gitkeep ├── 3481.feature.md ├── 3485.bugfix.md ├── 3523.bugfix.md ├── 3531.bugfix.md └── 3539.bugfix.md ├── pdm.lock ├── pyproject.toml ├── src └── pdm │ ├── __init__.py │ ├── __main__.py │ ├── __version__.py │ ├── _types.py │ ├── builders │ ├── __init__.py │ ├── base.py │ ├── editable.py │ ├── sdist.py │ └── wheel.py │ ├── cli │ ├── __init__.py │ ├── actions.py │ ├── commands │ │ ├── __init__.py │ │ ├── add.py │ │ ├── base.py │ │ ├── build.py │ │ ├── cache.py │ │ ├── completion.py │ │ ├── config.py │ │ ├── export.py │ │ ├── fix │ │ │ ├── __init__.py │ │ │ └── fixers.py │ │ ├── import_cmd.py │ │ ├── info.py │ │ ├── init.py │ │ ├── install.py │ │ ├── list.py │ │ ├── lock.py │ │ ├── new.py │ │ ├── outdated.py │ │ ├── publish │ │ │ ├── __init__.py │ │ │ ├── package.py │ │ │ └── repository.py │ │ ├── python.py │ │ ├── remove.py │ │ ├── run.py │ │ ├── search.py │ │ ├── self_cmd.py │ │ ├── show.py │ │ ├── sync.py │ │ ├── update.py │ │ ├── use.py │ │ └── venv │ │ │ ├── __init__.py │ │ │ ├── activate.py │ │ │ ├── backends.py │ │ │ ├── create.py │ │ │ ├── list.py │ │ │ ├── purge.py │ │ │ ├── remove.py │ │ │ └── utils.py │ ├── completions │ │ ├── __init__.py │ │ ├── pdm.bash │ │ ├── pdm.fish │ │ ├── pdm.ps1 │ │ └── pdm.zsh │ ├── filters.py │ ├── hooks.py │ ├── options.py │ ├── templates │ │ ├── __init__.py │ │ ├── default │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── __init__.py │ │ │ ├── pyproject.toml │ │ │ ├── src │ │ │ │ └── example_package │ │ │ │ │ └── __init__.py │ │ │ └── tests │ │ │ │ └── __init__.py │ │ └── minimal │ │ │ ├── .gitignore │ │ │ ├── __init__.py │ │ │ └── pyproject.toml │ └── utils.py │ ├── compat.py │ ├── core.py │ ├── environments │ ├── __init__.py │ ├── base.py │ ├── local.py │ └── python.py │ ├── exceptions.py │ ├── formats │ ├── __init__.py │ ├── base.py │ ├── flit.py │ ├── pipfile.py │ ├── poetry.py │ ├── pylock.py │ ├── requirements.py │ ├── setup_py.py │ └── uv.py │ ├── installers │ ├── __init__.py │ ├── base.py │ ├── core.py │ ├── installers.py │ ├── manager.py │ ├── synchronizers.py │ ├── uninstallers.py │ └── uv.py │ ├── models │ ├── __init__.py │ ├── auth.py │ ├── backends.py │ ├── cached_package.py │ ├── caches.py │ ├── candidates.py │ ├── finder.py │ ├── in_process │ │ ├── __init__.py │ │ ├── env_spec.py │ │ ├── parse_setup.py │ │ └── sysconfig_get_paths.py │ ├── markers.py │ ├── project_info.py │ ├── python.py │ ├── python_max_versions.json │ ├── reporter.py │ ├── repositories │ │ ├── __init__.py │ │ ├── base.py │ │ ├── lock.py │ │ └── pypi.py │ ├── requirements.py │ ├── search.py │ ├── serializers.py │ ├── session.py │ ├── setup.py │ ├── specifiers.py │ ├── venv.py │ ├── versions.py │ └── working_set.py │ ├── pep582 │ ├── __init__.py │ └── sitecustomize.py │ ├── project │ ├── __init__.py │ ├── config.py │ ├── core.py │ ├── lockfile │ │ ├── __init__.py │ │ ├── base.py │ │ ├── pdmlock.py │ │ └── pylock.py │ ├── project_file.py │ └── toml_file.py │ ├── py.typed │ ├── pytest.py │ ├── resolver │ ├── __init__.py │ ├── base.py │ ├── graph.py │ ├── providers.py │ ├── python.py │ ├── reporters.py │ ├── resolvelib.py │ └── uv.py │ ├── signals.py │ ├── termui.py │ └── utils.py ├── tasks ├── complete.py ├── max_versions.py └── release.py ├── tests ├── __init__.py ├── cli │ ├── __init__.py │ ├── conftest.py │ ├── test_add.py │ ├── test_build.py │ ├── test_cache.py │ ├── test_config.py │ ├── test_fix.py │ ├── test_hooks.py │ ├── test_init.py │ ├── test_install.py │ ├── test_list.py │ ├── test_lock.py │ ├── test_others.py │ ├── test_outdated.py │ ├── test_publish.py │ ├── test_python.py │ ├── test_remove.py │ ├── test_run.py │ ├── test_self_command.py │ ├── test_template.py │ ├── test_update.py │ ├── test_use.py │ ├── test_utils.py │ └── test_venv.py ├── conftest.py ├── fixtures │ ├── Pipfile │ ├── __init__.py │ ├── artifacts │ │ ├── PyFunctional-1.4.3-py3-none-any.whl │ │ ├── caj2pdf-restructured-0.1.0a6.tar.gz │ │ ├── celery-4.4.2-py2.py3-none-any.whl │ │ ├── demo-0.0.1-cp36-cp36m-win_amd64.whl │ │ ├── demo-0.0.1-py2.py3-none-any.whl │ │ ├── demo-0.0.1.tar.gz │ │ ├── demo-0.0.1.zip │ │ ├── editables-0.2-py3-none-any.whl │ │ ├── first-2.0.2-py2.py3-none-any.whl │ │ ├── flit_core-3.6.0-py3-none-any.whl │ │ ├── future_fstrings-1.2.0-py2.py3-none-any.whl │ │ ├── future_fstrings-1.2.0.tar.gz │ │ ├── importlib_metadata-4.8.3-py3-none-any.whl │ │ ├── jmespath-0.10.0-py2.py3-none-any.whl │ │ ├── pdm_backend-2.1.4-py3-none-any.whl │ │ ├── pdm_hello-0.1.0-py3-none-any.whl │ │ ├── pdm_hello-0.1.0-py3-none-win_amd64.whl │ │ ├── pdm_pep517-1.0.0-py3-none-any.whl │ │ ├── poetry_core-1.3.2-py3-none-any.whl │ │ ├── setuptools-68.0.0-py3-none-any.whl │ │ ├── typing_extensions-4.4.0-py3-none-any.whl │ │ ├── wheel-0.37.1-py2.py3-none-any.whl │ │ ├── zipp-3.6.0-py3-none-any.whl │ │ └── zipp-3.7.0-py3-none-any.whl │ ├── constraints.txt │ ├── index │ │ ├── demo.html │ │ ├── future-fstrings.html │ │ ├── pep345-legacy.html │ │ └── wheel.html │ ├── json │ │ └── zipp.json │ ├── poetry-error.toml │ ├── poetry-new.toml │ ├── projects │ │ ├── __init__.py │ │ ├── demo-#-with-hash │ │ │ ├── demo.py │ │ │ └── setup.py │ │ ├── demo-combined-extras │ │ │ ├── demo.py │ │ │ └── pyproject.toml │ │ ├── demo-failure-no-dep │ │ │ ├── demo.py │ │ │ └── setup.py │ │ ├── demo-failure │ │ │ ├── demo.py │ │ │ └── setup.py │ │ ├── demo-module │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── bar_module.py │ │ │ ├── foo_module.py │ │ │ └── pyproject.toml │ │ ├── demo-package-has-dep-with-extras │ │ │ ├── pdm.lock │ │ │ ├── pyproject.toml │ │ │ └── requirements.txt │ │ ├── demo-package │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── data_out.json │ │ │ ├── my_package │ │ │ │ ├── __init__.py │ │ │ │ └── data.json │ │ │ ├── pdm.lock │ │ │ ├── pyproject.toml │ │ │ ├── requirements.ini │ │ │ ├── requirements.txt │ │ │ ├── requirements_simple.txt │ │ │ ├── setup.txt │ │ │ └── single_module.py │ │ ├── demo-parent-package │ │ │ ├── README.md │ │ │ ├── package-a │ │ │ │ ├── foo.py │ │ │ │ └── setup.py │ │ │ └── package-b │ │ │ │ ├── bar.py │ │ │ │ └── pyproject.toml │ │ ├── demo-prerelease │ │ │ ├── demo.py │ │ │ └── setup.py │ │ ├── demo-src-package │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── data_out.json │ │ │ ├── pyproject.toml │ │ │ ├── single_module.py │ │ │ └── src │ │ │ │ └── my_package │ │ │ │ ├── __init__.py │ │ │ │ └── data.json │ │ ├── demo │ │ │ ├── demo.py │ │ │ ├── pdm.lock │ │ │ ├── pdm.no_groups.lock │ │ │ ├── pylock.toml │ │ │ └── pyproject.toml │ │ ├── demo_extras │ │ │ ├── demo.py │ │ │ └── setup.py │ │ ├── flit-demo │ │ │ ├── README.rst │ │ │ ├── doc │ │ │ │ └── index.html │ │ │ ├── flit.py │ │ │ └── pyproject.toml │ │ ├── poetry-demo │ │ │ ├── mylib.py │ │ │ └── pyproject.toml │ │ ├── poetry-with-circular-dep │ │ │ ├── packages │ │ │ │ └── child │ │ │ │ │ ├── child │ │ │ │ │ └── __init__.py │ │ │ │ │ └── pyproject.toml │ │ │ ├── parent │ │ │ │ └── __init__.py │ │ │ └── pyproject.toml │ │ ├── test-hatch-static │ │ │ ├── README.md │ │ │ └── pyproject.toml │ │ ├── test-monorepo │ │ │ ├── README.md │ │ │ ├── core │ │ │ │ ├── core.py │ │ │ │ └── pyproject.toml │ │ │ ├── package_a │ │ │ │ ├── alice.py │ │ │ │ └── pyproject.toml │ │ │ ├── package_b │ │ │ │ ├── bob.py │ │ │ │ └── pyproject.toml │ │ │ └── pyproject.toml │ │ ├── test-package-type-fixer │ │ │ ├── pyproject.toml │ │ │ └── src │ │ │ │ └── test_package_type_fixer │ │ │ │ └── __init__.py │ │ ├── test-plugin-pdm │ │ │ ├── hello.py │ │ │ └── pyproject.toml │ │ ├── test-plugin │ │ │ ├── hello.py │ │ │ └── setup.py │ │ ├── test-removal │ │ │ ├── __init__.py │ │ │ ├── bar.py │ │ │ ├── foo.py │ │ │ └── subdir │ │ │ │ └── __init__.py │ │ └── test-setuptools │ │ │ ├── AUTHORS │ │ │ ├── README.md │ │ │ ├── mymodule.py │ │ │ ├── setup.cfg │ │ │ └── setup.py │ ├── pypi.json │ ├── pyproject.toml │ ├── requirements-include.txt │ └── requirements.txt ├── models │ ├── __init__.py │ ├── test_backends.py │ ├── test_candidates.py │ ├── test_marker.py │ ├── test_requirements.py │ ├── test_serializers.py │ ├── test_session.py │ ├── test_setup_parsing.py │ ├── test_specifiers.py │ └── test_versions.py ├── resolver │ ├── __init__.py │ ├── test_graph.py │ ├── test_resolve.py │ └── test_uv_resolver.py ├── test_formats.py ├── test_installer.py ├── test_integration.py ├── test_plugin.py ├── test_project.py ├── test_signals.py └── test_utils.py ├── tox.ini └── typings └── shellingham.pyi /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: "Bug report" 2 | description: Create a report to help us improve 3 | labels: ['🐛 bug'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: "Thank you for taking the time to report a bug. Please provide as much information as possible to help us understand and resolve the issue." 8 | - type: textarea 9 | id: describe-bug 10 | attributes: 11 | label: Describe the bug 12 | description: "A clear and concise description of what the bug is." 13 | placeholder: "Describe the bug..." 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: reproduce-bug 18 | attributes: 19 | label: To reproduce 20 | description: "Steps to reproduce the behavior." 21 | placeholder: "Steps to reproduce the behavior..." 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: expected-behavior 26 | attributes: 27 | label: Expected Behavior 28 | description: "A clear and concise description of what you expected to happen." 29 | placeholder: "Explain what you expected to happen..." 30 | validations: 31 | required: true 32 | - type: textarea 33 | id: "environment-info" 34 | attributes: 35 | label: Environment Information 36 | description: "Paste the output of `pdm info && pdm info --env`" 37 | placeholder: "Paste the output of `pdm info && pdm info --env`" 38 | validations: 39 | required: true 40 | - type: textarea 41 | id: "pdm-debug-output" 42 | attributes: 43 | label: "Verbose Command Output" 44 | description: "Please provide the command output with `-v`." 45 | placeholder: "Add the command output with `-v`..." 46 | validations: 47 | required: false 48 | - type: textarea 49 | id: additional-context 50 | attributes: 51 | label: Additional Context 52 | description: "Add any other context about the problem here." 53 | placeholder: "Additional details..." 54 | validations: 55 | required: false 56 | - type: checkboxes 57 | id: willing-to-submit-pr 58 | attributes: 59 | label: "Are you willing to submit a PR to fix this bug?" 60 | description: "Let us know if you are willing to contribute a fix by submitting a Pull Request." 61 | options: 62 | - label: "Yes, I would like to submit a PR." 63 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: "Feature / Enhancement Proposal" 2 | description: Suggest an idea for this project 3 | labels: ['⭐ enhancement'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: "Thank you for suggesting a new feature. Please fill out the details below to help us understand your idea better." 8 | 9 | - type: textarea 10 | id: feature-description 11 | attributes: 12 | label: Feature Description 13 | description: "A detailed description of the feature you would like to see." 14 | placeholder: "Describe the feature you'd like..." 15 | validations: 16 | required: true 17 | 18 | - type: textarea 19 | id: problem-solution 20 | attributes: 21 | label: Problem and Solution 22 | description: "Describe the problem that this feature would solve. Explain how you envision it working." 23 | placeholder: "What problem does this feature solve? How do you envision it working?" 24 | validations: 25 | required: true 26 | 27 | - type: textarea 28 | id: additional-context 29 | attributes: 30 | label: Additional Context 31 | description: "Add any other context or screenshots about the feature request here." 32 | placeholder: "Add any other context or screenshots about the feature request here." 33 | validations: 34 | required: false 35 | 36 | - type: checkboxes 37 | id: willing-to-contribute 38 | attributes: 39 | label: "Are you willing to contribute to the development of this feature?" 40 | description: "Let us know if you are willing to help by contributing code or other resources." 41 | options: 42 | - label: "Yes, I am willing to contribute to the development of this feature." 43 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Pull Request Checklist 2 | 3 | - [ ] A news fragment is added in `news/` describing what is new. 4 | - [ ] Test cases added for changed code. 5 | 6 | ## Describe what you have changed in this PR. 7 | -------------------------------------------------------------------------------- /.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.toml 135 | .pdm-python 136 | temp.py 137 | 138 | # Pyannotate generated stubs 139 | type_info.json 140 | .pdm-build/ 141 | src/pdm/VERSION 142 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | autoupdate_schedule: monthly 3 | repos: 4 | - repo: https://github.com/astral-sh/ruff-pre-commit 5 | rev: 'v0.11.12' 6 | hooks: 7 | - id: ruff 8 | args: [--fix, --exit-non-zero-on-fix, --show-fixes] 9 | - id: ruff-format 10 | 11 | - repo: https://github.com/codespell-project/codespell 12 | rev: v2.4.1 13 | hooks: 14 | - id: codespell # See pyproject.toml for args 15 | additional_dependencies: 16 | - tomli 17 | 18 | - repo: https://github.com/pre-commit/mirrors-mypy 19 | rev: v1.16.0 20 | hooks: 21 | - id: mypy 22 | args: [src] 23 | pass_filenames: false 24 | additional_dependencies: 25 | - types-requests 26 | - types-certifi 27 | - pytest 28 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: pdm-lock-check 2 | name: pdm-lock-check 3 | description: run pdm lock --check to validate config 4 | entry: pdm lock --check 5 | language: python 6 | language_version: python3 7 | pass_filenames: false 8 | files: ^pyproject.toml$ 9 | - id: pdm-export 10 | name: pdm-export-lock 11 | description: export locked packages to requirements.txt or setup.py 12 | entry: pdm export 13 | language: python 14 | language_version: python3 15 | pass_filenames: false 16 | files: ^pdm.lock$ 17 | - id: pdm-sync 18 | name: pdm-sync 19 | description: sync current working set with pdm.lock 20 | entry: pdm sync 21 | language: python 22 | language_version: python3 23 | pass_filenames: false 24 | stages: 25 | - post-checkout 26 | - post-merge 27 | - post-rewrite 28 | always_run: true 29 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for MkDocs projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the version of Python and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.12" 12 | jobs: 13 | post_create_environment: 14 | - python install-pdm.py --path ~/.local/pdm 15 | - ~/.local/pdm/bin/pdm --version 16 | post_install: 17 | - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH ~/.local/pdm/bin/pdm install -dG doc 18 | 19 | mkdocs: 20 | configuration: mkdocs.yml 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-present 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 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | Latest minor version | :white_check_mark: | 11 | | Otherwise | :x: | 12 | 13 | ## Reporting a Vulnerability 14 | 15 | If you discover a potential security vulnerability, we kindly request that you refrain from sharing the information publicly and report it to us directly. 16 | Please send an email to me@frostming.com with the following details: 17 | 18 | - Description of the potential vulnerability. 19 | - Steps to reproduce the issue (if applicable). 20 | - Any relevant screenshots or logs. 21 | - Your contact information for further communication. 22 | 23 | Alternatively, you can [open a security advisory](https://github.com/pdm-project/pdm/security/advisories/new) on GitHub. 24 | 25 | ## Response Time 26 | 27 | Upon receiving your report, the maintainers will acknowledge receipt of your vulnerability report within 2 business days. 28 | We will then review the reported issue and strive to keep you informed about our progress towards resolving it. 29 | You can expect an update from us at least every 5 days until the issue is resolved. 30 | 31 | ## Vulnerability Validation 32 | 33 | The maintainers will assess the reported vulnerability and validate its existence. This process may involve a request for additional information from you. 34 | If the vulnerability is confirmed, we will classify it based on its severity and potential impact. 35 | 36 | If your reported vulnerability is validated and leads to a change in our systems, we will acknowledge your contribution in any public disclosure, unless you request anonymity. 37 | Otherwise, if the reported issue is not accepted as a vulnerability, we will provide a detailed explanation as to why we believe it does not pose a risk to our systems or users. 38 | We value all reports and encourage you to continue to report any potential vulnerabilities you may find in the future. 39 | -------------------------------------------------------------------------------- /chatbot/.gitignore: -------------------------------------------------------------------------------- 1 | .streamlit/ 2 | -------------------------------------------------------------------------------- /chatbot/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import streamlit as st 4 | from llama_index.core import Settings, SimpleDirectoryReader, VectorStoreIndex 5 | from llama_index.embeddings.azure_openai import AzureOpenAIEmbedding 6 | from llama_index.llms.azure_openai import AzureOpenAI 7 | 8 | st.set_page_config( 9 | page_title="Chat with the PDM docs", 10 | page_icon="📝", 11 | layout="centered", 12 | initial_sidebar_state="auto", 13 | menu_items=None, 14 | ) 15 | st.title("Chat with the PDM docs 💬🦙") 16 | st.info( 17 | "PDM - A modern Python package and dependency manager. " 18 | "Check out the full documentation at [PDM docs](https://pdm-project.org).", 19 | icon="📃", 20 | ) 21 | Settings.llm = AzureOpenAI( 22 | api_key=st.secrets.get("aoai_key"), 23 | azure_endpoint=st.secrets.get("aoai_endpoint"), 24 | engine="gpt-4o-mini", 25 | api_version="2024-02-15-preview", 26 | temperature=0.5, 27 | system_prompt="You are an expert on PDM and your job is to answer technical questions. " 28 | "Assume that all questions are related to PDM. Keep your answers technical and based on facts - do not hallucinate features.", 29 | ) 30 | Settings.embed_model = AzureOpenAIEmbedding( 31 | azure_deployment="embedding", 32 | api_key=st.secrets.get("aoai_key"), 33 | api_version="2023-05-15", 34 | azure_endpoint=st.secrets.get("aoai_endpoint"), 35 | ) 36 | 37 | DATA_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "docs/") 38 | 39 | if "messages" not in st.session_state.keys(): # Initialize the chat messages history 40 | st.session_state.messages = [ 41 | { 42 | "role": "assistant", 43 | "content": "Ask me a question about PDM!", 44 | } 45 | ] 46 | 47 | 48 | @st.cache_resource(show_spinner=False) 49 | def load_data(): 50 | with st.spinner(text="Loading and indexing the PDM docs - hang tight! This should take 1-2 minutes."): 51 | reader = SimpleDirectoryReader(input_dir=DATA_PATH, recursive=True, required_exts=[".md"]) 52 | docs = reader.load_data() 53 | index = VectorStoreIndex.from_documents(docs) 54 | return index 55 | 56 | 57 | index = load_data() 58 | 59 | if "chat_engine" not in st.session_state.keys(): # Initialize the chat engine 60 | st.session_state.chat_engine = index.as_chat_engine(chat_mode="condense_question", verbose=True) 61 | 62 | if prompt := st.chat_input("Your question"): # Prompt for user input and save to chat history 63 | st.session_state.messages.append({"role": "user", "content": prompt}) 64 | 65 | for message in st.session_state.messages: # Display the prior chat messages 66 | with st.chat_message(message["role"]): 67 | st.write(message["content"]) 68 | 69 | # If last message is not from assistant, generate a new response 70 | if st.session_state.messages[-1]["role"] != "assistant": 71 | with st.chat_message("assistant"): 72 | with st.spinner("Thinking..."): 73 | response = st.session_state.chat_engine.chat(prompt) 74 | st.write(response.response) 75 | message = {"role": "assistant", "content": response.response} 76 | st.session_state.messages.append(message) # Add response to message history 77 | -------------------------------------------------------------------------------- /chatbot/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "pdm-chatbot" 3 | version = "0.0.0" 4 | authors = [ 5 | {name = "Frost Ming", email = "me@frostming.com"}, 6 | ] 7 | dependencies = [ 8 | "setuptools>=68.2.2", 9 | "openai>=0.28.1", 10 | "streamlit>=1.28.1", 11 | "llama-index-llms-azure-openai>=0.3.0", 12 | "llama-index-core>=0.12.1", 13 | "llama-index-embeddings-azure-openai>=0.3.0", 14 | "llama-index-readers-file>=0.4.0", 15 | ] 16 | requires-python = ">=3.10,<3.12" 17 | readme = "README.md" 18 | license = {text = "MIT"} 19 | 20 | [tool.pdm] 21 | distribution = false 22 | 23 | [tool.pdm.scripts] 24 | start = "streamlit run app.py" 25 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | after_n_builds: 12 4 | wait_for_ci: false 5 | -------------------------------------------------------------------------------- /docs/assets/extra.css: -------------------------------------------------------------------------------- 1 | a.pdm-expansions { 2 | cursor: pointer; 3 | font-weight: bold; 4 | color: currentColor; 5 | } 6 | 7 | .bot-container { 8 | z-index: 9; 9 | position: fixed; 10 | width: 400px; 11 | right: 20px; 12 | bottom: 110px; 13 | display: flex; 14 | flex-direction: column; 15 | align-items:end; 16 | } 17 | 18 | .bot-container > iframe { 19 | width:100%; 20 | border:none; 21 | border-radius:0.5rem; 22 | transition: height 0.3s ease-in-out; 23 | height: 0; 24 | } 25 | .bot-button { 26 | padding: 0.8rem; 27 | border-radius: 50%; 28 | width: 80px; 29 | height: 80px; 30 | background-color: var(--md-primary-fg-color); 31 | transition: all 0.2s ease-in-out; 32 | fill: white; 33 | } 34 | 35 | .bot-button:hover { 36 | transform: translateY(-3px); 37 | padding: 0.7rem; 38 | } 39 | 40 | /* for readthedocs badge */ 41 | #readthedocs-embed-flyout { 42 | position: sticky; 43 | bottom: 0px; 44 | width: auto; 45 | max-width: 200px; 46 | } 47 | -------------------------------------------------------------------------------- /docs/assets/extra.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function () { 2 | const expansionRepo = 'https://github.com/pdm-project/pdm-expansions'; 3 | const expansionsApi = 'https://expansion.pdm-project.org/api/sample'; 4 | const el = document.querySelector('a.pdm-expansions'); 5 | 6 | function loadExpansions() { 7 | fetch(expansionsApi, { mode: 'cors', redirect: 'follow' }) 8 | .then((response) => { 9 | console.log(response); 10 | return response.json(); 11 | }) 12 | .then((data) => { 13 | window.expansionList = data.data; 14 | setExpansion(); 15 | }); 16 | } 17 | 18 | function setExpansion() { 19 | const { expansionList } = window; 20 | if (!expansionList || !expansionList.length) { 21 | window.location.href = expansionRepo; 22 | return; 23 | } 24 | const expansion = expansionList[expansionList.length - 1]; 25 | expansionList.splice(expansionList.length - 1, 1); 26 | el.innerText = expansion; 27 | if (el.style.display == 'none') { 28 | el.style.display = ''; 29 | } 30 | } 31 | loadExpansions(); 32 | el.addEventListener('click', function (e) { 33 | e.preventDefault(); 34 | setExpansion(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /docs/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/assets/logo_big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/docs/assets/logo_big.png -------------------------------------------------------------------------------- /docs/dev/benchmark.md: -------------------------------------------------------------------------------- 1 | # Benchmark 2 | 3 | This page has been removed, please visit [Python Package Manager Shootout by Lincoln Loop](https://lincolnloop.github.io/python-package-manager-shootout/) for a detailed benchmark report. 4 | -------------------------------------------------------------------------------- /docs/dev/changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | !!! warning "Attention" 4 | Major and minor releases also include changes listed within prior beta releases. 5 | 6 | --8<-- "CHANGELOG.md" 7 | -------------------------------------------------------------------------------- /docs/dev/contributing.md: -------------------------------------------------------------------------------- 1 | --8<-- "CONTRIBUTING.md" 2 | -------------------------------------------------------------------------------- /docs/dev/fixtures.md: -------------------------------------------------------------------------------- 1 | # Pytest fixtures 2 | 3 | ::: pdm.pytest 4 | options: 5 | show_source: false 6 | show_root_heading: false 7 | show_root_toc_entry: false 8 | heading_level: 2 9 | -------------------------------------------------------------------------------- /docs/overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block announce %} 4 | 5 | {% include ".icons/octicons/heart-fill-24.svg" %} 6 | 7 | 8 | {% endblock %} 9 | 10 | {% block footer %} 11 |
12 | {{ super() }} 13 |
14 | 15 | {% include ".icons/fontawesome/solid/robot.svg" %} 16 | 17 | 20 |
21 | 54 | {% endblock %} 55 | -------------------------------------------------------------------------------- /docs/reference/api.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | ::: pdm.core.Core 4 | options: 5 | show_root_heading: yes 6 | show_source: false 7 | heading_level: 2 8 | 9 | ::: pdm.core.Project 10 | options: 11 | show_root_heading: yes 12 | show_source: false 13 | heading_level: 2 14 | 15 | ## Signals 16 | 17 | +++ 1.12.0 18 | 19 | ::: pdm.signals 20 | options: 21 | heading_level: 3 22 | -------------------------------------------------------------------------------- /docs/reference/build.md: -------------------------------------------------------------------------------- 1 | # Build Configuration 2 | 3 | `pdm` uses the [PEP 517](https://www.python.org/dev/peps/pep-0517/) to build the package. It acts as a build frontend that calls the build backend to build the package. 4 | 5 | A build backend is what drives the build system to build source distributions and wheels from arbitrary source trees. 6 | 7 | If you run [`pdm init`](../reference/cli.md#init), PDM will let you choose the build backend to use. Unlike other package managers, PDM does not force you to use a specific build backend. You can choose the one you like. Here is a list of build backends and corresponding configurations initially supported by PDM: 8 | 9 | === "pdm-backend" 10 | 11 | `pyproject.toml` configuration: 12 | 13 | ```toml 14 | [build-system] 15 | requires = ["pdm-backend"] 16 | build-backend = "pdm.backend" 17 | ``` 18 | 19 | [:book: Read the docs](https://backend.pdm-project.org/) 20 | 21 | === "setuptools" 22 | 23 | `pyproject.toml` configuration: 24 | 25 | ```toml 26 | [build-system] 27 | requires = ["setuptools", "wheel"] 28 | build-backend = "setuptools.build_meta" 29 | ``` 30 | 31 | [:book: Read the docs](https://setuptools.pypa.io/) 32 | 33 | === "flit" 34 | 35 | `pyproject.toml` configuration: 36 | 37 | ```toml 38 | [build-system] 39 | requires = ["flit_core >=3.2,<4"] 40 | build-backend = "flit_core.buildapi" 41 | ``` 42 | 43 | [:book: Read the docs](https://flit.pypa.io/) 44 | 45 | === "hatchling" 46 | 47 | `pyproject.toml` configuration: 48 | 49 | ```toml 50 | [build-system] 51 | requires = ["hatchling"] 52 | build-backend = "hatchling.build" 53 | ``` 54 | 55 | [:book: Read the docs](https://hatch.pypa.io/) 56 | 57 | === "maturin" 58 | 59 | `pyproject.toml` configuration: 60 | 61 | ```toml 62 | [build-system] 63 | requires = ["maturin>=1.4,<2.0"] 64 | build-backend = "maturin" 65 | ``` 66 | 67 | [:book: Read the docs](https://www.maturin.rs/) 68 | 69 | Apart from the above mentioned backends, you can also use any other backend that supports PEP 621, however, [poetry-core](https://python-poetry.org/) is not supported because it does not support reading PEP 621 metadata. 70 | 71 | !!! info 72 | If you are using a custom build backend that is not in the above list, PDM will handle the relative paths as PDM-style(`${PROJECT_ROOT}` variable). 73 | -------------------------------------------------------------------------------- /docs/reference/cli.md: -------------------------------------------------------------------------------- 1 | # CLI Reference 2 | 3 | ```python exec="1" idprefix="" 4 | import argparse 5 | import re 6 | from pdm.core import Core 7 | 8 | parser = Core().parser 9 | 10 | MONOSPACED = ("pyproject.toml", "pdm.lock", ".pdm-python", ":pre", ":post", ":all") 11 | 12 | def clean_help(help: str) -> str: 13 | # Make dunders monospaced avoiding italic markdown rendering 14 | help = re.sub(r"__([\w\d\_]+)__", r"`__\1__`", help) 15 | # Make env vars monospaced 16 | help = re.sub(r"env var: ([A-Z_]+)", r"env var: `\1`", help) 17 | for monospaced in MONOSPACED: 18 | help = re.sub(rf"\s(['\"]?{monospaced}['\"]?)", f"`{monospaced}`", help) 19 | return help 20 | 21 | 22 | def render_parser( 23 | parser: argparse.ArgumentParser, title: str, heading_level: int = 2 24 | ) -> str: 25 | """Render the parser help documents as a string.""" 26 | result = [f"{'#' * heading_level} {title}\n"] 27 | if parser.description and title != "pdm": 28 | result.append("> " + parser.description + "\n") 29 | 30 | for group in sorted( 31 | parser._action_groups, key=lambda g: g.title.lower(), reverse=True 32 | ): 33 | if not any( 34 | bool(action.option_strings or action.dest) 35 | or isinstance(action, argparse._SubParsersAction) 36 | for action in group._group_actions 37 | ): 38 | continue 39 | 40 | result.append(f"{group.title.title()}:\n") 41 | for action in group._group_actions: 42 | if isinstance(action, argparse._SubParsersAction): 43 | for name, subparser in action._name_parser_map.items(): 44 | result.append(render_parser(subparser, name, heading_level + 1)) 45 | continue 46 | 47 | opts = [f"`{opt}`" for opt in action.option_strings] 48 | if not opts: 49 | line = f"- `{action.dest}`" 50 | else: 51 | line = f"- {', '.join(opts)}" 52 | if action.metavar: 53 | line += f" `{action.metavar}`" 54 | line += f": {clean_help(action.help)}" 55 | if action.default and action.default != argparse.SUPPRESS: 56 | default = action.default 57 | if any(opt.startswith("--no-") for opt in action.option_strings) and default is True: 58 | default = not default 59 | line += f" (default: `{default}`)" 60 | result.append(line) 61 | result.append("") 62 | 63 | return "\n".join(result) 64 | 65 | 66 | print(render_parser(parser, "pdm")) 67 | ``` 68 | -------------------------------------------------------------------------------- /docs/usage/publish.md: -------------------------------------------------------------------------------- 1 | # Build and Publish 2 | 3 | If you are developing a library, after adding dependencies to your project, and finishing the coding, it's time to build and publish your package. It is as simple as one command: 4 | 5 | ```bash 6 | pdm publish 7 | ``` 8 | 9 | This will automatically build a wheel and a source distribution(sdist), and upload them to the PyPI index. 10 | 11 | PyPI requires API tokens to publish packages, you can use `__token__` as the username and API token as the password. 12 | 13 | To specify another repository other than PyPI, use the `--repository` option, the parameter can be either the upload URL or the name of the repository stored in the config file. 14 | 15 | ```bash 16 | pdm publish --repository testpypi 17 | pdm publish --repository https://test.pypi.org/legacy/ 18 | ``` 19 | 20 | ## Publish with trusted publishers 21 | 22 | You can configure trusted publishers for PyPI so that you don't need to expose the PyPI tokens in the release workflow. To do this, follow 23 | [the guide](https://docs.pypi.org/trusted-publishers/adding-a-publisher/) to add a publisher write a action as below: 24 | 25 | ### GitHub Actions 26 | 27 | ```yaml 28 | on: 29 | release: 30 | types: [published] 31 | 32 | jobs: 33 | pypi-publish: 34 | name: upload release to PyPI 35 | runs-on: ubuntu-latest 36 | permissions: 37 | # This permission is needed for private repositories. 38 | contents: read 39 | # IMPORTANT: this permission is mandatory for trusted publishing 40 | id-token: write 41 | steps: 42 | - uses: actions/checkout@v4 43 | 44 | - uses: pdm-project/setup-pdm@v4 45 | 46 | - name: Publish package distributions to PyPI 47 | run: pdm publish 48 | ``` 49 | 50 | ### GitLab CI 51 | 52 | ```yaml 53 | image: python:3.12-bookworm 54 | before_script: 55 | - pip install pdm 56 | 57 | publish-package: 58 | stage: release 59 | environment: production 60 | id_tokens: 61 | PYPI_ID_TOKEN: # for testpypi: TESTPYPI_ID_TOKEN 62 | aud: "pypi" # testpypi 63 | script: 64 | - pdm publish 65 | ``` 66 | 67 | ## Build and publish separately 68 | 69 | You can also build the package and upload it in two steps, to allow you to inspect the built artifacts before uploading. 70 | 71 | ```bash 72 | pdm build 73 | ``` 74 | 75 | There are many options to control the build process, depending on the backend used. Refer to the [build configuration](../reference/build.md) section for more details. 76 | 77 | The artifacts will be created at `dist/` and able to upload to PyPI. 78 | 79 | ```bash 80 | pdm publish --no-build 81 | ``` 82 | -------------------------------------------------------------------------------- /docs/usage/template.md: -------------------------------------------------------------------------------- 1 | # Create Project From a Template 2 | 3 | Similar to `yarn create` and `npm create`, PDM also supports initializing or creating a project from a template. 4 | The template is given as a positional argument of `pdm new`, in one of the following forms: 5 | 6 | - `pdm new django my-project` - Create a new project `my-project` from the template `https://github.com/pdm-project/template-django` 7 | - `pdm new https://github.com/frostming/pdm-template-django my-project` - Initialize the project from a Git URL. Both HTTPS and SSH URL are acceptable. 8 | - `pdm new django@v2 my-project` - To check out the specific branch or tag. Full Git URL also supports it. 9 | - `pdm new /path/to/template my-project` - Initialize the project from a template directory on local filesystem. 10 | - `pdm new minimal my-project` - Initialize with the builtin "minimal" template, that only generates a `pyproject.toml`. 11 | 12 | And `pdm new my-project` will use the default template built in and create a project at the given path. 13 | 14 | `pdm init` command also supports the same template argument. The project will be initialized at the current directory, existing files with the same name will be overwritten. 15 | 16 | ## Contribute a template 17 | 18 | According to the first form of the template argument, `pdm init ` will refer to the template repository located at `https://github.com/pdm-project/template-`. To contribute a template, you can create a template repository and establish a request to transfer the 19 | ownership to `pdm-project` organization(it can be found at the bottom of the repository settings page). The administrators of the organization will review the request and complete the subsequent steps. You will be added as the repository maintainer if the transfer is accepted. 20 | 21 | ## Requirements for a template 22 | 23 | A template repository must be a pyproject-based project, which contains a `pyproject.toml` file with PEP-621 compliant metadata. 24 | No other special config files are required. 25 | 26 | ## Project name replacement 27 | 28 | On initialization, the project name in the template will be replaced by the name of the new project. This is done by a recursive full-text search and replace. The import name, which is derived from the project name by replacing all non-alphanumeric characters with underscores and lowercasing, will also be replaced in the same way. 29 | 30 | For example, if the project name is `foo-project` in the template and you want to initialize a new project named `bar-project`, the following replacements will be made: 31 | 32 | - `foo-project` -> `bar-project` in all `.md` files and `.rst` files 33 | - `foo_project` -> `bar_project` in all `.py` files 34 | - `foo_project` -> `bar_project` in the directory name 35 | - `foo_project.py` -> `bar_project.py` in the file name 36 | 37 | Therefore, we don't support name replacement if the import name isn't derived from the project name. 38 | 39 | ## Use other project generators 40 | 41 | If you are seeking for a more powerful project generator, you can use [cookiecutter](https://github.com/cookiecutter/cookiecutter) via `--cookiecutter` option and [copier](https://github.com/copier-org/copier) via `--copier` option. 42 | 43 | You need to install `cookiecutter` and `copier` respectively to use them. You can do this by running `pdm self add `. 44 | To use them: 45 | 46 | ```bash 47 | pdm init --cookiecutter gh:cjolowicz/cookiecutter-hypermodern-python 48 | # or 49 | pdm init --copier gh:pawamoy/copier-pdm --UNSAFE 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/usage/uv.md: -------------------------------------------------------------------------------- 1 | # Use uv (Experimental) 2 | 3 | +++ 2.19.0 4 | 5 | PDM has experimental support for [uv](https://github.com/astral-sh/uv) as the resolver and installer. To enable it: 6 | 7 | ``` 8 | pdm config use_uv true 9 | ``` 10 | 11 | PDM will automatically detect the `uv` binary on your system. You need to install `uv` first. See [uv's installation guide](https://docs.astral.sh/uv/getting-started/installation/) for more details. 12 | 13 | ## Reuse the Python installations of uv 14 | 15 | uv also supports installing Python interpreters. To avoid overhead, you can configure PDM to reuse the Python installations of uv by: 16 | 17 | ``` 18 | pdm config python.install_root $(uv python dir) 19 | ``` 20 | 21 | ## Limitations 22 | 23 | Despite the significant performance improvements brought by uv, it is important to note the following limitations: 24 | 25 | - The cache files are stored in uv's own cache directory, and you have to use `uv` command to manage them. 26 | - PEP 582 local packages layout is not supported. 27 | - `inherit_metadata` lock strategy is not supported by uv. This will be ignored when writing to the lock file. 28 | - Update strategies other than `all` and `reuse` are not supported. 29 | - Editable requirement must be a local path. Requirements like `-e git+` are not supported. 30 | - `excludes` settings under `[tool.pdm.resolution]` are not supported. 31 | - Cross-platform lock targets are not needed by uv resolver, uv always generates universal lock files. 32 | - `include_packages` and `exclude_packages` settings under `[tool.pdm.source]` are not supported. 33 | -------------------------------------------------------------------------------- /install-pdm.py.sha256: -------------------------------------------------------------------------------- 1 | e91cea16112b6e8e754944f4a29fe57fe5e206d42aee61fd35c77a9124388de3 install-pdm.py 2 | -------------------------------------------------------------------------------- /news/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/news/.gitkeep -------------------------------------------------------------------------------- /news/3481.feature.md: -------------------------------------------------------------------------------- 1 | Support pylock as alternative lock format and make it opt-in by config. 2 | -------------------------------------------------------------------------------- /news/3485.bugfix.md: -------------------------------------------------------------------------------- 1 | Fix Windows 11 install pdm error, which is because of msgpack install failure. 2 | -------------------------------------------------------------------------------- /news/3523.bugfix.md: -------------------------------------------------------------------------------- 1 | Change the return type of `array_of_inline_tables` to list[dict] from list[str] -------------------------------------------------------------------------------- /news/3531.bugfix.md: -------------------------------------------------------------------------------- 1 | Ensure uv resolver to include hash for package files. 2 | -------------------------------------------------------------------------------- /news/3539.bugfix.md: -------------------------------------------------------------------------------- 1 | Avoid infinite recursion when reading pyproject.toml with circular file dependencies. 2 | -------------------------------------------------------------------------------- /src/pdm/__init__.py: -------------------------------------------------------------------------------- 1 | import pkgutil 2 | 3 | __path__ = pkgutil.extend_path(__path__, __name__) 4 | -------------------------------------------------------------------------------- /src/pdm/__main__.py: -------------------------------------------------------------------------------- 1 | from pdm.core import main 2 | 3 | if __name__ == "__main__": 4 | main() 5 | -------------------------------------------------------------------------------- /src/pdm/__version__.py: -------------------------------------------------------------------------------- 1 | from pdm.compat import importlib_metadata, resources_read_text 2 | 3 | 4 | def read_version() -> str: 5 | try: 6 | return importlib_metadata.version(__package__ or "pdm") 7 | except importlib_metadata.PackageNotFoundError: 8 | return resources_read_text("pdm", "VERSION").strip() 9 | 10 | 11 | __version__ = read_version() 12 | -------------------------------------------------------------------------------- /src/pdm/builders/__init__.py: -------------------------------------------------------------------------------- 1 | from pdm.builders.editable import EditableBuilder 2 | from pdm.builders.sdist import SdistBuilder 3 | from pdm.builders.wheel import WheelBuilder 4 | 5 | __all__ = ["EditableBuilder", "SdistBuilder", "WheelBuilder"] 6 | -------------------------------------------------------------------------------- /src/pdm/builders/editable.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | 5 | from pdm.builders.base import EnvBuilder, wrap_error 6 | 7 | 8 | class EditableBuilder(EnvBuilder): 9 | """Build egg-info in isolated env with managed Python.""" 10 | 11 | @wrap_error 12 | def prepare_metadata(self, out_dir: str) -> str: 13 | if self.isolated: 14 | self.install(self._requires, shared=True) 15 | requires = self._hook.get_requires_for_build_editable(self.config_settings) 16 | self.install(requires) 17 | filename = self._hook.prepare_metadata_for_build_editable(out_dir, self.config_settings) 18 | return os.path.join(out_dir, filename) 19 | 20 | @wrap_error 21 | def build(self, out_dir: str, metadata_directory: str | None = None) -> str: 22 | if self.isolated: 23 | self.install(self._requires, shared=True) 24 | requires = self._hook.get_requires_for_build_editable(self.config_settings) 25 | self.install(requires) 26 | filename = self._hook.build_editable(out_dir, self.config_settings, metadata_directory) 27 | return os.path.join(out_dir, filename) 28 | -------------------------------------------------------------------------------- /src/pdm/builders/sdist.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | 5 | from pdm.builders.base import EnvBuilder, wrap_error 6 | 7 | 8 | class SdistBuilder(EnvBuilder): 9 | """Build sdist in isolated env with managed Python.""" 10 | 11 | @wrap_error 12 | def build(self, out_dir: str, metadata_directory: str | None = None) -> str: 13 | if self.isolated: 14 | self.install(self._requires, shared=True) 15 | requires = self._hook.get_requires_for_build_sdist(self.config_settings) 16 | self.install(requires) 17 | filename = self._hook.build_sdist(out_dir, self.config_settings) 18 | return os.path.join(out_dir, filename) 19 | -------------------------------------------------------------------------------- /src/pdm/builders/wheel.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | 5 | from pdm.builders.base import EnvBuilder, wrap_error 6 | 7 | 8 | class WheelBuilder(EnvBuilder): 9 | """Build wheel in isolated env with managed Python.""" 10 | 11 | @wrap_error 12 | def prepare_metadata(self, out_dir: str) -> str: 13 | if self.isolated: 14 | self.install(self._requires, shared=True) 15 | requires = self._hook.get_requires_for_build_wheel(self.config_settings) 16 | self.install(requires) 17 | filename = self._hook.prepare_metadata_for_build_wheel(out_dir, self.config_settings) 18 | return os.path.join(out_dir, filename) 19 | 20 | @wrap_error 21 | def build(self, out_dir: str, metadata_directory: str | None = None) -> str: 22 | if self.isolated: 23 | self.install(self._requires, shared=True) 24 | requires = self._hook.get_requires_for_build_wheel(self.config_settings) 25 | self.install(requires) 26 | filename = self._hook.build_wheel(out_dir, self.config_settings, metadata_directory) 27 | return os.path.join(out_dir, filename) 28 | -------------------------------------------------------------------------------- /src/pdm/cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/src/pdm/cli/__init__.py -------------------------------------------------------------------------------- /src/pdm/cli/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/src/pdm/cli/commands/__init__.py -------------------------------------------------------------------------------- /src/pdm/cli/commands/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import argparse 4 | from argparse import _SubParsersAction 5 | from typing import Any, Sequence, TypeVar 6 | 7 | from pdm.cli.options import Option, global_option, project_option, verbose_option 8 | from pdm.project import Project 9 | 10 | C = TypeVar("C", bound="BaseCommand") 11 | 12 | 13 | class BaseCommand: 14 | """A CLI subcommand""" 15 | 16 | # The subcommand's name 17 | name: str | None = None 18 | # The subcommand's help string, if not given, __doc__ will be used. 19 | description: str | None = None 20 | # A list of pre-defined options which will be loaded on initializing 21 | # Rewrite this if you don't want the default ones 22 | arguments: Sequence[Option] = (verbose_option, global_option, project_option) 23 | 24 | @classmethod 25 | def init_parser(cls: type[C], parser: argparse.ArgumentParser) -> C: 26 | cmd = cls() 27 | for arg in cmd.arguments: 28 | arg.add_to_parser(parser) 29 | cmd.add_arguments(parser) 30 | return cmd 31 | 32 | @classmethod 33 | def register_to(cls, subparsers: _SubParsersAction, name: str | None = None, **kwargs: Any) -> None: 34 | """Register a subcommand to the subparsers, 35 | with an optional name of the subcommand. 36 | """ 37 | help_text = cls.description or cls.__doc__ 38 | name = name or cls.name or "" 39 | # Remove the existing subparser as it will raise an error on Python 3.11+ 40 | subparsers._name_parser_map.pop(name, None) 41 | subactions = subparsers._get_subactions() 42 | subactions[:] = [action for action in subactions if action.dest != name] 43 | parser = subparsers.add_parser( 44 | name, 45 | description=help_text, 46 | help=help_text, 47 | **kwargs, 48 | ) 49 | command = cls.init_parser(parser) 50 | command.name = name 51 | # Store the command instance in the parsed args. See pdm/core.py for more details 52 | parser.set_defaults(command=command) 53 | 54 | def add_arguments(self, parser: argparse.ArgumentParser) -> None: 55 | """Manipulate the argument parser to add more arguments""" 56 | pass 57 | 58 | def handle(self, project: Project, options: argparse.Namespace) -> None: 59 | """The command handler function. 60 | 61 | :param project: the pdm project instance 62 | :param options: the parsed Namespace object 63 | """ 64 | raise NotImplementedError 65 | -------------------------------------------------------------------------------- /src/pdm/cli/commands/completion.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import argparse 4 | import sys 5 | 6 | from pdm.cli.commands.base import BaseCommand 7 | from pdm.compat import resources_read_text 8 | from pdm.exceptions import PdmUsageError 9 | from pdm.project import Project 10 | 11 | 12 | class Command(BaseCommand): 13 | """Generate completion scripts for the given shell""" 14 | 15 | arguments = () 16 | SUPPORTED_SHELLS = ("bash", "zsh", "fish", "powershell", "pwsh") 17 | 18 | def add_arguments(self, parser: argparse.ArgumentParser) -> None: 19 | parser.add_argument( 20 | "shell", 21 | nargs="?", 22 | help="The shell to generate the scripts for. If not given, PDM will properly guess from `SHELL` env var.", 23 | ) 24 | 25 | def handle(self, project: Project, options: argparse.Namespace) -> None: 26 | import shellingham 27 | 28 | shell = options.shell or shellingham.detect_shell()[0] 29 | if shell not in self.SUPPORTED_SHELLS: 30 | raise PdmUsageError(f"Unsupported shell: {shell}") 31 | suffix = "ps1" if shell in {"powershell", "pwsh"} else shell 32 | completion = resources_read_text("pdm.cli.completions", f"pdm.{suffix}") 33 | # Can't use rich print or otherwise the rich markups will be interpreted 34 | print(completion.replace("%{python_executable}", sys.executable)) 35 | -------------------------------------------------------------------------------- /src/pdm/cli/commands/fix/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import argparse 4 | 5 | from pdm.cli.commands.base import BaseCommand 6 | from pdm.cli.commands.fix.fixers import BaseFixer, LockStrategyFixer, PackageTypeFixer, ProjectConfigFixer 7 | from pdm.exceptions import PdmUsageError 8 | from pdm.project import Project 9 | from pdm.termui import Emoji 10 | 11 | 12 | class Command(BaseCommand): 13 | """Fix the project problems according to the latest version of PDM""" 14 | 15 | def add_arguments(self, parser: argparse.ArgumentParser) -> None: 16 | parser.add_argument("problem", nargs="?", help="Fix the specific problem, or all if not given") 17 | parser.add_argument("--dry-run", action="store_true", help="Only show the problems") 18 | 19 | @staticmethod 20 | def find_problems(project: Project) -> list[tuple[str, BaseFixer]]: 21 | """Get the problems in the project""" 22 | problems: list[tuple[str, BaseFixer]] = [] 23 | for fixer in Command.get_fixers(project): 24 | if fixer.check(): 25 | problems.append((fixer.identifier, fixer)) 26 | return problems 27 | 28 | @staticmethod 29 | def check_problems(project: Project, strict: bool = True) -> None: 30 | """Check the problems in the project""" 31 | problems = Command.find_problems(project) 32 | if not problems: 33 | return 34 | breaking = False 35 | project.core.ui.warn("The following problems are found in your project:") 36 | for name, fixer in problems: 37 | project.core.ui.echo(f" [b]{name}[/]: {fixer.get_message()}", err=True) 38 | if fixer.breaking: 39 | breaking = True 40 | extra_option = " -g" if project.is_global else "" 41 | project.core.ui.echo( 42 | f"Run [success]pdm fix{extra_option}[/] to fix all or [success]pdm fix{extra_option} [/]" 43 | " to fix individual problem.", 44 | err=True, 45 | ) 46 | if breaking and strict: 47 | raise SystemExit(1) 48 | 49 | @staticmethod 50 | def get_fixers(project: Project) -> list[BaseFixer]: 51 | """Return a list of fixers to check, the order matters""" 52 | return [ProjectConfigFixer(project), PackageTypeFixer(project), LockStrategyFixer(project)] 53 | 54 | def handle(self, project: Project, options: argparse.Namespace) -> None: 55 | if options.dry_run: 56 | return self.check_problems(project) 57 | problems = self.find_problems(project) 58 | if options.problem: 59 | fixer = next((fixer for name, fixer in problems if name == options.problem), None) 60 | if not fixer: 61 | raise PdmUsageError( 62 | f"The problem doesn't exist: [success]{options.problem}[/], " 63 | f"possible values are {[p[0] for p in problems]}", 64 | ) 65 | project.core.ui.echo(f"Fixing [success]{fixer.identifier}[/]...", end=" ") 66 | fixer.fix() 67 | project.core.ui.echo(f"[success]{Emoji.SUCC}[/]") 68 | return 69 | if not problems: 70 | project.core.ui.echo("No problem is found, nothing to fix.") 71 | return 72 | for name, fixer in problems: 73 | project.core.ui.echo(f"Fixing [success]{name}[/]...", end=" ") 74 | fixer.fix() 75 | project.core.ui.echo(f"[success]{Emoji.SUCC}[/]") 76 | -------------------------------------------------------------------------------- /src/pdm/cli/commands/info.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | 4 | from rich import print_json 5 | 6 | from pdm.cli.commands.base import BaseCommand 7 | from pdm.cli.options import ArgumentGroup, venv_option 8 | from pdm.cli.utils import check_project_file 9 | from pdm.project import Project 10 | 11 | 12 | class Command(BaseCommand): 13 | """Show the project information""" 14 | 15 | def add_arguments(self, parser: argparse.ArgumentParser) -> None: 16 | venv_option.add_to_parser(parser) 17 | group = ArgumentGroup("fields", is_mutually_exclusive=True) 18 | group.add_argument("--python", action="store_true", help="Show the interpreter path") 19 | group.add_argument( 20 | "--where", 21 | dest="where", 22 | action="store_true", 23 | help="Show the project root path", 24 | ) 25 | group.add_argument("--packages", action="store_true", help="Show the local packages root") 26 | group.add_argument("--env", action="store_true", help="Show PEP 508 environment markers") 27 | group.add_argument("--json", action="store_true", help="Dump the information in JSON") 28 | group.add_to_parser(parser) 29 | 30 | def handle(self, project: Project, options: argparse.Namespace) -> None: 31 | check_project_file(project) 32 | interpreter = project.environment.interpreter 33 | packages_path = "" 34 | if project.environment.is_local: 35 | packages_path = project.environment.packages_path # type: ignore[attr-defined] 36 | if options.python: 37 | project.core.ui.echo(str(interpreter.executable)) 38 | elif options.where: 39 | project.core.ui.echo(str(project.root)) 40 | elif options.packages: 41 | project.core.ui.echo(str(packages_path)) 42 | elif options.env: 43 | project.core.ui.echo(json.dumps(project.environment.spec.markers_with_defaults(), indent=2)) 44 | elif options.json: 45 | print_json( 46 | data={ 47 | "pdm": {"version": project.core.version}, 48 | "python": { 49 | "interpreter": str(interpreter.executable), 50 | "version": interpreter.identifier, 51 | "markers": project.environment.spec.markers_with_defaults(), 52 | }, 53 | "project": { 54 | "root": str(project.root), 55 | "pypackages": str(packages_path), 56 | }, 57 | } 58 | ) 59 | else: 60 | for name, value in zip( 61 | [ 62 | f"[primary]{key}[/]:" 63 | for key in [ 64 | "PDM version", 65 | f"{'Global ' if project.is_global else ''}Python Interpreter", 66 | f"{'Global ' if project.is_global else ''}Project Root", 67 | f"{'Global ' if project.is_global else ''}Local Packages", 68 | ] 69 | ], 70 | [ 71 | project.core.version, 72 | f"{interpreter.executable} ({interpreter.identifier})", 73 | project.root.as_posix(), 74 | str(packages_path), 75 | ], 76 | ): 77 | project.core.ui.echo(f"{name}\n {value}") 78 | -------------------------------------------------------------------------------- /src/pdm/cli/commands/new.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | 4 | from pdm.cli.commands.base import verbose_option 5 | from pdm.cli.commands.init import Command as InitCommand 6 | from pdm.project.core import Project 7 | 8 | 9 | class Command(InitCommand): 10 | """Create a new Python project at """ 11 | 12 | supports_other_generator = False 13 | 14 | arguments = (verbose_option,) 15 | 16 | def add_arguments(self, parser: argparse.ArgumentParser) -> None: 17 | super().add_arguments(parser) 18 | parser.add_argument("project_path", help="The path to create the new project") 19 | 20 | def handle(self, project: Project, options: argparse.Namespace) -> None: 21 | new_project = project.core.create_project( 22 | options.project_path, global_config=options.config or os.getenv("PDM_CONFIG_FILE") 23 | ) 24 | return super().handle(new_project, options) 25 | -------------------------------------------------------------------------------- /src/pdm/cli/commands/search.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import argparse 4 | import sys 5 | import textwrap 6 | from shutil import get_terminal_size 7 | 8 | from pdm import termui 9 | from pdm._types import SearchResults 10 | from pdm.cli.commands.base import BaseCommand 11 | from pdm.cli.options import verbose_option 12 | from pdm.environments import BareEnvironment 13 | from pdm.models.working_set import WorkingSet 14 | from pdm.project import Project 15 | from pdm.utils import normalize_name 16 | 17 | 18 | def print_results( 19 | ui: termui.UI, 20 | hits: SearchResults, 21 | working_set: WorkingSet, 22 | terminal_width: int | None = None, 23 | ) -> None: 24 | if not hits: 25 | return 26 | name_column_width = max(len(hit.name) + len(hit.version or "") for hit in hits) + 4 27 | 28 | for hit in hits: 29 | name = hit.name 30 | summary = hit.summary or "" 31 | if terminal_width is not None: 32 | target_width = terminal_width - name_column_width - 5 33 | if target_width > 10: 34 | # wrap and indent summary to fit terminal 35 | summary = ("\n" + " " * (name_column_width + 2)).join(textwrap.wrap(summary, target_width)) 36 | current_width = len(name) + 1 37 | spaces = " " * (name_column_width - current_width) 38 | line = f"[req]{name}[/]{spaces} - {summary}" 39 | try: 40 | ui.echo(line) 41 | if normalize_name(name) in working_set: 42 | dist = working_set[normalize_name(name)] 43 | ui.echo(f" INSTALLED: {dist.version}") 44 | except UnicodeEncodeError: 45 | pass 46 | 47 | 48 | class Command(BaseCommand): 49 | """Search for PyPI packages""" 50 | 51 | arguments = (verbose_option,) 52 | 53 | def add_arguments(self, parser: argparse.ArgumentParser) -> None: 54 | parser.add_argument("query", help="Query string to search") 55 | 56 | def handle(self, project: Project, options: argparse.Namespace) -> None: 57 | project.environment = BareEnvironment(project) 58 | result = project.get_repository().search(options.query) 59 | terminal_width = None 60 | if sys.stdout.isatty(): 61 | terminal_width = get_terminal_size()[0] 62 | working_set = project.environment.get_working_set() 63 | print_results(project.core.ui, result, working_set, terminal_width) 64 | -------------------------------------------------------------------------------- /src/pdm/cli/commands/show.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import argparse 4 | from typing import TYPE_CHECKING 5 | 6 | from pdm.cli.commands.base import BaseCommand 7 | from pdm.cli.options import venv_option 8 | from pdm.exceptions import PdmUsageError 9 | from pdm.models.candidates import Candidate 10 | from pdm.models.project_info import ProjectInfo 11 | from pdm.models.requirements import parse_requirement 12 | from pdm.project import Project 13 | from pdm.utils import normalize_name, parse_version 14 | 15 | if TYPE_CHECKING: 16 | from unearth import Package 17 | 18 | 19 | def filter_stable(package: Package) -> bool: 20 | assert package.version 21 | return not parse_version(package.version).is_prerelease 22 | 23 | 24 | class Command(BaseCommand): 25 | """Show the package information""" 26 | 27 | metadata_keys = ("name", "version", "summary", "license", "platform", "keywords") 28 | 29 | def add_arguments(self, parser: argparse.ArgumentParser) -> None: 30 | venv_option.add_to_parser(parser) 31 | parser.add_argument( 32 | "package", 33 | type=normalize_name, 34 | nargs=argparse.OPTIONAL, 35 | help="Specify the package name, or show this package if not given", 36 | ) 37 | for option in self.metadata_keys: 38 | parser.add_argument(f"--{option}", action="store_true", help=f"Show {option}") 39 | 40 | def handle(self, project: Project, options: argparse.Namespace) -> None: 41 | package = options.package 42 | if package: 43 | with project.environment.get_finder() as finder: 44 | best_match = finder.find_best_match(package, allow_prereleases=True) 45 | if not best_match.applicable: 46 | project.core.ui.warn(f"No match found for the package {package!r}") 47 | return 48 | latest = Candidate.from_installation_candidate(best_match.best, parse_requirement(package)) 49 | latest_stable = next(filter(filter_stable, best_match.applicable), None) 50 | metadata = latest.prepare(project.environment).metadata 51 | else: 52 | if not project.is_distribution: 53 | raise PdmUsageError("This project is not a library") 54 | package = normalize_name(project.name) 55 | metadata = project.make_self_candidate(False).prepare(project.environment).prepare_metadata(True) 56 | latest_stable = None 57 | project_info = ProjectInfo.from_distribution(metadata) 58 | 59 | if any(getattr(options, key, None) for key in self.metadata_keys): 60 | for key in self.metadata_keys: 61 | if getattr(options, key, None): 62 | project.core.ui.echo(getattr(project_info, key)) 63 | return 64 | 65 | installed = project.environment.get_working_set().get(package) 66 | if latest_stable: 67 | project_info.latest_stable_version = str(latest_stable.version) 68 | if installed: 69 | project_info.installed_version = str(installed.version) 70 | project.core.ui.display_columns(list(project_info.generate_rows())) 71 | -------------------------------------------------------------------------------- /src/pdm/cli/commands/sync.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | from pdm.cli import actions 4 | from pdm.cli.commands.base import BaseCommand 5 | from pdm.cli.filters import GroupSelection 6 | from pdm.cli.hooks import HookManager 7 | from pdm.cli.options import ( 8 | clean_group, 9 | dry_run_option, 10 | groups_group, 11 | install_group, 12 | lockfile_option, 13 | skip_option, 14 | venv_option, 15 | ) 16 | from pdm.project import Project 17 | 18 | 19 | class Command(BaseCommand): 20 | """Synchronize the current working set with lock file""" 21 | 22 | arguments = ( 23 | *BaseCommand.arguments, 24 | groups_group, 25 | dry_run_option, 26 | lockfile_option, 27 | skip_option, 28 | clean_group, 29 | install_group, 30 | venv_option, 31 | ) 32 | 33 | def add_arguments(self, parser: argparse.ArgumentParser) -> None: 34 | parser.add_argument( 35 | "-r", 36 | "--reinstall", 37 | action="store_true", 38 | help="Force reinstall existing dependencies", 39 | ) 40 | 41 | def handle(self, project: Project, options: argparse.Namespace) -> None: 42 | actions.check_lockfile(project) 43 | selection = GroupSelection.from_options(project, options) 44 | actions.do_sync( 45 | project, 46 | selection=selection, 47 | dry_run=options.dry_run, 48 | clean=options.clean, 49 | quiet=options.verbose == -1, 50 | no_editable=options.no_editable, 51 | no_self=options.no_self or "default" not in selection, 52 | reinstall=options.reinstall, 53 | only_keep=options.only_keep, 54 | hooks=HookManager(project, options.skip), 55 | ) 56 | -------------------------------------------------------------------------------- /src/pdm/cli/commands/venv/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import argparse 4 | 5 | from pdm.cli.commands.base import BaseCommand 6 | from pdm.cli.commands.venv.activate import ActivateCommand 7 | from pdm.cli.commands.venv.create import CreateCommand 8 | from pdm.cli.commands.venv.list import ListCommand 9 | from pdm.cli.commands.venv.purge import PurgeCommand 10 | from pdm.cli.commands.venv.remove import RemoveCommand 11 | from pdm.cli.commands.venv.utils import get_venv_with_name 12 | from pdm.cli.options import project_option 13 | from pdm.project import Project 14 | 15 | 16 | class Command(BaseCommand): 17 | """Virtualenv management""" 18 | 19 | name = "venv" 20 | arguments = (project_option,) 21 | 22 | def add_arguments(self, parser: argparse.ArgumentParser) -> None: 23 | group = parser.add_mutually_exclusive_group() 24 | group.add_argument("--path", help="Show the path to the given virtualenv") 25 | group.add_argument("--python", help="Show the python interpreter path for the given virtualenv") 26 | subparser = parser.add_subparsers(title="commands", metavar="") 27 | CreateCommand.register_to(subparser, "create") 28 | ListCommand.register_to(subparser, "list") 29 | RemoveCommand.register_to(subparser, "remove") 30 | ActivateCommand.register_to(subparser, "activate") 31 | PurgeCommand.register_to(subparser, "purge") 32 | self.parser = parser 33 | 34 | def handle(self, project: Project, options: argparse.Namespace) -> None: 35 | if options.path: 36 | venv = get_venv_with_name(project, options.path) 37 | project.core.ui.echo(str(venv.root)) 38 | elif options.python: 39 | venv = get_venv_with_name(project, options.python) 40 | project.core.ui.echo(str(venv.interpreter)) 41 | else: 42 | self.parser.print_help() 43 | -------------------------------------------------------------------------------- /src/pdm/cli/commands/venv/activate.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import platform 3 | import shlex 4 | from pathlib import Path 5 | 6 | import shellingham 7 | 8 | from pdm.cli.commands.base import BaseCommand 9 | from pdm.cli.commands.venv.utils import get_venv_with_name 10 | from pdm.cli.options import verbose_option 11 | from pdm.models.venv import VirtualEnv 12 | from pdm.project import Project 13 | 14 | 15 | class ActivateCommand(BaseCommand): 16 | """Print the command to activate the virtualenv with the given name""" 17 | 18 | arguments = (verbose_option,) 19 | 20 | def add_arguments(self, parser: argparse.ArgumentParser) -> None: 21 | parser.add_argument("env", nargs="?", help="The key of the virtualenv") 22 | 23 | def handle(self, project: Project, options: argparse.Namespace) -> None: 24 | if options.env: 25 | venv = get_venv_with_name(project, options.env) 26 | else: 27 | # Use what is saved in .pdm-python 28 | interpreter = project._saved_python 29 | if not interpreter: 30 | project.core.ui.warn( 31 | "The project doesn't have a saved python.path. Run [success]pdm use[/] to pick one." 32 | ) 33 | raise SystemExit(1) 34 | venv_like = VirtualEnv.from_interpreter(Path(interpreter)) 35 | if venv_like is None: 36 | project.core.ui.warn( 37 | f"Can't activate a non-venv Python [success]{interpreter}[/], " 38 | "you can specify one with [success]pdm venv activate [/]", 39 | ) 40 | raise SystemExit(1) 41 | venv = venv_like 42 | project.core.ui.echo(self.get_activate_command(venv)) 43 | 44 | def get_activate_command(self, venv: VirtualEnv) -> str: # pragma: no cover 45 | try: 46 | shell, _ = shellingham.detect_shell() 47 | except shellingham.ShellDetectionFailure: 48 | shell = "" 49 | if shell == "fish": 50 | command, filename = "source", "activate.fish" 51 | elif shell in ["csh", "tcsh"]: 52 | command, filename = "source", "activate.csh" 53 | elif shell in ["powershell", "pwsh"]: 54 | command, filename = ".", "Activate.ps1" 55 | else: 56 | command, filename = "source", "activate" 57 | activate_script = venv.interpreter.with_name(filename) 58 | if activate_script.exists(): 59 | if platform.system() == "Windows": 60 | return f"{self.quote(str(activate_script), shell)}" 61 | return f"{command} {self.quote(str(activate_script), shell)}" 62 | # Conda backed virtualenvs don't have activate scripts 63 | return f"conda activate {self.quote(str(venv.root), shell)}" 64 | 65 | @staticmethod 66 | def quote(command: str, shell: str) -> str: 67 | if shell in ["powershell", "pwsh"] or platform.system() == "Windows": 68 | return "{}".format(command.replace("'", "''")) 69 | return shlex.quote(command) 70 | -------------------------------------------------------------------------------- /src/pdm/cli/commands/venv/create.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | from pdm.cli.commands.base import BaseCommand 4 | from pdm.cli.commands.venv.backends import BACKENDS 5 | from pdm.cli.options import verbose_option 6 | from pdm.project import Project 7 | 8 | 9 | class CreateCommand(BaseCommand): 10 | """Create a virtualenv 11 | 12 | pdm venv create [-other args] 13 | """ 14 | 15 | description = "Create a virtualenv" 16 | arguments = (verbose_option,) 17 | 18 | def add_arguments(self, parser: argparse.ArgumentParser) -> None: 19 | parser.add_argument( 20 | "-w", 21 | "--with", 22 | dest="backend", 23 | choices=BACKENDS.keys(), 24 | help="Specify the backend to create the virtualenv", 25 | ) 26 | parser.add_argument( 27 | "-f", 28 | "--force", 29 | action="store_true", 30 | help="Recreate if the virtualenv already exists", 31 | ) 32 | parser.add_argument("-n", "--name", help="Specify the name of the virtualenv") 33 | parser.add_argument("--with-pip", action="store_true", help="Install pip with the virtualenv") 34 | parser.add_argument( 35 | "python", 36 | nargs="?", 37 | help="Specify which python should be used to create the virtualenv", 38 | ) 39 | parser.add_argument( 40 | "venv_args", 41 | nargs=argparse.REMAINDER, 42 | help="Additional arguments that will be passed to the backend", 43 | ) 44 | 45 | def handle(self, project: Project, options: argparse.Namespace) -> None: 46 | in_project = project.config["venv.in_project"] and not options.name 47 | backend: str = options.backend or project.config["venv.backend"] 48 | venv_backend = BACKENDS[backend](project, options.python) 49 | with project.core.ui.open_spinner(f"Creating virtualenv using [success]{backend}[/]..."): 50 | path = venv_backend.create( 51 | options.name, 52 | options.venv_args, 53 | options.force, 54 | in_project, 55 | prompt=project.config["venv.prompt"], 56 | with_pip=options.with_pip or project.config["venv.with_pip"], 57 | ) 58 | project.core.ui.echo(f"Virtualenv [success]{path}[/] is created successfully") 59 | -------------------------------------------------------------------------------- /src/pdm/cli/commands/venv/list.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from pathlib import Path 3 | 4 | from pdm.cli.commands.base import BaseCommand 5 | from pdm.cli.commands.venv.utils import iter_venvs 6 | from pdm.cli.options import verbose_option 7 | from pdm.project import Project 8 | 9 | 10 | class ListCommand(BaseCommand): 11 | """List all virtualenvs associated with this project""" 12 | 13 | arguments = (verbose_option,) 14 | 15 | def handle(self, project: Project, options: argparse.Namespace) -> None: 16 | project.core.ui.echo("Virtualenvs created with this project:\n") 17 | for ident, venv in iter_venvs(project): 18 | saved_python = project._saved_python 19 | if saved_python and Path(saved_python).parent.parent == venv.root: 20 | mark = "*" 21 | else: 22 | mark = "-" 23 | project.core.ui.echo(f"{mark} [success]{ident}[/]: {venv.root}") 24 | -------------------------------------------------------------------------------- /src/pdm/cli/commands/venv/purge.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import shutil 3 | from pathlib import Path 4 | 5 | from pdm import termui 6 | from pdm.cli.commands.base import BaseCommand 7 | from pdm.cli.commands.venv.utils import iter_central_venvs 8 | from pdm.cli.options import verbose_option 9 | from pdm.project import Project 10 | 11 | 12 | class PurgeCommand(BaseCommand): 13 | """Purge selected/all created Virtualenvs""" 14 | 15 | arguments = (verbose_option,) 16 | 17 | def add_arguments(self, parser: argparse.ArgumentParser) -> None: 18 | parser.add_argument( 19 | "-f", 20 | "--force", 21 | action="store_true", 22 | help="Force purging without prompting for confirmation", 23 | ) 24 | parser.add_argument( 25 | "-i", 26 | "--interactive", 27 | action="store_true", 28 | help="Interactively purge selected Virtualenvs", 29 | ) 30 | 31 | def handle(self, project: Project, options: argparse.Namespace) -> None: 32 | all_central_venvs = list(iter_central_venvs(project)) 33 | if not all_central_venvs: 34 | project.core.ui.echo("No virtualenvs to purge, quitting.", style="success") 35 | return 36 | 37 | if not options.force: 38 | project.core.ui.echo("The following Virtualenvs will be purged:", style="warning") 39 | for i, venv in enumerate(all_central_venvs): 40 | project.core.ui.echo(f"{i}. [success]{venv[0]}[/]") 41 | 42 | if not options.interactive: 43 | if options.force or termui.confirm("continue?", default=True): 44 | return self.del_all_venvs(project) 45 | 46 | selection = termui.ask( 47 | "Please select", 48 | choices=([str(i) for i in range(len(all_central_venvs))] + ["all", "none"]), 49 | default="none", 50 | show_choices=False, 51 | ) 52 | 53 | if selection == "all": 54 | self.del_all_venvs(project) 55 | elif selection != "none": 56 | for i, venv in enumerate(all_central_venvs): 57 | if i == int(selection): 58 | shutil.rmtree(venv[1]) 59 | project.core.ui.echo("Purged successfully!") 60 | 61 | def del_all_venvs(self, project: Project) -> None: 62 | saved_python = project._saved_python 63 | for _, venv in iter_central_venvs(project): 64 | shutil.rmtree(venv) 65 | if saved_python and Path(saved_python).parent.parent == venv: 66 | project._saved_python = None 67 | project.core.ui.echo("Purged successfully!") 68 | -------------------------------------------------------------------------------- /src/pdm/cli/commands/venv/remove.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import shutil 3 | from pathlib import Path 4 | 5 | from pdm import termui 6 | from pdm.cli.commands.base import BaseCommand 7 | from pdm.cli.commands.venv.utils import get_venv_with_name 8 | from pdm.cli.options import verbose_option 9 | from pdm.project import Project 10 | 11 | 12 | class RemoveCommand(BaseCommand): 13 | """Remove the virtualenv with the given name""" 14 | 15 | arguments = (verbose_option,) 16 | 17 | def add_arguments(self, parser: argparse.ArgumentParser) -> None: 18 | parser.add_argument( 19 | "-y", 20 | "--yes", 21 | action="store_true", 22 | help="Answer yes on the following question", 23 | ) 24 | parser.add_argument("env", help="The key of the virtualenv") 25 | 26 | def handle(self, project: Project, options: argparse.Namespace) -> None: 27 | project.core.ui.echo("Virtualenvs created with this project:") 28 | venv = get_venv_with_name(project, options.env) 29 | if options.yes or termui.confirm(f"[warning]Will remove: [success]{venv.root}[/], continue?", default=True): 30 | shutil.rmtree(venv.root) 31 | saved_python = project._saved_python 32 | if saved_python and Path(saved_python).parent.parent == venv.root: 33 | project._saved_python = None 34 | project.core.ui.echo("Removed successfully!") 35 | -------------------------------------------------------------------------------- /src/pdm/cli/commands/venv/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import base64 4 | import hashlib 5 | import typing as t 6 | from pathlib import Path 7 | 8 | from findpython import BaseProvider, PythonVersion 9 | 10 | from pdm.exceptions import PdmUsageError 11 | from pdm.models.venv import VirtualEnv 12 | from pdm.project import Project 13 | 14 | 15 | def hash_path(path: str) -> str: 16 | """Generate a hash for the given path.""" 17 | return base64.urlsafe_b64encode(hashlib.new("md5", path.encode(), usedforsecurity=False).digest()).decode()[:8] 18 | 19 | 20 | def get_in_project_venv(root: Path) -> VirtualEnv | None: 21 | """Get the python interpreter path of venv-in-project""" 22 | for possible_dir in (".venv", "venv", "env"): 23 | venv = VirtualEnv.get(root / possible_dir) 24 | if venv is not None: 25 | return venv 26 | return None 27 | 28 | 29 | def get_venv_prefix(project: Project) -> str: 30 | """Get the venv prefix for the project""" 31 | path = project.root 32 | name_hash = hash_path(path.as_posix()) 33 | return f"{path.name}-{name_hash}-" 34 | 35 | 36 | def iter_venvs(project: Project) -> t.Iterable[tuple[str, VirtualEnv]]: 37 | """Return an iterable of venv paths associated with the project""" 38 | in_project_venv = get_in_project_venv(project.root) 39 | if in_project_venv is not None: 40 | yield "in-project", in_project_venv 41 | venv_prefix = get_venv_prefix(project) 42 | venv_parent = Path(project.config["venv.location"]) 43 | for path in venv_parent.glob(f"{venv_prefix}*"): 44 | ident = path.name[len(venv_prefix) :] 45 | venv = VirtualEnv.get(path) 46 | if venv is not None: 47 | yield ident, venv 48 | 49 | 50 | def iter_central_venvs(project: Project) -> t.Iterable[tuple[str, Path]]: 51 | """Return an iterable of all managed venvs and their paths.""" 52 | venv_parent = Path(project.config["venv.location"]) 53 | for venv in venv_parent.glob("*"): 54 | ident = venv.name 55 | yield ident, venv 56 | 57 | 58 | class VenvProvider(BaseProvider): 59 | """A Python provider for project venv pythons""" 60 | 61 | def __init__(self, project: Project) -> None: 62 | self.project = project 63 | 64 | @classmethod 65 | def create(cls) -> t.Self | None: 66 | return None 67 | 68 | def find_pythons(self) -> t.Iterable[PythonVersion]: 69 | for _, venv in iter_venvs(self.project): 70 | yield PythonVersion(venv.interpreter, _interpreter=venv.interpreter, keep_symlink=True) 71 | 72 | 73 | def get_venv_with_name(project: Project, name: str) -> VirtualEnv: 74 | all_venvs = dict(iter_venvs(project)) 75 | try: 76 | return all_venvs[name] 77 | except KeyError: 78 | raise PdmUsageError( 79 | f"No virtualenv with key '{name}' is found, must be one of {list(all_venvs)}.\n" 80 | "You can create one with 'pdm venv create'.", 81 | ) from None 82 | -------------------------------------------------------------------------------- /src/pdm/cli/completions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/src/pdm/cli/completions/__init__.py -------------------------------------------------------------------------------- /src/pdm/cli/hooks.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import contextlib 4 | from typing import Any, Generator 5 | 6 | from pdm.project.core import Project 7 | from pdm.signals import pdm_signals 8 | 9 | 10 | class HookManager: 11 | def __init__(self, project: Project, skip: list[str] | None = None): 12 | self.project = project 13 | self.skip = skip or [] 14 | 15 | @contextlib.contextmanager 16 | def skipping(self, *names: str) -> Generator[None]: 17 | """ 18 | Temporarily skip some hooks. 19 | """ 20 | old_skip = self.skip[:] 21 | self.skip.extend(names) 22 | yield 23 | self.skip = old_skip 24 | 25 | @property 26 | def skip_all(self) -> bool: 27 | return ":all" in self.skip 28 | 29 | @property 30 | def skip_pre(self) -> bool: 31 | return ":pre" in self.skip 32 | 33 | @property 34 | def skip_post(self) -> bool: 35 | return ":post" in self.skip 36 | 37 | def should_run(self, name: str) -> bool: 38 | """ 39 | Tells whether a task given its name should run or not 40 | according to the current skipping rules. 41 | """ 42 | return ( 43 | not self.skip_all 44 | and name not in self.skip 45 | and not (self.skip_pre and name.startswith("pre_")) 46 | and not (self.skip_post and name.startswith("post_")) 47 | ) 48 | 49 | def try_emit(self, name: str, **kwargs: Any) -> None: 50 | """ 51 | Emit a hook signal if rules allow it. 52 | """ 53 | if self.should_run(name): 54 | pdm_signals.signal(name).send(self.project, hooks=self, **kwargs) 55 | -------------------------------------------------------------------------------- /src/pdm/cli/templates/default/README.md: -------------------------------------------------------------------------------- 1 | # example-package 2 | -------------------------------------------------------------------------------- /src/pdm/cli/templates/default/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/src/pdm/cli/templates/default/__init__.py -------------------------------------------------------------------------------- /src/pdm/cli/templates/default/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "example-package" 3 | version = "0.1.0" 4 | description = "Default template for PDM package" 5 | authors = [] 6 | dependencies = [] 7 | requires-python = ">=3.9" 8 | readme = "README.md" 9 | license = {text = "MIT"} 10 | 11 | [build-system] 12 | requires = ["pdm-backend"] 13 | build-backend = "pdm.backend" 14 | -------------------------------------------------------------------------------- /src/pdm/cli/templates/default/src/example_package/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/src/pdm/cli/templates/default/src/example_package/__init__.py -------------------------------------------------------------------------------- /src/pdm/cli/templates/default/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/src/pdm/cli/templates/default/tests/__init__.py -------------------------------------------------------------------------------- /src/pdm/cli/templates/minimal/.gitignore: -------------------------------------------------------------------------------- 1 | .py[cod] 2 | __pycache__/ 3 | .mypy_cache/ 4 | .pytest_cache/ 5 | .ruff_cache/ 6 | .pdm-python 7 | -------------------------------------------------------------------------------- /src/pdm/cli/templates/minimal/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/src/pdm/cli/templates/minimal/__init__.py -------------------------------------------------------------------------------- /src/pdm/cli/templates/minimal/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "example-package" 3 | version = "0.1.0" 4 | description = "Default template for PDM package" 5 | authors = [] 6 | dependencies = [] 7 | requires-python = ">=3.9" 8 | readme = "README.md" 9 | license = {text = "MIT"} 10 | 11 | [build-system] 12 | requires = ["pdm-backend"] 13 | build-backend = "pdm.backend" 14 | -------------------------------------------------------------------------------- /src/pdm/environments/__init__.py: -------------------------------------------------------------------------------- 1 | from pdm.environments.base import BareEnvironment, BaseEnvironment 2 | from pdm.environments.local import PythonLocalEnvironment 3 | from pdm.environments.python import PythonEnvironment 4 | 5 | __all__ = [ 6 | "BareEnvironment", 7 | "BaseEnvironment", 8 | "PythonEnvironment", 9 | "PythonLocalEnvironment", 10 | ] 11 | -------------------------------------------------------------------------------- /src/pdm/environments/python.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | from typing import TYPE_CHECKING 5 | 6 | from pdm.environments.base import BaseEnvironment 7 | from pdm.models.in_process import get_sys_config_paths 8 | from pdm.models.working_set import WorkingSet 9 | 10 | if TYPE_CHECKING: 11 | from pdm.project import Project 12 | 13 | 14 | class PythonEnvironment(BaseEnvironment): 15 | """A project environment that is directly derived from a Python interpreter""" 16 | 17 | def __init__( 18 | self, 19 | project: Project, 20 | *, 21 | python: str | None = None, 22 | prefix: str | None = None, 23 | extra_paths: list[str] | None = None, 24 | ) -> None: 25 | super().__init__(project, python=python) 26 | self.prefix = prefix 27 | self.extra_paths = extra_paths or [] 28 | 29 | def get_paths(self, dist_name: str | None = None) -> dict[str, str]: 30 | is_venv = self.interpreter.get_venv() is not None 31 | if self.prefix is not None: 32 | replace_vars = {"base": self.prefix, "platbase": self.prefix} 33 | kind = "prefix" 34 | else: 35 | replace_vars = None 36 | kind = "user" if not is_venv and self.project.global_config["global_project.user_site"] else "default" 37 | paths = get_sys_config_paths(str(self.interpreter.executable), replace_vars, kind=kind) 38 | if is_venv: 39 | python_xy = f"python{self.interpreter.identifier}" 40 | paths["include"] = os.path.join(paths["data"], "include", "site", python_xy) 41 | elif not dist_name: 42 | dist_name = "UNKNOWN" 43 | if dist_name: 44 | paths["include"] = os.path.join(paths["include"], dist_name) 45 | paths["prefix"] = paths["data"] 46 | paths["headers"] = paths["include"] 47 | return paths 48 | 49 | @property 50 | def process_env(self) -> dict[str, str]: 51 | env = super().process_env 52 | venv = self.interpreter.get_venv() 53 | if venv is not None and self.prefix is None: 54 | env.update(venv.env_vars()) 55 | return env 56 | 57 | def get_working_set(self) -> WorkingSet: 58 | scheme = self.get_paths() 59 | paths = [scheme["platlib"], scheme["purelib"]] 60 | venv = self.interpreter.get_venv() 61 | shared_paths = self.extra_paths[:] 62 | if venv is not None and venv.include_system_site_packages: 63 | shared_paths.extend(venv.base_paths) 64 | return WorkingSet(paths, shared_paths=list(dict.fromkeys(shared_paths))) 65 | -------------------------------------------------------------------------------- /src/pdm/exceptions.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import warnings 4 | from typing import TYPE_CHECKING 5 | 6 | if TYPE_CHECKING: 7 | from pdm.models.candidates import Candidate 8 | 9 | 10 | class PdmException(Exception): 11 | pass 12 | 13 | 14 | class ResolutionError(PdmException): 15 | pass 16 | 17 | 18 | class PdmArgumentError(PdmException): 19 | pass 20 | 21 | 22 | class PdmUsageError(PdmException): 23 | pass 24 | 25 | 26 | class RequirementError(PdmUsageError, ValueError): 27 | pass 28 | 29 | 30 | class PublishError(PdmUsageError): 31 | pass 32 | 33 | 34 | class InvalidPyVersion(PdmUsageError, ValueError): 35 | pass 36 | 37 | 38 | class CandidateNotFound(PdmException): 39 | pass 40 | 41 | 42 | class CandidateInfoNotFound(PdmException): 43 | def __init__(self, candidate: Candidate) -> None: 44 | message = f"No metadata information is available for [success]{candidate!s}[/]." 45 | self.candidate = candidate 46 | super().__init__(message) 47 | 48 | 49 | class PDMWarning(Warning): 50 | pass 51 | 52 | 53 | class PackageWarning(PDMWarning): 54 | pass 55 | 56 | 57 | class PDMDeprecationWarning(PDMWarning, DeprecationWarning): 58 | pass 59 | 60 | 61 | warnings.simplefilter("default", category=PDMDeprecationWarning) 62 | 63 | 64 | class ExtrasWarning(PDMWarning): 65 | def __init__(self, project_name: str, extras: list[str]) -> None: 66 | super().__init__(f"Extras not found for {project_name}: [{','.join(extras)}]") 67 | self.extras = tuple(extras) 68 | 69 | 70 | class ProjectError(PdmUsageError): 71 | pass 72 | 73 | 74 | class InstallationError(PdmException): 75 | pass 76 | 77 | 78 | class UninstallError(PdmException): 79 | pass 80 | 81 | 82 | class NoConfigError(PdmUsageError, KeyError): 83 | def __str__(self) -> str: 84 | return f"No such config key: {self.args[0]!r}" 85 | 86 | 87 | class NoPythonVersion(PdmUsageError): 88 | pass 89 | 90 | 91 | class BuildError(PdmException, RuntimeError): 92 | pass 93 | -------------------------------------------------------------------------------- /src/pdm/formats/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, cast 4 | 5 | from pdm.formats import flit, pipfile, poetry, requirements, setup_py 6 | from pdm.formats.base import MetaConvertError as MetaConvertError 7 | 8 | if TYPE_CHECKING: 9 | from argparse import Namespace 10 | from pathlib import Path 11 | from typing import Iterable, Mapping, Protocol, Union 12 | 13 | from pdm.models.candidates import Candidate 14 | from pdm.models.requirements import Requirement 15 | from pdm.project import Project 16 | 17 | ExportItems = Union[Iterable[Candidate], Iterable[Requirement]] 18 | 19 | class _Format(Protocol): 20 | def check_fingerprint(self, project: Project | None, filename: str | Path) -> bool: ... 21 | 22 | def convert( 23 | self, 24 | project: Project | None, 25 | filename: str | Path, 26 | options: Namespace | None, 27 | ) -> tuple[Mapping, Mapping]: ... 28 | 29 | def export(self, project: Project, candidates: ExportItems, options: Namespace | None) -> str: ... 30 | 31 | 32 | FORMATS: Mapping[str, _Format] = { 33 | "pipfile": cast("_Format", pipfile), 34 | "poetry": cast("_Format", poetry), 35 | "flit": cast("_Format", flit), 36 | "setuppy": cast("_Format", setup_py), 37 | "requirements": cast("_Format", requirements), 38 | } 39 | -------------------------------------------------------------------------------- /src/pdm/formats/pipfile.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import functools 4 | import operator 5 | import os 6 | from typing import TYPE_CHECKING, Any 7 | 8 | from packaging.markers import default_environment 9 | 10 | from pdm.compat import tomllib 11 | from pdm.formats.base import make_array 12 | from pdm.models.markers import Marker, get_marker 13 | from pdm.models.requirements import FileRequirement, Requirement 14 | 15 | if TYPE_CHECKING: 16 | from argparse import Namespace 17 | from os import PathLike 18 | 19 | from pdm._types import RequirementDict 20 | from pdm.models.backends import BuildBackend 21 | from pdm.project import Project 22 | 23 | MARKER_KEYS = list(default_environment().keys()) 24 | 25 | 26 | def convert_pipfile_requirement(name: str, req: RequirementDict, backend: BuildBackend) -> str: 27 | if isinstance(req, dict): 28 | markers: list[Marker] = [] 29 | if "markers" in req: 30 | markers.append(get_marker(req["markers"])) # type: ignore[arg-type] 31 | for key in MARKER_KEYS: 32 | if key in req: 33 | marker = get_marker(f"{key}{req[key]}") 34 | markers.append(marker) 35 | del req[key] 36 | 37 | if markers: 38 | marker = functools.reduce(operator.and_, markers) 39 | req["marker"] = str(marker).replace('"', "'") 40 | r = Requirement.from_req_dict(name, req) 41 | if isinstance(r, FileRequirement): 42 | r.relocate(backend) 43 | return r.as_line() 44 | 45 | 46 | def check_fingerprint(project: Project, filename: PathLike) -> bool: 47 | return os.path.basename(filename) == "Pipfile" 48 | 49 | 50 | def convert(project: Project, filename: PathLike, options: Namespace | None) -> tuple[dict[str, Any], dict[str, Any]]: 51 | with open(filename, "rb") as fp: 52 | data = tomllib.load(fp) 53 | result = {} 54 | settings: dict[str, Any] = {} 55 | backend = project.backend 56 | if "pipenv" in data and "allow_prereleases" in data["pipenv"]: 57 | settings.setdefault("resolution", {})["allow-prereleases"] = data["pipenv"]["allow_prereleases"] 58 | if "requires" in data: 59 | python_version = data["requires"].get("python_full_version") or data["requires"].get("python_version") 60 | result["requires-python"] = f">={python_version}" 61 | if "source" in data: 62 | settings["source"] = data["source"] 63 | result["dependencies"] = make_array( # type: ignore[assignment] 64 | [convert_pipfile_requirement(k, req, backend) for k, req in data.get("packages", {}).items()], 65 | True, 66 | ) 67 | settings["dev-dependencies"] = { 68 | "dev": make_array( 69 | [convert_pipfile_requirement(k, req, backend) for k, req in data.get("dev-packages", {}).items()], 70 | True, 71 | ) 72 | } 73 | return result, settings 74 | 75 | 76 | def export(project: Project, candidates: list, options: Any) -> None: 77 | raise NotImplementedError() 78 | -------------------------------------------------------------------------------- /src/pdm/formats/setup_py.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | from pathlib import Path 5 | from typing import TYPE_CHECKING, Any, Mapping 6 | 7 | from pdm.formats.base import array_of_inline_tables, make_array, make_inline_table 8 | 9 | if TYPE_CHECKING: 10 | from pdm.project import Project 11 | 12 | 13 | def check_fingerprint(project: Project, filename: Path) -> bool: 14 | return os.path.basename(filename) in ("setup.py", "setup.cfg") 15 | 16 | 17 | def convert(project: Project, filename: Path, options: Any | None) -> tuple[Mapping[str, Any], Mapping[str, Any]]: 18 | from pdm.models.in_process import parse_setup_py 19 | 20 | parsed = parse_setup_py(str(project.environment.interpreter.executable), os.path.dirname(filename)) 21 | metadata: dict[str, Any] = {} 22 | settings: dict[str, Any] = {} 23 | for name in [ 24 | "name", 25 | "version", 26 | "description", 27 | "keywords", 28 | "urls", 29 | "readme", 30 | ]: 31 | if name in parsed: 32 | metadata[name] = parsed[name] 33 | if "authors" in parsed: 34 | metadata["authors"] = array_of_inline_tables(parsed["authors"]) 35 | if "maintainers" in parsed: 36 | metadata["maintainers"] = array_of_inline_tables(parsed["maintainers"]) 37 | if "classifiers" in parsed: 38 | metadata["classifiers"] = make_array(sorted(parsed["classifiers"]), True) 39 | if "python_requires" in parsed: 40 | metadata["requires-python"] = parsed["python_requires"] 41 | if "install_requires" in parsed: 42 | metadata["dependencies"] = make_array(sorted(parsed["install_requires"]), True) 43 | if "extras_require" in parsed: 44 | metadata["optional-dependencies"] = { 45 | k: make_array(sorted(v), True) for k, v in parsed["extras_require"].items() 46 | } 47 | if "license" in parsed: 48 | metadata["license"] = make_inline_table({"text": parsed["license"]}) 49 | if "package_dir" in parsed: 50 | settings["package-dir"] = parsed["package_dir"] 51 | 52 | entry_points = parsed.get("entry_points", {}) 53 | if "console_scripts" in entry_points: 54 | metadata["scripts"] = entry_points.pop("console_scripts") 55 | if "gui_scripts" in entry_points: 56 | metadata["gui-scripts"] = entry_points.pop("gui_scripts") 57 | if entry_points: 58 | metadata["entry-points"] = entry_points 59 | # reset the environment as `requires-python` may change 60 | project.environment = None # type: ignore[assignment] 61 | return metadata, settings 62 | 63 | 64 | def export(project: Project, candidates: list, options: Any | None) -> str: 65 | raise NotImplementedError() 66 | -------------------------------------------------------------------------------- /src/pdm/installers/__init__.py: -------------------------------------------------------------------------------- 1 | from pdm.installers.base import BaseSynchronizer 2 | from pdm.installers.manager import InstallManager 3 | from pdm.installers.synchronizers import Synchronizer 4 | from pdm.installers.uv import UvSynchronizer 5 | 6 | __all__ = ["BaseSynchronizer", "InstallManager", "Synchronizer", "UvSynchronizer"] 7 | -------------------------------------------------------------------------------- /src/pdm/installers/core.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Iterable 4 | 5 | from pdm.environments import BaseEnvironment 6 | from pdm.models.requirements import Requirement 7 | from pdm.resolver.reporters import LockReporter 8 | 9 | 10 | def install_requirements( 11 | reqs: Iterable[Requirement], 12 | environment: BaseEnvironment, 13 | clean: bool = False, 14 | use_install_cache: bool = False, 15 | allow_uv: bool = True, 16 | ) -> None: # pragma: no cover 17 | """Resolve and install the given requirements into the environment.""" 18 | reqs = [req for req in reqs if not req.marker or req.marker.matches(environment.spec)] 19 | reporter = LockReporter() 20 | project = environment.project 21 | backend = project.backend 22 | for req in reqs: 23 | if req.is_file_or_url: 24 | req.relocate(backend) # type: ignore[attr-defined] 25 | resolver = project.get_resolver(allow_uv=allow_uv)( 26 | environment=environment, 27 | requirements=reqs, 28 | update_strategy="all", 29 | strategies=project.lockfile.default_strategies, 30 | target=environment.spec, 31 | tracked_names=(), 32 | keep_self=True, 33 | reporter=reporter, 34 | ) 35 | resolved = resolver.resolve().packages 36 | syncer = environment.project.get_synchronizer(quiet=True, allow_uv=allow_uv)( 37 | environment, 38 | clean=clean, 39 | retry_times=0, 40 | install_self=False, 41 | use_install_cache=use_install_cache, 42 | packages=resolved, 43 | requirements=reqs, 44 | ) 45 | syncer.synchronize() 46 | -------------------------------------------------------------------------------- /src/pdm/installers/manager.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from pdm import termui 6 | from pdm.compat import Distribution 7 | from pdm.exceptions import UninstallError 8 | from pdm.installers.installers import install_wheel 9 | from pdm.installers.uninstallers import BaseRemovePaths, StashedRemovePaths 10 | 11 | if TYPE_CHECKING: 12 | from pdm.environments import BaseEnvironment 13 | from pdm.models.candidates import Candidate 14 | 15 | 16 | class InstallManager: 17 | """The manager that performs the installation and uninstallation actions.""" 18 | 19 | # The packages below are needed to load paths and thus should not be cached. 20 | NO_CACHE_PACKAGES = ("editables",) 21 | 22 | def __init__( 23 | self, environment: BaseEnvironment, *, use_install_cache: bool = False, rename_pth: bool = False 24 | ) -> None: 25 | self.environment = environment 26 | self.use_install_cache = use_install_cache 27 | self.rename_pth = rename_pth 28 | 29 | def install(self, candidate: Candidate) -> Distribution: 30 | """Install a candidate into the environment, return the distribution""" 31 | prepared = candidate.prepare(self.environment) 32 | dist_info = install_wheel( 33 | prepared.build(), 34 | self.environment, 35 | direct_url=prepared.direct_url(), 36 | install_links=self.use_install_cache and not candidate.req.editable, 37 | rename_pth=self.rename_pth, 38 | requested=candidate.requested, 39 | ) 40 | return Distribution.at(dist_info) 41 | 42 | def get_paths_to_remove(self, dist: Distribution) -> BaseRemovePaths: 43 | """Get the path collection to be removed from the disk""" 44 | return StashedRemovePaths.from_dist(dist, environment=self.environment) 45 | 46 | def uninstall(self, dist: Distribution) -> None: 47 | """Perform the uninstallation for a given distribution""" 48 | remove_path = self.get_paths_to_remove(dist) 49 | dist_name = dist.metadata.get("Name") 50 | termui.logger.info("Removing distribution %s", dist_name) 51 | try: 52 | remove_path.remove() 53 | remove_path.commit() 54 | except OSError as e: 55 | termui.logger.warning("Error occurred during uninstallation, roll back the changes now.") 56 | remove_path.rollback() 57 | raise UninstallError(e) from e 58 | 59 | def overwrite(self, dist: Distribution, candidate: Candidate) -> None: 60 | """An in-place update to overwrite the distribution with a new candidate""" 61 | paths_to_remove = self.get_paths_to_remove(dist) 62 | termui.logger.info("Overwriting distribution %s", dist.metadata.get("Name")) 63 | installed = self.install(candidate) 64 | installed_paths = self.get_paths_to_remove(installed) 65 | # Remove the paths that are in the new distribution 66 | paths_to_remove.difference_update(installed_paths) 67 | try: 68 | paths_to_remove.remove() 69 | paths_to_remove.commit() 70 | except OSError as e: 71 | termui.logger.warning("Error occurred during overwriting, roll back the changes now.") 72 | paths_to_remove.rollback() 73 | raise UninstallError(e) from e 74 | -------------------------------------------------------------------------------- /src/pdm/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/src/pdm/models/__init__.py -------------------------------------------------------------------------------- /src/pdm/models/cached_package.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import shutil 5 | from functools import cached_property 6 | from pathlib import Path 7 | from typing import Any, ClassVar, ContextManager 8 | 9 | from pdm.termui import logger 10 | 11 | 12 | class CachedPackage: 13 | """A package cached in the central package store. 14 | The directory name is similar to wheel's filename: 15 | 16 | $PACKAGE_ROOT//----/ 17 | 18 | The checksum is stored in a file named `.checksum` under the directory. 19 | 20 | Under the directory there could be a text file named `.referrers`. 21 | Each line of the file is a distribution path that refers to this package. 22 | *Only wheel installations will be cached* 23 | """ 24 | 25 | cache_files: ClassVar[tuple[str, ...]] = (".lock", ".checksum", ".referrers") 26 | """List of files storing cache metadata and not being part of the package""" 27 | 28 | def __init__(self, path: str | Path, original_wheel: Path | None = None) -> None: 29 | self.path = Path(os.path.normcase(os.path.expanduser(path))).resolve() 30 | self.original_wheel = original_wheel 31 | self._referrers: set[str] | None = None 32 | 33 | def lock(self) -> ContextManager[Any]: 34 | import filelock 35 | 36 | return filelock.FileLock(self.path / ".lock") 37 | 38 | @cached_property 39 | def checksum(self) -> str: 40 | """The checksum of the path""" 41 | return self.path.joinpath(".checksum").read_text().strip() 42 | 43 | @cached_property 44 | def dist_info(self) -> Path: 45 | """The dist-info directory of the wheel""" 46 | from installer.exceptions import InvalidWheelSource 47 | 48 | try: 49 | return next(self.path.glob("*.dist-info")) 50 | except StopIteration: 51 | raise InvalidWheelSource(f"The wheel doesn't contain metadata {self.path!r}") from None 52 | 53 | @property 54 | def referrers(self) -> set[str]: 55 | """A set of entries in referrers file""" 56 | if self._referrers is None: 57 | filepath = self.path / ".referrers" 58 | if not filepath.is_file(): 59 | return set() 60 | self._referrers = { 61 | line.strip() 62 | for line in filepath.read_text("utf8").splitlines() 63 | if line.strip() and os.path.exists(line.strip()) 64 | } 65 | return self._referrers 66 | 67 | def add_referrer(self, path: str) -> None: 68 | """Add a new referrer""" 69 | path = os.path.normcase(os.path.expanduser(os.path.abspath(path))) 70 | referrers = self.referrers | {path} 71 | (self.path / ".referrers").write_text("\n".join(sorted(referrers)) + "\n", "utf8") 72 | self._referrers = None 73 | 74 | def remove_referrer(self, path: str) -> None: 75 | """Remove a referrer""" 76 | path = os.path.normcase(os.path.expanduser(os.path.abspath(path))) 77 | referrers = self.referrers - {path} 78 | (self.path / ".referrers").write_text("\n".join(referrers) + "\n", "utf8") 79 | self._referrers = None 80 | 81 | def cleanup(self) -> None: 82 | logger.info("Clean up cached package %s", self.path) 83 | shutil.rmtree(self.path) 84 | -------------------------------------------------------------------------------- /src/pdm/models/in_process/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | A collection of functions that need to be called via a subprocess call. 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | import contextlib 8 | import functools 9 | import json 10 | import os 11 | import subprocess 12 | import tempfile 13 | from typing import Any, Generator 14 | 15 | from pdm.compat import resources_path 16 | from pdm.models.markers import EnvSpec 17 | 18 | 19 | @contextlib.contextmanager 20 | def _in_process_script(name: str) -> Generator[str, None, None]: 21 | with resources_path(__name__, name) as script: 22 | yield str(script) 23 | 24 | 25 | def get_sys_config_paths(executable: str, vars: dict[str, str] | None = None, kind: str = "default") -> dict[str, str]: 26 | """Return the sys_config.get_paths() result for the python interpreter""" 27 | env = os.environ.copy() 28 | env.pop("__PYVENV_LAUNCHER__", None) 29 | if vars is not None: 30 | env["_SYSCONFIG_VARS"] = json.dumps(vars) 31 | 32 | with _in_process_script("sysconfig_get_paths.py") as script: 33 | cmd = [executable, "-Es", script, kind] 34 | return json.loads(subprocess.check_output(cmd, env=env)) 35 | 36 | 37 | def parse_setup_py(executable: str, path: str) -> dict[str, Any]: 38 | """Parse setup.py and return the kwargs""" 39 | with _in_process_script("parse_setup.py") as script: 40 | _, outfile = tempfile.mkstemp(suffix=".json") 41 | cmd = [executable, script, path, outfile] 42 | subprocess.check_call(cmd) 43 | with open(outfile, "rb") as fp: 44 | return json.load(fp) 45 | 46 | 47 | @functools.lru_cache 48 | def get_env_spec(executable: str) -> EnvSpec: 49 | """Get the environment spec of the python interpreter""" 50 | from pdm.core import importlib_metadata 51 | 52 | required_libs = ["dep_logic", "packaging"] 53 | shared_libs = {str(importlib_metadata.distribution(lib).locate_file("")) for lib in required_libs} 54 | 55 | with _in_process_script("env_spec.py") as script: 56 | return EnvSpec.from_spec(**json.loads(subprocess.check_output([executable, "-EsS", script, *shared_libs]))) 57 | -------------------------------------------------------------------------------- /src/pdm/models/in_process/env_spec.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | import platform 5 | import site 6 | import sys 7 | import sysconfig 8 | 9 | 10 | def get_current_env_spec() -> dict[str, str | bool]: 11 | from dep_logic.tags import Platform 12 | 13 | python_version = f"{sys.version_info[0]}.{sys.version_info[1]}.{sys.version_info[2]}" 14 | return { 15 | "requires_python": f"=={python_version}", 16 | "platform": str(Platform.current()), 17 | "implementation": platform.python_implementation().lower(), 18 | "gil_disabled": sysconfig.get_config_var("Py_GIL_DISABLED") or False, 19 | } 20 | 21 | 22 | if __name__ == "__main__": 23 | for shared_lib in sys.argv[1:]: 24 | site.addsitedir(shared_lib) 25 | print(json.dumps(get_current_env_spec(), indent=2)) 26 | -------------------------------------------------------------------------------- /src/pdm/models/in_process/sysconfig_get_paths.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | import sysconfig 5 | 6 | 7 | def _running_under_venv(): 8 | """This handles PEP 405 compliant virtual environments.""" 9 | return sys.prefix != getattr(sys, "base_prefix", sys.prefix) 10 | 11 | 12 | def _running_under_regular_virtualenv(): 13 | """This handles virtual environments created with pypa's virtualenv.""" 14 | # pypa/virtualenv case 15 | return hasattr(sys, "real_prefix") 16 | 17 | 18 | def running_under_virtualenv(): 19 | """Return True if we're running inside a virtualenv, False otherwise.""" 20 | return _running_under_venv() or _running_under_regular_virtualenv() 21 | 22 | 23 | def _get_user_scheme(): 24 | if os.name == "nt": 25 | return "nt_user" 26 | if sys.platform == "darwin" and sys._framework: 27 | return "osx_framework_user" 28 | return "posix_user" 29 | 30 | 31 | def get_paths(kind="default", vars=None): 32 | scheme_names = sysconfig.get_scheme_names() 33 | if kind == "user" and not running_under_virtualenv(): 34 | scheme = _get_user_scheme() 35 | if scheme not in scheme_names: 36 | raise ValueError(f"{scheme} is not a valid scheme on the system, or user site may be disabled.") 37 | return sysconfig.get_paths(scheme, vars=vars) 38 | else: 39 | if ( 40 | (sys.platform == "darwin" and "osx_framework_library" in scheme_names) or sys.platform == "linux" 41 | ) and kind == "prefix": 42 | return sysconfig.get_paths("posix_prefix", vars=vars) 43 | return sysconfig.get_paths(vars=vars) 44 | 45 | 46 | def main(): 47 | vars = None 48 | if "_SYSCONFIG_VARS" in os.environ: 49 | vars = json.loads(os.environ["_SYSCONFIG_VARS"]) 50 | kind = sys.argv[1] if len(sys.argv) > 1 else "default" 51 | print(json.dumps(get_paths(kind, vars))) 52 | 53 | 54 | if __name__ == "__main__": 55 | main() 56 | -------------------------------------------------------------------------------- /src/pdm/models/python.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | from functools import cached_property 5 | from pathlib import Path 6 | from typing import TYPE_CHECKING, Any 7 | 8 | from packaging.version import InvalidVersion, Version 9 | 10 | from pdm.models.venv import VirtualEnv 11 | 12 | if TYPE_CHECKING: 13 | from findpython import PythonVersion 14 | 15 | 16 | class PythonInfo: 17 | """ 18 | A convenient helper class that holds all information of a Python interpreter. 19 | """ 20 | 21 | def __init__(self, py_version: PythonVersion) -> None: 22 | self._py_ver = py_version 23 | 24 | @classmethod 25 | def from_path(cls, path: str | Path) -> PythonInfo: 26 | from findpython import PythonVersion 27 | 28 | py_ver = PythonVersion(Path(path)) 29 | return cls(py_ver) 30 | 31 | @cached_property 32 | def valid(self) -> bool: 33 | return self._py_ver.executable.exists() and self._py_ver.is_valid() 34 | 35 | def __hash__(self) -> int: 36 | return hash(self._py_ver) 37 | 38 | def __eq__(self, o: Any) -> bool: 39 | if not isinstance(o, PythonInfo): 40 | return False 41 | return self.path == o.path 42 | 43 | @property 44 | def path(self) -> Path: 45 | return self._py_ver.executable 46 | 47 | @property 48 | def executable(self) -> Path: 49 | return self._py_ver.interpreter 50 | 51 | @cached_property 52 | def version(self) -> Version: 53 | return self._py_ver.version 54 | 55 | @cached_property 56 | def implementation(self) -> str: 57 | return self._py_ver.implementation.lower() 58 | 59 | @property 60 | def major(self) -> int: 61 | return self.version.major 62 | 63 | @property 64 | def minor(self) -> int: 65 | return self.version.minor 66 | 67 | @property 68 | def micro(self) -> int: 69 | return self.version.micro 70 | 71 | @property 72 | def version_tuple(self) -> tuple[int, ...]: 73 | return (self.major, self.minor, self.micro) 74 | 75 | @property 76 | def is_32bit(self) -> bool: 77 | return "32bit" in self._py_ver.architecture 78 | 79 | def for_tag(self) -> str: 80 | return f"{self.major}{self.minor}" 81 | 82 | @property 83 | def identifier(self) -> str: 84 | try: 85 | if os.name == "nt" and self.is_32bit: 86 | return f"{self.major}.{self.minor}-32" 87 | return f"{self.major}.{self.minor}" 88 | except InvalidVersion: 89 | return "unknown" 90 | 91 | def get_venv(self) -> VirtualEnv | None: 92 | return VirtualEnv.from_interpreter(self.executable) 93 | -------------------------------------------------------------------------------- /src/pdm/models/python_max_versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "2": 7, 3 | "2.0": 1, 4 | "2.1": 3, 5 | "2.2": 3, 6 | "2.3": 7, 7 | "2.4": 6, 8 | "2.5": 6, 9 | "2.6": 9, 10 | "2.7": 18, 11 | "3": 13, 12 | "3.0": 1, 13 | "3.1": 5, 14 | "3.10": 17, 15 | "3.11": 12, 16 | "3.12": 10, 17 | "3.13": 3, 18 | "3.2": 6, 19 | "3.3": 7, 20 | "3.4": 10, 21 | "3.5": 10, 22 | "3.6": 15, 23 | "3.7": 17, 24 | "3.8": 20, 25 | "3.9": 22 26 | } 27 | -------------------------------------------------------------------------------- /src/pdm/models/repositories/__init__.py: -------------------------------------------------------------------------------- 1 | from pdm.models.repositories.base import BaseRepository as BaseRepository 2 | from pdm.models.repositories.base import CandidateMetadata as CandidateMetadata 3 | from pdm.models.repositories.lock import LockedRepository as LockedRepository 4 | from pdm.models.repositories.lock import Package as Package 5 | from pdm.models.repositories.pypi import PyPIRepository as PyPIRepository 6 | -------------------------------------------------------------------------------- /src/pdm/models/search.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import functools 4 | from dataclasses import dataclass 5 | from html.parser import HTMLParser 6 | from typing import Callable 7 | 8 | from pdm._types import SearchResult 9 | 10 | 11 | @dataclass 12 | class Result: 13 | name: str = "" 14 | version: str = "" 15 | description: str = "" 16 | 17 | def as_frozen(self) -> SearchResult: 18 | return SearchResult(self.name, self.version, self.description) 19 | 20 | 21 | class SearchResultParser(HTMLParser): 22 | """A simple HTML parser for pypi.org search results.""" 23 | 24 | def __init__(self) -> None: 25 | super().__init__() 26 | self.results: list[SearchResult] = [] 27 | self._current: Result | None = None 28 | self._nest_anchors = 0 29 | self._data_callback: Callable[[str], None] | None = None 30 | 31 | @staticmethod 32 | def _match_class(attrs: list[tuple[str, str | None]], name: str) -> bool: 33 | attrs_map = dict(attrs) 34 | return name in (attrs_map.get("class") or "").split() 35 | 36 | def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: 37 | if not self._current: 38 | if tag == "a" and self._match_class(attrs, "package-snippet"): 39 | self._current = Result() 40 | self._nest_anchors = 1 41 | else: 42 | if tag == "span" and self._match_class(attrs, "package-snippet__name"): 43 | self._data_callback = functools.partial(setattr, self._current, "name") 44 | elif tag == "span" and self._match_class(attrs, "package-snippet__version"): 45 | self._data_callback = functools.partial(setattr, self._current, "version") 46 | elif tag == "p" and self._match_class(attrs, "package-snippet__description"): 47 | self._data_callback = functools.partial(setattr, self._current, "description") 48 | elif tag == "a": 49 | self._nest_anchors += 1 50 | 51 | def handle_data(self, data: str) -> None: 52 | if self._data_callback is not None: 53 | self._data_callback(data) 54 | self._data_callback = None 55 | 56 | def handle_endtag(self, tag: str) -> None: 57 | if tag != "a" or self._current is None: 58 | return 59 | self._nest_anchors -= 1 60 | if self._nest_anchors == 0: 61 | if self._current.name: 62 | self.results.append(self._current.as_frozen()) 63 | self._current = None 64 | -------------------------------------------------------------------------------- /src/pdm/models/venv.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import dataclasses as dc 4 | import sys 5 | from functools import cached_property 6 | from pathlib import Path 7 | 8 | from pdm.models.in_process import get_sys_config_paths 9 | from pdm.utils import find_python_in_path, get_venv_like_prefix 10 | 11 | IS_WIN = sys.platform == "win32" 12 | BIN_DIR = "Scripts" if IS_WIN else "bin" 13 | 14 | 15 | def get_venv_python(venv: Path) -> Path: 16 | """Get the interpreter path inside the given venv.""" 17 | suffix = ".exe" if IS_WIN else "" 18 | result = venv / BIN_DIR / f"python{suffix}" 19 | if IS_WIN and not result.exists(): 20 | result = venv / "bin" / f"python{suffix}" # for mingw64/msys2 21 | if result.exists(): 22 | return result 23 | else: 24 | return venv / "python.exe" # for conda 25 | return result 26 | 27 | 28 | def is_conda_venv(root: Path) -> bool: 29 | return (root / "conda-meta").exists() 30 | 31 | 32 | @dc.dataclass(frozen=True) 33 | class VirtualEnv: 34 | root: Path 35 | is_conda: bool 36 | interpreter: Path 37 | 38 | @classmethod 39 | def get(cls, root: Path) -> VirtualEnv | None: 40 | path = get_venv_python(root) 41 | if not path.exists(): 42 | return None 43 | return cls(root, is_conda_venv(root), path) 44 | 45 | @classmethod 46 | def from_interpreter(cls, interpreter: Path) -> VirtualEnv | None: 47 | root, is_conda = get_venv_like_prefix(interpreter) 48 | if root is not None: 49 | return cls(root, is_conda, interpreter) 50 | return None 51 | 52 | def env_vars(self) -> dict[str, str]: 53 | key = "CONDA_PREFIX" if self.is_conda else "VIRTUAL_ENV" 54 | return {key: str(self.root)} 55 | 56 | @cached_property 57 | def venv_config(self) -> dict[str, str]: 58 | venv_cfg = self.root / "pyvenv.cfg" 59 | if not venv_cfg.exists(): 60 | return {} 61 | parsed: dict[str, str] = {} 62 | with venv_cfg.open(encoding="utf-8") as fp: 63 | for line in fp: 64 | if "=" in line: 65 | k, v = line.split("=", 1) 66 | k = k.strip().lower() 67 | v = v.strip() 68 | if k == "include-system-site-packages": 69 | v = v.lower() 70 | parsed[k] = v 71 | return parsed 72 | 73 | @property 74 | def include_system_site_packages(self) -> bool: 75 | return self.venv_config.get("include-system-site-packages") == "true" 76 | 77 | @cached_property 78 | def base_paths(self) -> list[str]: 79 | home = Path(self.venv_config["home"]) 80 | base_executable = find_python_in_path(home) or find_python_in_path(home.parent) 81 | assert base_executable is not None 82 | paths = get_sys_config_paths(str(base_executable)) 83 | return [paths["purelib"], paths["platlib"]] 84 | -------------------------------------------------------------------------------- /src/pdm/models/working_set.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import itertools 4 | import sys 5 | from collections import ChainMap 6 | from pathlib import Path 7 | from typing import Iterable, Iterator, Mapping 8 | 9 | from pdm.compat import importlib_metadata as im 10 | from pdm.utils import normalize_name 11 | 12 | default_context = im.DistributionFinder.Context() 13 | 14 | 15 | class EgglinkFinder(im.DistributionFinder): 16 | @classmethod 17 | def find_distributions(cls, context: im.DistributionFinder.Context = default_context) -> Iterable[im.Distribution]: 18 | found_links = cls._search_paths(context.name, context.path) 19 | # For Py3.7 compatibility, handle both classmethod and instance method 20 | meta_finder = im.MetadataPathFinder() 21 | for link in found_links: 22 | name = link.stem 23 | with link.open("rb") as file_link: 24 | link_pointer = Path(file_link.readline().decode().strip()) 25 | dist = next( 26 | iter( 27 | meta_finder.find_distributions(im.DistributionFinder.Context(name=name, path=[str(link_pointer)])) 28 | ), 29 | None, 30 | ) 31 | if not dist: 32 | continue 33 | dist.link_file = link.absolute() # type: ignore[attr-defined] 34 | yield dist 35 | 36 | @classmethod 37 | def _search_paths(cls, name: str | None, paths: list[str]) -> Iterable[Path]: 38 | for path in paths: 39 | if name: 40 | if Path(path).joinpath(f"{name}.egg-link").is_file(): 41 | yield Path(path).joinpath(f"{name}.egg-link") 42 | else: 43 | yield from Path(path).glob("*.egg-link") 44 | 45 | 46 | def distributions(path: list[str]) -> Iterable[im.Distribution]: 47 | """Find distributions in the paths. Similar to `importlib.metadata`'s 48 | implementation but with the ability to discover egg-links. 49 | """ 50 | context = im.DistributionFinder.Context(path=path) 51 | resolvers = itertools.chain( 52 | filter( 53 | None, 54 | (getattr(finder, "find_distributions", None) for finder in sys.meta_path), 55 | ), 56 | (EgglinkFinder.find_distributions,), 57 | ) 58 | return itertools.chain.from_iterable(resolver(context) for resolver in resolvers) 59 | 60 | 61 | class WorkingSet(Mapping[str, im.Distribution]): 62 | """A dictionary of currently installed distributions""" 63 | 64 | def __init__(self, paths: list[str] | None = None, shared_paths: list[str] | None = None) -> None: 65 | if paths is None: 66 | paths = sys.path 67 | if shared_paths is None: 68 | shared_paths = [] 69 | self._dist_map = { 70 | normalize_name(dist.metadata["Name"]): dist 71 | for dist in distributions(path=list(dict.fromkeys(paths))) 72 | if dist.metadata.get("Name") 73 | } 74 | self._shared_map = { 75 | normalize_name(dist.metadata["Name"]): dist 76 | for dist in distributions(path=list(dict.fromkeys(shared_paths))) 77 | if dist.metadata.get("Name") 78 | } 79 | self._iter_map = ChainMap(self._dist_map, self._shared_map) 80 | 81 | def __getitem__(self, key: str) -> im.Distribution: 82 | return self._iter_map[key] 83 | 84 | def is_owned(self, key: str) -> bool: 85 | return key in self._dist_map 86 | 87 | def __len__(self) -> int: 88 | return len(self._iter_map) 89 | 90 | def __iter__(self) -> Iterator[str]: 91 | return iter(self._iter_map) 92 | 93 | def __repr__(self) -> str: 94 | return repr(self._iter_map) 95 | -------------------------------------------------------------------------------- /src/pdm/pep582/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/src/pdm/pep582/__init__.py -------------------------------------------------------------------------------- /src/pdm/project/__init__.py: -------------------------------------------------------------------------------- 1 | from pdm.project.config import Config, ConfigItem 2 | from pdm.project.core import Project 3 | 4 | __all__ = ["Config", "ConfigItem", "Project"] 5 | -------------------------------------------------------------------------------- /src/pdm/project/lockfile/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | from typing import TYPE_CHECKING 6 | 7 | from pdm.project.lockfile.base import ( 8 | FLAG_CROSS_PLATFORM, 9 | FLAG_DIRECT_MINIMAL_VERSIONS, 10 | FLAG_INHERIT_METADATA, 11 | FLAG_STATIC_URLS, 12 | Lockfile, 13 | ) 14 | from pdm.project.lockfile.pdmlock import PDMLock 15 | from pdm.project.lockfile.pylock import PyLock 16 | 17 | if sys.version_info >= (3, 11): 18 | import tomllib 19 | else: 20 | import tomli as tomllib 21 | 22 | 23 | if TYPE_CHECKING: 24 | from pdm.project import Project 25 | 26 | __all__ = [ 27 | "FLAG_CROSS_PLATFORM", 28 | "FLAG_DIRECT_MINIMAL_VERSIONS", 29 | "FLAG_INHERIT_METADATA", 30 | "FLAG_STATIC_URLS", 31 | "Lockfile", 32 | "PDMLock", 33 | "PyLock", 34 | "load_lockfile", 35 | ] 36 | 37 | 38 | def load_lockfile(project: Project, path: str | Path) -> Lockfile: 39 | """Load a lockfile from the given path.""" 40 | 41 | default_lockfile = PyLock if project.config["lock.format"] == "pylock" else PDMLock 42 | 43 | try: 44 | with open(path, "rb") as f: 45 | data = tomllib.load(f) 46 | except OSError: 47 | return default_lockfile(path, ui=project.core.ui) 48 | else: 49 | if data.get("metadata", {}).get("lock_version"): 50 | return PDMLock(path, ui=project.core.ui) 51 | elif data.get("lock-version"): 52 | return PyLock(path, ui=project.core.ui) 53 | else: # pragma: no cover 54 | return default_lockfile(path, ui=project.core.ui) 55 | -------------------------------------------------------------------------------- /src/pdm/project/lockfile/pylock.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from functools import cached_property 4 | from typing import Iterable 5 | 6 | from pdm.exceptions import PdmUsageError 7 | from pdm.models.repositories.lock import LockedRepository 8 | from pdm.project.lockfile.base import ( 9 | FLAG_DIRECT_MINIMAL_VERSIONS, 10 | FLAG_INHERIT_METADATA, 11 | FLAG_STATIC_URLS, 12 | Compatibility, 13 | Lockfile, 14 | ) 15 | 16 | 17 | class PyLock(Lockfile): 18 | SUPPORTED_FLAGS = frozenset([FLAG_DIRECT_MINIMAL_VERSIONS, FLAG_INHERIT_METADATA, FLAG_STATIC_URLS]) 19 | 20 | @property 21 | def hash(self) -> tuple[str, str]: 22 | return next(iter(self._data.get("tool", {}).get("pdm", {}).get("hashes", {}).items()), ("", "")) 23 | 24 | def update_hash(self, hash_value: str, algo: str = "sha256") -> None: 25 | self._data.setdefault("tool", {}).setdefault("pdm", {}).setdefault("hashes", {})[algo] = hash_value 26 | 27 | @property 28 | def groups(self) -> list[str] | None: 29 | return [*self._data.get("dependency-groups", []), *self._data.get("extras", [])] 30 | 31 | @cached_property 32 | def default_strategies(self) -> set[str]: 33 | return {FLAG_INHERIT_METADATA, FLAG_STATIC_URLS} 34 | 35 | @property 36 | def strategy(self) -> set[str]: 37 | return set(self._data.get("tool", {}).get("pdm", {}).get("strategy", self.default_strategies)) 38 | 39 | def apply_strategy_change(self, changes: Iterable[str]) -> set[str]: 40 | for change in changes: 41 | change = change.replace("-", "_").lower() 42 | if change.startswith("no_") and change[3:] != FLAG_DIRECT_MINIMAL_VERSIONS: 43 | raise PdmUsageError(f"Unsupported strategy change for pylock: {change}") 44 | return super().apply_strategy_change(changes) 45 | 46 | def format_lockfile(self, repository: LockedRepository, groups: Iterable[str] | None, strategy: set[str]) -> None: 47 | from pdm.formats.pylock import PyLockConverter 48 | 49 | converter = PyLockConverter(repository.environment.project, repository) 50 | data = converter.convert(groups) 51 | data["tool"]["pdm"]["strategy"] = sorted(strategy) 52 | self.set_data(data) 53 | 54 | def compatibility(self) -> Compatibility: # pragma: no cover 55 | return Compatibility.SAME 56 | -------------------------------------------------------------------------------- /src/pdm/project/toml_file.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | from typing import Any, Mapping 5 | 6 | import tomlkit 7 | from tomlkit.toml_document import TOMLDocument 8 | from tomlkit.toml_file import TOMLFile 9 | 10 | from pdm import termui 11 | 12 | 13 | class TOMLBase(TOMLFile): 14 | def __init__(self, path: str | Path, *, ui: termui.UI) -> None: 15 | super().__init__(path) 16 | self._path = Path(path) 17 | self.ui = ui 18 | self._data = self.read() 19 | 20 | def read(self) -> TOMLDocument: 21 | if not self._path.exists(): 22 | return tomlkit.document() 23 | return super().read() 24 | 25 | def set_data(self, data: Mapping[str, Any]) -> None: 26 | """Set the data of the TOML file.""" 27 | self._data = tomlkit.document() 28 | self._data.update(data) 29 | 30 | def reload(self) -> None: 31 | self._data = self.read() 32 | 33 | def write(self) -> None: 34 | self._path.parent.mkdir(parents=True, exist_ok=True) 35 | return super().write(self._data) 36 | 37 | def exists(self) -> bool: 38 | return self._path.exists() 39 | 40 | def empty(self) -> bool: 41 | return not self._data 42 | -------------------------------------------------------------------------------- /src/pdm/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/src/pdm/py.typed -------------------------------------------------------------------------------- /src/pdm/resolver/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import Resolver 2 | from .resolvelib import RLResolver 3 | from .uv import UvResolver 4 | 5 | __all__ = ["RLResolver", "Resolver", "UvResolver"] 6 | -------------------------------------------------------------------------------- /src/pdm/resolver/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import abc 4 | import typing as t 5 | from dataclasses import dataclass, field 6 | 7 | from resolvelib import BaseReporter 8 | 9 | from pdm.models.candidates import Candidate 10 | from pdm.models.repositories import LockedRepository 11 | 12 | if t.TYPE_CHECKING: 13 | from pdm.environments import BaseEnvironment 14 | from pdm.models.markers import EnvSpec 15 | from pdm.models.repositories import Package 16 | from pdm.models.requirements import Requirement 17 | from pdm.project import Project 18 | 19 | 20 | class Resolution(t.NamedTuple): 21 | """The resolution result.""" 22 | 23 | packages: t.Iterable[Package] 24 | """The list of pinned packages with dependencies.""" 25 | collected_groups: set[str] 26 | """The list of collected groups.""" 27 | 28 | @property 29 | def candidates(self) -> dict[str, Candidate]: 30 | return {entry.candidate.identify(): entry.candidate for entry in self.packages} 31 | 32 | 33 | @dataclass 34 | class Resolver(abc.ABC): 35 | """The resolver class.""" 36 | 37 | environment: BaseEnvironment 38 | """The environment instance.""" 39 | requirements: list[Requirement] 40 | """The list of requirements to resolve.""" 41 | update_strategy: str 42 | """The update strategy to use [all|reuse|eager|reuse-installed].""" 43 | strategies: set[str] 44 | """The list of strategies to use.""" 45 | target: EnvSpec 46 | """The target environment specification.""" 47 | tracked_names: t.Collection[str] = () 48 | """The list of tracked names.""" 49 | keep_self: bool = False 50 | """Whether to keep self dependencies.""" 51 | locked_repository: LockedRepository | None = None 52 | """The repository with all locked dependencies.""" 53 | reporter: BaseReporter = field(default_factory=BaseReporter) 54 | """The reporter to use.""" 55 | requested_groups: set[str] = field(default_factory=set, init=False) 56 | """The list of requested groups.""" 57 | 58 | def __post_init__(self) -> None: 59 | self.requested_groups = {g for r in self.requirements for g in r.groups} 60 | 61 | @abc.abstractmethod 62 | def resolve(self) -> Resolution: 63 | """Resolve the requirements.""" 64 | pass 65 | 66 | @property 67 | def project(self) -> Project: 68 | """The project instance.""" 69 | return self.environment.project 70 | -------------------------------------------------------------------------------- /src/pdm/resolver/python.py: -------------------------------------------------------------------------------- 1 | """ 2 | Special requirement and candidate classes to describe a requires-python constraint 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | from typing import Iterable, Iterator, Mapping, cast 8 | 9 | from pdm.models.candidates import Candidate 10 | from pdm.models.requirements import NamedRequirement, Requirement 11 | from pdm.models.specifiers import PySpecSet 12 | 13 | 14 | class PythonCandidate(Candidate): 15 | def format(self) -> str: 16 | return f"[req]{self.name}[/][warning]{self.req.specifier!s}[/]" 17 | 18 | 19 | class PythonRequirement(NamedRequirement): 20 | @classmethod 21 | def from_pyspec_set(cls, spec: PySpecSet) -> PythonRequirement: 22 | return cls(name="python", specifier=spec) 23 | 24 | def as_candidate(self) -> PythonCandidate: 25 | return PythonCandidate(self) 26 | 27 | 28 | def find_python_matches( 29 | identifier: str, 30 | requirements: Mapping[str, Iterator[Requirement]], 31 | ) -> Iterable[Candidate]: 32 | """All requires-python except for the first one(must come from the project) 33 | must be superset of the first one. 34 | """ 35 | python_reqs = cast(Iterator[PythonRequirement], iter(requirements[identifier])) 36 | project_req = next(python_reqs) 37 | python_specs = cast(Iterator[PySpecSet], (req.specifier for req in python_reqs)) 38 | if all(spec.is_superset(project_req.specifier or "") for spec in python_specs): 39 | return [project_req.as_candidate()] 40 | else: 41 | # There is a conflict, no match is found. 42 | return [] 43 | 44 | 45 | def is_python_satisfied_by(requirement: Requirement, candidate: Candidate) -> bool: 46 | return cast(PySpecSet, requirement.specifier).is_superset(candidate.req.specifier) 47 | -------------------------------------------------------------------------------- /tasks/complete.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pycomplete 4 | 5 | from pdm.core import Core 6 | 7 | COMPLETIONS = Path(__file__).parent.parent / "src/pdm/cli/completions" 8 | 9 | 10 | def main(): 11 | core = Core() 12 | core.init_parser() 13 | 14 | completer = pycomplete.Completer(core.parser, ["pdm"]) 15 | for shell in ("bash", "fish"): 16 | COMPLETIONS.joinpath(f"pdm.{shell}").write_text(completer.render(shell)) 17 | 18 | 19 | if __name__ == "__main__": 20 | main() 21 | -------------------------------------------------------------------------------- /tasks/max_versions.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | from html.parser import HTMLParser 5 | from pathlib import Path 6 | 7 | import httpx 8 | 9 | PROJECT_DIR = Path(__file__).parent.parent 10 | 11 | 12 | class PythonVersionParser(HTMLParser): 13 | def __init__(self, *, convert_charrefs: bool = True) -> None: 14 | super().__init__(convert_charrefs=convert_charrefs) 15 | self._parsing_release_number_span = False 16 | self._parsing_release_number_a = False 17 | self.parsed_python_versions: list[str] = [] 18 | 19 | def handle_starttag(self, tag: str, attrs: list[tuple[str, str]]) -> None: 20 | if tag == "span" and any("release-number" in value for key, value in attrs if key == "class"): 21 | self._parsing_release_number_span = True 22 | return 23 | 24 | if self._parsing_release_number_span and tag == "a": 25 | self._parsing_release_number_a = True 26 | 27 | def handle_endtag(self, tag: str) -> None: 28 | if self._parsing_release_number_span and tag == "span": 29 | self._parsing_release_number_span = False 30 | 31 | if self._parsing_release_number_a and tag == "a": 32 | self._parsing_release_number_a = False 33 | 34 | def handle_data(self, data: str) -> None: 35 | if self._parsing_release_number_a: 36 | self.parsed_python_versions.append(data[7:]) 37 | 38 | 39 | def dump_python_version_module(dest_file) -> None: 40 | resp = httpx.get("https://python.org/downloads/", follow_redirects=True) 41 | resp_text = resp.text 42 | parser = PythonVersionParser() 43 | parser.feed(resp_text) 44 | python_versions = sorted(parser.parsed_python_versions) 45 | max_versions: dict[str, int] = {} 46 | for version in python_versions: 47 | major, minor, patch = version.split(".") 48 | major_minor = f"{major}.{minor}" 49 | if major not in max_versions or max_versions[major] < int(minor): 50 | max_versions[major] = int(minor) 51 | if major_minor not in max_versions or max_versions[major_minor] < int(patch): 52 | max_versions[major_minor] = int(patch) 53 | with open(dest_file, "w") as f: 54 | json.dump(max_versions, f, sort_keys=True, indent=4) 55 | f.write("\n") 56 | 57 | 58 | if __name__ == "__main__": 59 | dump_python_version_module(PROJECT_DIR / "src/pdm/models/python_max_versions.json") 60 | -------------------------------------------------------------------------------- /tasks/release.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import argparse 4 | import subprocess 5 | import sys 6 | from pathlib import Path 7 | from typing import TYPE_CHECKING, Any, cast 8 | 9 | import parver 10 | from rich.console import Console 11 | 12 | if TYPE_CHECKING: 13 | from parver._typing import PreTag 14 | 15 | _console = Console(highlight=False) 16 | _err_console = Console(stderr=True, highlight=False) 17 | 18 | 19 | def echo(*args: str, err: bool = False, **kwargs: Any): 20 | if err: 21 | _err_console.print(*args, **kwargs) 22 | else: 23 | _console.print(*args, **kwargs) 24 | 25 | 26 | PROJECT_DIR = Path(__file__).parent.parent 27 | 28 | 29 | def get_current_version() -> str: 30 | return subprocess.check_output(["git", "describe", "--abbrev=0", "--tags"], cwd=PROJECT_DIR).decode().strip() 31 | 32 | 33 | def bump_version(pre: str | None = None, major: bool = False, minor: bool = False) -> str: 34 | if major and minor: 35 | echo("Only one option should be provided among (--major, --minor)", style="red", err=True) 36 | sys.exit(1) 37 | current_version = parver.Version.parse(get_current_version()) 38 | if major or minor: 39 | version_idx = [major, minor].index(True) 40 | version = current_version.bump_release(index=version_idx) 41 | elif pre is not None and current_version.is_prerelease: 42 | version = current_version 43 | else: 44 | version = current_version.bump_release(index=2) 45 | if pre is not None: 46 | if version.pre_tag != pre: 47 | version = version.replace(pre_tag=cast("PreTag", pre), pre=0) 48 | else: 49 | version = version.bump_pre() 50 | else: 51 | version = version.replace(pre=None, post=None) 52 | version = version.replace(local=None, dev=None) 53 | return str(version) 54 | 55 | 56 | def release( 57 | dry_run: bool = False, commit: bool = True, pre: str | None = None, major: bool = False, minor: bool = False 58 | ) -> None: 59 | new_version = bump_version(pre, major, minor) 60 | echo(f"Bump version to: {new_version}", style="yellow") 61 | if dry_run: 62 | subprocess.check_call(["towncrier", "build", "--version", new_version, "--draft"]) 63 | else: 64 | subprocess.check_call(["towncrier", "build", "--yes", "--version", new_version]) 65 | subprocess.check_call(["git", "add", "."]) 66 | if commit: 67 | subprocess.check_call(["git", "commit", "-m", f"chore: Release {new_version}"]) 68 | subprocess.check_call(["git", "tag", "-a", new_version, "-m", f"v{new_version}"]) 69 | subprocess.check_call(["git", "push"]) 70 | subprocess.check_call(["git", "push", "--tags"]) 71 | 72 | 73 | def parse_args(argv=None): 74 | parser = argparse.ArgumentParser("release.py") 75 | 76 | parser.add_argument("--dry-run", action="store_true", help="Dry run mode") 77 | parser.add_argument( 78 | "--no-commit", 79 | action="store_false", 80 | dest="commit", 81 | default=True, 82 | help="Do not commit to Git", 83 | ) 84 | group = parser.add_argument_group(title="version part") 85 | group.add_argument("--pre", help="Bump with the pre tag", choices=["a", "b", "rc"]) 86 | group.add_argument("--major", action="store_true", help="Bump major version") 87 | group.add_argument("--minor", action="store_true", help="Bump minor version") 88 | 89 | return parser.parse_args(argv) 90 | 91 | 92 | if __name__ == "__main__": 93 | args = parse_args() 94 | release(args.dry_run, args.commit, args.pre, args.major, args.minor) 95 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | FIXTURES = Path(__file__).parent / "fixtures" 4 | -------------------------------------------------------------------------------- /tests/cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/cli/__init__.py -------------------------------------------------------------------------------- /tests/cli/test_fix.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | 4 | 5 | def test_fix_non_existing_problem(project, pdm): 6 | result = pdm(["fix", "non-existing"], obj=project) 7 | assert result.exit_code == 1 8 | 9 | 10 | def test_fix_individual_problem(project, pdm): 11 | project._saved_python = None 12 | old_config = project.root / ".pdm.toml" 13 | old_config.write_text(f'[python]\nuse_pyenv = false\npath = "{Path(sys.executable).as_posix()}"\n') 14 | pdm(["fix", "project-config"], obj=project, strict=True) 15 | assert not old_config.exists() 16 | 17 | 18 | def test_show_fix_command(project, pdm): 19 | old_config = project.root / ".pdm.toml" 20 | old_config.write_text(f'[python]\nuse_pyenv = false\npath = "{Path(sys.executable).as_posix()}"\n') 21 | result = pdm(["info"], obj=project) 22 | assert "Run pdm fix to fix all" in result.stderr 23 | 24 | result = pdm(["fix", "-h"], obj=project) 25 | assert "Run pdm fix to fix all" not in result.stderr 26 | 27 | 28 | def test_show_fix_command_global_project(core, pdm, project_no_init): 29 | project = core.create_project(None, True, project_no_init.global_config.config_file) 30 | old_config = project.root / ".pdm.toml" 31 | old_config.write_text(f'[python]\nuse_pyenv = false\npath = "{Path(sys.executable).as_posix()}"\n') 32 | result = pdm(["info"], obj=project) 33 | assert "Run pdm fix -g to fix all" in result.stderr 34 | 35 | result = pdm(["fix", "-h"], obj=project) 36 | assert "Run pdm fix -g to fix all" not in result.stderr 37 | 38 | 39 | def test_fix_project_config(project, pdm): 40 | project._saved_python = None 41 | old_config = project.root / ".pdm.toml" 42 | old_config.write_text(f'[python]\nuse_pyenv = false\npath = "{Path(sys.executable).as_posix()}"\n') 43 | assert project.project_config["python.use_pyenv"] is False 44 | assert project._saved_python == Path(sys.executable).as_posix() 45 | pdm(["fix"], obj=project, strict=True) 46 | assert not old_config.exists() 47 | assert project.root.joinpath("pdm.toml").read_text() == "[python]\nuse_pyenv = false\n" 48 | assert project.root.joinpath(".pdm-python").read_text().strip() == Path(sys.executable).as_posix() 49 | -------------------------------------------------------------------------------- /tests/cli/test_outdated.py: -------------------------------------------------------------------------------- 1 | import json 2 | from unittest import mock 3 | 4 | import pytest 5 | from rich.box import ASCII 6 | 7 | 8 | @mock.patch("pdm.termui.ROUNDED", ASCII) 9 | @pytest.mark.usefixtures("working_set") 10 | def test_outdated(project, pdm, index): 11 | pdm(["add", "requests"], obj=project, strict=True, cleanup=False) 12 | project.project_config["pypi.url"] = "https://my.pypi.org/simple" 13 | del project.pyproject.settings["source"] 14 | project.pyproject.write() 15 | index["/simple/requests/"] = b"""\ 16 | 17 | 18 | 19 |

requests

20 | 24 | requests-2.20.0-py3-none-any.whl 25 | 26 | 27 | 28 | 29 | """ 30 | 31 | result = pdm(["outdated"], obj=project, strict=True, cleanup=False) 32 | assert "| requests | default | 2.19.1 | 2.19.1 | 2.20.0 |" in result.stdout 33 | 34 | result = pdm(["outdated", "re*"], obj=project, strict=True, cleanup=False) 35 | assert "| requests | default | 2.19.1 | 2.19.1 | 2.20.0 |" in result.stdout 36 | 37 | result = pdm(["outdated", "--json"], obj=project, strict=True, cleanup=False) 38 | json_output = json.loads(result.stdout) 39 | assert json_output == [ 40 | { 41 | "package": "requests", 42 | "groups": ["default"], 43 | "installed_version": "2.19.1", 44 | "pinned_version": "2.19.1", 45 | "latest_version": "2.20.0", 46 | } 47 | ] 48 | -------------------------------------------------------------------------------- /tests/cli/test_template.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from pdm.cli.templates import ProjectTemplate 6 | from pdm.exceptions import PdmException 7 | 8 | 9 | def test_non_pyproject_template_disallowed(project_no_init): 10 | with ProjectTemplate("tests/fixtures/projects/demo_extras") as template: 11 | with pytest.raises(PdmException, match="Template pyproject.toml not found"): 12 | template.generate(project_no_init.root, {"project": {"name": "foo"}}) 13 | 14 | 15 | def test_module_project_template(project_no_init): 16 | metadata = { 17 | "project": {"name": "foo", "version": "0.1.0", "requires-python": ">=3.10"}, 18 | "build-system": {"requires": ["pdm-backend"], "build-backend": "pdm.backend"}, 19 | } 20 | 21 | with ProjectTemplate("tests/fixtures/projects/demo") as template: 22 | template.generate(project_no_init.root, metadata) 23 | 24 | project_no_init.pyproject.reload() 25 | assert project_no_init.pyproject.metadata["name"] == "foo" 26 | assert project_no_init.pyproject.metadata["requires-python"] == ">=3.10" 27 | assert project_no_init.pyproject._data["build-system"] == metadata["build-system"] 28 | assert project_no_init.pyproject.metadata["dependencies"] == ["idna", "chardet; os_name=='nt'"] 29 | assert project_no_init.pyproject.metadata["optional-dependencies"]["tests"] == ["pytest"] 30 | assert (project_no_init.root / "foo.py").exists() 31 | assert os.access(project_no_init.root / "foo.py", os.W_OK) 32 | 33 | 34 | def test_module_project_template_generate_application(project_no_init): 35 | metadata = { 36 | "project": {"name": "", "version": "", "requires-python": ">=3.10"}, 37 | } 38 | 39 | with ProjectTemplate("tests/fixtures/projects/demo") as template: 40 | template.generate(project_no_init.root, metadata) 41 | 42 | project_no_init.pyproject.reload() 43 | assert project_no_init.pyproject.metadata["name"] == "" 44 | assert "build-system" not in project_no_init.pyproject._data 45 | assert project_no_init.pyproject.metadata["dependencies"] == ["idna", "chardet; os_name=='nt'"] 46 | assert (project_no_init.root / "demo.py").exists() 47 | 48 | 49 | def test_package_project_template(project_no_init): 50 | metadata = { 51 | "project": {"name": "foo", "version": "0.1.0", "requires-python": ">=3.10"}, 52 | "build-system": {"requires": ["pdm-backend"], "build-backend": "pdm.backend"}, 53 | } 54 | 55 | with ProjectTemplate("tests/fixtures/projects/demo-package") as template: 56 | template.generate(project_no_init.root, metadata) 57 | 58 | project_no_init.pyproject.reload() 59 | assert project_no_init.pyproject.metadata["name"] == "foo" 60 | assert project_no_init.pyproject.metadata["requires-python"] == ">=3.10" 61 | assert project_no_init.pyproject._data["build-system"] == metadata["build-system"] 62 | assert (project_no_init.root / "foo").is_dir() 63 | assert (project_no_init.root / "foo/__init__.py").exists() 64 | assert project_no_init.pyproject.settings["version"] == {"path": "foo/__init__.py", "source": "file"} 65 | -------------------------------------------------------------------------------- /tests/cli/test_utils.py: -------------------------------------------------------------------------------- 1 | def test_help_with_unknown_arguments(pdm): 2 | result = pdm(["add", "--unknown-args"]) 3 | assert "Usage: pdm add " in result.stderr 4 | assert result.exit_code == 2 5 | 6 | 7 | def test_output_similar_command_when_typo(pdm): 8 | result = pdm(["instal"]) 9 | assert "install" in result.stderr 10 | assert result.exit_code == 2 11 | -------------------------------------------------------------------------------- /tests/fixtures/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | requests = "*" 8 | pywinusb = {version = "*", sys_platform = "== 'win32'"} 9 | 10 | [pipenv] 11 | allow_prereleases = true 12 | 13 | [requires] 14 | python_version = "3.6" 15 | -------------------------------------------------------------------------------- /tests/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/artifacts/PyFunctional-1.4.3-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/artifacts/PyFunctional-1.4.3-py3-none-any.whl -------------------------------------------------------------------------------- /tests/fixtures/artifacts/caj2pdf-restructured-0.1.0a6.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/artifacts/caj2pdf-restructured-0.1.0a6.tar.gz -------------------------------------------------------------------------------- /tests/fixtures/artifacts/celery-4.4.2-py2.py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/artifacts/celery-4.4.2-py2.py3-none-any.whl -------------------------------------------------------------------------------- /tests/fixtures/artifacts/demo-0.0.1-cp36-cp36m-win_amd64.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/artifacts/demo-0.0.1-cp36-cp36m-win_amd64.whl -------------------------------------------------------------------------------- /tests/fixtures/artifacts/demo-0.0.1-py2.py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/artifacts/demo-0.0.1-py2.py3-none-any.whl -------------------------------------------------------------------------------- /tests/fixtures/artifacts/demo-0.0.1.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/artifacts/demo-0.0.1.tar.gz -------------------------------------------------------------------------------- /tests/fixtures/artifacts/demo-0.0.1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/artifacts/demo-0.0.1.zip -------------------------------------------------------------------------------- /tests/fixtures/artifacts/editables-0.2-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/artifacts/editables-0.2-py3-none-any.whl -------------------------------------------------------------------------------- /tests/fixtures/artifacts/first-2.0.2-py2.py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/artifacts/first-2.0.2-py2.py3-none-any.whl -------------------------------------------------------------------------------- /tests/fixtures/artifacts/flit_core-3.6.0-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/artifacts/flit_core-3.6.0-py3-none-any.whl -------------------------------------------------------------------------------- /tests/fixtures/artifacts/future_fstrings-1.2.0-py2.py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/artifacts/future_fstrings-1.2.0-py2.py3-none-any.whl -------------------------------------------------------------------------------- /tests/fixtures/artifacts/future_fstrings-1.2.0.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/artifacts/future_fstrings-1.2.0.tar.gz -------------------------------------------------------------------------------- /tests/fixtures/artifacts/importlib_metadata-4.8.3-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/artifacts/importlib_metadata-4.8.3-py3-none-any.whl -------------------------------------------------------------------------------- /tests/fixtures/artifacts/jmespath-0.10.0-py2.py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/artifacts/jmespath-0.10.0-py2.py3-none-any.whl -------------------------------------------------------------------------------- /tests/fixtures/artifacts/pdm_backend-2.1.4-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/artifacts/pdm_backend-2.1.4-py3-none-any.whl -------------------------------------------------------------------------------- /tests/fixtures/artifacts/pdm_hello-0.1.0-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/artifacts/pdm_hello-0.1.0-py3-none-any.whl -------------------------------------------------------------------------------- /tests/fixtures/artifacts/pdm_hello-0.1.0-py3-none-win_amd64.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/artifacts/pdm_hello-0.1.0-py3-none-win_amd64.whl -------------------------------------------------------------------------------- /tests/fixtures/artifacts/pdm_pep517-1.0.0-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/artifacts/pdm_pep517-1.0.0-py3-none-any.whl -------------------------------------------------------------------------------- /tests/fixtures/artifacts/poetry_core-1.3.2-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/artifacts/poetry_core-1.3.2-py3-none-any.whl -------------------------------------------------------------------------------- /tests/fixtures/artifacts/setuptools-68.0.0-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/artifacts/setuptools-68.0.0-py3-none-any.whl -------------------------------------------------------------------------------- /tests/fixtures/artifacts/typing_extensions-4.4.0-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/artifacts/typing_extensions-4.4.0-py3-none-any.whl -------------------------------------------------------------------------------- /tests/fixtures/artifacts/wheel-0.37.1-py2.py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/artifacts/wheel-0.37.1-py2.py3-none-any.whl -------------------------------------------------------------------------------- /tests/fixtures/artifacts/zipp-3.6.0-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/artifacts/zipp-3.6.0-py3-none-any.whl -------------------------------------------------------------------------------- /tests/fixtures/artifacts/zipp-3.7.0-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/artifacts/zipp-3.7.0-py3-none-any.whl -------------------------------------------------------------------------------- /tests/fixtures/constraints.txt: -------------------------------------------------------------------------------- 1 | # This is a pip constraints file 2 | requests==2.20.0b1 3 | django==1.11.8 4 | certifi==2018.11.17 5 | chardet==3.0.4 6 | idna==2.7 7 | pytz==2019.3 8 | urllib3==1.23b0 9 | -------------------------------------------------------------------------------- /tests/fixtures/index/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Demo

5 | 6 | demo-0.0.1-cp36-cp36m-win_amd64.whl 7 | 8 | 9 | demo-0.0.1-py2.py3-none-any.whl 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/fixtures/index/future-fstrings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

future-fstrings

5 | 8 | future_fstrings-1.2.0-py2.py3-none-any.whl 9 | 10 | 13 | future_fstrings-1.2.0.tar.gz 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/fixtures/index/pep345-legacy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

pep345-legacy

5 | 9 | pep345_legacy-0.0.1-py2.py3-none-any.whl 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/fixtures/index/wheel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

wheel

5 | 9 | wheel-0.37.1-py2.py3-none-any.whl 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/fixtures/json/zipp.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "api-version": "1.0" 4 | }, 5 | "name": "zipp", 6 | "files": [ 7 | { 8 | "filename": "zipp-3.6.0-py3-none-any.whl", 9 | "url": "http://fixtures.test/artifacts/zipp-3.6.0-py3-none-any.whl", 10 | "hashes": { 11 | "sha256": "9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc" 12 | }, 13 | "upload-time": "2023-02-01T00:00:00.000000Z", 14 | "requires-python": ">=3.7" 15 | }, 16 | { 17 | "filename": "zipp-3.7.0-py3-none-any.whl", 18 | "url": "http://fixtures.test/artifacts/zipp-3.7.0-py3-none-any.whl", 19 | "hashes": { 20 | "sha256": "b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375" 21 | }, 22 | "upload-time": "2024-02-01T00:00:00.000000Z", 23 | "requires-python": ">=3.7" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /tests/fixtures/poetry-error.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "test-poetry" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Frost Ming "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.11" 10 | foo = ">=1.0||^2.1" 11 | 12 | [build-system] 13 | requires = ["poetry-core"] 14 | build-backend = "poetry.core.masonry.api" 15 | -------------------------------------------------------------------------------- /tests/fixtures/poetry-new.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "test-poetry" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Frost Ming "] 6 | readme = "README.md" 7 | packages = [{include = "test_poetry"}] 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.9" 11 | httpx = "*" 12 | pendulum = "*" 13 | 14 | [tool.poetry.group.test.dependencies] 15 | pytest = "^6.0.0" 16 | pytest-mock = "*" 17 | 18 | [build-system] 19 | requires = ["poetry-core"] 20 | build-backend = "poetry.core.masonry.api" 21 | -------------------------------------------------------------------------------- /tests/fixtures/projects/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/projects/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-#-with-hash/demo.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import chardet 4 | 5 | print(os.name) 6 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-#-with-hash/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | setup( 5 | name="demo", 6 | version="0.0.1", 7 | description="test demo", 8 | py_modules=["demo"], 9 | python_requires=">=3.3", 10 | install_requires=["idna", "chardet; os_name=='nt'"], 11 | extras_require={ 12 | "tests": ["pytest"], 13 | "security": ['requests; python_version>="3.6"'], 14 | }, 15 | ) 16 | -------------------------------------------------------------------------------- /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 | [project] 2 | # PEP 621 project metadata 3 | # See https://www.python.org/dev/peps/pep-0621/ 4 | authors = [ 5 | {name = "frostming", email = "mianghong@gmail.com"}, 6 | ] 7 | version = "0.1.0" 8 | requires-python = ">=3.5" 9 | license = {text = "MIT"} 10 | dependencies = ["urllib3"] 11 | description = "" 12 | name = "demo-package-extra" 13 | 14 | [project.optional-dependencies] 15 | be = ["idna"] 16 | te = ["chardet"] 17 | all = ["idna", "chardet"] 18 | 19 | [build-system] 20 | requires = ["pdm-backend"] 21 | build-backend = "pdm.backend" 22 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-failure-no-dep/demo.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import chardet 4 | 5 | print(os.name) 6 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-failure-no-dep/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | if True: 4 | raise RuntimeError("This mimics the build error on unmatched platform") 5 | 6 | setup( 7 | name="demo", 8 | version="0.0.1", 9 | description="test demo", 10 | py_modules=["demo"], 11 | python_requires=">=3.3", 12 | ) 13 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-failure/demo.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import chardet 4 | 5 | print(os.name) 6 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-failure/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | import first 4 | 5 | setup( 6 | name="demo", 7 | version="0.0.1", 8 | description="test demo", 9 | py_modules=["demo"], 10 | python_requires=">=3.3", 11 | install_requires=["idna", "chardet; os_name=='nt'"], 12 | extras_require={ 13 | "tests": ["pytest"], 14 | "security": ['requests; python_version>="3.6"'], 15 | }, 16 | ) 17 | -------------------------------------------------------------------------------- /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" 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 | # PEP 621 project metadata 7 | # See https://www.python.org/dev/peps/pep-0621/ 8 | authors = [ 9 | {name = "frostming", email = "mianghong@gmail.com"}, 10 | ] 11 | dynamic = ["version"] 12 | requires-python = ">=3.5" 13 | license = {text = "MIT"} 14 | dependencies = [] 15 | description = "" 16 | name = "demo-module" 17 | 18 | [project.optional-dependencies] 19 | 20 | [tool.pdm.version] 21 | source = "file" 22 | path = "foo_module.py" 23 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-has-dep-with-extras/pdm.lock: -------------------------------------------------------------------------------- 1 | # This file is @generated by PDM. 2 | # It is not intended for manual editing. 3 | 4 | [metadata] 5 | groups = ["default"] 6 | strategy = ["cross_platform", "inherit_metadata"] 7 | lock_version = "4.4.2" 8 | content_hash = "sha256:436a711ecb4a672c165c06d03952762d4f43b85f49633c53654a53aca6584643" 9 | 10 | [[package]] 11 | name = "certifi" 12 | version = "2021.10.8" 13 | summary = "Python package for providing Mozilla's CA Bundle." 14 | groups = ["default"] 15 | files = [ 16 | {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, 17 | {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, 18 | ] 19 | 20 | [[package]] 21 | name = "charset-normalizer" 22 | version = "2.0.7" 23 | requires_python = ">=3.5.0" 24 | summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 25 | groups = ["default"] 26 | marker = "python_version >= \"3\"" 27 | files = [ 28 | {file = "charset-normalizer-2.0.7.tar.gz", hash = "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0"}, 29 | {file = "charset_normalizer-2.0.7-py3-none-any.whl", hash = "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b"}, 30 | ] 31 | 32 | [[package]] 33 | name = "idna" 34 | version = "3.3" 35 | requires_python = ">=3.5" 36 | summary = "Internationalized Domain Names in Applications (IDNA)" 37 | groups = ["default"] 38 | marker = "python_version >= \"3\"" 39 | files = [ 40 | {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, 41 | {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, 42 | ] 43 | 44 | [[package]] 45 | name = "requests" 46 | version = "2.26.0" 47 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 48 | summary = "Python HTTP for Humans." 49 | groups = ["default"] 50 | dependencies = [ 51 | "certifi>=2017.4.17", 52 | "charset-normalizer~=2.0.0; python_version >= \"3\"", 53 | "idna<4,>=2.5; python_version >= \"3\"", 54 | "urllib3<1.27,>=1.21.1", 55 | ] 56 | files = [ 57 | {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, 58 | {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, 59 | ] 60 | 61 | [[package]] 62 | name = "requests" 63 | version = "2.26.0" 64 | extras = ["security"] 65 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 66 | summary = "Python HTTP for Humans." 67 | groups = ["default"] 68 | dependencies = [ 69 | "requests==2.26.0", 70 | ] 71 | files = [ 72 | {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, 73 | {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, 74 | ] 75 | 76 | [[package]] 77 | name = "urllib3" 78 | version = "1.26.7" 79 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 80 | summary = "HTTP library with thread-safe connection pooling, file post, and more." 81 | groups = ["default"] 82 | files = [ 83 | {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, 84 | {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, 85 | ] 86 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-has-dep-with-extras/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "" 3 | version = "" 4 | description = "" 5 | authors = [ 6 | {name = "Frost Ming", email = "mianghong@gmail.com"}, 7 | ] 8 | dependencies = [ 9 | "requests[security]~=2.26", 10 | ] 11 | requires-python = ">=3.6" 12 | license = {text = "MIT"} 13 | 14 | [build-system] 15 | requires = ["pdm-backend"] 16 | build-backend = "pdm.backend" 17 | 18 | [tool] 19 | [tool.pdm] 20 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package-has-dep-with-extras/requirements.txt: -------------------------------------------------------------------------------- 1 | # This file is @generated by PDM. 2 | # Please do not edit it manually. 3 | 4 | certifi==2021.10.8 5 | charset-normalizer==2.0.7; python_version >= "3" 6 | idna==3.3; python_version >= "3" 7 | requests==2.26.0 8 | urllib3==1.26.7 9 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package/README.md: -------------------------------------------------------------------------------- 1 | # my-package 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/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pdm-backend"] 3 | build-backend = "pdm.backend" 4 | 5 | [project] 6 | # PEP 621 project metadata 7 | # See https://www.python.org/dev/peps/pep-0621/ 8 | authors = [ 9 | {name = "frostming", email = "mianghong@gmail.com"}, 10 | ] 11 | dynamic = ["version"] 12 | requires-python = ">=3.5" 13 | license = {text = "MIT"} 14 | dependencies = ["flask"] 15 | description = "" 16 | name = "my-package" 17 | readme = "README.md" 18 | 19 | [project.optional-dependencies] 20 | 21 | [tool.pdm.version] 22 | source = "file" 23 | path = "my_package/__init__.py" 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/requirements.ini: -------------------------------------------------------------------------------- 1 | # This file is @generated by PDM. 2 | # Please do not edit it manually. 3 | 4 | flask 5 | --extra-index-url https://test.pypi.org/simple 6 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package/requirements_simple.txt: -------------------------------------------------------------------------------- 1 | # This file is @generated by PDM. 2 | # Please do not edit it manually. 3 | 4 | click==7.1.2 5 | flask==1.1.4 6 | itsdangerous==1.1.0 7 | jinja2==2.11.3 8 | markupsafe==1.1.1 9 | werkzeug==1.0.1 10 | --extra-index-url https://test.pypi.org/simple 11 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package/setup.txt: -------------------------------------------------------------------------------- 1 | 2 | # -*- coding: utf-8 -*- 3 | from setuptools import setup 4 | 5 | import codecs 6 | 7 | with codecs.open('README.md', encoding="utf-8") as fp: 8 | long_description = fp.read() 9 | INSTALL_REQUIRES = [ 10 | 'flask', 11 | ] 12 | 13 | setup_kwargs = { 14 | 'name': 'my-package', 15 | 'version': '0.1.0', 16 | 'description': '', 17 | 'long_description': long_description, 18 | 'license': 'MIT', 19 | 'author': '', 20 | 'author_email': 'frostming ', 21 | 'maintainer': None, 22 | 'maintainer_email': None, 23 | 'url': '', 24 | 'packages': [ 25 | 'my_package', 26 | ], 27 | 'package_data': {'': ['*']}, 28 | 'long_description_content_type': 'text/markdown', 29 | 'install_requires': INSTALL_REQUIRES, 30 | 'python_requires': '>=3.5', 31 | 32 | } 33 | 34 | 35 | setup(**setup_kwargs) 36 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-package/single_module.py: -------------------------------------------------------------------------------- 1 | print("hello world") 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-parent-package/README.md: -------------------------------------------------------------------------------- 1 | # Package Package 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-parent-package/package-a/foo.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-parent-package/package-a/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | setup(name="package-a", py_modules=["foo"], version="0.1.0", install_requires=["flask"]) 5 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-parent-package/package-b/bar.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-parent-package/package-b/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pdm-backend"] 3 | build-backend = "pdm.backend" 4 | 5 | [project] 6 | name = "package-b" 7 | dependencies = ["django"] 8 | dynamic = ["version"] 9 | 10 | [tool.pdm.version] 11 | source = "file" 12 | path = "bar.py" 13 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-prerelease/demo.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import chardet 4 | 5 | print(os.name) 6 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo-prerelease/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | setup( 5 | name="demo", 6 | version="0.0.2b0", 7 | description="test demo", 8 | py_modules=["demo"], 9 | python_requires=">=3.3", 10 | install_requires=["idna", "chardet; os_name=='nt'"], 11 | extras_require={ 12 | "tests": ["pytest"], 13 | "security": ['requests; python_version>="3.6"'], 14 | }, 15 | ) 16 | -------------------------------------------------------------------------------- /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 | # PEP 621 project metadata 7 | # See https://www.python.org/dev/peps/pep-0621/ 8 | authors = [ 9 | {name = "frostming", email = "mianghong@gmail.com"}, 10 | ] 11 | dynamic = ["version"] 12 | requires-python = ">=3.5" 13 | license = {text = "MIT"} 14 | dependencies = [] 15 | description = "" 16 | name = "demo-package" 17 | 18 | [project.optional-dependencies] 19 | 20 | [tool.pdm.version] 21 | source = "file" 22 | path = "src/my_package/__init__.py" 23 | -------------------------------------------------------------------------------- /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/demo.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import chardet 4 | 5 | print(os.name) 6 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo/pyproject.toml: -------------------------------------------------------------------------------- 1 | 2 | [project] 3 | name = "demo" 4 | version = "0.0.1" 5 | description = "test demo" 6 | requires-python = ">=3.3" 7 | dependencies = [ 8 | "idna", 9 | "chardet; os_name=='nt'", 10 | ] 11 | 12 | [project.optional-dependencies] 13 | tests = [ 14 | "pytest", 15 | ] 16 | security = [ 17 | "requests; python_version>=\"3.6\"", 18 | ] 19 | 20 | [build-system] 21 | requires = ["pdm-backend"] 22 | build-backend = "pdm.backend" 23 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo_extras/demo.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | print(os.name) 4 | -------------------------------------------------------------------------------- /tests/fixtures/projects/demo_extras/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | setup( 5 | name="demo-extras", 6 | version="0.0.1", 7 | description="test demo", 8 | py_modules=["demo"], 9 | install_requires=[], 10 | extras_require={"extra1": ["requests[security]"], "extra2": ["requests[socks]"]}, 11 | ) 12 | -------------------------------------------------------------------------------- /tests/fixtures/projects/flit-demo/README.rst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/projects/flit-demo/README.rst -------------------------------------------------------------------------------- /tests/fixtures/projects/flit-demo/doc/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/projects/flit-demo/doc/index.html -------------------------------------------------------------------------------- /tests/fixtures/projects/flit-demo/flit.py: -------------------------------------------------------------------------------- 1 | """An awesome flit demo""" 2 | __version__ = "0.1.0" 3 | -------------------------------------------------------------------------------- /tests/fixtures/projects/flit-demo/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.flit.metadata] 2 | module="flit" 3 | author="Thomas Kluyver" 4 | author-email="thomas@kluyver.me.uk" 5 | home-page="https://github.com/takluyver/flit" 6 | requires = [ 7 | "requests>=2.6", 8 | "configparser; python_version == \"2.7\"", 9 | ] 10 | dist-name = "pyflit" 11 | requires-python=">=3.5" 12 | description-file="README.rst" 13 | classifiers=[ 14 | "Intended Audience :: Developers", 15 | "License :: OSI Approved :: BSD License", 16 | "Programming Language :: Python :: 3", 17 | "Topic :: Software Development :: Libraries :: Python Modules", 18 | ] 19 | 20 | [tool.flit.metadata.urls] 21 | Documentation = "https://flit.readthedocs.io/en/latest/" 22 | 23 | [tool.flit.metadata.requires-extra] 24 | test = [ 25 | "pytest >=2.7.3", 26 | "pytest-cov", 27 | ] 28 | doc = ["sphinx"] 29 | 30 | [tool.flit.scripts] 31 | flit = "flit:main" 32 | 33 | [tool.flit.entrypoints."pygments.lexers"] 34 | dogelang = "dogelang.lexer:DogeLexer" 35 | 36 | [tool.flit.sdist] 37 | include = ["doc/"] 38 | exclude = ["doc/*.html"] 39 | 40 | [build-system] 41 | requires = ["flit_core >=2,<4"] 42 | build-backend = "flit_core.buildapi" 43 | -------------------------------------------------------------------------------- /tests/fixtures/projects/poetry-demo/mylib.py: -------------------------------------------------------------------------------- 1 | FOO = "bar" 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/poetry-demo/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "poetry-demo" 3 | version = "0.1.0" 4 | authors = ["Thomas Kluyver "] 5 | homepage = "https://github.com/takluyver/flit" 6 | license = "BSD-3-Clause" 7 | description = "A demo project for Poetry" 8 | classifiers = [ 9 | "Intended Audience :: Developers", 10 | "Programming Language :: Python :: 3", 11 | "Topic :: Software Development :: Libraries :: Python Modules", 12 | ] 13 | 14 | packages = [ 15 | { include = "mylib.py" }, 16 | ] 17 | 18 | [tool.poetry.urls] 19 | Documentation = "https://flit.readthedocs.io/en/latest/" 20 | 21 | [tool.poetry.dependencies] 22 | python = "^3.6" 23 | requests = "^2.6" 24 | pytest = {version = "^2.7.3", optional = true} 25 | pytest-cov = {version = "*", optional = true} 26 | sphinx = {version = "*", optional = true} 27 | 28 | [tool.poetry.extras] 29 | test = ["pytest", "pytest-cov"] 30 | doc = ["sphinx"] 31 | 32 | [build-system] 33 | requires = ["poetry-core"] 34 | build-backend = "poetry.core.masonry.api" 35 | -------------------------------------------------------------------------------- /tests/fixtures/projects/poetry-with-circular-dep/packages/child/child/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/projects/poetry-with-circular-dep/packages/child/child/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/projects/poetry-with-circular-dep/packages/child/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "child" 3 | version = "0.1.0" 4 | authors = ["PDM "] 5 | description = "Child project" 6 | license = "Apache-2.0" 7 | packages = [{include = "child"}] 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.9.1" 11 | 12 | [tool.poetry.group.dev.dependencies] 13 | parent = {path = "../..", develop = true} 14 | 15 | [build-system] 16 | requires = ["poetry-core"] 17 | build-backend = "poetry.core.masonry.api" 18 | -------------------------------------------------------------------------------- /tests/fixtures/projects/poetry-with-circular-dep/parent/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/projects/poetry-with-circular-dep/parent/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/projects/poetry-with-circular-dep/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "parent" 3 | version = "0.1.0" 4 | authors = ["PDM "] 5 | description = "Parent project" 6 | license = "Apache-2.0" 7 | packages = [{include = "parent"}] 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.9.1" 11 | 12 | [tool.poetry.group.dev.dependencies] 13 | child = {path = "./packages/child", develop = true} 14 | 15 | [build-system] 16 | requires = ["poetry-core"] 17 | build-backend = "poetry.core.masonry.api" 18 | -------------------------------------------------------------------------------- /tests/fixtures/projects/test-hatch-static/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/projects/test-hatch-static/README.md -------------------------------------------------------------------------------- /tests/fixtures/projects/test-hatch-static/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling>=0.15.0"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "test-hatch" 7 | version = "0.1.0" 8 | description = "Test hatch project" 9 | readme = "README.md" 10 | license = "MIT" 11 | requires-python = ">=3.7" 12 | authors = [{ name = "John", email = "john@example.org" }] 13 | classifiers = [ 14 | "License :: OSI Approved :: MIT License", 15 | ] 16 | dependencies = ["requests", "click"] 17 | -------------------------------------------------------------------------------- /tests/fixtures/projects/test-monorepo/README.md: -------------------------------------------------------------------------------- 1 | # pdm_test 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/test-monorepo/core/core.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/projects/test-monorepo/core/core.py -------------------------------------------------------------------------------- /tests/fixtures/projects/test-monorepo/core/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "core" 3 | version = "0.0.1" 4 | description = "" 5 | requires-python = ">= 3.7" 6 | dependencies = [] 7 | 8 | [build-system] 9 | requires = ["pdm-backend"] 10 | build-backend = "pdm.backend" 11 | -------------------------------------------------------------------------------- /tests/fixtures/projects/test-monorepo/package_a/alice.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/projects/test-monorepo/package_a/alice.py -------------------------------------------------------------------------------- /tests/fixtures/projects/test-monorepo/package_a/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "package_a" 3 | version = "0.0.1" 4 | description = "" 5 | requires-python = ">= 3.7" 6 | dependencies = [ 7 | "core @ file:///${PROJECT_ROOT}/../core", 8 | ] 9 | 10 | [build-system] 11 | requires = ["pdm-backend"] 12 | build-backend = "pdm.backend" 13 | -------------------------------------------------------------------------------- /tests/fixtures/projects/test-monorepo/package_b/bob.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/projects/test-monorepo/package_b/bob.py -------------------------------------------------------------------------------- /tests/fixtures/projects/test-monorepo/package_b/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "package_b" 3 | version = "0.0.1" 4 | description = "" 5 | requires-python = ">= 3.7" 6 | dependencies = [ 7 | "core @ file:///${PROJECT_ROOT}/../core", 8 | ] 9 | 10 | [build-system] 11 | requires = ["pdm-backend"] 12 | build-backend = "pdm.backend" 13 | -------------------------------------------------------------------------------- /tests/fixtures/projects/test-monorepo/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | requires-python = ">= 3.7" 3 | dependencies = [ 4 | "package_a @ file:///${PROJECT_ROOT}/package_a", 5 | "package_b @ file:///${PROJECT_ROOT}/package_b", 6 | ] 7 | -------------------------------------------------------------------------------- /tests/fixtures/projects/test-package-type-fixer/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pdm-backend"] 3 | build-backend = "pdm.backend" 4 | 5 | [project] 6 | version = "0.0.1" 7 | dependencies = [] 8 | name = "test-package-type-fixer" 9 | requires-python = ">=3.9" 10 | 11 | [dependency-groups] 12 | dev = [ 13 | "requests==2.19.1" 14 | ] 15 | 16 | [tool.pdm.version] 17 | source = "file" 18 | path = "src/test_package_type_fixer/__init__.py" 19 | 20 | [tool.pdm] 21 | package-type = "application" 22 | -------------------------------------------------------------------------------- /tests/fixtures/projects/test-package-type-fixer/src/test_package_type_fixer/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.1.0' -------------------------------------------------------------------------------- /tests/fixtures/projects/test-plugin-pdm/hello.py: -------------------------------------------------------------------------------- 1 | from pdm.cli.commands.base import BaseCommand 2 | 3 | 4 | class HelloCommand(BaseCommand): 5 | """Say hello to somebody""" 6 | 7 | def add_arguments(self, parser): 8 | parser.add_argument("-n", "--name", help="the person's name") 9 | 10 | def handle(self, project, options): 11 | print(f"Hello, {options.name or 'world'}") 12 | 13 | 14 | def main(core): 15 | core.register_command(HelloCommand, "hello") 16 | -------------------------------------------------------------------------------- /tests/fixtures/projects/test-plugin-pdm/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pdm-backend"] 3 | build-backend = "pdm.backend" 4 | 5 | [project] 6 | # PEP 621 project metadata 7 | # See https://www.python.org/dev/peps/pep-0621/ 8 | version = "0.0.1" 9 | dependencies = [] 10 | name = "test-plugin-pdm" 11 | 12 | [project.optional-dependencies] 13 | 14 | [project.entry-points."pdm.plugin"] 15 | hello = "hello:main" 16 | -------------------------------------------------------------------------------- /tests/fixtures/projects/test-plugin/hello.py: -------------------------------------------------------------------------------- 1 | from pdm.cli.commands.base import BaseCommand 2 | 3 | 4 | class HelloCommand(BaseCommand): 5 | """Say hello to somebody""" 6 | 7 | def add_arguments(self, parser): 8 | parser.add_argument("-n", "--name", help="the person's name") 9 | 10 | def handle(self, project, options): 11 | print(f"Hello, {options.name or 'world'}") 12 | 13 | 14 | def main(core): 15 | core.register_command(HelloCommand, "hello") 16 | -------------------------------------------------------------------------------- /tests/fixtures/projects/test-plugin/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | setup( 5 | name="test-plugin", 6 | version="0.0.1", 7 | py_modules=["hello"], 8 | entry_points={"pdm.plugin": ["hello = hello:main"]}, 9 | ) 10 | -------------------------------------------------------------------------------- /tests/fixtures/projects/test-removal/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/projects/test-removal/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/projects/test-removal/bar.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/projects/test-removal/bar.py -------------------------------------------------------------------------------- /tests/fixtures/projects/test-removal/foo.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/projects/test-removal/foo.py -------------------------------------------------------------------------------- /tests/fixtures/projects/test-removal/subdir/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/fixtures/projects/test-removal/subdir/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/projects/test-setuptools/AUTHORS: -------------------------------------------------------------------------------- 1 | frostming 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/test-setuptools/README.md: -------------------------------------------------------------------------------- 1 | # My Module 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/test-setuptools/mymodule.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | -------------------------------------------------------------------------------- /tests/fixtures/projects/test-setuptools/setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = mymodule 3 | description = A test module 4 | keywords = one, two 5 | classifiers = 6 | Framework :: Django 7 | Programming Language :: Python :: 3 8 | 9 | [options] 10 | zip_safe = False 11 | include_package_data = True 12 | python_requires = >=3.5 13 | package_dir = = src 14 | install_requires = 15 | requests 16 | importlib-metadata; python_version<"3.10" 17 | 18 | [options.entry_points] 19 | console_scripts = 20 | mycli = mymodule:main 21 | -------------------------------------------------------------------------------- /tests/fixtures/projects/test-setuptools/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from mymodule import __version__ 3 | 4 | with open("AUTHORS") as f: 5 | authors = f.read().strip() 6 | 7 | kwargs = { 8 | "name": "mymodule", 9 | "version": __version__, 10 | "author": authors, 11 | } 12 | 13 | if 1 + 1 >= 2: 14 | kwargs.update(license="MIT") 15 | 16 | 17 | if __name__ == "__main__": 18 | setup(**kwargs) 19 | -------------------------------------------------------------------------------- /tests/fixtures/pypi.json: -------------------------------------------------------------------------------- 1 | { 2 | "certifi": { 3 | "2018.11.17": {} 4 | }, 5 | "chardet": { 6 | "3.0.4": {} 7 | }, 8 | "demo": { 9 | "0.0.1": { 10 | "dependencies": [ 11 | "idna", 12 | "chardet; os_name=='nt'", 13 | "pytest; extra=='tests'", 14 | "requests; python_version>='3.6' and extra=='security'" 15 | ], 16 | "requires_python": ">=3.3" 17 | } 18 | }, 19 | "django": { 20 | "1.11.8": { 21 | "dependencies": [ 22 | "pytz" 23 | ] 24 | }, 25 | "2.2.9": { 26 | "dependencies": [ 27 | "pytz", 28 | "sqlparse" 29 | ], 30 | "requires_python": ">=3.5" 31 | } 32 | }, 33 | "django-toolbar": { 34 | "1.0": { 35 | "dependencies": [ 36 | "django<2" 37 | ] 38 | } 39 | }, 40 | "editables": { 41 | "0.2": {} 42 | }, 43 | "idna": { 44 | "2.7": {} 45 | }, 46 | "pyopenssl": { 47 | "0.14": {} 48 | }, 49 | "pysocks": { 50 | "1.5.6": {} 51 | }, 52 | "pytz": { 53 | "2019.3": {} 54 | }, 55 | "requests": { 56 | "2.19.1": { 57 | "dependencies": [ 58 | "certifi>=2017.4.17", 59 | "chardet<3.1.0,>=3.0.2", 60 | "idna<2.8,>=2.5", 61 | "urllib3<1.24,>=1.21.1", 62 | "PySocks>=1.5.6,!=1.5.7; extra=='socks'", 63 | "pyOpenSSL>=0.14; extra=='security'" 64 | ] 65 | }, 66 | "2.20.0b1": { 67 | "dependencies": [ 68 | "certifi>=2017.4.17", 69 | "chardet<3.1.0,>=3.0.2", 70 | "idna<2.8,>=2.5", 71 | "urllib3<1.24,>=1.23b0", 72 | "PySocks>=1.5.6,!=1.5.7; extra=='socks'", 73 | "pyOpenSSL>=0.14; extra=='security'" 74 | ] 75 | } 76 | }, 77 | "sqlparse": { 78 | "0.3.0": { 79 | "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 80 | } 81 | }, 82 | "urllib3": { 83 | "1.22": {}, 84 | "1.23b0": {} 85 | }, 86 | "using-demo": { 87 | "0.1.0": { 88 | "dependencies": [ 89 | "demo" 90 | ], 91 | "requires_python": ">=3.3" 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /tests/fixtures/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "poetry" 3 | version = "1.0.0" 4 | description = "Python dependency management and packaging made easy." 5 | authors = [ 6 | "Sébastien Eustace ", 7 | "Example, Inc. " 8 | ] 9 | license = "MIT" 10 | 11 | readme = "README.md" 12 | 13 | homepage = "https://python-poetry.org/" 14 | repository = "https://github.com/python-poetry/poetry" 15 | documentation = "https://python-poetry.org/docs" 16 | 17 | packages = [ 18 | {include="my_package", from="lib/"}, 19 | {include="tests", format="sdist"} 20 | ] 21 | 22 | include = ["CHANGELOG.md"] 23 | exclude = ["my_package/excluded.py"] 24 | 25 | [tool.poetry.dependencies] 26 | python = "~2.7 || ^3.4" 27 | cleo = { version = "^0.7.6", markers = "python_version ~= '2.7'" } 28 | cachecontrol = { version = "^0.12.4", extras = ["filecache"], python = "^3.4" } 29 | flask = { git = "https://github.com/pallets/flask.git", rev = "38eb5d3b" } 30 | psycopg2 = { version = "^2.7", optional = true } 31 | mysqlclient = { version = "^1.3", optional = true } 32 | babel = "2.9.0" 33 | 34 | [tool.poetry.dev-dependencies] 35 | demo-dir = { path = "./projects/demo" } 36 | demo = { path = "./artifacts/demo-0.0.1-py2.py3-none-any.whl" } 37 | 38 | [tool.poetry.extras] 39 | mysql = ["mysqlclient"] 40 | pgsql = ["psycopg2"] 41 | 42 | [tool.poetry.urls] 43 | "Bug Tracker" = "https://github.com/python-poetry/poetry/issues" 44 | 45 | [tool.poetry.plugins."blogtool.parsers"] 46 | ".rst" = "some_module:SomeClass" 47 | 48 | [tool.poetry.scripts] 49 | poetry = 'poetry.console:run' 50 | -------------------------------------------------------------------------------- /tests/fixtures/requirements-include.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | -------------------------------------------------------------------------------- /tests/fixtures/requirements.txt: -------------------------------------------------------------------------------- 1 | --index-url=https://pypi.org/simple 2 | --extra-index-url=https://pypi.example.com/simple 3 | webassets==2.0 4 | werkzeug==0.16.0 5 | whoosh==2.7.4; sys_platform == "win32" 6 | wtforms==2.2.1 --hash=sha256:0cdbac3e7f6878086c334aa25dc5a33869a3954e9d1e015130d65a69309b3b61 --hash=sha256:e3ee092c827582c50877cdbd49e9ce6d2c5c1f6561f849b3b068c1b8029626f1 7 | -e git+https://github.com/pypa/pip.git@main#egg=pip 8 | git+https://github.com/techalchemy/test-project.git@master#egg=pep508-package&subdirectory=parent_folder/pep508-package 9 | -------------------------------------------------------------------------------- /tests/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/models/__init__.py -------------------------------------------------------------------------------- /tests/models/test_marker.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pdm.models.markers import EnvSpec, get_marker 4 | from pdm.models.specifiers import PySpecSet 5 | 6 | 7 | @pytest.mark.parametrize( 8 | "original,marker,py_spec", 9 | [ 10 | ("python_version > '3'", "", ">=3.1"), 11 | ("python_version > '3.8'", "", ">=3.9"), 12 | ("python_version != '3.8'", "", "!=3.8.*"), 13 | ("python_version == '3.7'", "", "==3.7.*"), 14 | ("python_version in '3.6 3.7'", "", ">=3.6.0,<3.8.0"), 15 | ("python_full_version >= '3.6.0'", "", ">=3.6"), 16 | ("python_full_version not in '3.8.3'", "", "!=3.8.3"), 17 | # mixed marker and python version 18 | ("python_version > '3.7' and os_name == 'nt'", 'os_name == "nt"', ">=3.8"), 19 | ( 20 | "python_version > '3.7' or os_name == 'nt'", 21 | 'python_version > "3.7" or os_name == "nt"', 22 | "", 23 | ), 24 | ], 25 | ) 26 | def test_split_pyspec(original, marker, py_spec): 27 | m = get_marker(original) 28 | a, b = m.split_pyspec() 29 | assert marker == str(a) 30 | assert b == PySpecSet(py_spec) 31 | 32 | 33 | @pytest.mark.parametrize( 34 | "marker,env_spec,expected", 35 | [ 36 | ("os_name == 'nt'", EnvSpec.from_spec(">=3.10", "windows"), True), 37 | ("os_name == 'nt'", EnvSpec.from_spec(">=3.10"), True), 38 | ("os_name != 'nt'", EnvSpec.from_spec(">=3.10", "windows"), False), 39 | ("python_version >= '3.7' and os_name == 'nt'", EnvSpec.from_spec(">=3.10"), True), 40 | ("python_version < '3.7' and os_name == 'nt'", EnvSpec.from_spec(">=3.10"), False), 41 | ("python_version < '3.7' or os_name == 'nt'", EnvSpec.from_spec(">=3.10"), False), 42 | ("python_version >= '3.7' and os_name == 'nt'", EnvSpec.from_spec(">=3.10", "linux"), False), 43 | ("python_version >= '3.7' or os_name == 'nt'", EnvSpec.from_spec(">=3.10", "linux"), True), 44 | ("python_version >= '3.7' and implementation_name == 'pypy'", EnvSpec.from_spec(">=3.10"), True), 45 | ( 46 | "python_version >= '3.7' and implementation_name == 'pypy'", 47 | EnvSpec.from_spec(">=3.10", implementation="cpython"), 48 | False, 49 | ), 50 | ], 51 | ) 52 | def test_match_env_spec(marker, env_spec, expected): 53 | m = get_marker(marker) 54 | assert m.matches(env_spec) is expected 55 | -------------------------------------------------------------------------------- /tests/models/test_serializers.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | from datetime import datetime 5 | 6 | import pytest 7 | from hishel._serializers import Metadata 8 | from httpcore import Request, Response 9 | 10 | from pdm.models.serializers import Encoder, MsgPackSerializer 11 | 12 | 13 | @pytest.mark.msgpack 14 | def test_compatibility(): 15 | try: 16 | import msgpack 17 | except ImportError: 18 | pytest.skip("msgpack is not installed, skipping compatibility tests") 19 | 20 | response = Response(200, headers={"key": "value"}, content=b"I'm a teapot.", extensions={"http_version": "2.0"}) 21 | response.read() 22 | request = Request("POST", "http://test.com", headers={"user-agent": ""}, extensions={"timeout": 10}) 23 | metadata = Metadata(number_of_uses=1, created_at=datetime.now(), cache_key="foo") 24 | serializer = MsgPackSerializer() 25 | 26 | # dumped by msgpack, loads by msgpack is OK 27 | cached_bytes = serializer.dumps(response, request, metadata) 28 | resp, req, meta = serializer.loads(cached_bytes) 29 | resp.read() 30 | assert not cached_bytes.startswith(b"{") # Ensure that it was dumped by msgpack 31 | assert resp.status == response.status 32 | assert resp.content == response.content 33 | assert resp.headers == response.headers 34 | assert resp.extensions == response.extensions 35 | assert req.method == request.method 36 | assert req.extensions == request.extensions 37 | assert req.headers == request.headers 38 | assert meta == metadata 39 | 40 | # dumped by msgpack, loads by json will return None 41 | origin_msgpack_loads = msgpack.loads 42 | msgpack.loads = lambda data, raw: json.loads(data, object_hook=Encoder.object_hook) 43 | assert serializer.loads(cached_bytes) is None 44 | 45 | # dumped by json, loads by json is OK 46 | msgpack.packb = lambda data, use_bin_type: json.dumps(data, cls=Encoder).encode() 47 | cached_bytes = serializer.dumps(response, request, metadata) 48 | assert cached_bytes.startswith(b"{") # Ensure that it was dumped by json 49 | resp, req, meta = serializer.loads(cached_bytes) 50 | resp.read() 51 | assert resp.status == response.status 52 | assert resp.content == response.content 53 | assert resp.headers == response.headers 54 | assert resp.extensions == response.extensions 55 | assert req.method == request.method 56 | assert req.extensions == request.extensions 57 | assert req.headers == request.headers 58 | assert meta == metadata 59 | 60 | # dumped by json, loads with msgpack installed is OK too 61 | msgpack.loads = origin_msgpack_loads 62 | resp, req, meta = serializer.loads(cached_bytes) 63 | resp.read() 64 | assert resp.status == response.status 65 | assert resp.content == response.content 66 | assert resp.headers == response.headers 67 | assert resp.extensions == response.extensions 68 | assert req.method == request.method 69 | assert req.extensions == request.extensions 70 | assert req.headers == request.headers 71 | assert meta == metadata 72 | -------------------------------------------------------------------------------- /tests/models/test_session.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | if TYPE_CHECKING: 6 | from pdm.project.core import Project 7 | 8 | 9 | def test_session_sources_all_proxy(project: Project, mocker, monkeypatch): 10 | monkeypatch.setenv("all_proxy", "http://localhost:8888") 11 | mock_get_transport = mocker.patch("pdm.models.session._get_transport") 12 | 13 | assert project.environment.session is not None 14 | transport_args = mock_get_transport.call_args 15 | assert transport_args is not None 16 | assert transport_args.kwargs["proxy"].url == "http://localhost:8888" 17 | 18 | monkeypatch.setenv("no_proxy", "pypi.org") 19 | mock_get_transport.reset_mock() 20 | del project.environment.session 21 | assert project.environment.session is not None 22 | transport_args = mock_get_transport.call_args 23 | assert transport_args is not None 24 | assert transport_args.kwargs["proxy"] is None 25 | -------------------------------------------------------------------------------- /tests/models/test_setup_parsing.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pdm.models.setup import Setup 4 | 5 | 6 | @pytest.mark.parametrize( 7 | "content, result", 8 | [ 9 | ( 10 | """[metadata] 11 | name = foo 12 | version = 0.1.0 13 | """, 14 | Setup("foo", "0.1.0"), 15 | ), 16 | ( 17 | """[metadata] 18 | name = foo 19 | version = attr:foo.__version__ 20 | """, 21 | Setup("foo", "0.0.0"), 22 | ), 23 | ( 24 | """[metadata] 25 | name = foo 26 | version = 0.1.0 27 | 28 | [options] 29 | python_requires = >=3.6 30 | install_requires = 31 | click 32 | requests 33 | [options.extras_require] 34 | tui = 35 | rich 36 | """, 37 | Setup("foo", "0.1.0", ["click", "requests"], {"tui": ["rich"]}, ">=3.6"), 38 | ), 39 | ], 40 | ) 41 | def test_parse_setup_cfg(content, result, tmp_path): 42 | tmp_path.joinpath("setup.cfg").write_text(content) 43 | assert Setup.from_directory(tmp_path) == result 44 | 45 | 46 | @pytest.mark.parametrize( 47 | "content,result", 48 | [ 49 | ( 50 | """from setuptools import setup 51 | 52 | setup(name="foo", version="0.1.0") 53 | """, 54 | Setup("foo", "0.1.0"), 55 | ), 56 | ( 57 | """import setuptools 58 | 59 | setuptools.setup(name="foo", version="0.1.0") 60 | """, 61 | Setup("foo", "0.1.0"), 62 | ), 63 | ( 64 | """from setuptools import setup 65 | 66 | kwargs = {"name": "foo", "version": "0.1.0"} 67 | setup(**kwargs) 68 | """, 69 | Setup("foo", "0.1.0"), 70 | ), 71 | ( 72 | """from setuptools import setup 73 | name = 'foo' 74 | setup(name=name, version="0.1.0") 75 | """, 76 | Setup("foo", "0.1.0"), 77 | ), 78 | ( 79 | """from setuptools import setup 80 | 81 | setup(name="foo", version="0.1.0", install_requires=['click', 'requests'], 82 | python_requires='>=3.6', extras_require={'tui': ['rich']}) 83 | """, 84 | Setup("foo", "0.1.0", ["click", "requests"], {"tui": ["rich"]}, ">=3.6"), 85 | ), 86 | ( 87 | """from pathlib import Path 88 | from setuptools import setup 89 | 90 | version = Path('__version__.py').read_text().strip() 91 | 92 | setup(name="foo", version=version) 93 | """, 94 | Setup("foo", "0.0.0"), 95 | ), 96 | ], 97 | ) 98 | def test_parse_setup_py(content, result, tmp_path): 99 | tmp_path.joinpath("setup.py").write_text(content) 100 | assert Setup.from_directory(tmp_path) == result 101 | 102 | 103 | def test_parse_pyproject_toml(tmp_path): 104 | content = """[project] 105 | name = "foo" 106 | version = "0.1.0" 107 | requires-python = ">=3.6" 108 | dependencies = ["click", "requests"] 109 | 110 | [project.optional-dependencies] 111 | tui = ["rich"] 112 | """ 113 | tmp_path.joinpath("pyproject.toml").write_text(content) 114 | result = Setup("foo", "0.1.0", ["click", "requests"], {"tui": ["rich"]}, ">=3.6") 115 | assert Setup.from_directory(tmp_path) == result 116 | -------------------------------------------------------------------------------- /tests/models/test_specifiers.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pdm.models.specifiers import PySpecSet 4 | 5 | 6 | @pytest.mark.filterwarnings("ignore::FutureWarning") 7 | @pytest.mark.parametrize( 8 | "original,normalized", 9 | [ 10 | (">=3.6", ">=3.6"), 11 | ("<3.8", "<3.8"), 12 | ("~=2.7.0", "~=2.7.0"), 13 | ("", ""), 14 | (">=3.6,<3.8", "<3.8,>=3.6"), 15 | (">3.6", ">3.6"), 16 | ("<=3.7", "<=3.7"), 17 | (">=3.4.*", ">=3.4.0"), 18 | (">3.4.*", ">=3.4.0"), 19 | ("<=3.4.*", "<3.4.0"), 20 | ("<3.4.*", "<3.4.0"), 21 | (">=3.0+g1234", ">=3.0"), 22 | ("<3.0+g1234", "<3.0"), 23 | ("<3.10.0a6", "<3.10.0a6"), 24 | ("<3.10.2a3", "<3.10.2a3"), 25 | ], 26 | ) 27 | def test_normalize_pyspec(original, normalized): 28 | spec = PySpecSet(original) 29 | assert str(spec) == normalized 30 | 31 | 32 | @pytest.mark.parametrize( 33 | "left,right,result", 34 | [ 35 | (">=3.6", ">=3.0", ">=3.6"), 36 | (">=3.6", "<3.8", "<3.8,>=3.6"), 37 | ("", ">=3.6", ">=3.6"), 38 | (">=3.6", "<3.2", ""), 39 | (">=2.7,!=3.0.*", "!=3.1.*", "!=3.0.*,!=3.1.*,>=2.7"), 40 | (">=3.11.0a2", "<3.11.0b", ">=3.11.0a2,<3.11.0b0"), 41 | ("<3.11.0a2", ">3.11.0b", ""), 42 | ], 43 | ) 44 | def test_pyspec_and_op(left, right, result): 45 | left = PySpecSet(left) 46 | right = PySpecSet(right) 47 | assert left & right == PySpecSet(result) 48 | 49 | 50 | @pytest.mark.parametrize( 51 | "left,right,result", 52 | [ 53 | (">=3.6", ">=3.0", ">=3.0"), 54 | ("", ">=3.6", ""), 55 | (">=3.6", "<3.7", ""), 56 | (">=3.6,<3.8", ">=3.4,<3.7", "<3.8,>=3.4"), 57 | ("~=2.7", ">=3.6", "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"), 58 | ("<2.7.15", ">=3.0", "!=2.7.15,!=2.7.16,!=2.7.17,!=2.7.18"), 59 | (">3.11.0a2", ">3.11.0b", ">3.11.0a2"), 60 | ], 61 | ) 62 | def test_pyspec_or_op(left, right, result): 63 | left = PySpecSet(left) 64 | right = PySpecSet(right) 65 | assert str(left | right) == result 66 | 67 | 68 | def test_impossible_pyspec(): 69 | spec = PySpecSet(">=3.6,<3.4") 70 | a = PySpecSet(">=2.7") 71 | assert spec.is_empty() 72 | assert (spec & a).is_empty() 73 | assert spec | a == a 74 | 75 | 76 | @pytest.mark.filterwarnings("ignore::FutureWarning") 77 | @pytest.mark.parametrize( 78 | "left,right", 79 | [ 80 | ("~=2.7", ">=2.7"), 81 | (">=3.6", ""), 82 | (">=3.7", ">=3.6,<4.0"), 83 | (">=2.7,<3.0", ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"), 84 | (">=3.6", ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"), 85 | ( 86 | ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*", 87 | ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*", 88 | ), 89 | (">=3.11.*", ">=3.11.0rc"), 90 | ], 91 | ) 92 | def test_pyspec_is_subset_superset(left, right): 93 | left = PySpecSet(left) 94 | right = PySpecSet(right) 95 | assert left.is_subset(right), f"{left}, {right}" 96 | assert right.is_superset(left), f"{left}, {right}" 97 | 98 | 99 | @pytest.mark.parametrize( 100 | "left,right", 101 | [ 102 | ("~=2.7", ">=2.6,<2.7.15"), 103 | (">=3.7", ">=3.6,<3.9"), 104 | (">=3.7,<3.6", "==2.7"), 105 | (">=3.0,!=3.4.*", ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"), 106 | (">=3.11.0", "<3.11.0a"), 107 | ], 108 | ) 109 | def test_pyspec_isnot_subset_superset(left, right): 110 | left = PySpecSet(left) 111 | right = PySpecSet(right) 112 | assert not left.is_subset(right), f"{left}, {right}" 113 | assert not left.is_superset(right), f"{left}, {right}" 114 | -------------------------------------------------------------------------------- /tests/models/test_versions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pdm.models.versions import InvalidPyVersion, Version 4 | 5 | 6 | def test_unsupported_post_version() -> None: 7 | with pytest.raises(InvalidPyVersion): 8 | Version("3.10.0post1") 9 | 10 | 11 | def test_support_prerelease_version() -> None: 12 | assert not Version("3.9.0").is_prerelease 13 | v = Version("3.9.0a4") 14 | assert v.is_prerelease 15 | assert str(v) == "3.9.0a4" 16 | assert v.complete() == v 17 | assert v.bump() == Version("3.9.0a5") 18 | assert v.bump(2) == Version("3.9.1") 19 | 20 | 21 | def test_normalize_non_standard_version(): 22 | version = Version("3.9*") 23 | assert str(version) == "3.9.*" 24 | 25 | 26 | def test_version_comparison(): 27 | assert Version("3.9.0") < Version("3.9.1") 28 | assert Version("3.4") < Version("3.9.1") 29 | assert Version("3.7.*") < Version("3.7.5") 30 | assert Version("3.7") == Version((3, 7)) 31 | 32 | assert Version("3.9.0a") != Version("3.9.0") 33 | assert Version("3.9.0a") == Version("3.9.0a0") 34 | assert Version("3.10.0a9") < Version("3.10.0a12") 35 | assert Version("3.10.0a12") < Version("3.10.0b1") 36 | assert Version("3.7.*") < Version("3.7.1b") 37 | 38 | 39 | def test_version_is_wildcard(): 40 | assert not Version("3").is_wildcard 41 | assert Version("3.*").is_wildcard 42 | 43 | 44 | def test_version_is_py2(): 45 | assert not Version("3.8").is_py2 46 | assert Version("2.7").is_py2 47 | 48 | 49 | @pytest.mark.parametrize( 50 | "version,args,result", 51 | [("3.9", (), "3.9.0"), ("3.9", ("*",), "3.9.*"), ("3", (0, 2), "3.0")], 52 | ) 53 | def test_version_complete(version, args, result): 54 | assert str(Version(version).complete(*args)) == result 55 | 56 | 57 | @pytest.mark.parametrize( 58 | "version,idx,result", 59 | [ 60 | ("3.8.0", -1, "3.8.1"), 61 | ("3.8", -1, "3.9.0"), 62 | ("3", 0, "4.0.0"), 63 | ("3.8.1", 1, "3.9.0"), 64 | ], 65 | ) 66 | def test_version_bump(version, idx, result): 67 | assert str(Version(version).bump(idx)) == result 68 | 69 | 70 | @pytest.mark.parametrize( 71 | "version,other,result", 72 | [ 73 | ("3.8.0", "3.8", True), 74 | ("3.8.*", "3.8", True), 75 | ("3.8.1", "3.7", False), 76 | ("3.8", "3.8.2", False), 77 | ], 78 | ) 79 | def test_version_startswith(version, other, result): 80 | assert Version(version).startswith(Version(other)) is result 81 | 82 | 83 | def test_version_getitem(): 84 | version = Version("3.8.6") 85 | assert version[0] == 3 86 | assert version[1] == 8 87 | assert version[2] == 6 88 | assert version[1:2] == Version("8") 89 | assert version[:-1] == Version("3.8") 90 | 91 | 92 | def test_version_setitem(): 93 | version = Version("3.8.*") 94 | version1 = version.complete() 95 | version1[-1] = 0 96 | assert version1 == Version("3.8.0") 97 | 98 | version2 = version.complete() 99 | version2[0] = 4 100 | assert version2 == Version("4.8.*") 101 | 102 | version3 = version.complete() 103 | with pytest.raises(TypeError): 104 | version3[:2] = (1, 2) 105 | -------------------------------------------------------------------------------- /tests/resolver/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm/a93208db12ba98ee6d69396af034978e4c932558/tests/resolver/__init__.py -------------------------------------------------------------------------------- /tests/resolver/test_graph.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | from pdm.resolver.graph import OrderedSet 4 | 5 | 6 | def test_ordered_set(): 7 | elems = ["A", "bb", "c3"] 8 | all_sets = set() 9 | for case in itertools.permutations(elems): 10 | s = OrderedSet(case) 11 | all_sets.add(s) 12 | assert list(s) == list(case) 13 | assert len(s) == len(case) 14 | for e in elems: 15 | assert e in s 16 | assert e + "1" not in s 17 | assert str(s) == f"{{{', '.join(map(repr, case))}}}" 18 | assert repr(s) == f"OrderedSet({{{', '.join(map(repr, case))}}})" 19 | 20 | assert len(all_sets) == 1 21 | -------------------------------------------------------------------------------- /tests/resolver/test_uv_resolver.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pdm.models.markers import EnvSpec 4 | from pdm.models.requirements import parse_requirement 5 | 6 | pytestmark = [pytest.mark.network, pytest.mark.uv] 7 | 8 | 9 | def resolve(environment, requirements, target=None): 10 | from pdm.resolver.uv import UvResolver 11 | 12 | reqs = [] 13 | for req in requirements: 14 | if isinstance(req, str): 15 | req = parse_requirement(req) 16 | req.groups = ["default"] 17 | reqs.append(req) 18 | 19 | resolver = UvResolver( 20 | environment, 21 | requirements=reqs, 22 | target=target or environment.spec, 23 | update_strategy="all", 24 | strategies=set(), 25 | ) 26 | return resolver.resolve() 27 | 28 | 29 | def test_resolve_requirements(project): 30 | requirements = ["requests==2.32.0", "urllib3<2"] 31 | resolution = resolve(project.environment, requirements) 32 | mapping = {p.candidate.identify(): p.candidate for p in resolution.packages} 33 | assert mapping["requests"].version == "2.32.0" 34 | assert mapping["urllib3"].version.startswith("1.26") 35 | 36 | 37 | def test_resolve_vcs_requirement(project): 38 | requirements = ["git+https://github.com/pallets/click.git@8.1.0"] 39 | resolution = resolve(project.environment, requirements) 40 | mapping = {p.candidate.identify(): p.candidate for p in resolution.packages} 41 | assert "colorama" in mapping 42 | assert mapping["click"].req.is_vcs 43 | 44 | 45 | def test_resolve_with_python_requires(project): 46 | requirements = ["urllib3<2; python_version<'3.10'", "urllib3>=2; python_version>='3.10'"] 47 | if project.python.version_tuple >= (3, 10): 48 | resolution = resolve(project.environment, requirements, EnvSpec.from_spec(">=3.10")) 49 | packages = list(resolution.packages) 50 | assert len(packages) == 1 51 | assert packages[0].candidate.version.startswith("2.") 52 | 53 | resolution = resolve(project.environment, requirements, EnvSpec.from_spec(">=3.8")) 54 | packages = list(resolution.packages) 55 | assert len(packages) == 2 56 | 57 | 58 | def test_resolve_dependencies_with_nested_extras(project): 59 | name = project.name 60 | project.add_dependencies(["urllib3"], "default", write=False) 61 | project.add_dependencies(["idna"], "extra1", write=False) 62 | project.add_dependencies(["chardet", f"{name}[extra1]"], "extra2", write=False) 63 | project.add_dependencies([f"{name}[extra1,extra2]"], "all") 64 | 65 | dependencies = [*project.get_dependencies(), *project.get_dependencies("all")] 66 | assert len(dependencies) == 3, [dep.identify() for dep in dependencies] 67 | resolution = resolve(project.environment, dependencies) 68 | assert resolution.collected_groups == {"default", "extra1", "extra2", "all"} 69 | mapping = {p.candidate.identify(): p.candidate for p in resolution.packages} 70 | assert set(mapping) == {"urllib3", "idna", "chardet"} 71 | 72 | 73 | @pytest.mark.parametrize("overrides", ("2.31.0", "==2.31.0")) 74 | def test_resolve_dependencies_with_overrides(project, overrides): 75 | requirements = ["requests==2.32.0"] 76 | 77 | project.pyproject.settings["resolution"] = {"overrides": {"requests": overrides}} 78 | 79 | resolution = resolve(project.environment, requirements) 80 | 81 | mapping = {p.candidate.identify(): p.candidate for p in resolution.packages} 82 | assert mapping["requests"].version == "2.31.0" 83 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # https://pypi.org/project/tox-pdm/ is needed to run this tox configuration 2 | [tox] 3 | envlist = py3{9,10,11,12,13} 4 | passenv = LD_PRELOAD 5 | isolated_build = True 6 | 7 | [testenv] 8 | groups = test 9 | commands = test {posargs} 10 | -------------------------------------------------------------------------------- /typings/shellingham.pyi: -------------------------------------------------------------------------------- 1 | def detect_shell(pid: int | None = None, max_depth: int = 10) -> tuple[str, str]: ... 2 | 3 | class ShellDetectionFailure(OSError): ... 4 | --------------------------------------------------------------------------------