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

Merge pull request #222 from frostming/feature/219

Auto disable PEP 582 when a venv-like python is given
...@@ -53,10 +53,10 @@ jobs: ...@@ -53,10 +53,10 @@ jobs:
with: with:
path: __pypackages__ path: __pypackages__
# Look to see if there is a cache hit for the corresponding requirements file # Look to see if there is a cache hit for the corresponding requirements file
key: ${{ runner.os }}-packages-${{ steps.set_variables.outputs.PY }}-${{ hashFiles('pdm.lock') }} key: ${{ runner.os }}-${{ matrix.arch }}-packages-${{ steps.set_variables.outputs.PY }}-${{ hashFiles('pdm.lock') }}
restore-keys: | restore-keys: |
${{ runner.os }}-packages-${{ steps.set_variables.outputs.PY }}- ${{ runner.os }}-${{ matrix.arch }}-packages-${{ steps.set_variables.outputs.PY }}-
${{ runner.os }}-packages- ${{ runner.os }}-${{ matrix.arch }}-packages-
- name: Install Current PDM - name: Install Current PDM
run: | run: |
pip install -U pip pip install -U pip
......
...@@ -125,6 +125,8 @@ by the configuration item `use_venv`. When it is set to `True` PDM will use the ...@@ -125,6 +125,8 @@ by the configuration item `use_venv`. When it is set to `True` PDM will use the
- an virtualenv is already activated. - an virtualenv is already activated.
- any of `venv`, `.venv`, `env` is an valid virtualenv folder. - any of `venv`, `.venv`, `env` is an valid virtualenv folder.
Besides, when `use-venv` is on and the interpreter path given is a venv-like path, PDM will reuse that venv directory as well.
## Import project metadata from existing project files ## 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. If you are already other package manager tools like Pipenv or Poetry, it is easy to migrate to PDM.
......
Auto disable PEP 582 when a venv-like python is given as the interpreter path.
import hashlib import hashlib
import json import json
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, Optional from typing import TYPE_CHECKING, Dict, Optional
from pip._vendor import requests
from pdm._types import CandidateInfo from pdm._types import CandidateInfo
from pdm.exceptions import CorruptedCacheError from pdm.exceptions import CorruptedCacheError
...@@ -11,6 +9,8 @@ from pdm.models import pip_shims ...@@ -11,6 +9,8 @@ from pdm.models import pip_shims
from pdm.utils import open_file from pdm.utils import open_file
if TYPE_CHECKING: if TYPE_CHECKING:
from pip._vendor import requests
from pdm.models.candidates import Candidate from pdm.models.candidates import Candidate
...@@ -19,7 +19,7 @@ class CandidateInfoCache: ...@@ -19,7 +19,7 @@ class CandidateInfoCache:
def __init__(self, cache_file: Path) -> None: def __init__(self, cache_file: Path) -> None:
self.cache_file = cache_file self.cache_file = cache_file
self._cache = {} # type: Dict[str, Any] self._cache = {} # type: Dict[str, CandidateInfo]
self._read_cache() self._read_cache()
def _read_cache(self) -> None: def _read_cache(self) -> None:
...@@ -80,7 +80,7 @@ class HashCache(pip_shims.SafeFileCache): ...@@ -80,7 +80,7 @@ class HashCache(pip_shims.SafeFileCache):
Hashes are only cached when the URL appears to contain a hash in it and the Hashes are only cached when the URL appears to contain a hash in it and the
cache key includes the hash value returned from the server). This ought to cache key includes the hash value returned from the server). This ought to
avoid ssues where the location on the server changes. avoid issues where the location on the server changes.
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
......
...@@ -102,10 +102,8 @@ class Environment: ...@@ -102,10 +102,8 @@ class Environment:
def python_executable(self) -> str: def python_executable(self) -> str:
"""Get the Python interpreter path.""" """Get the Python interpreter path."""
config = self.project.config config = self.project.config
if config.get("python.path"): if self.project.project_config.get("python.path"):
return config["python.path"] return self.project.project_config["python.path"]
if PYENV_INSTALLED and config.get("python.use_pyenv", True):
return os.path.join(PYENV_ROOT, "shims", "python")
if "VIRTUAL_ENV" in os.environ: if "VIRTUAL_ENV" in os.environ:
stream.echo( stream.echo(
"An activated virtualenv is detected, reuse the interpreter now.", "An activated virtualenv is detected, reuse the interpreter now.",
...@@ -113,6 +111,8 @@ class Environment: ...@@ -113,6 +111,8 @@ class Environment:
verbosity=stream.DETAIL, verbosity=stream.DETAIL,
) )
return get_venv_python(self.project.root) return get_venv_python(self.project.root)
if PYENV_INSTALLED and config.get("python.use_pyenv", True):
return os.path.join(PYENV_ROOT, "shims", "python")
# First try what `python` refers to. # First try what `python` refers to.
path = shutil.which("python") path = shutil.which("python")
...@@ -131,6 +131,9 @@ class Environment: ...@@ -131,6 +131,9 @@ class Environment:
if self.python_requires.contains(version): if self.python_requires.contains(version):
path = sys.executable path = sys.executable
if path: if path:
if os.path.normcase(path) == os.path.normcase(sys.executable):
# Refer to the base interpreter to allow for venvs
path = getattr(sys, "_base_executable", sys.executable)
stream.echo( stream.echo(
"Using Python interpreter: {} ({})".format(stream.green(path), version) "Using Python interpreter: {} ({})".format(stream.green(path), version)
) )
...@@ -414,7 +417,7 @@ class Environment: ...@@ -414,7 +417,7 @@ class Environment:
scripts = self.get_paths()["scripts"] scripts = self.get_paths()["scripts"]
maker = ScriptMaker(None, None) maker = ScriptMaker(None, None)
maker.executable = new_path maker.executable = new_path
shebang = maker._get_shebang("utf-8").rstrip() shebang = maker._get_shebang("utf-8").rstrip().replace(b"\\", b"\\\\")
for child in Path(scripts).iterdir(): for child in Path(scripts).iterdir():
if not child.is_file() or child.suffix not in (".exe", ".py", ""): if not child.is_file() or child.suffix not in (".exe", ".py", ""):
continue continue
......
...@@ -149,15 +149,16 @@ class Config(MutableMapping): ...@@ -149,15 +149,16 @@ class Config(MutableMapping):
fp.write(tomlkit.dumps(toml_data)) fp.write(tomlkit.dumps(toml_data))
def __getitem__(self, key: str) -> Any: def __getitem__(self, key: str) -> Any:
if key not in self._data:
raise NoConfigError(key)
env_var = self._config_map[key].env_var env_var = self._config_map[key].env_var
if env_var is not None and env_var in os.environ: if env_var is not None and env_var in os.environ:
env_value = os.environ[env_var] env_value = os.environ[env_var]
if isinstance(self._config_map[key].default, bool): if isinstance(self._config_map[key].default, bool):
env_value = ensure_boolean(env_value) env_value = ensure_boolean(env_value)
return env_value return env_value
return self._data[key] try:
return self._data[key]
except KeyError:
raise NoConfigError(key) from None
def __setitem__(self, key: str, value: Any) -> None: def __setitem__(self, key: str, value: Any) -> None:
if key not in self._config_map: if key not in self._config_map:
...@@ -176,7 +177,7 @@ class Config(MutableMapping): ...@@ -176,7 +177,7 @@ class Config(MutableMapping):
stream.echo( stream.echo(
stream.yellow( stream.yellow(
"WARNING: the config is shadowed by env var '{}', " "WARNING: the config is shadowed by env var '{}', "
"set value won't take effect.".format(env_var) "the value set won't take effect.".format(env_var)
) )
) )
self._data[key] = value self._data[key] = value
......
...@@ -142,11 +142,19 @@ class Project: ...@@ -142,11 +142,19 @@ class Project:
"==" + get_python_version(env.python_executable, True)[0] "==" + get_python_version(env.python_executable, True)[0]
) )
return env return env
if self.config["use_venv"]: if not self.project_config.get("python.path") and self.config["use_venv"]:
venv_python = get_venv_python(self.root) venv_python = get_venv_python(self.root)
if venv_python: if venv_python:
self.project_config["python.path"] = venv_python self.project_config["python.path"] = venv_python
return GlobalEnvironment(self) if (
self.config["use_venv"]
and self.project_config.get("python.path")
and Path(self.project_config.get("python.path"))
.parent.parent.joinpath("pyvenv.cfg")
.exists()
):
# Only recognize venv created by python -m venv and virtualenv>20
return GlobalEnvironment(self)
return Environment(self) return Environment(self)
@property @property
......
...@@ -10,7 +10,7 @@ import tempfile ...@@ -10,7 +10,7 @@ import tempfile
import urllib.parse as parse import urllib.parse as parse
from contextlib import contextmanager from contextlib import contextmanager
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Union from typing import Any, Callable, Dict, Generic, List, Optional, Tuple, TypeVar, Union
from distlib.wheel import Wheel from distlib.wheel import Wheel
from tomlkit.container import Container from tomlkit.container import Container
...@@ -29,13 +29,15 @@ try: ...@@ -29,13 +29,15 @@ try:
from functools import cached_property from functools import cached_property
except ImportError: except ImportError:
class cached_property: _T = TypeVar("_T")
def __init__(self, func):
class cached_property(Generic[_T]):
def __init__(self, func: Callable[[Any], _T]):
self.func = func self.func = func
self.attr_name = func.__name__ self.attr_name = func.__name__
self.__doc__ = func.__doc__ self.__doc__ = func.__doc__
def __get__(self, inst, cls=None): def __get__(self, inst: Any, cls=None) -> _T:
if inst is None: if inst is None:
return self return self
if self.attr_name not in inst.__dict__: if self.attr_name not in inst.__dict__:
......
...@@ -229,7 +229,7 @@ def project_no_init(tmp_path, mocker): ...@@ -229,7 +229,7 @@ def project_no_init(tmp_path, mocker):
mocker.patch("pdm.project.core.Config.HOME_CONFIG", tmp_path) mocker.patch("pdm.project.core.Config.HOME_CONFIG", tmp_path)
old_config_map = Config._config_map.copy() old_config_map = Config._config_map.copy()
p.global_config["cache_dir"] = tmp_path.joinpath("caches").as_posix() p.global_config["cache_dir"] = tmp_path.joinpath("caches").as_posix()
do_use(p, sys.executable) do_use(p, getattr(sys, "_base_executable", sys.executable))
with temp_environ(): with temp_environ():
os.environ.pop("VIRTUAL_ENV", None) os.environ.pop("VIRTUAL_ENV", None)
os.environ.pop("PYTHONPATH", None) os.environ.pop("PYTHONPATH", None)
......
import os import os
import sys import sys
import venv
from pathlib import Path from pathlib import Path
import distlib.wheel import distlib.wheel
...@@ -57,19 +58,17 @@ def test_global_project(tmp_path): ...@@ -57,19 +58,17 @@ def test_global_project(tmp_path):
assert project.environment.is_global assert project.environment.is_global
def test_project_use_venv(project, mocker): def test_project_use_venv(project):
del project.project_config["python.path"] del project.project_config["python.path"]
scripts = "Scripts" if os.name == "nt" else "bin" scripts = "Scripts" if os.name == "nt" else "bin"
suffix = ".exe" if os.name == "nt" else "" suffix = ".exe" if os.name == "nt" else ""
venv.create(project.root / "venv")
os.environ["VIRTUAL_ENV"] = "/path/to/env"
mocker.patch("pdm.models.environment.get_python_version", return_value="3.7.0")
project.project_config["use_venv"] = True project.project_config["use_venv"] = True
env = project.environment env = project.environment
assert ( assert (
Path(env.python_executable) Path(env.python_executable)
== Path("/path/to/env") / scripts / f"python{suffix}" == project.root / "venv" / scripts / f"python{suffix}"
) )
assert env.is_global assert env.is_global
...@@ -95,3 +94,18 @@ def test_project_packages_path(project): ...@@ -95,3 +94,18 @@ def test_project_packages_path(project):
assert packages_path.name == version + "-32" assert packages_path.name == version + "-32"
else: else:
assert packages_path.name == version assert packages_path.name == version
def test_project_auto_detect_venv(project):
venv.create(project.root / "test_venv")
scripts = "Scripts" if os.name == "nt" else "bin"
suffix = ".exe" if os.name == "nt" else ""
project.project_config["use_venv"] = True
project.project_config["python.path"] = (
project.root / "test_venv" / scripts / f"python{suffix}"
).as_posix()
assert project.environment.is_global
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册