未验证 提交 cc2ad1e2 编写于 作者: F Frost Ming 提交者: GitHub

feat: pdm venv integration (#1193)

上级 0e725b18
......@@ -38,11 +38,11 @@ jobs:
install-via: [pip]
arch: [x64]
include:
- python-version: 3.9
- python-version: "3.10"
os: ubuntu-latest
install-via: script
arch: x64
- python-version: 3.9
- python-version: "3.10"
os: windows-latest
install-via: pip
arch: x86
......
......@@ -105,7 +105,7 @@ celerybeat.pid
.env
.venv
env/
venv/
/venv/
ENV/
env.bak/
venv.bak/
......
......@@ -30,23 +30,9 @@ with `Pipenv` or `Poetry` and don't want to introduce another package manager,
just stick to it. But if you are missing something that is not present in those tools,
you can probably find some goodness in `pdm`.
[PEP 582] proposes a project structure as below:
```
foo
__pypackages__
3.8
lib
bottle
myscript.py
```
There is a `__pypackages__` directory in the project root to hold all dependent libraries, just like what `npm` does.
Read more about the specification [here](https://www.python.org/dev/peps/pep-0582/#specification).
## Highlights of features
- [PEP 582] local package installer and runner, no virtualenv involved at all.
- Opt-in [PEP 582] support, no virtualenv involved at all.
- Simple and fast dependency resolver, mainly for large binary distributions.
- A [PEP 517] build backend.
- [PEP 621] project metadata.
......@@ -59,7 +45,7 @@ Read more about the specification [here](https://www.python.org/dev/peps/pep-058
[pep 621]: https://www.python.org/dev/peps/pep-0621
[pnpm]: https://pnpm.io/motivation#saving-disk-space-and-boosting-installation-speed
## Why not virtualenv?
## What is PEP 582?
The majority of Python packaging tools also act as virtualenv managers to gain the ability
to isolate project environments. But things get tricky when it comes to nested venvs: One
......@@ -71,6 +57,20 @@ all those venvs and upgrade them if required.
environments. It is a relatively new proposal and there are not many tools supporting it (one that does
is [pyflow], but it is written with Rust and thus can't get much help from the big Python community and for the same reason it can't act as a [PEP 517] backend).
[PEP 582] proposes a project structure as below:
```
foo
__pypackages__
3.8
lib
bottle
myscript.py
```
There is a `__pypackages__` directory in the project root to hold all dependent libraries, just like what `npm` does.
Read more about the specification [here](https://www.python.org/dev/peps/pep-0582/#specification).
## Installation
PDM requires python version 3.7 or higher.
......@@ -160,7 +160,7 @@ pdm init
Answer the questions following the guide, and a PDM project with a `pyproject.toml` file will be ready to use.
**Install dependencies into the `__pypackages__` directory**
**Install dependencies**
```bash
pdm add requests flask
......@@ -170,6 +170,9 @@ You can add multiple dependencies in the same command. After a while, check the
**Run your script with [PEP 582] support**
By default, PDM will create `.venv` in the project root, when doing `pdm install` on an existing project, as other package managers do.
But you can make PEP 582 the default by `pdm config python.use_venv false`. To enable the full power of PEP 582, do the following steps to make the Python interpreter use it.
Suppose you have a script `app.py` placed next to the `__pypackages__` directory with the following content(taken from Flask's website):
```python
......
......@@ -26,23 +26,9 @@ PDM 旨在成为下一代 Python 软件包管理工具。它最初是为个人
`poetry` 用着非常好,并不想引入一个新的包管理器,那么继续使用它们吧;但如果你发现有些东西这些
工具不支持,那么你很可能可以在 `pdm` 中找到。
[PEP 582] 提出下面这种项目的目录结构:
```
foo
__pypackages__
3.8
lib
bottle
myscript.py
```
项目目录中包含一个`__pypackages__`目录,用来放置所有依赖的库文件,就像`npm``node_modules`一样。
你可以在[这里](https://www.python.org/dev/peps/pep-0582/#specification)阅读更多提案的细节。
## 主要特性
- [PEP 582] 本地项目库目录,支持安装与运行命令,完全不需要虚拟环境。
- [PEP 582] 支持,完全不需要虚拟环境。
- 一个简单且相对快速的依赖解析器,特别是对于大的二进制包发布。
- 兼容 [PEP 517] 的构建后端,用于构建发布包(源码格式与 wheel 格式)
- 灵活且强大的插件系统
......@@ -66,6 +52,20 @@ foo
没有很多相关的工具实现它,这其中就有 [pyflow]。但 pyflow 又是用 Rust 写的,不是所有 Python 的社区
都会用 Rust,这样就没法贡献代码,而且,基于同样的原因,pyflow 并不支持 [PEP 517] 构建。
[PEP 582] 提出下面这种项目的目录结构:
```
foo
__pypackages__
3.8
lib
bottle
myscript.py
```
项目目录中包含一个`__pypackages__`目录,用来放置所有依赖的库文件,就像`npm``node_modules`一样。
你可以在[这里](https://www.python.org/dev/peps/pep-0582/#specification)阅读更多提案的细节。
## 安装
PDM 需要 Python 3.7 或更高版本。
......@@ -148,7 +148,7 @@ pdm init
按照指引回答提示的问题,一个 PDM 项目和对应的`pyproject.toml`文件就创建好了。
**把依赖安装到 `__pypackages__` 文件夹中**
**添加依赖**
```bash
pdm add requests flask
......@@ -158,6 +158,12 @@ pdm add requests flask
**在 [PEP 582] 加持下运行你的脚本**
默认情况下,当你在一个项目中第一次运行 `pdm install`, PDM 会为你在项目根目录的 `.venv` 中创建一个虚拟环境,和其他包管理器一样。
但你也可以把 PEP 582 设为默认,只需要运行 `pdm config python.use_venv false` 就可以了。除此之外,你还需要一点点的配置,让 Python 解释器
可以用 PEP 582 的 `__papackages__` 目录来查找包。
````bash
假设你在`__pypackages__`同级的目录下有一个`app.py`脚本,内容如下(从 Flask 的官网例子复制而来):
```python
......@@ -170,7 +176,7 @@ def hello_world():
if __name__ == '__main__':
app.run()
```
````
如果你使用的是 Bash,可以通过执行`eval "$(pdm --pep582)"`设置环境变量,现在你可以用你最熟悉的 **Python 解释器** 运行脚本了:
......
......@@ -11,7 +11,7 @@ in a similar way to `npm` that doesn't need to create a virtualenv at all!
## Feature highlights
- [PEP 582] local package installer and runner, no virtualenv involved at all.
- Opt-in [PEP 582] support, no virtualenv involved at all.
- Simple and fast dependency resolver, mainly for large binary distributions.
- A [PEP 517] build backend.
- [PEP 621] project metadata.
......@@ -132,30 +132,6 @@ You can either pass the options after the script or set the env var value.
.\pw --init pdm
```
### Enable PEP 582 globally
To make the Python interpreters aware of PEP 582 packages, one need to add the `pdm/pep582/sitecustomize.py`
to the Python library search path.
#### For Windows users
One just needs to execute `pdm --pep582`, then environment variable will be changed automatically. Don't forget
to restart the terminal session to take effect.
#### For Mac and Linux users
The command to change the environment variables can be printed by `pdm --pep582 [<SHELL>]`. If `<SHELL>`
isn't given, PDM will pick one based on some guesses. You can run `eval "$(pdm --pep582)"` to execute the command.
You may want to write a line in your `.bash_profile`(or similar profiles) to make it effective when logging in.
For example, in bash you can do this:
```bash
pdm --pep582 >> ~/.bash_profile
```
Once again, Don't forget to restart the terminal session to take effect.
## Shell Completion
PDM supports generating completion scripts for Bash, Zsh, Fish or Powershell. Here are some common locations for each shell:
......@@ -203,129 +179,10 @@ PDM supports generating completion scripts for Bash, Zsh, Fish or Powershell. He
pdm completion powershell | Out-File -Encoding utf8 $PROFILE\..\Completions\pdm_completion.ps1
```
## Use with IDE
Now there are not built-in support or plugins for PEP 582 in most IDEs, you have to configure your tools manually.
PDM will write and store project-wide configurations in `.pdm.toml` and you are recommended to add following lines
in the `.gitignore`:
```
.pdm.toml
__pypackages__/
```
### PyCharm
Mark `__pypackages__/<major.minor>/lib` as [Sources Root](https://www.jetbrains.com/help/pycharm/configuring-project-structure.html#mark-dir-project-view).
Then, select as [Python interpreter](https://www.jetbrains.com/help/pycharm/configuring-python-interpreter.html#interpreter) a Python installation with the same `<major.minor>` version.
Additionally, if you want to use tools from the environment (e.g. `pytest`), you have to add the
`__pypackages__/<major.minor>/bin` directory to the `PATH` variable in the corresponding
run/debug configuration.
### VSCode
Add the following two entries to the top-level dict in `.vscode/settings.json`:
```json
{
"python.autoComplete.extraPaths": ["__pypackages__/<major.minor>/lib"],
"python.analysis.extraPaths": ["__pypackages__/<major.minor>/lib"]
}
```
[Enable PEP582 globally](#enable-pep-582-globally),
and make sure VSCode runs using the same user and shell you enabled PEP582 for.
??? note "Cannot enable PEP582 globally?"
If for some reason you cannot enable PEP582 globally, you can still configure each "launch" in each project:
set the `PYTHONPATH` environment variable in your launch configuration, in `.vscode/launch.json`.
For example, to debug your `pytest` run:
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "pytest",
"type": "python",
"request": "launch",
"module": "pytest",
"args": ["tests"],
"justMyCode": false,
"env": {"PYTHONPATH": "__pypackages__/<major.minor>/lib"}
}
]
}
```
If your package resides in a `src` directory, add it to `PYTHONPATH` as well:
```json
"env": {"PYTHONPATH": "src:__pypackages__/<major.minor>/lib"}
```
??? note "Using Pylance/Pyright?"
If you have configured `"python.analysis.diagnosticMode": "workspace"`,
and you see a ton of errors/warnings as a result.
you may need to create `pyrightconfig.json` in the workspace directory, and fill in the following fields:
```json
{
"exclude": ["__pypackages__"]
}
```
Then restart the language server or VS Code and you're good to go.
In the future ([microsoft/pylance-release#1150](https://github.com/microsoft/pylance-release/issues/1150)), maybe the problem will be solved.
??? note "Using Jupyter Notebook?"
If you wish to use pdm to install jupyter notebook and use it in vscode in conjunction with the python extension:
1. Use `pdm add notebook` or so to install notebook
2. Add a `.env` file inside of your project director with contents like the following:
```
PYTHONPATH=/your-workspace-path/__pypackages__/<major>.<minor>/lib
```
If the above still doesn't work, it's most likely because the environment variable is not properly loaded when the Notebook starts. There are two workarounds.
1. Run `code .` in Terminal. It will open a new VSCode window in the current directory with the path set correctly. Use the Jupyter Notebook in the new window
2. If you prefer not to open a new window, run the following at the beginning of your Jupyter Notebook to explicitly set the path:
```
import sys
sys.path.append('/your-workspace-path/__pypackages__/<major>.<minor>/lib')
```
> [Reference Issue](https://github.com/pdm-project/pdm/issues/848)
#### Task Provider
In addition, there is a [VSCode Task Provider extension][pdm task provider] available for download.
This makes it possible for VSCode to automatically detect [pdm scripts][pdm scripts] so they
can be run natively as [VSCode Tasks][vscode tasks].
[vscode tasks]: https://code.visualstudio.com/docs/editor/tasks
[pdm task provider]: https://marketplace.visualstudio.com/items?itemName=knowsuchagency.pdm-task-provider
[pdm scripts]: usage/scripts.md
### Neovim
If using [neovim-lsp](https://github.com/neovim/nvim-lspconfig) with
[pyright](https://github.com/Microsoft/pyright) and want your
`__pypackages__` directory to be added to the path, you can add this to your
project's `pyproject.toml`.
```toml
[tool.pyright]
extraPaths = ["__pypackages__/<major.minor>/lib/"]
```
## Virtualenv and PEP 582
### [Seek for other IDEs or editors](usage/advanced.md#integrate-with-other-ide-or-editors)
In addition to the virtualenv management, PDM supports [PEP 582](https://www.python.org/dev/peps/pep-0582/) as an opt-in feature.
You can learn more about the two modes in the corresponding chapters in [Working with virtualenv](usage/venv.md) and [Working with PEP 582](usage/pep582.md)
## PDM Eco-system
......
......@@ -129,7 +129,7 @@ by `pip install -e .` or `python setup.py develop` in the **traditional** Python
as there is no such `setup.py` in a PDM project, how can we do that?
Fortunately, it becomes even easier with PDM and PEP 582. First, you should enable PEP 582 globally following the
[corresponding part of this doc](../index.md#enable-pep-582-globally). Then you just need to install all dependencies into the `__pypackages__` directory by:
[corresponding part of this doc](../usage/pep582.md#enable-pep-582-globally). Then you just need to install all dependencies into the `__pypackages__` directory by:
```bash
pdm install
......
## Available Configurations
The following configuration items can be retrieved and modified by [`pdm config`](usage/cli_reference.md#exec-0--config) command.
The following configuration items can be retrieved and modified by [`pdm config`](../usage/cli_reference.md#exec-0--config) command.
| Config Item | Description | Default Value | Available in Project | Env var |
| ----------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------- | -------------------- | ------------------------ |
| `build_isolation` | Isolate the build environment from the project environment | Yes | True | `PDM_BUILD_ISOLATION` |
| `cache_dir` | The root directory of cached files | The default cache location on OS | No | |
| `check_update` | Check if there is any newer version available | True | No | |
| `global_project.fallback` | Use the global project implicitly if no local project is found | `False` | No | |
| `global_project.fallback_verbose` | If True show message when global project is used implicitly | `True` | No | |
| `global_project.path` | The path to the global project | `<default config location on OS>/global-project` | No | |
| `global_project.user_site` | Whether to install to user site | `False` | No | |
| `install.cache` | Enable caching of wheel installations | False | Yes | |
| `install.cache_method` | Specify how to create links to the caches(`symlink` or `pth`) | `symlink` | Yes | |
| `install.parallel` | Whether to perform installation and uninstallation in parallel | `True` | Yes | `PDM_PARALLEL_INSTALL` |
| `project_max_depth` | The max depth to search for a project through the parents | 5 | No | `PDM_PROJECT_MAX_DEPTH` |
| `python.path` | The Python interpreter path | | Yes | `PDM_PYTHON` |
| `python.use_pyenv` | Use the pyenv interpreter | `True` | Yes | |
| `python.use_venv` | Install packages into the activated venv site packages instead of PEP 582 | `False` | Yes | `PDM_USE_VENV` |
| `pypi.url` | The URL of PyPI mirror | Read `index-url` in `pip.conf`, or `https://pypi.org/simple` if not found | Yes | `PDM_PYPI_URL` |
| `pypi.verify_ssl` | Verify SSL certificate when query PyPI | Read `trusted-hosts` in `pip.conf`, defaults to `True` | Yes | |
| `pypi.json_api` | Consult PyPI's JSON API for package metadata | `False` | Yes | `PDM_PYPI_JSON_API` |
| `strategy.save` | Specify how to save versions when a package is added | `compatible`(can be: `exact`, `wildcard`, `minimum`) | Yes | |
| `strategy.update` | The default strategy for updating packages | `reuse`(can be : `eager`) | Yes | |
| `strategy.resolve_max_rounds` | Specify the max rounds of resolution process | 1000 | Yes | `PDM_RESOLVE_MAX_ROUNDS` |
| Config Item | Description | Default Value | Available in Project | Env var |
| --------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------- | -------------------- | ------------------------ |
| `build_isolation` | Isolate the build environment from the project environment | Yes | True | `PDM_BUILD_ISOLATION` |
| `cache_dir` | The root directory of cached files | The default cache location on OS | No | |
| `check_update` | Check if there is any newer version available | True | No | |
| `global_project.fallback` | Use the global project implicitly if no local project is found | `False` | No | |
| `global_project.fallback_verbose` | If True show message when global project is used implicitly | `True` | No | |
| `global_project.path` | The path to the global project | `<default config location on OS>/global-project` | No | |
| `global_project.user_site` | Whether to install to user site | `False` | No | |
| `install.cache` | Enable caching of wheel installations | False | Yes | |
| `install.cache_method` | Specify how to create links to the caches(`symlink` or `pth`) | `symlink` | Yes | |
| `install.parallel` | Whether to perform installation and uninstallation in parallel | `True` | Yes | `PDM_PARALLEL_INSTALL` |
| `project_max_depth` | The max depth to search for a project through the parents | 5 | No | `PDM_PROJECT_MAX_DEPTH` |
| `python.path` | The Python interpreter path | | Yes | `PDM_PYTHON` |
| `python.use_pyenv` | Use the pyenv interpreter | `True` | Yes | |
| `python.use_venv` | Install packages into the activated venv site packages instead of PEP 582 | `False` | Yes | `PDM_USE_VENV` |
| `pypi.url` | The URL of PyPI mirror | Read `index-url` in `pip.conf`, or `https://pypi.org/simple` if not found | Yes | `PDM_PYPI_URL` |
| `pypi.verify_ssl` | Verify SSL certificate when query PyPI | Read `trusted-hosts` in `pip.conf`, defaults to `True` | Yes | |
| `pypi.json_api` | Consult PyPI's JSON API for package metadata | `False` | Yes | `PDM_PYPI_JSON_API` |
| `strategy.save` | Specify how to save versions when a package is added | `compatible`(can be: `exact`, `wildcard`, `minimum`) | Yes | |
| `strategy.update` | The default strategy for updating packages | `reuse`(can be : `eager`) | Yes | |
| `strategy.resolve_max_rounds` | Specify the max rounds of resolution process | 1000 | Yes | `PDM_RESOLVE_MAX_ROUNDS` |
| `venv.location` | Parent directory for virtualenvs | `<default data location on OS>/venvs` | No | |
| `venv.backend` | Default backend to create virtualenv | `virtualenv` | Yes | `PDM_VENV_BACKEND` |
| `venv.in-project` | Create virtualenv in `.venv` under project root | `False` | Yes | `PDM_VENV_IN_PROJECT` |
_If the corresponding env var is set, the value will take precedence over what is saved in the config file._
# Working with PEP 582
With [PEP 582](https://www.python.org/dev/peps/pep-0582/), dependencies will be installed into `__pypackages__` directory under the project root. With [PEP 582 enabled globally](#enable-pep-582-globally), you can also use the project interpreter to run scripts directly.
**When the project interpreter is a normal Python, this mode is enabled.**
Besides, on a project you work with for the first time on your machine, if it contains an empty `__pypackages__` directory, PEP 582 is enabled automatically, and virtualenv won't be created.
## Enable PEP 582 globally
To make the Python interpreters aware of PEP 582 packages, one need to add the `pdm/pep582/sitecustomize.py`
to the Python library search path.
=== "Windows"
One just needs to execute `pdm --pep582`, then environment variable will be changed automatically. Don't forget
to restart the terminal session to take effect.
=== "Mac and Linux"
The command to change the environment variables can be printed by `pdm --pep582 [<SHELL>]`. If `<SHELL>`
isn't given, PDM will pick one based on some guesses. You can run `eval "$(pdm --pep582)"` to execute the command.
You may want to write a line in your `.bash_profile`(or similar profiles) to make it effective when logging in.
For example, in bash you can do this:
```bash
pdm --pep582 >> ~/.bash_profile
```
Once again, Don't forget to restart the terminal session to take effect.
??? note "How is it done?"
Thanks to the [site packages loading](https://docs.python.org/3/library/site.html) on Python startup. It is possible to patch the `sys.path`
by executing the `sitecustomize.py` shipped with PDM. The interpreter can search the directories
for the nearest `__pypackage__` folder and append it to the `sys.path` variable.
## Configure IDE to support PEP 582
Now there are no built-in support or plugins for PEP 582 in most IDEs, you have to configure your tools manually.
PDM will write and store project-wide configurations in `.pdm.toml` and you are recommended to add following lines
in the `.gitignore`:
```
.pdm.toml
__pypackages__/
```
### PyCharm
Mark `__pypackages__/<major.minor>/lib` as [Sources Root](https://www.jetbrains.com/help/pycharm/configuring-project-structure.html#mark-dir-project-view).
Then, select as [Python interpreter](https://www.jetbrains.com/help/pycharm/configuring-python-interpreter.html#interpreter) a Python installation with the same `<major.minor>` version.
Additionally, if you want to use tools from the environment (e.g. `pytest`), you have to add the
`__pypackages__/<major.minor>/bin` directory to the `PATH` variable in the corresponding
run/debug configuration.
### VSCode
Add the following two entries to the top-level dict in `.vscode/settings.json`:
```json
{
"python.autoComplete.extraPaths": ["__pypackages__/<major.minor>/lib"],
"python.analysis.extraPaths": ["__pypackages__/<major.minor>/lib"]
}
```
[Enable PEP582 globally](#enable-pep-582-globally),
and make sure VSCode runs using the same user and shell you enabled PEP582 for.
??? note "Cannot enable PEP582 globally?"
If for some reason you cannot enable PEP582 globally, you can still configure each "launch" in each project:
set the `PYTHONPATH` environment variable in your launch configuration, in `.vscode/launch.json`.
For example, to debug your `pytest` run:
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "pytest",
"type": "python",
"request": "launch",
"module": "pytest",
"args": ["tests"],
"justMyCode": false,
"env": {"PYTHONPATH": "__pypackages__/<major.minor>/lib"}
}
]
}
```
If your package resides in a `src` directory, add it to `PYTHONPATH` as well:
```json
"env": {"PYTHONPATH": "src:__pypackages__/<major.minor>/lib"}
```
??? note "Using Pylance/Pyright?"
If you have configured `"python.analysis.diagnosticMode": "workspace"`,
and you see a ton of errors/warnings as a result.
you may need to create `pyrightconfig.json` in the workspace directory, and fill in the following fields:
```json
{
"exclude": ["__pypackages__"]
}
```
Then restart the language server or VS Code and you're good to go.
In the future ([microsoft/pylance-release#1150](https://github.com/microsoft/pylance-release/issues/1150)), maybe the problem will be solved.
??? note "Using Jupyter Notebook?"
If you wish to use pdm to install jupyter notebook and use it in vscode in conjunction with the python extension:
1. Use `pdm add notebook` or so to install notebook
2. Add a `.env` file inside of your project director with contents like the following:
```
PYTHONPATH=/your-workspace-path/__pypackages__/<major>.<minor>/lib
```
If the above still doesn't work, it's most likely because the environment variable is not properly loaded when the Notebook starts. There are two workarounds.
1. Run `code .` in Terminal. It will open a new VSCode window in the current directory with the path set correctly. Use the Jupyter Notebook in the new window
2. If you prefer not to open a new window, run the following at the beginning of your Jupyter Notebook to explicitly set the path:
```
import sys
sys.path.append('/your-workspace-path/__pypackages__/<major>.<minor>/lib')
```
> [Reference Issue](https://github.com/pdm-project/pdm/issues/848)
??? note "PDM Task Provider"
In addition, there is a [VSCode Task Provider extension][pdm task provider] available for download.
This makes it possible for VSCode to automatically detect [pdm scripts][pdm scripts] so they
can be run natively as [VSCode Tasks][vscode tasks].
[vscode tasks]: https://code.visualstudio.com/docs/editor/tasks
[pdm task provider]: https://marketplace.visualstudio.com/items?itemName=knowsuchagency.pdm-task-provider
[pdm scripts]: scripts.md
### Neovim
If using [neovim-lsp](https://github.com/neovim/nvim-lspconfig) with
[pyright](https://github.com/Microsoft/pyright) and want your
`__pypackages__` directory to be added to the path, you can add this to your
project's `pyproject.toml`.
```toml
[tool.pyright]
extraPaths = ["__pypackages__/<major.minor>/lib/"]
```
### [Seek for other IDEs or editors](advanced.md#integrate-with-other-ide-or-editors)
......@@ -5,7 +5,7 @@
If you have used [`pdm init`](cli_reference.md#exec-0--init), you must have already seen how PDM detects and selects the Python
interpreter. After initialized, you can also change the settings by `pdm use <python_version_or_path>`.
The argument can be either a version specifier of any length, or a relative or absolute path to the
python interpreter, but remember the Python interpreter must conform with the `python_requires`
python interpreter, but remember the Python interpreter must conform with the `requires-python`
constraint in the project file.
### How `requires-python` controls the project
......@@ -159,9 +159,16 @@ The caches are located under `$(pdm config cache_dir)/packages`. One can view th
```bash
$ pdm info
Python Interpreter: D:/Programs/Python/Python38/python.exe (3.8.0)
Project Root: D:/Workspace/pdm
[10:42]
PDM version:
2.0.0
Python Interpreter:
/opt/homebrew/opt/python@3.9/bin/python3.9 (3.9)
Project Root:
/Users/fming/wkspace/github/test-pdm
Project Packages:
/Users/fming/wkspace/github/test-pdm/__pypackages__/3.9
# Show environment info
$ pdm info --env
{
"implementation_name": "cpython",
......@@ -178,6 +185,11 @@ $ pdm info --env
}
```
[This command](cli_reference.md#exec-0--info) is useful for checking which mode is being used by the project:
- If *Project Packages* is `None`, [virtualenv mode](venv.md) is enabled.
- Otherwise, [PEP 582 mode](pep582.md) is enabled.
## Manage global project
Sometimes users may want to keep track of the dependencies of global Python interpreter as well.
......@@ -199,19 +211,6 @@ project path via `-p/--project <path>` option.
!!! attention "CAUTION"
Be careful with `remove` and `sync --clean/--pure` commands when global project is used, because it may remove packages installed in your system Python.
## Working with a virtualenv
Although PDM enforces PEP 582 by default, it also allows users to install packages into the virtualenv. It is controlled
by the configuration item `python.use_venv`. When it is set to `True`, PDM will use the virtualenv if:
- a virtualenv is already activated.
- any of `venv`, `.venv`, `env` is a valid virtualenv folder.
Besides, when `python.use_venv` is on and the interpreter path given is a venv-like path, PDM will reuse that venv directory as well.
For enhanced virtualenv support such as virtualenv management and auto-creation, please go for [pdm-venv](https://github.com/pdm-project/pdm-venv),
which can be installed as a plugin.
## Import project metadata from existing project files
If you are already other package manager tools like Pipenv or Poetry, it is easy to migrate to PDM.
......@@ -279,9 +278,3 @@ PDM provides a convenient command group to manage the cache, there are four kind
See the current cache usage by typing `pdm cache info`. Besides, you can use `add`, `remove` and `list` subcommands to manage the cache content.
Find the usage by the `--help` option of each command.
## How we make PEP 582 packages available to the Python interpreter
Thanks to the [site packages loading](https://docs.python.org/3/library/site.html) on Python startup. It is possible to patch the `sys.path`
by executing the `sitecustomize.py` shipped with PDM. The interpreter can search the directories
for the nearest `__pypackage__` folder and append it to the `sys.path` variable.
# Working with virtualenv
When you run [`pdm init`](cli_reference.md#exec-0--init) command, PDM will [ask for the Python interpreter to use](project.md#choose-a-python-interpreter) in the project, which is the base interpreter to install dependencies and run tasks.
Compared to [PEP 582](https://www.python.org/dev/peps/pep-0582/), virtual environments are considered more mature and have better support in the Python ecosystem as well as IDEs. Therefore, virtualenv is the default mode if not configured otherwise.
**Virtual environments will be used if the project interpreter(the interpreter stored in `.pdm.toml`, which can be checked by `pdm info`) is from a virtualenv.**
## Virtualenv auto-creation
By default, PDM prefers to use the virtualenv layout as other package managers do. When you run `pdm install` the first time on a new PDM-managed project, whose Python interpreter is not decided yet, PDM will create a virtualenv in `<project_root>/.venv`, and install dependencies into it. In the interactive session of `pdm init`, PDM will also ask to create a virtualenv for you.
You can choose the backend used by PDM to create a virtualenv. Currently it supports three backends:
- [`virtualenv`](https://virtualenv.pypa.io/)(default)
- `venv`
- `conda`
You can change it by `pdm config venv.backend [virtualenv|venv|conda]`.
## Create a virtualenv yourself
You can create more than one virtualenvs with whatever Python version you want.
```bash
# Create a virtualenv based on 3.8 interpreter
$ pdm venv create 3.8
# Assign a different name other than the version string
$ pdm venv create --name for-test 3.8
# Use venv as the backend to create, support 3 backends: virtualenv(default), venv, conda
$ pdm venv create --with venv 3.9
```
## The location of virtualenvs
For the first time, PDM will try to create a virtualenv **in project**, unless `.venv` already exists.
Other virtualenvs go to the location specified by the `venv.location` configuration. They are named as `<project_name>-<path_hash>-<name_or_python_version>` to avoid name collision. A virtualenv created with `--name` option will always go to this location. You can disable the in-project virtualenv creation by `pdm config venv.in_project false`.
## Virtualenv auto-detection
When no interpreter is stored in the project config or `PDM_IGNORE_SAVED_PYTHON` env var is set, PDM will try to detect possible virtualenvs to use:
- `venv`, `env`, `.venv` directories in the project root
- The currently activated virtualenv
## List all virtualenvs created with this project
```bash
$ pdm venv list
Virtualenvs created with this project:
- 3.8.6: C:\Users\Frost Ming\AppData\Local\pdm\pdm\venvs\test-project-8Sgn_62n-3.8.6
- for-test: C:\Users\Frost Ming\AppData\Local\pdm\pdm\venvs\test-project-8Sgn_62n-for-test
- 3.9.1: C:\Users\Frost Ming\AppData\Local\pdm\pdm\venvs\test-project-8Sgn_62n-3.9.1
```
## Remove a virtualenv
```bash
$ pdm venv remove for-test
Virtualenvs created with this project:
Will remove: C:\Users\Frost Ming\AppData\Local\pdm\pdm\venvs\test-project-8Sgn_62n-for-test, continue? [y/N]:y
Removed C:\Users\Frost Ming\AppData\Local\pdm\pdm\venvs\test-project-8Sgn_62n-for-test
```
## Activate a virtualenv
Instead of spawning a subshell like what `pipenv` and `poetry` do, `pdm-venv` doesn't create the shell for you but print the activate command to the console. In this way you won't leave the current shell. You can then feed the output to `eval` to activate the virtualenv:
=== "bash/csh/zsh"
```bash
$ eval $(pdm venv activate for-test)
(test-project-8Sgn_62n-for-test) $ # Virtualenv entered
Fish
$ eval (pdm venv activate for-test)
```
=== "Powershell"
```ps1
PS1> Invoke-Expression (pdm venv activate for-test)
```
You can make your own shell shortcut function to avoid the input of long command. Here is an example of Bash:
```ps1
pdm_venv_activate() {
eval $('pdm' 'venv' 'activate' "$1")
}
```
Then you can activate it by `pdm_venv_activate $venv_name` and deactivate by deactivate directly.
Additionally, if the project interpreter is a venv Python, you can omit the name argument following activate.
!!! NOTE
`venv activate` **does not** switch the Python interpreter used by the project. It only changes the shell by injecting the virtualenv paths to environment variables. For the forementioned purpose, use the `pdm use` command.
For more CLI usage, see the [`pdm venv`](cli_reference.md#exec-0--venv) documentation.
## Disable virtualenv mode
You can disable the auto-creation and auto-detection for virtualenv by `pdm config python.use_venv` false.
**If venv is disabled, PEP 582 mode will always be used even if the selected interpreter is from a virtualenv.**
......@@ -41,8 +41,10 @@ plugins:
nav:
- Usage:
- Home: index.md
- usage/dependency.md
- usage/project.md
- usage/venv.md
- usage/pep582.md
- usage/dependency.md
- usage/scripts.md
- usage/hooks.md
- usage/advanced.md
......@@ -76,7 +78,7 @@ markdown_extensions:
custom_fences:
- name: mermaid
class: mermaid
format: !!python/name:pymdownx.superfences.fence_code_format
format: "!!python/name:pymdownx.superfences.fence_code_format"
copyright: Copyright &copy; 2019-2021 <a href="https://frostming.com">Frost Ming</a>
......
Integrate `pdm venv` commands into the main program. Make PEP 582 an opt-in feature.
Remove the useless `--no-clean` option from `pdm sync` command.
......@@ -132,7 +132,7 @@ summary = "A platform independent file lock."
[[package]]
name = "findpython"
version = "0.1.6"
version = "0.2.0"
requires_python = ">=3.7"
summary = "A utility to find python versions on your system"
dependencies = [
......@@ -711,7 +711,7 @@ summary = "Backport of pathlib-compatible object wrapper for zip files"
[metadata]
lock_version = "3.1"
content_hash = "sha256:849f0d5912f6dc46efa4f7919c586cae25c43c3dc5ff8939a3e7ef0f0497e237"
content_hash = "sha256:0f7b10222fac8f4982fd1632d5ee9385d3e9fc6ffaa4317cdd1fb965627ce585"
[metadata.files]
"arpeggio 1.10.2" = [
......@@ -819,9 +819,9 @@ content_hash = "sha256:849f0d5912f6dc46efa4f7919c586cae25c43c3dc5ff8939a3e7ef0f0
{file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"},
{file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"},
]
"findpython 0.1.6" = [
{file = "findpython-0.1.6-py3-none-any.whl", hash = "sha256:79ec09965019b73c83f49df0de9a056d2055abf694f3813966a4841e80cf734e"},
{file = "findpython-0.1.6.tar.gz", hash = "sha256:9fd6185cdcb96baa7109308447efb493b2c7f1a8f569e128af14d726b2a69e18"},
"findpython 0.2.0" = [
{file = "findpython-0.2.0-py3-none-any.whl", hash = "sha256:110ec222a43aca3fcd154fd90b911f465c70e86787ae0532bab2266a95870fc9"},
{file = "findpython-0.2.0.tar.gz", hash = "sha256:c2099ee0b71fc2714b64f68fd1f40bc0ee47f49dfe9547fb64d7cbcc02fe0871"},
]
"ghp-import 2.1.0" = [
{file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"},
......
......@@ -590,8 +590,9 @@ def do_use(
python: str = "",
first: bool = False,
ignore_remembered: bool = False,
ignore_requires_python: bool = False,
hooks: HookManager | None = None,
) -> None:
) -> PythonInfo:
"""Use the specified python version and save in project config.
The python can be a version string or interpreter path.
"""
......@@ -601,7 +602,9 @@ def do_use(
python = python.strip()
def version_matcher(py_version: PythonInfo) -> bool:
return project.python_requires.contains(str(py_version.version), True)
return ignore_requires_python or project.python_requires.contains(
str(py_version.version), True
)
if not project.cache_dir.exists():
project.cache_dir.mkdir(parents=True)
......@@ -681,6 +684,7 @@ def do_use(
project.core.ui.echo("Updating executable scripts...", style="cyan")
project.environment.update_shebangs(selected_python.executable.as_posix())
hooks.try_emit("post_use", python=selected_python)
return selected_python
def do_import(
......
......@@ -5,8 +5,9 @@ from pdm.cli import actions
from pdm.cli.commands.base import BaseCommand
from pdm.cli.hooks import HookManager
from pdm.cli.options import skip_option
from pdm.models.python import PythonInfo
from pdm.project import Project
from pdm.utils import get_user_email_from_git
from pdm.utils import get_user_email_from_git, get_venv_like_prefix
class Command(BaseCommand):
......@@ -35,6 +36,8 @@ class Command(BaseCommand):
parser.set_defaults(search_parent=False)
def handle(self, project: Project, options: argparse.Namespace) -> None:
from pdm.cli.commands.venv.utils import get_venv_python
hooks = HookManager(project, options.skip)
if project.pyproject_file.exists():
project.core.ui.echo(
......@@ -45,13 +48,36 @@ class Command(BaseCommand):
self.set_interactive(not options.non_interactive)
if self.interactive:
actions.do_use(project, hooks=hooks)
python = actions.do_use(project, ignore_requires_python=True, hooks=hooks)
if (
project.config["python.use_venv"]
and get_venv_like_prefix(python.executable) is None
):
if termui.confirm(
"Would you like to create a virtualenv with "
f"[green]{python.executable}[/]?",
default=True,
):
try:
path = project._create_virtualenv()
project.python = PythonInfo.from_path(get_venv_python(path))
except Exception as e: # pragma: no cover
project.core.ui.echo(
f"Error occured when creating virtualenv: {e}\n"
"Please fix it and create later.",
style="red",
err=True,
)
else:
actions.do_use(project, "3", True, hooks=hooks)
is_library = (
termui.confirm(
"Is the project a library that will be uploaded to PyPI", default=False
actions.do_use(project, "3", True, ignore_requires_python=True, hooks=hooks)
if get_venv_like_prefix(project.python.executable) is None:
project.core.ui.echo(
"You are using the PEP 582 mode, no virtualenv is created.\n"
"For more info, please visit https://peps.python.org/pep-0582/",
style="green",
)
is_library = (
termui.confirm("Is the project a library that will be uploaded to PyPI")
if self.interactive
else False
)
......
......@@ -166,7 +166,9 @@ class RemoveCommand(BaseCommand):
sys.exit(1)
if not (
options.yes
or termui.confirm(f"Will remove: {packages_to_remove}, continue?")
or termui.confirm(
f"Will remove: {packages_to_remove}, continue?", default=True
)
):
return
pip_args = (
......
import argparse
from typing import List
from pdm import Project
from pdm.cli.commands.base import BaseCommand
from pdm.cli.commands.venv.activate import ActivateCommand
from pdm.cli.commands.venv.create import CreateCommand
from pdm.cli.commands.venv.list import ListCommand
from pdm.cli.commands.venv.purge import PurgeCommand
from pdm.cli.commands.venv.remove import RemoveCommand
from pdm.cli.options import Option
class Command(BaseCommand):
"""Virtualenv management"""
name = "venv"
arguments: List[Option] = []
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
subparser = parser.add_subparsers()
CreateCommand.register_to(subparser, "create")
ListCommand.register_to(subparser, "list")
RemoveCommand.register_to(subparser, "remove")
ActivateCommand.register_to(subparser, "activate")
PurgeCommand.register_to(subparser, "purge")
self.parser = parser
def handle(self, project: Project, options: argparse.Namespace) -> None:
self.parser.print_help()
import argparse
import shlex
from pathlib import Path
import shellingham
from pdm.cli.commands.base import BaseCommand
from pdm.cli.commands.venv.utils import BIN_DIR, iter_venvs
from pdm.cli.options import verbose_option
from pdm.project import Project
from pdm.utils import get_venv_like_prefix
class ActivateCommand(BaseCommand):
"""Activate the virtualenv with the given name"""
arguments = [verbose_option]
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
parser.add_argument("env", nargs="?", help="The key of the virtualenv")
def handle(self, project: Project, options: argparse.Namespace) -> None:
if options.env:
venv = next(
(venv for key, venv in iter_venvs(project) if key == options.env), None
)
if not venv:
project.core.ui.echo(
f"No virtualenv with key [green]{options.env}[/] is found",
style="yellow",
err=True,
)
raise SystemExit(1)
else:
# Use what is saved in .pdm.toml
interpreter = project.python_executable
venv = get_venv_like_prefix(interpreter)
if venv is None:
project.core.ui.echo(
f"Can't activate a non-venv Python [green]{interpreter}[/], "
"you can specify one with [green]pdm venv activate <env_name>[/]",
style="yellow",
err=True,
)
raise SystemExit(1)
project.core.ui.echo(self.get_activate_command(venv))
def get_activate_command(self, venv: Path) -> str: # pragma: no cover
shell, _ = shellingham.detect_shell()
if shell == "fish":
command, filename = "source", "activate.fish"
elif shell == "csh":
command, filename = "source", "activate.csh"
elif shell in ["powershell", "pwsh"]:
command, filename = ".", "Activate.ps1"
else:
command, filename = "source", "activate"
activate_script = venv / BIN_DIR / filename
if activate_script.exists():
return f"{command} {shlex.quote(str(activate_script))}"
# Conda backed virtualenvs don't have activate scripts
return f"conda activate {shlex.quote(str(venv))}"
import abc
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Any, List, Mapping, Optional, Tuple, Type
from pdm import Project, termui
from pdm.cli.commands.venv.utils import get_venv_prefix
from pdm.exceptions import PdmUsageError, ProjectError
from pdm.models.python import PythonInfo
from pdm.utils import cached_property
class VirtualenvCreateError(ProjectError):
pass
class Backend(abc.ABC):
"""The base class for virtualenv backends"""
def __init__(self, project: Project, python: Optional[str]) -> None:
self.project = project
self.python = python
@cached_property
def _resolved_interpreter(self) -> PythonInfo:
if not self.python:
return self.project.python
try: # pragma: no cover
return next(iter(self.project.find_interpreters(self.python)))
except StopIteration: # pragma: no cover
raise VirtualenvCreateError(f"Can't find python interpreter {self.python}")
@property
def ident(self) -> str:
"""Get the identifier of this virtualenv.
self.python can be one of:
3.8
/usr/bin/python
3.9.0a4
python3.8
"""
return self._resolved_interpreter.identifier
def subprocess_call(self, cmd: List[str], **kwargs: Any) -> None:
self.project.core.ui.echo(
f"Run command: [green]{cmd}[/]", verbosity=termui.Verbosity.DETAIL, err=True
)
try:
subprocess.check_call(
cmd,
stdout=subprocess.DEVNULL
if self.project.core.ui.verbosity < termui.Verbosity.DETAIL
else None,
)
except subprocess.CalledProcessError as e: # pragma: no cover
raise VirtualenvCreateError(e) from None
def _ensure_clean(self, location: Path, force: bool = False) -> None:
if not location.exists():
return
if not force:
raise VirtualenvCreateError(f"The location {location} is not empty")
self.project.core.ui.echo(
f"Cleaning existing target directory {location}", err=True
)
shutil.rmtree(location)
def get_location(self, name: Optional[str]) -> Path:
venv_parent = Path(self.project.config["venv.location"])
if not venv_parent.is_dir():
venv_parent.mkdir(exist_ok=True, parents=True)
return venv_parent / f"{get_venv_prefix(self.project)}{name or self.ident}"
def create(
self,
name: Optional[str] = None,
args: Tuple[str, ...] = (),
force: bool = False,
in_project: bool = False,
) -> Path:
if in_project:
location = self.project.root / ".venv"
else:
location = self.get_location(name)
self._ensure_clean(location, force)
self.perform_create(location, args)
return location
@abc.abstractmethod
def perform_create(self, location: Path, args: Tuple[str, ...]) -> None:
pass
class VirtualenvBackend(Backend):
def perform_create(self, location: Path, args: Tuple[str, ...]) -> None:
cmd = [
sys.executable,
"-m",
"virtualenv",
"--no-pip",
"--no-setuptools",
"--no-wheel",
str(location),
]
cmd.extend(["-p", str(self._resolved_interpreter.executable)])
cmd.extend(args)
self.subprocess_call(cmd)
class VenvBackend(VirtualenvBackend):
def perform_create(self, location: Path, args: Tuple[str, ...]) -> None:
cmd = [
str(self._resolved_interpreter.executable),
"-m",
"venv",
"--without-pip",
str(location),
] + list(args)
self.subprocess_call(cmd)
class CondaBackend(Backend):
@property
def ident(self) -> str:
# Conda supports specifying python that doesn't exist,
# use the passed-in name directly
if self.python:
return self.python
return super().ident
def perform_create(self, location: Path, args: Tuple[str, ...]) -> None:
if self.python:
python_ver = self.python
else:
python = self._resolved_interpreter
python_ver = f"{python.major}.{python.minor}"
if any(arg.startswith("python=") for arg in args):
raise PdmUsageError("Cannot use python= in conda creation arguments")
cmd = [
"conda",
"create",
"--yes",
"--prefix",
str(location),
f"python={python_ver}",
*args,
]
self.subprocess_call(cmd)
BACKENDS: Mapping[str, Type[Backend]] = {
"virtualenv": VirtualenvBackend,
"venv": VenvBackend,
"conda": CondaBackend,
}
import argparse
from pdm import BaseCommand, Project
from pdm.cli.commands.venv.backends import BACKENDS
from pdm.cli.options import verbose_option
class CreateCommand(BaseCommand):
"""Create a virtualenv
pdm venv create <python> [-other args]
"""
description = "Create a virtualenv"
arguments = [verbose_option]
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"-w",
"--with",
dest="backend",
choices=BACKENDS.keys(),
help="Specify the backend to create the virtualenv",
)
parser.add_argument(
"-f",
"--force",
action="store_true",
help="Recreate if the virtualenv already exists",
)
parser.add_argument("-n", "--name", help="Specify the name of the virtualenv")
parser.add_argument(
"python",
nargs="?",
help="Specify which python should be used to create the virtualenv",
)
parser.add_argument(
"venv_args",
nargs=argparse.REMAINDER,
help="Additional arguments that will be passed to the backend",
)
def handle(self, project: Project, options: argparse.Namespace) -> None:
in_project = (
project.config["venv.in_project"]
and not options.name
and not project.root.joinpath(".venv").exists()
)
backend: str = options.backend or project.config["venv.backend"]
venv_backend = BACKENDS[backend](project, options.python)
with project.core.ui.open_spinner(
f"Creating virtualenv using [green]{backend}[/]..."
):
path = venv_backend.create(
options.name, options.venv_args, options.force, in_project
)
project.core.ui.echo(f"Virtualenv [green]{path}[/] is created successfully")
import argparse
from pathlib import Path
from pdm.cli.commands.base import BaseCommand
from pdm.cli.commands.venv.utils import iter_venvs
from pdm.cli.options import verbose_option
from pdm.project import Project
class ListCommand(BaseCommand):
"""List all virtualenvs associated with this project"""
arguments = [verbose_option]
def handle(self, project: Project, options: argparse.Namespace) -> None:
project.core.ui.echo("Virtualenvs created with this project:\n")
for ident, venv in iter_venvs(project):
saved_python = project.project_config.get("python.path")
if saved_python and Path(saved_python).parent.parent == venv:
mark = "*"
else:
mark = "-"
project.core.ui.echo(f"{mark} [green]{ident}[/]: {venv}")
import argparse
import shutil
from pdm import Project, termui
from pdm.cli.commands.base import BaseCommand
from pdm.cli.commands.venv.utils import iter_central_venvs
from pdm.cli.options import verbose_option
class PurgeCommand(BaseCommand):
"""Purge selected/all created Virtualenvs"""
arguments = [verbose_option]
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"-f",
"--force",
action="store_true",
help="Force purging without prompting for confirmation",
)
parser.add_argument(
"-i",
"--interactive",
action="store_true",
help="Interatively purge selected Virtualenvs",
)
def handle(self, project: Project, options: argparse.Namespace) -> None:
all_central_venvs = list(iter_central_venvs(project))
if not all_central_venvs:
project.core.ui.echo("No virtualenvs to purge, quitting.", style="green")
return
if not options.force:
project.core.ui.echo(
"The following Virtualenvs will be purged:", style="red"
)
for i, venv in enumerate(all_central_venvs):
project.core.ui.echo(f"{i}. [green]{venv[0]}[/]")
if not options.interactive:
if options.force or termui.confirm("continue?", default=True):
return self.del_all_venvs(project)
selection = termui.ask(
"Please select",
choices=([str(i) for i in range(len(all_central_venvs))] + ["all", "none"]),
default="none",
show_choices=False,
)
if selection == "all":
self.del_all_venvs(project)
elif selection != "none":
for i, venv in enumerate(all_central_venvs):
if i == int(selection):
shutil.rmtree(venv[1])
project.core.ui.echo("Purged successfully!")
def del_all_venvs(self, project: Project) -> None:
for _, venv in iter_central_venvs(project):
shutil.rmtree(venv)
project.core.ui.echo("Purged successfully!")
import argparse
import shutil
from pathlib import Path
from pdm import Project, termui
from pdm.cli.commands.base import BaseCommand
from pdm.cli.commands.venv.utils import iter_venvs
from pdm.cli.options import verbose_option
class RemoveCommand(BaseCommand):
"""Remove the virtualenv with the given name"""
arguments = [verbose_option]
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"-y",
"--yes",
action="store_true",
help="Answer yes on the following question",
)
parser.add_argument("env", help="The key of the virtualenv")
def handle(self, project: Project, options: argparse.Namespace) -> None:
project.core.ui.echo("Virtualenvs created with this project:")
for ident, venv in iter_venvs(project):
if ident == options.env:
if options.yes or termui.confirm(
f"[yellow]Will remove: [green]{venv}[/], continue?", default=True
):
shutil.rmtree(venv)
if (
project.project_config.get("python.path")
and Path(project.project_config["python.path"]).parent.parent
== venv
):
del project.project_config["python.path"]
project.core.ui.echo("Removed successfully!")
break
else:
project.core.ui.echo(
f"No virtualenv with key [green]{options.env}[/] is found",
style="yellow",
err=True,
)
raise SystemExit(1)
import base64
import hashlib
import sys
from pathlib import Path
from typing import Iterable, Optional, Tuple, Type, TypeVar
from findpython import PythonVersion
from findpython.providers import BaseProvider
from pdm.project import Project
IS_WIN = sys.platform == "win32"
BIN_DIR = "Scripts" if IS_WIN else "bin"
def hash_path(path: str) -> str:
"""Generate a hash for the given path."""
return base64.urlsafe_b64encode(hashlib.md5(path.encode()).digest()).decode()[:8]
def get_in_project_venv_python(root: Path) -> Optional[Path]:
"""Get the python interpreter path of venv-in-project"""
for possible_dir in (".venv", "venv", "env"):
venv_python = get_venv_python(root / possible_dir)
if venv_python.exists():
return venv_python
return None
def get_venv_prefix(project: Project) -> str:
"""Get the venv prefix for the project"""
path = project.root
name_hash = hash_path(path.as_posix())
return f"{path.name}-{name_hash}-"
def iter_venvs(project: Project) -> Iterable[Tuple[str, Path]]:
"""Return an iterable of venv paths associated with the project"""
in_project_venv_python = get_in_project_venv_python(project.root)
if in_project_venv_python is not None:
yield "in-project", Path(in_project_venv_python).parent.parent
venv_prefix = get_venv_prefix(project)
venv_parent = Path(project.config["venv.location"])
for venv in venv_parent.glob(f"{venv_prefix}*"):
ident = venv.name[len(venv_prefix) :]
yield ident, venv
def get_venv_python(venv: Path) -> Path:
"""Get the interpreter path inside the given venv."""
suffix = ".exe" if IS_WIN else ""
return venv / BIN_DIR / f"python{suffix}"
def iter_central_venvs(project: Project) -> Iterable[Tuple[str, Path]]:
"""Return an iterable of all managed venvs and their paths."""
venv_parent = Path(project.config["venv.location"])
for venv in venv_parent.glob("*"):
ident = venv.name
yield ident, venv
T = TypeVar("T", bound=BaseProvider)
class VenvProvider(BaseProvider):
"""A Python provider for project venv pythons"""
def __init__(self, project: Project) -> None:
self.project = project
@classmethod
def create(cls: Type[T]) -> Optional[T]: # pragma: no cover
return None
def find_pythons(self) -> Iterable[PythonVersion]:
for _, venv in iter_venvs(self.project):
python = get_venv_python(venv)
if python.exists():
yield PythonVersion(python, _interpreter=python, keep_symlink=True)
# BASH completion script for pdm
# Generated by pycomplete 0.3.2
_pdm_25182a7ef85b840e_complete()
_pdm_a919b69078acdf0a_complete()
{
local cur script coms opts com
COMPREPLY=()
......@@ -101,7 +101,7 @@ _pdm_25182a7ef85b840e_complete()
;;
(sync)
opts="--clean --dev --dry-run --global --group --help --lockfile --only-keep --no-default --no-editable --no-isolation --no-self --production --project --reinstall --skip --verbose"
opts="--clean --dev --dry-run --global --group --help --lockfile --no-default --no-editable --no-isolation --no-self --only-keep --production --project --reinstall --skip --verbose"
;;
(update)
......@@ -109,7 +109,11 @@ _pdm_25182a7ef85b840e_complete()
;;
(use)
opts="--first --global --help --ignore-remembered --project --verbose"
opts="--first --global --help --ignore-remembered --project --skip --verbose"
;;
(venv)
opts="--help"
;;
esac
......@@ -122,7 +126,7 @@ _pdm_25182a7ef85b840e_complete()
# completing for a command
if [[ $cur == $com ]]; then
coms="add build cache completion config export import info init install list lock plugin publish remove run search show sync update use"
coms="add build cache completion config export import info init install list lock plugin publish remove run search show sync update use venv"
COMPREPLY=($(compgen -W "${coms}" -- ${cur}))
__ltrim_colon_completions "$cur"
......@@ -131,4 +135,4 @@ _pdm_25182a7ef85b840e_complete()
fi
}
complete -o default -F _pdm_25182a7ef85b840e_complete pdm
complete -o default -F _pdm_a919b69078acdf0a_complete pdm
# FISH completion script for pdm
# Generated by pycomplete 0.3.2
function __fish_pdm_7426f3abf02b4bb8_complete_no_subcommand
function __fish_pdm_a919b69078acdf0a_complete_no_subcommand
for i in (commandline -opc)
if contains -- $i add build cache completion config export import info init install list lock plugin publish remove run search show sync update use
if contains -- $i add build cache completion config export import info init install list lock plugin publish remove run search show sync update use venv
return 1
end
end
......@@ -11,35 +11,36 @@ function __fish_pdm_7426f3abf02b4bb8_complete_no_subcommand
end
# global options
complete -c pdm -n '__fish_pdm_7426f3abf02b4bb8_complete_no_subcommand' -l config -d 'Specify another config file path(env var: PDM_CONFIG_FILE)'
complete -c pdm -n '__fish_pdm_7426f3abf02b4bb8_complete_no_subcommand' -l help -d 'show this help message and exit'
complete -c pdm -n '__fish_pdm_7426f3abf02b4bb8_complete_no_subcommand' -l ignore-python -d 'Ignore the Python path saved in the pdm.toml config'
complete -c pdm -n '__fish_pdm_7426f3abf02b4bb8_complete_no_subcommand' -l pep582 -d 'Print the command line to be eval\'d by the shell'
complete -c pdm -n '__fish_pdm_7426f3abf02b4bb8_complete_no_subcommand' -l verbose -d '-v for detailed output and -vv for more detailed'
complete -c pdm -n '__fish_pdm_7426f3abf02b4bb8_complete_no_subcommand' -l version -d 'Show version'
complete -c pdm -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -l config -d 'Specify another config file path(env var: PDM_CONFIG_FILE)'
complete -c pdm -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -l help -d 'show this help message and exit'
complete -c pdm -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -l ignore-python -d 'Ignore the Python path saved in the pdm.toml config'
complete -c pdm -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -l pep582 -d 'Print the command line to be eval\'d by the shell'
complete -c pdm -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -l verbose -d '-v for detailed output and -vv for more detailed'
complete -c pdm -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -l version -d 'Show version'
# commands
complete -c pdm -f -n '__fish_pdm_7426f3abf02b4bb8_complete_no_subcommand' -a add -d 'Add package(s) to pyproject.toml and install them'
complete -c pdm -f -n '__fish_pdm_7426f3abf02b4bb8_complete_no_subcommand' -a build -d 'Build artifacts for distribution'
complete -c pdm -f -n '__fish_pdm_7426f3abf02b4bb8_complete_no_subcommand' -a cache -d 'Control the caches of PDM'
complete -c pdm -f -n '__fish_pdm_7426f3abf02b4bb8_complete_no_subcommand' -a completion -d 'Generate completion scripts for the given shell'
complete -c pdm -f -n '__fish_pdm_7426f3abf02b4bb8_complete_no_subcommand' -a config -d 'Display the current configuration'
complete -c pdm -f -n '__fish_pdm_7426f3abf02b4bb8_complete_no_subcommand' -a export -d 'Export the locked packages set to other formats'
complete -c pdm -f -n '__fish_pdm_7426f3abf02b4bb8_complete_no_subcommand' -a import -d 'Import project metadata from other formats'
complete -c pdm -f -n '__fish_pdm_7426f3abf02b4bb8_complete_no_subcommand' -a info -d 'Show the project information'
complete -c pdm -f -n '__fish_pdm_7426f3abf02b4bb8_complete_no_subcommand' -a init -d 'Initialize a pyproject.toml for PDM'
complete -c pdm -f -n '__fish_pdm_7426f3abf02b4bb8_complete_no_subcommand' -a install -d 'Install dependencies from lock file'
complete -c pdm -f -n '__fish_pdm_7426f3abf02b4bb8_complete_no_subcommand' -a list -d 'List packages installed in the current working set'
complete -c pdm -f -n '__fish_pdm_7426f3abf02b4bb8_complete_no_subcommand' -a lock -d 'Resolve and lock dependencies'
complete -c pdm -f -n '__fish_pdm_7426f3abf02b4bb8_complete_no_subcommand' -a plugin -d 'Manage the PDM plugins'
complete -c pdm -f -n '__fish_pdm_7426f3abf02b4bb8_complete_no_subcommand' -a publish -d 'Build and publish the project to PyPI'
complete -c pdm -f -n '__fish_pdm_7426f3abf02b4bb8_complete_no_subcommand' -a remove -d 'Remove packages from pyproject.toml'
complete -c pdm -f -n '__fish_pdm_7426f3abf02b4bb8_complete_no_subcommand' -a run -d 'Run commands or scripts with local packages loaded'
complete -c pdm -f -n '__fish_pdm_7426f3abf02b4bb8_complete_no_subcommand' -a search -d 'Search for PyPI packages'
complete -c pdm -f -n '__fish_pdm_7426f3abf02b4bb8_complete_no_subcommand' -a show -d 'Show the package information'
complete -c pdm -f -n '__fish_pdm_7426f3abf02b4bb8_complete_no_subcommand' -a sync -d 'Synchronize the current working set with lock file'
complete -c pdm -f -n '__fish_pdm_7426f3abf02b4bb8_complete_no_subcommand' -a update -d 'Update package(s) in pyproject.toml'
complete -c pdm -f -n '__fish_pdm_7426f3abf02b4bb8_complete_no_subcommand' -a use -d 'Use the given python version or path as base interpreter'
complete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a add -d 'Add package(s) to pyproject.toml and install them'
complete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a build -d 'Build artifacts for distribution'
complete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a cache -d 'Control the caches of PDM'
complete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a completion -d 'Generate completion scripts for the given shell'
complete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a config -d 'Display the current configuration'
complete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a export -d 'Export the locked packages set to other formats'
complete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a import -d 'Import project metadata from other formats'
complete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a info -d 'Show the project information'
complete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a init -d 'Initialize a pyproject.toml for PDM'
complete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a install -d 'Install dependencies from lock file'
complete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a list -d 'List packages installed in the current working set'
complete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a lock -d 'Resolve and lock dependencies'
complete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a plugin -d 'Manage the PDM plugins'
complete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a publish -d 'Build and publish the project to PyPI'
complete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a remove -d 'Remove packages from pyproject.toml'
complete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a run -d 'Run commands or scripts with local packages loaded'
complete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a search -d 'Search for PyPI packages'
complete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a show -d 'Show the package information'
complete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a sync -d 'Synchronize the current working set with lock file'
complete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a update -d 'Update package(s) in pyproject.toml'
complete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a use -d 'Use the given python version or path as base interpreter'
complete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a venv -d 'Virtualenv management'
# command options
......@@ -233,18 +234,18 @@ complete -c pdm -A -n '__fish_seen_subcommand_from show' -l verbose -d '-v for d
complete -c pdm -A -n '__fish_seen_subcommand_from show' -l version -d 'Show version'
# sync
complete -c pdm -A -n '__fish_seen_subcommand_from sync' -l clean -d 'clean unused packages'
complete -c pdm -A -n '__fish_seen_subcommand_from sync' -l clean -d 'clean packages not in the lockfile'
complete -c pdm -A -n '__fish_seen_subcommand_from sync' -l dev -d 'Select dev dependencies'
complete -c pdm -A -n '__fish_seen_subcommand_from sync' -l dry-run -d 'Show the difference only and don\'t perform any action'
complete -c pdm -A -n '__fish_seen_subcommand_from sync' -l global -d 'Use the global project, supply the project root with `-p` option'
complete -c pdm -A -n '__fish_seen_subcommand_from sync' -l group -d 'Select group of optional-dependencies or dev-dependencies(with -d). Can be supplied multiple times, use ":all" to include all groups under the same species.'
complete -c pdm -A -n '__fish_seen_subcommand_from sync' -l help -d 'show this help message and exit'
complete -c pdm -A -n '__fish_seen_subcommand_from sync' -l lockfile -d 'Specify another lockfile path. Default: pdm.lock. [env var: PDM_LOCKFILE]'
complete -c pdm -A -n '__fish_seen_subcommand_from sync' -l only-keep -d 'don\'t clean unused packages'
complete -c pdm -A -n '__fish_seen_subcommand_from sync' -l no-default -d 'Don\'t include dependencies from the default group'
complete -c pdm -A -n '__fish_seen_subcommand_from sync' -l no-editable -d 'Install non-editable versions for all packages'
complete -c pdm -A -n '__fish_seen_subcommand_from sync' -l no-isolation -d 'Do not isolate the build in a clean environment'
complete -c pdm -A -n '__fish_seen_subcommand_from sync' -l no-self -d 'Don\'t install the project itself'
complete -c pdm -A -n '__fish_seen_subcommand_from sync' -l only-keep -d 'only keep the selected packages'
complete -c pdm -A -n '__fish_seen_subcommand_from sync' -l production -d 'Unselect dev dependencies'
complete -c pdm -A -n '__fish_seen_subcommand_from sync' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__'
complete -c pdm -A -n '__fish_seen_subcommand_from sync' -l reinstall -d 'Force reinstall existing dependencies'
......@@ -284,4 +285,8 @@ complete -c pdm -A -n '__fish_seen_subcommand_from use' -l global -d 'Use the gl
complete -c pdm -A -n '__fish_seen_subcommand_from use' -l help -d 'show this help message and exit'
complete -c pdm -A -n '__fish_seen_subcommand_from use' -l ignore-remembered -d 'Ignore the remembered selection'
complete -c pdm -A -n '__fish_seen_subcommand_from use' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__'
complete -c pdm -A -n '__fish_seen_subcommand_from use' -l skip -d 'Skip some tasks and/or hooks by their comma-separated names. Can be supplied multiple times. Use ":all" to skip all hooks. Use ":pre" and ":post" to skip all pre or post hooks.'
complete -c pdm -A -n '__fish_seen_subcommand_from use' -l verbose -d '-v for detailed output and -vv for more detailed'
# venv
complete -c pdm -A -n '__fish_seen_subcommand_from venv' -l help -d 'show this help message and exit'
......@@ -380,7 +380,7 @@ function TabExpansion($line, $lastWord) {
"use" {
$completer.AddOpts(
@(
[Option]::new(@("--global", "-g", "-f", "--first", "-i", "--ignore-remembered")),
[Option]::new(@("--global", "-g", "-f", "--first", "-i", "--ignore-remembered", "--skip")),
$projectOption
))
break
......
......@@ -24,7 +24,7 @@ from pdm.models.in_process import (
from pdm.models.python import PythonInfo
from pdm.models.session import PDMSession
from pdm.models.working_set import WorkingSet
from pdm.utils import cached_property, get_index_urls, is_venv_python, pdm_scheme
from pdm.utils import cached_property, get_index_urls, get_venv_like_prefix, pdm_scheme
if TYPE_CHECKING:
from pdm._types import Source
......@@ -241,7 +241,7 @@ class GlobalEnvironment(Environment):
is_global = True
def get_paths(self) -> dict[str, str]:
is_venv = is_venv_python(self.interpreter.executable)
is_venv = bool(get_venv_like_prefix(self.interpreter.executable))
paths = get_sys_config_paths(
str(self.interpreter.executable),
kind="user"
......
......@@ -186,7 +186,7 @@ class Config(MutableMapping[str, str]):
),
"python.use_venv": ConfigItem(
"Install packages into the activated venv site packages instead of PEP 582",
False,
True,
env_var="PDM_USE_VENV",
coerce=ensure_boolean,
replace="use_venv",
......@@ -205,6 +205,22 @@ class Config(MutableMapping[str, str]):
env_var="PDM_PYPI_JSON_API",
coerce=ensure_boolean,
),
"venv.location": ConfigItem(
"Parent directory for virtualenvs",
os.path.join(platformdirs.user_data_dir("pdm"), "venvs"),
global_only=True,
),
"venv.backend": ConfigItem(
"Default backend to create virtualenv",
default="virtualenv",
env_var="PDM_VENV_BACKEND",
),
"venv.in_project": ConfigItem(
"Create virtualenv in `.venv` under project root",
default=True,
env_var="PDM_VENV_IN_PROJECT",
coerce=ensure_boolean,
),
}
@classmethod
......
......@@ -34,7 +34,6 @@ from pdm.utils import (
expand_env_vars_in_auth,
find_project_root,
find_python_in_path,
get_in_project_venv_python,
get_venv_like_prefix,
normalize_name,
path_to_url,
......@@ -239,18 +238,69 @@ class Project:
def get_environment(self) -> Environment:
"""Get the environment selected by this project"""
from pdm.cli.commands.venv.utils import get_venv_python, iter_venvs
if self.is_global:
env = GlobalEnvironment(self)
# Rewrite global project's python requires to be
# compatible with the exact version
env.python_requires = PySpecSet(f"=={self.python.version}")
return env
if self.config["python.use_venv"] and get_venv_like_prefix(
self.python.executable
if not self.config["python.use_venv"]:
return Environment(self)
if self.project_config.get("python.path") and not os.getenv(
"PDM_IGNORE_SAVED_PYTHON"
):
# Only recognize venv created by python -m venv and virtualenv>20
return (
GlobalEnvironment(self)
if get_venv_like_prefix(self.python.executable) is not None
else Environment(self)
)
if os.getenv("VIRTUAL_ENV"):
venv = cast(str, os.getenv("VIRTUAL_ENV"))
self.core.ui.echo(
f"Detected inside an active virtualenv [green]{venv}[/], reuse it.",
style="yellow",
err=True,
)
# Temporary usage, do not save in .pdm.toml
self._python = PythonInfo.from_path(get_venv_python(Path(venv)))
return GlobalEnvironment(self)
return Environment(self)
existing_venv = next((venv for _, venv in iter_venvs(self)), None)
if existing_venv:
self.core.ui.echo(
f"Virtualenv [green]{existing_venv}[/] is reused.",
err=True,
)
path = existing_venv
elif self.root.joinpath("__pypackages__").exists():
self.core.ui.echo(
"__pypackages__ is detected, use the PEP 582 mode",
style="green",
err=True,
)
return Environment(self)
else:
# Create a virtualenv using the selected Python interpreter
self.core.ui.echo(
"python.use_venv is on, creating a virtualenv for this project...",
style="yellow",
err=True,
)
path = self._create_virtualenv()
self.python = PythonInfo.from_path(get_venv_python(path))
return GlobalEnvironment(self)
def _create_virtualenv(self) -> Path:
from pdm.cli.commands.venv.backends import BACKENDS
backend: str = self.config["venv.backend"]
venv_backend = BACKENDS[backend](self, None)
path = venv_backend.create(in_project=self.config["venv.in_project"])
self.core.ui.echo(
f"Virtualenv is created successfully at [green]{path}[/]", err=True
)
return path
@property
def environment(self) -> Environment:
......@@ -630,13 +680,6 @@ dependencies = ["pip", "setuptools", "wheel"]
yield PythonInfo.from_path(pyenv_shim)
elif os.path.exists(pyenv_shim.replace("python3", "python")):
yield PythonInfo.from_path(pyenv_shim.replace("python3", "python"))
if config.get("python.use_venv"):
python = get_in_project_venv_python(self.root)
if python:
yield PythonInfo.from_path(python)
python = shutil.which("python")
if python:
yield PythonInfo.from_path(python)
args = []
else:
if not all(c.isdigit() for c in python_spec.split(".")):
......@@ -650,9 +693,21 @@ dependencies = ["pip", "setuptools", "wheel"]
yield PythonInfo.from_path(python)
return
args = [int(v) for v in python_spec.split(".") if v != ""]
finder = Finder(resolve_symlinks=True)
finder = self._get_python_finder()
for entry in finder.find_all(*args):
yield PythonInfo(entry)
if not python_spec:
python = shutil.which("python")
if python:
yield PythonInfo.from_path(python)
# Return the host Python as well
this_python = getattr(sys, "_base_executable", sys.executable)
yield PythonInfo.from_path(this_python)
def _get_python_finder(self) -> Finder:
from pdm.cli.commands.venv.utils import VenvProvider
finder = Finder(resolve_symlinks=True)
if self.config["python.use_venv"]:
finder._providers.insert(0, VenvProvider(self))
return finder
......@@ -69,6 +69,7 @@ def style(
def confirm(*args: str, **kwargs: Any) -> str:
kwargs.setdefault("default", False)
return Confirm.ask(*args, **kwargs)
......
......@@ -187,21 +187,6 @@ def add_ssh_scheme_to_git_uri(uri: str) -> str:
return uri
def get_in_project_venv_python(root: Path) -> Path | None:
"""Get the python interpreter path of venv-in-project"""
if os.name == "nt":
suffix = ".exe"
scripts = "Scripts"
else:
suffix = ""
scripts = "bin"
for possible_dir in ("venv", ".venv", "env"):
if (root / possible_dir / scripts / f"python{suffix}").exists():
venv = root / possible_dir
return venv / scripts / f"python{suffix}"
return None
@contextlib.contextmanager
def atomic_open_for_write(
filename: str | Path, *, mode: str = "w", encoding: str = "utf-8"
......@@ -345,16 +330,6 @@ def is_path_relative_to(path: str | Path, other: str | Path) -> bool:
return True
def is_venv_python(interpreter: str | Path) -> bool:
"""Check if the given interpreter path is from a virtualenv"""
interpreter = Path(interpreter)
if interpreter.parent.parent.joinpath("pyvenv.cfg").exists():
return True
virtual_env = os.getenv("VIRTUAL_ENV")
return bool(virtual_env and is_path_relative_to(interpreter, virtual_env))
def get_venv_like_prefix(interpreter: str | Path) -> Path | None:
"""Check if the given interpreter path is from a virtualenv,
and return the prefix if found.
......
......@@ -9,24 +9,25 @@ requires-python = ">=3.7"
license = {text = "MIT"}
dependencies = [
"blinker",
"findpython",
"importlib-metadata; python_version < \"3.8\"",
"installer>=0.5.1,<0.6",
"packaging",
"pdm-pep517>=1.0.0,<2.0.0",
"pep517>=0.11.0",
"pip>=20",
"packaging",
"platformdirs",
"python-dotenv>=0.15",
"resolvelib>=0.8,<0.9",
"rich>=12.3.0",
"shellingham>=1.3.2",
"tomli>=1.1.0; python_version < \"3.11\"",
"virtualenv>=20",
"pep517>=0.11.0",
"requests-toolbelt",
"findpython>=0.2.0",
"tomlkit>=0.8.0,<1",
"typing-extensions; python_version < \"3.8\"",
"shellingham>=1.3.2",
"python-dotenv>=0.15",
"resolvelib>=0.8,<0.9",
"unearth>=0.4.1,<0.5.0",
"installer>=0.5.1,<0.6",
"pdm-pep517>=1.0.0,<2.0.0",
"cachecontrol[filecache]>=0.12.11",
"requests-toolbelt",
"tomli>=1.1.0; python_version < \"3.11\"",
"typing-extensions; python_version < \"3.8\"",
"importlib-metadata; python_version < \"3.8\"",
]
name = "pdm"
description = "A modern Python package and dependency manager supporting the latest PEP standards"
......
import os
import tarfile
import zipfile
......@@ -169,7 +168,7 @@ def test_build_with_no_isolation(fixture_project, invoke, isolated):
assert result.exit_code == int(isolated)
def test_build_ignoring_pip_environment(fixture_project):
def test_build_ignoring_pip_environment(fixture_project, monkeypatch):
project = fixture_project("demo-module")
os.environ["PIP_REQUIRE_VIRTUALENV"] = "1"
monkeypatch.setenv("PIP_REQUIRE_VIRTUALENV", "1")
actions.do_build(project)
import os
import pytest
from pdm.exceptions import PdmUsageError
......@@ -53,8 +51,8 @@ def test_config_del_command(project, invoke):
assert result.output.strip() == "True"
def test_config_env_var_shadowing(project, invoke):
os.environ["PDM_PYPI_URL"] = "https://example.org/simple"
def test_config_env_var_shadowing(project, invoke, monkeypatch):
monkeypatch.setenv("PDM_PYPI_URL", "https://example.org/simple")
result = invoke(["config", "pypi.url"], obj=project)
assert result.output.strip() == "https://example.org/simple"
......@@ -63,7 +61,7 @@ def test_config_env_var_shadowing(project, invoke):
result = invoke(["config", "pypi.url"], obj=project)
assert result.output.strip() == "https://example.org/simple"
del os.environ["PDM_PYPI_URL"]
monkeypatch.delenv("PDM_PYPI_URL")
result = invoke(["config", "pypi.url"], obj=project)
assert result.output.strip() == "https://test.pypi.org/pypi"
......@@ -102,14 +100,14 @@ def test_rename_deprecated_config(tmp_path, invoke):
)
def test_specify_config_file(tmp_path, invoke):
def test_specify_config_file(tmp_path, invoke, monkeypatch):
tmp_path.joinpath("global_config.toml").write_text("project_max_depth = 9\n")
with cd(tmp_path):
result = invoke(["-c", "global_config.toml", "config", "project_max_depth"])
assert result.exit_code == 0
assert result.output.strip() == "9"
os.environ["PDM_CONFIG_FILE"] = "global_config.toml"
monkeypatch.setenv("PDM_CONFIG_FILE", "global_config.toml")
result = invoke(["config", "project_max_depth"])
assert result.exit_code == 0
assert result.output.strip() == "9"
......
import shlex
import sys
from collections import namedtuple
from textwrap import dedent
......@@ -9,6 +10,8 @@ from pdm.cli.hooks import KNOWN_HOOKS
from pdm.cli.options import from_splitted_env
from pdm.models.requirements import parse_requirement
pytestmark = pytest.mark.usefixtures("repository", "working_set", "local_finder")
def test_pre_script_fail_fast(project, invoke, capfd, mocker):
project.tool_settings["scripts"] = {
......@@ -193,6 +196,7 @@ def test_skip_option_default_from_env(env, expected, monkeypatch):
HookSpecs = namedtuple("HookSpecs", ["command", "hooks", "fixtures"])
this_python_version = f"{sys.version_info[0]}.{sys.version_info[1]}"
KNOWN_COMMAND_HOOKS = (
("add", "add requests", ("pre_lock", "post_lock"), ["working_set"]),
......@@ -214,7 +218,7 @@ KNOWN_COMMAND_HOOKS = (
("remove", "remove requests", ("pre_lock", "post_lock"), ["lock"]),
("sync", "sync", ("pre_install", "post_install"), ["lock"]),
("update", "update", ("pre_install", "post_install", "pre_lock", "post_lock"), []),
("use", "use -f 3.7", ("post_use",), []),
("use", f"use -f {this_python_version}", ("post_use",), []),
)
parametrize_with_commands = pytest.mark.parametrize(
......
from unittest.mock import ANY
import pytest
from pdm.cli import actions
def test_init_validate_python_requires(project_no_init):
with pytest.raises(ValueError):
actions.do_init(project_no_init, python_requires="3.7")
def test_init_command(project_no_init, invoke, mocker):
mocker.patch(
"pdm.cli.commands.init.get_user_email_from_git",
return_value=("Testing", "me@example.org"),
)
do_init = mocker.patch.object(actions, "do_init")
invoke(["init"], input="\n\n\n\n\n\n", strict=True, obj=project_no_init)
python_version = f"{project_no_init.python.major}.{project_no_init.python.minor}"
do_init.assert_called_with(
project_no_init,
name="",
version="",
description="",
license="MIT",
author="Testing",
email="me@example.org",
python_requires=f">={python_version}",
hooks=ANY,
)
def test_init_command_library(project_no_init, invoke, mocker):
mocker.patch(
"pdm.cli.commands.init.get_user_email_from_git",
return_value=("Testing", "me@example.org"),
)
do_init = mocker.patch.object(actions, "do_init")
result = invoke(
["init"],
input="\ny\ntest-project\n\nTest Project\n\n\n\n\n",
obj=project_no_init,
)
assert result.exit_code == 0
python_version = f"{project_no_init.python.major}.{project_no_init.python.minor}"
do_init.assert_called_with(
project_no_init,
name="test-project",
version="0.1.0",
description="Test Project",
license="MIT",
author="Testing",
email="me@example.org",
python_requires=f">={python_version}",
hooks=ANY,
)
def test_init_non_interactive(project_no_init, invoke, mocker):
mocker.patch(
"pdm.cli.commands.init.get_user_email_from_git",
return_value=("Testing", "me@example.org"),
)
do_init = mocker.patch.object(actions, "do_init")
do_use = mocker.patch.object(actions, "do_use")
result = invoke(["init", "-n"], obj=project_no_init)
assert result.exit_code == 0
python_version = f"{project_no_init.python.major}.{project_no_init.python.minor}"
do_use.assert_called_once_with(
project_no_init,
ANY,
True,
ignore_requires_python=True,
hooks=ANY,
)
do_init.assert_called_with(
project_no_init,
name="",
version="",
description="",
license="MIT",
author="Testing",
email="me@example.org",
python_requires=f">={python_version}",
hooks=ANY,
)
def test_init_auto_create_venv(project_no_init, invoke, mocker):
mocker.patch("pdm.cli.commands.init.get_venv_like_prefix", return_value=None)
project_no_init.project_config["python.use_venv"] = True
result = invoke(["init"], input="\n\n\n\n\n\n\n", obj=project_no_init)
assert result.exit_code == 0
assert (
project_no_init.python.executable.parent.parent
== project_no_init.root / ".venv"
)
def test_init_auto_create_venv_answer_no(project_no_init, invoke, mocker):
mocker.patch("pdm.cli.commands.init.get_venv_like_prefix", return_value=None)
creator = mocker.patch("pdm.cli.commands.venv.backends.Backend.create")
project_no_init.project_config["python.use_venv"] = True
result = invoke(["init"], input="\nn\n\n\n\n\n\n\n", obj=project_no_init)
assert result.exit_code == 0
creator.assert_not_called()
assert (
project_no_init.python.executable.parent.parent
!= project_no_init.root / ".venv"
)
from unittest.mock import ANY
import pytest
from pdm.cli import actions
......@@ -33,11 +31,6 @@ def test_project_no_init_error(project_no_init):
handler(project_no_init)
def test_init_validate_python_requires(project_no_init):
with pytest.raises(ValueError):
actions.do_init(project_no_init, python_requires="3.7")
def test_help_option(invoke):
result = invoke(["--help"])
assert "Usage: pdm [-h]" in result.output
......@@ -78,82 +71,6 @@ def test_uncaught_error(invoke, mocker):
assert isinstance(result.exception, RuntimeError)
def test_init_command(project_no_init, invoke, mocker):
mocker.patch(
"pdm.cli.commands.init.get_user_email_from_git",
return_value=("Testing", "me@example.org"),
)
do_init = mocker.patch.object(actions, "do_init")
invoke(["init"], input="\n\n\n\n\n\n", strict=True, obj=project_no_init)
python_version = f"{project_no_init.python.major}.{project_no_init.python.minor}"
do_init.assert_called_with(
project_no_init,
name="",
version="",
description="",
license="MIT",
author="Testing",
email="me@example.org",
python_requires=f">={python_version}",
hooks=ANY,
)
def test_init_command_library(project_no_init, invoke, mocker):
mocker.patch(
"pdm.cli.commands.init.get_user_email_from_git",
return_value=("Testing", "me@example.org"),
)
do_init = mocker.patch.object(actions, "do_init")
result = invoke(
["init"],
input="\ny\ntest-project\n\nTest Project\n\n\n\n\n",
obj=project_no_init,
)
assert result.exit_code == 0
python_version = f"{project_no_init.python.major}.{project_no_init.python.minor}"
do_init.assert_called_with(
project_no_init,
name="test-project",
version="0.1.0",
description="Test Project",
license="MIT",
author="Testing",
email="me@example.org",
python_requires=f">={python_version}",
hooks=ANY,
)
def test_init_non_interactive(project_no_init, invoke, mocker):
mocker.patch(
"pdm.cli.commands.init.get_user_email_from_git",
return_value=("Testing", "me@example.org"),
)
do_init = mocker.patch.object(actions, "do_init")
do_use = mocker.patch.object(actions, "do_use")
result = invoke(["init", "-n"], obj=project_no_init)
assert result.exit_code == 0
python_version = f"{project_no_init.python.major}.{project_no_init.python.minor}"
do_use.assert_called_once_with(
project_no_init,
ANY,
True,
hooks=ANY,
)
do_init.assert_called_with(
project_no_init,
name="",
version="",
description="",
license="MIT",
author="Testing",
email="me@example.org",
python_requires=f">={python_version}",
hooks=ANY,
)
@pytest.mark.parametrize(
"filename",
[
......
......@@ -189,7 +189,7 @@ def test_run_script_with_extra_args(project, invoke, capfd):
assert out.splitlines()[-3:] == ["-a", "-b", "-c"]
def test_run_expand_env_vars(project, invoke, capfd):
def test_run_expand_env_vars(project, invoke, capfd, monkeypatch):
(project.root / "test_script.py").write_text("import os; print(os.getenv('FOO'))")
project.tool_settings["scripts"] = {
"test_cmd": 'python -c "foo, bar = 0, 1;print($FOO)"',
......@@ -201,7 +201,7 @@ def test_run_expand_env_vars(project, invoke, capfd):
project.write_pyproject()
capfd.readouterr()
with cd(project.root):
os.environ["FOO"] = "bar"
monkeypatch.setenv("FOO", "bar")
invoke(["run", "test_cmd"], obj=project)
assert capfd.readouterr()[0].strip() == "1"
......
import os
import re
import shutil
import sys
from unittest.mock import ANY
import pytest
from pdm.cli.commands.venv import backends
from pdm.cli.commands.venv.utils import get_venv_prefix
@pytest.fixture()
def fake_create(monkeypatch):
def fake_create(self, location, *args):
location.mkdir(parents=True)
monkeypatch.setattr(backends.VirtualenvBackend, "perform_create", fake_create)
monkeypatch.setattr(backends.VenvBackend, "perform_create", fake_create)
monkeypatch.setattr(backends.CondaBackend, "perform_create", fake_create)
@pytest.mark.usefixtures("fake_create")
def test_venv_create(invoke, project):
project.project_config["venv.in_project"] = False
result = invoke(["venv", "create"], obj=project)
assert result.exit_code == 0, result.stderr
venv_path = re.match(
r"Virtualenv (.+) is created successfully", result.output
).group(1)
assert os.path.exists(venv_path)
@pytest.mark.usefixtures("fake_create")
def test_venv_create_in_project(invoke, project):
project.project_config["venv.in_project"] = True
invoke(["venv", "create"], obj=project, strict=True)
invoke(["venv", "create"], obj=project, strict=True)
venv_path = project.root / ".venv"
assert venv_path.exists()
assert len(os.listdir(project.root / "venvs")) == 1
@pytest.mark.usefixtures("fake_create")
def test_venv_list(invoke, project):
project.project_config["venv.in_project"] = False
result = invoke(["venv", "create"], obj=project)
assert result.exit_code == 0, result.stderr
venv_path = re.match(
r"Virtualenv (.+) is created successfully", result.output
).group(1)
result = invoke(["venv", "list"], obj=project)
assert result.exit_code == 0, result.stderr
assert venv_path in result.output
@pytest.mark.usefixtures("fake_create")
def test_venv_remove(invoke, project):
project.project_config["venv.in_project"] = False
result = invoke(["venv", "create"], obj=project)
assert result.exit_code == 0, result.stderr
venv_path = re.match(
r"Virtualenv (.+) is created successfully", result.output
).group(1)
key = os.path.basename(venv_path)[len(get_venv_prefix(project)) :]
result = invoke(["venv", "remove", "non-exist"], obj=project)
assert result.exit_code != 0
result = invoke(["venv", "remove", "-y", key], obj=project)
assert result.exit_code == 0, result.stderr
assert not os.path.exists(venv_path)
@pytest.mark.usefixtures("fake_create")
def test_venv_recreate(invoke, project):
project.project_config["venv.in_project"] = False
result = invoke(["venv", "create"], obj=project)
assert result.exit_code == 0, result.stderr
result = invoke(["venv", "create"], obj=project)
assert result.exit_code != 0
result = invoke(["venv", "create", "-f"], obj=project)
assert result.exit_code == 0, result.stderr
@pytest.mark.usefixtures("venv_backends")
def test_venv_activate(invoke, mocker, project):
project.project_config["venv.in_project"] = False
result = invoke(["venv", "create"], obj=project)
assert result.exit_code == 0, result.stderr
venv_path = re.match(
r"Virtualenv (.+) is created successfully", result.output
).group(1)
key = os.path.basename(venv_path)[len(get_venv_prefix(project)) :]
mocker.patch("shellingham.detect_shell", return_value=("bash", None))
result = invoke(["venv", "activate", key], obj=project)
assert result.exit_code == 0, result.stderr
backend = project.config["venv.backend"]
if backend == "conda":
assert result.output.startswith("conda activate")
else:
assert result.output.strip("'\"\n").endswith("activate")
assert result.output.startswith("source")
@pytest.mark.usefixtures("fake_create")
def test_venv_activate_error(invoke, project):
project.project_config["venv.in_project"] = False
result = invoke(["venv", "create"], obj=project, strict=True)
result = invoke(["venv", "activate", "foo"], obj=project)
assert result.exit_code != 0
assert "No virtualenv with key" in result.stderr
result = invoke(["venv", "activate"], obj=project)
assert result.exit_code != 0
assert "Can't activate a non-venv Python" in result.stderr
@pytest.mark.usefixtures("fake_create")
@pytest.mark.parametrize("keep_pypackages", [True, False])
def test_venv_auto_create(invoke, mocker, project, keep_pypackages):
creator = mocker.patch("pdm.cli.commands.venv.backends.Backend.create")
del project.project_config["python.path"]
if keep_pypackages:
project.root.joinpath("__pypackages__").mkdir(exist_ok=True)
else:
shutil.rmtree(project.root / "__pypackages__", ignore_errors=True)
project.project_config["python.use_venv"] = True
invoke(["install"], obj=project)
if keep_pypackages:
creator.assert_not_called()
else:
creator.assert_called_once()
@pytest.mark.usefixtures("fake_create")
def test_venv_purge(invoke, project):
project.project_config["venv.in_project"] = False
result = invoke(["venv", "purge"], obj=project)
assert result.exit_code == 0, result.stderr
result = invoke(["venv", "create"], obj=project)
assert result.exit_code == 0, result.stderr
venv_path = re.match(
r"Virtualenv (.+) is created successfully", result.output
).group(1)
result = invoke(["venv", "purge"], input="y", obj=project)
assert result.exit_code == 0, result.stderr
assert not os.path.exists(venv_path)
@pytest.mark.usefixtures("fake_create")
def test_venv_purge_force(invoke, project):
project.project_config["venv.in_project"] = False
result = invoke(["venv", "create"], obj=project)
assert result.exit_code == 0, result.stderr
venv_path = re.match(
r"Virtualenv (.+) is created successfully", result.output
).group(1)
result = invoke(["venv", "purge", "-f"], obj=project)
assert result.exit_code == 0, result.stderr
assert not os.path.exists(venv_path)
user_options = [("none", True), ("0", False), ("all", False)]
@pytest.mark.usefixtures("venv_backends")
@pytest.mark.parametrize("user_choices, is_path_exists", user_options)
def test_venv_purge_interactive(invoke, user_choices, is_path_exists, project):
project.project_config["venv.in_project"] = False
result = invoke(["venv", "create"], obj=project)
assert result.exit_code == 0, result.stderr
venv_path = re.match(
r"Virtualenv (.+) is created successfully", result.output
).group(1)
result = invoke(["venv", "purge", "-i"], input=user_choices, obj=project)
assert result.exit_code == 0, result.stderr
assert os.path.exists(venv_path) == is_path_exists
def test_virtualenv_backend_create(project, mocker):
interpreter = project.python_executable
backend = backends.VirtualenvBackend(project, None)
assert backend.ident
mock_call = mocker.patch("subprocess.check_call")
location = backend.create()
mock_call.assert_called_once_with(
[
sys.executable,
"-m",
"virtualenv",
"--no-pip",
"--no-setuptools",
"--no-wheel",
str(location),
"-p",
interpreter,
],
stdout=ANY,
)
def test_venv_backend_create(project, mocker):
interpreter = project.python_executable
backend = backends.VenvBackend(project, None)
assert backend.ident
mock_call = mocker.patch("subprocess.check_call")
location = backend.create()
mock_call.assert_called_once_with(
[interpreter, "-m", "venv", "--without-pip", str(location)], stdout=ANY
)
def test_conda_backend_create(project, mocker):
backend = backends.CondaBackend(project, "3.8")
assert backend.ident == "3.8"
mock_call = mocker.patch("subprocess.check_call")
location = backend.create()
mock_call.assert_called_once_with(
["conda", "create", "--yes", "--prefix", str(location), "python=3.8"],
stdout=ANY,
)
backend = backends.CondaBackend(project, None)
python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
assert backend.ident.startswith(python_version)
location = backend.create()
mock_call.assert_called_with(
[
"conda",
"create",
"--yes",
"--prefix",
str(location),
f"python={python_version}",
],
stdout=ANY,
)
......@@ -6,7 +6,6 @@ import json
import os
import shutil
import sys
from contextlib import contextmanager
from dataclasses import dataclass
from io import BytesIO, StringIO
from pathlib import Path
......@@ -40,16 +39,6 @@ from tests import FIXTURES
os.environ.update(CI="1", PDM_CHECK_UPDATE="0")
@contextmanager
def temp_environ():
environ = os.environ.copy()
try:
yield
finally:
os.environ.clear()
os.environ.update(environ)
class LocalFileAdapter(requests.adapters.BaseAdapter):
def __init__(self, aliases, overrides=None, strip_suffix=False):
super().__init__()
......@@ -269,6 +258,8 @@ def remove_pep582_path_from_pythonpath(pythonpath):
@pytest.fixture()
def core():
old_config_map = Config._config_map.copy()
# Turn off use_venv by default, for testing
Config._config_map["python.use_venv"].default = False
main = Core()
yield main
# Restore the config items
......@@ -281,7 +272,7 @@ def index():
@pytest.fixture()
def project_no_init(tmp_path, mocker, core, index):
def project_no_init(tmp_path, mocker, core, index, monkeypatch):
test_home = tmp_path / ".pdm-home"
test_home.mkdir(parents=True)
test_home.joinpath("config.toml").write_text(
......@@ -292,6 +283,7 @@ def project_no_init(tmp_path, mocker, core, index):
p = core.create_project(
tmp_path, global_config=test_home.joinpath("config.toml").as_posix()
)
p.global_config["venv.location"] = str(tmp_path / "venvs")
mocker.patch(
"pdm.models.environment.PDMSession",
functools.partial(get_pypi_session, overrides=index),
......@@ -303,16 +295,15 @@ def project_no_init(tmp_path, mocker, core, index):
getattr(sys, "_base_executable", sys.executable),
HookManager(p, ["post_use"]),
)
with temp_environ():
os.environ.pop("VIRTUAL_ENV", None)
os.environ.pop("CONDA_PREFIX", None)
os.environ.pop("PEP582_PACKAGES", None)
os.environ.pop("NO_SITE_PACKAGES", None)
pythonpath = os.environ.pop("PYTHONPATH", "")
pythonpath = remove_pep582_path_from_pythonpath(pythonpath)
if pythonpath:
os.environ["PYTHONPATH"] = pythonpath
yield p
monkeypatch.delenv("VIRTUAL_ENV", raising=False)
monkeypatch.delenv("CONDA_PREFIX", raising=False)
monkeypatch.delenv("PEP582_PACKAGES", raising=False)
monkeypatch.delenv("NO_SITE_PACKAGES", raising=False)
pythonpath = os.getenv("PYTHONPATH", "")
pythonpath = remove_pep582_path_from_pythonpath(pythonpath)
if pythonpath:
monkeypatch.setenv("PYTHONPATH", pythonpath)
yield p
@pytest.fixture()
......@@ -447,3 +438,13 @@ def invoke(core, monkeypatch):
return result
return caller
BACKENDS = ["virtualenv", "venv"]
@pytest.fixture(params=BACKENDS)
def venv_backends(project, request):
project.project_config["venv.backend"] = request.param
project.project_config["python.use_venv"] = True
shutil.rmtree(project.root / "__pypackages__", ignore_errors=True)
......@@ -6,14 +6,15 @@ from pathlib import Path
import pytest
from packaging.version import parse
from pdm.cli.commands.venv.utils import get_venv_python
from pdm.utils import cd
def test_project_python_with_pyenv_support(project, mocker):
def test_project_python_with_pyenv_support(project, mocker, monkeypatch):
del project.project_config["python.path"]
project._python = None
os.environ["PDM_IGNORE_SAVED_PYTHON"] = "1"
monkeypatch.setenv("PDM_IGNORE_SAVED_PYTHON", "1")
mocker.patch("pdm.project.core.PYENV_ROOT", str(project.root))
pyenv_python = project.root / "shims/python"
if os.name == "nt":
......@@ -61,9 +62,9 @@ def test_project_sources_overriding(project):
assert project.sources[0]["url"] == "https://example.org/simple"
def test_project_sources_env_var_expansion(project):
os.environ["PYPI_USER"] = "user"
os.environ["PYPI_PASS"] = "password"
def test_project_sources_env_var_expansion(project, monkeypatch):
monkeypatch.setenv("PYPI_USER", "user")
monkeypatch.setenv("PYPI_PASS", "password")
project.project_config[
"pypi.url"
] = "https://${PYPI_USER}:${PYPI_PASS}@test.pypi.org/simple"
......@@ -162,13 +163,14 @@ def test_project_auto_detect_venv(project):
assert project.environment.is_global
def test_ignore_saved_python(project):
@pytest.mark.path
def test_ignore_saved_python(project, monkeypatch):
project.project_config["python.use_venv"] = True
project._python = None
scripts = "Scripts" if os.name == "nt" else "bin"
suffix = ".exe" if os.name == "nt" else ""
venv.create(project.root / "venv")
os.environ["PDM_IGNORE_SAVED_PYTHON"] = "1"
monkeypatch.setenv("PDM_IGNORE_SAVED_PYTHON", "1")
assert project.python.executable != project.project_config["python.path"]
assert (
project.python.executable == project.root / "venv" / scripts / f"python{suffix}"
......@@ -213,4 +215,53 @@ def test_global_python_path_config(project_no_init, tmp_path):
def test_set_non_exist_python_path(project_no_init):
project_no_init.project_config["python.path"] = "non-exist-python"
project_no_init._python = None
assert project_no_init.python.executable == Path(sys.executable)
assert project_no_init.python.executable.name != "non-exist-python"
@pytest.mark.usefixtures("venv_backends")
def test_create_venv_first_time(invoke, project):
project.project_config.update({"venv.in_project": False})
del project.project_config["python.path"]
result = invoke(["install"], obj=project)
assert result.exit_code == 0
venv_parent = project.root / "venvs"
venv_path = next(venv_parent.iterdir(), None)
assert venv_path is not None
assert Path(project.project_config["python.path"]).relative_to(venv_path)
@pytest.mark.usefixtures("venv_backends")
def test_create_venv_in_project(invoke, project):
project.project_config.update({"venv.in_project": True})
del project.project_config["python.path"]
result = invoke(["install"], obj=project)
assert result.exit_code == 0
assert project.root.joinpath(".venv").exists()
@pytest.mark.usefixtures("venv_backends")
def test_find_interpreters_from_venv(invoke, project):
project.project_config.update({"venv.in_project": False})
del project.project_config["python.path"]
result = invoke(["install"], obj=project)
assert result.exit_code == 0
venv_parent = project.root / "venvs"
venv_path = next(venv_parent.iterdir(), None)
venv_python = get_venv_python(venv_path)
assert any(venv_python == p.executable for p in project.find_interpreters())
def test_iter_project_venvs(project):
from pdm.cli.commands.venv import utils
venv_parent = Path(project.config["venv.location"])
venv_prefix = utils.get_venv_prefix(project)
for name in ("foo", "bar", "baz"):
venv_parent.joinpath(venv_prefix + name).mkdir(parents=True)
dot_venv_python = utils.get_venv_python(project.root / ".venv")
dot_venv_python.parent.mkdir(parents=True)
dot_venv_python.touch()
venv_keys = [key for key, _ in utils.iter_venvs(project)]
assert sorted(venv_keys) == ["bar", "baz", "foo", "in-project"]
import pathlib
import re
import sys
import pytest
......@@ -50,6 +49,10 @@ def test_expend_env_vars_in_auth(given, expected, monkeypatch):
assert utils.expand_env_vars_in_auth(given) == expected
def compare_python_paths(path1, path2):
return path1.parent == path2.parent
@pytest.mark.path
def test_find_python_in_path(tmp_path):
......@@ -58,17 +61,10 @@ def test_find_python_in_path(tmp_path):
== pathlib.Path(sys.executable).resolve()
)
posix_path_to_executable = pathlib.Path(sys.executable).as_posix().lower()
if sys.platform == "darwin":
found_version_of_executable = re.split(
r"(python@[\d.]*\d+)", posix_path_to_executable
)
posix_path_to_executable = "".join(found_version_of_executable[0:2])
assert (
utils.find_python_in_path(sys.prefix)
.as_posix()
.lower()
.startswith(posix_path_to_executable)
posix_path_to_executable = pathlib.Path(sys.executable)
assert compare_python_paths(
utils.find_python_in_path(sys.prefix),
posix_path_to_executable,
)
assert not utils.find_python_in_path(tmp_path)
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册