├── .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 |
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 | Python Dependency Manager
8 | {% endblock %}
9 |
10 | {% block footer %}
11 |
12 | {{ super() }}
13 |
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 |
--------------------------------------------------------------------------------