diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f51242cb26..18d8051a6a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: fail-fast: false matrix: python-version: [3.7, 3.8, 3.9, "3.10", "3.11.0-alpha - 3.11.0"] - os: [ubuntu-latest, macOS-latest, windows-latest] + os: [ubuntu-latest, windows-latest, macos-latest] arch: [x64] install-via: [pip] include: @@ -82,10 +82,6 @@ jobs: run: | echo "::set-output name=PY::$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')" echo "::set-output name=PIP_CACHE::$(pip cache dir)" - - name: Fix Ubuntu Env - run: | - echo "LD_PRELOAD=/lib/x86_64-linux-gnu/libgcc_s.so.1" >> $GITHUB_ENV - if: runner.os == 'Linux' - name: Cache PIP uses: actions/cache@v2 with: @@ -103,6 +99,7 @@ jobs: - name: Install Dev Dependencies run: | pdm install -v -dGtest + pdm info - name: Run Tests run: pdm run pytest -n auto --cov pdm --cov-config=setup.cfg --cov-report=xml tests - name: Upload coverage to Codecov diff --git a/news/930.dep.md b/news/930.dep.md new file mode 100644 index 0000000000..b9d1c9d673 --- /dev/null +++ b/news/930.dep.md @@ -0,0 +1 @@ +Switch from `pythonfinder` to `findpython` as the Python version finder. diff --git a/pdm.lock b/pdm.lock index 411e4d9c03..adfc071367 100644 --- a/pdm.lock +++ b/pdm.lock @@ -97,6 +97,15 @@ version = "1.9.0" requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" summary = "execnet: rapid multi-Python deployment" +[[package]] +name = "findpython" +version = "0.1.1" +requires_python = ">=3.7" +summary = "A utility to find python versions on your system" +dependencies = [ + "packaging>=20", +] + [[package]] name = "ghp-import" version = "2.0.2" @@ -456,19 +465,6 @@ name = "python-dotenv" version = "0.17.1" summary = "Read key-value pairs from a .env file and set them as environment variables" -[[package]] -name = "pythonfinder" -version = "1.2.6" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -summary = "A cross-platform python discovery tool to help locate python on any system." -dependencies = [ - "attrs", - "cached-property", - "click", - "packaging", - "six", -] - [[package]] name = "pytkdocs" version = "0.15.0" @@ -618,7 +614,7 @@ summary = "Backport of pathlib-compatible object wrapper for zip files" [metadata] lock_version = "3.1" -content_hash = "sha256:b55f0759b18852d3a85bc5ead3d0f7397c2e7bddb3cc712eb5d9fbc29602bc92" +content_hash = "sha256:10013526fea19dec4436180b0eb53cb826f87db9c52540aab6a8dc7cdc7331eb" [metadata.files] "arpeggio 1.10.2" = [ @@ -720,6 +716,10 @@ content_hash = "sha256:b55f0759b18852d3a85bc5ead3d0f7397c2e7bddb3cc712eb5d9fbc29 {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, ] +"findpython 0.1.1" = [ + {file = "findpython-0.1.1-py3-none-any.whl", hash = "sha256:9eadc269949fcc17a1f120cde9aa9a1580c2f4d6187840475167d5db000aa2b9"}, + {file = "findpython-0.1.1.tar.gz", hash = "sha256:bc64ecfeb03f92950af40da41c2447eda5c8ed22b35b0f901304f90eb4e8b02d"}, +] "ghp-import 2.0.2" = [ {file = "ghp_import-2.0.2-py3-none-any.whl", hash = "sha256:5f8962b30b20652cdffa9c5a9812f7de6bcb56ec475acac579807719bf242c46"}, {file = "ghp-import-2.0.2.tar.gz", hash = "sha256:947b3771f11be850c852c64b561c600fdddf794bab363060854c1ee7ad05e071"}, @@ -945,10 +945,6 @@ content_hash = "sha256:b55f0759b18852d3a85bc5ead3d0f7397c2e7bddb3cc712eb5d9fbc29 {file = "python_dotenv-0.17.1-py2.py3-none-any.whl", hash = "sha256:00aa34e92d992e9f8383730816359647f358f4a3be1ba45e5a5cefd27ee91544"}, {file = "python-dotenv-0.17.1.tar.gz", hash = "sha256:b1ae5e9643d5ed987fc57cc2583021e38db531946518130777734f9589b3141f"}, ] -"pythonfinder 1.2.6" = [ - {file = "pythonfinder-1.2.6-py2.py3-none-any.whl", hash = "sha256:3712d0d63307b93c4dcb11cac2c391b16fe9b8c6c67dfcf48f4c0777c6d509e8"}, - {file = "pythonfinder-1.2.6.tar.gz", hash = "sha256:21ffb77b152ae14c5c7d9b1c98c6df0a1a34d4b3e050da39f561224e7664a5f4"}, -] "pytkdocs 0.15.0" = [ {file = "pytkdocs-0.15.0-py3-none-any.whl", hash = "sha256:d6b2aec34448ec89acb8c1c25062cc1e70c6b26395d46fc7ee753b7e5a4e736a"}, {file = "pytkdocs-0.15.0.tar.gz", hash = "sha256:4b45af89d6fa5fa50f979b0f9f54539286b84e245c81991bb838149aa2d9d9c9"}, diff --git a/pdm/_vendor/halo/_utils.py b/pdm/_vendor/halo/_utils.py index c8ddeafc48..c0b5522d0e 100644 --- a/pdm/_vendor/halo/_utils.py +++ b/pdm/_vendor/halo/_utils.py @@ -3,7 +3,6 @@ """ import codecs import platform -import six try: from shutil import get_terminal_size except ImportError: @@ -87,10 +86,7 @@ def is_text_type(text): bool Whether parameter is a string or not """ - if isinstance(text, six.text_type) or isinstance(text, six.string_types): - return True - - return False + return isinstance(text, str) def decode_utf_8_text(text): diff --git a/pdm/builders/base.py b/pdm/builders/base.py index bfae50923c..0489fc52b2 100644 --- a/pdm/builders/base.py +++ b/pdm/builders/base.py @@ -163,7 +163,7 @@ def __init__(self, src_dir: str | Path, environment: Environment) -> None: Otherwise, the environment of the host Python will be used. """ self._env = environment - self.executable = self._env.interpreter.executable + self.executable = self._env.interpreter.executable.as_posix() self.src_dir = src_dir self.isolated = environment.project.config["build_isolation"] logger.debug("Preparing isolated env for PEP 517 build...") diff --git a/pdm/cli/actions.py b/pdm/cli/actions.py index 3aaf0132b7..edb2d901d0 100644 --- a/pdm/cli/actions.py +++ b/pdm/cli/actions.py @@ -10,7 +10,6 @@ from argparse import Namespace from collections import defaultdict from itertools import chain -from pathlib import Path from typing import Iterable, Mapping, Sequence, cast import click @@ -536,7 +535,7 @@ def do_use( python = python.strip() def version_matcher(py_version: PythonInfo) -> bool: - return project.python_requires.contains(str(py_version.version)) + return project.python_requires.contains(str(py_version.version), True) if not project.cache_dir.exists(): project.cache_dir.mkdir(parents=True) @@ -584,7 +583,7 @@ def version_matcher(py_version: PythonInfo) -> bool: ) selected_python = found_interpreters[int(selection)] if python: - use_cache.set(python, selected_python.path) + use_cache.set(python, selected_python.path.as_posix()) old_python = project.python if "python.path" in project.config else None project.core.ui.echo( @@ -596,11 +595,11 @@ def version_matcher(py_version: PythonInfo) -> bool: project.python = selected_python if ( old_python - and Path(old_python.path) != Path(selected_python.path) + and old_python.path != selected_python.path and not project.environment.is_global ): project.core.ui.echo(termui.cyan("Updating executable scripts...")) - project.environment.update_shebangs(selected_python.executable) + project.environment.update_shebangs(selected_python.executable.as_posix()) def do_import( diff --git a/pdm/cli/commands/info.py b/pdm/cli/commands/info.py index a893d73126..37928e5b05 100644 --- a/pdm/cli/commands/info.py +++ b/pdm/cli/commands/info.py @@ -34,9 +34,9 @@ def handle(self, project: Project, options: argparse.Namespace) -> None: check_project_file(project) interpreter = project.python if options.python: - project.core.ui.echo(interpreter.executable) + project.core.ui.echo(str(interpreter.executable)) elif options.where: - project.core.ui.echo(project.root.as_posix()) + project.core.ui.echo(str(project.root)) elif options.packages: project.core.ui.echo(str(project.environment.packages_path)) elif options.env: @@ -49,7 +49,7 @@ def handle(self, project: Project, options: argparse.Namespace) -> None: (termui.cyan("PDM version:", bold=True), project.core.version), ( termui.cyan("Python Interpreter:", bold=True), - interpreter.executable + f" ({interpreter.identifier})", + f"{interpreter.executable} ({interpreter.identifier})", ), (termui.cyan("Project Root:", bold=True), project.root.as_posix()), ( diff --git a/pdm/installers/installers.py b/pdm/installers/installers.py index 70ce1f4ccb..d1097b0e74 100644 --- a/pdm/installers/installers.py +++ b/pdm/installers/installers.py @@ -170,7 +170,7 @@ def install_wheel( } destination = InstallDestination( scheme_dict=environment.get_paths(), - interpreter=environment.interpreter.executable, + interpreter=str(environment.interpreter.executable), script_kind=_get_kind(environment), ) _install_wheel( @@ -187,7 +187,7 @@ def install_wheel_with_cache( wheel_stem = Path(wheel).stem cache_path = environment.project.cache("packages") / wheel_stem package_cache = CachedPackage(cache_path) - interpreter = environment.interpreter.executable + interpreter = str(environment.interpreter.executable) script_kind = _get_kind(environment) supports_symlink = ( environment.project.config["install.cache_method"] == "symlink" diff --git a/pdm/models/environment.py b/pdm/models/environment.py index f5d20300eb..cf49dd311b 100644 --- a/pdm/models/environment.py +++ b/pdm/models/environment.py @@ -117,7 +117,7 @@ def get_finder( sources = self.project.sources python_version = self.interpreter.version_tuple - python_abi_tag = get_python_abi_tag(self.interpreter.executable) + python_abi_tag = get_python_abi_tag(str(self.interpreter.executable)) finder = get_finder( sources, self.project.cache_dir.as_posix(), @@ -138,7 +138,7 @@ def get_working_set(self) -> WorkingSet: @cached_property def marker_environment(self) -> dict[str, str]: """Get environment for marker evaluation""" - return get_pep508_environment(self.interpreter.executable) + return get_pep508_environment(str(self.interpreter.executable)) def which(self, command: str) -> str | None: """Get the full path of the given executable against this environment.""" @@ -147,7 +147,7 @@ def which(self, command: str) -> str | None: version = python[6:] this_version = self.interpreter.version if not version or str(this_version).startswith(version): - return self.interpreter.executable + return str(self.interpreter.executable) # Fallback to use shutil.which to find the executable this_path = self.get_paths()["scripts"] python_root = os.path.dirname(self.interpreter.executable) @@ -194,7 +194,7 @@ def pip_command(self) -> list[str]: from pip import __file__ as pip_location python_major = self.interpreter.major - executable = self.interpreter.executable + executable = str(self.interpreter.executable) proc = subprocess.run( [executable, "-Esm", "pip", "--version"], capture_output=True ) @@ -217,7 +217,7 @@ class GlobalEnvironment(Environment): is_global = True def get_paths(self) -> dict[str, str]: - paths = get_sys_config_paths(self.interpreter.executable) + paths = get_sys_config_paths(str(self.interpreter.executable)) if is_venv_python(self.interpreter.executable): python_xy = f"python{self.interpreter.identifier}" paths["include"] = os.path.join(paths["data"], "include", "site", python_xy) diff --git a/pdm/models/in_process/__init__.py b/pdm/models/in_process/__init__.py index 8f7c91f334..44d73d7c30 100644 --- a/pdm/models/in_process/__init__.py +++ b/pdm/models/in_process/__init__.py @@ -4,9 +4,7 @@ import functools import json import os -import platform import subprocess -import sys from pathlib import Path from typing import Dict, Optional @@ -50,35 +48,3 @@ def get_pep508_environment(executable: str) -> Dict[str, str]: script = str(FOLDER_PATH / "pep508.py") args = [executable, "-Es", script] return json.loads(subprocess.check_output(args)) - - -@functools.lru_cache() -def get_architecture(executable: str) -> str: - """Get the architecture bits for the given python executable""" - if os.path.normpath(executable) == os.path.normpath(sys.executable): - return platform.architecture()[0] - bits, _ = platform.architecture(executable, "_DEFAULT") - if bits != "_DEFAULT": - return bits - # On non-Unix platforms that do not support 'file' command, - # platform.architecture(executable) cannot return the correct arch. - # Retrieve it in subprocess instead. - return ( - subprocess.check_output( - [executable, "-Esc", "import platform;print(platform.architecture()[0])"] - ) - .decode("utf8") - .strip() - ) - - -@functools.lru_cache() -def get_underlying_executable(executable: str) -> str: - """Find the real sys.executable under the wrapper script if any""" - return ( - subprocess.check_output( - [executable, "-Esc", "import sys;print(sys.executable)"] - ) - .decode("utf8") - .strip() - ) diff --git a/pdm/models/python.py b/pdm/models/python.py index f323f58d2b..3ae11dc536 100644 --- a/pdm/models/python.py +++ b/pdm/models/python.py @@ -1,71 +1,70 @@ from __future__ import annotations -import dataclasses import os from pathlib import Path from typing import Any +from findpython import PythonVersion from packaging.version import Version -from pythonfinder import InvalidPythonVersion -from pythonfinder.models.python import PythonVersion from pdm.exceptions import InvalidPyVersion -from pdm.models.in_process import get_architecture, get_underlying_executable -from pdm.utils import cached_property -@dataclasses.dataclass class PythonInfo: """ A convenient helper class that holds all information of a Python interepreter. """ - path: str - version: Version - executable: str = dataclasses.field(init=False) - - def __post_init__(self) -> None: - executable = get_underlying_executable(self.path) - self.executable = Path(executable).as_posix() - - @classmethod - def from_python_version(cls, py_version: PythonVersion) -> "PythonInfo": - return cls(path=py_version.executable, version=py_version.version) + def __init__(self, py_version: PythonVersion) -> None: + self._py_ver = py_version @classmethod def from_path(cls, path: str | Path) -> "PythonInfo": - try: - return cls.from_python_version(PythonVersion.from_path(str(path))) - except InvalidPythonVersion as e: - raise InvalidPyVersion(str(e)) from e + py_ver = PythonVersion(Path(path)) + if py_ver.executable.exists() and py_ver.is_valid(): + return cls(py_ver) + else: + raise InvalidPyVersion(f"Invalid Python interpreter: {path}") def __hash__(self) -> int: - return hash(os.path.normcase(self.executable)) + return hash(self._py_ver) def __eq__(self, o: Any) -> bool: if not isinstance(o, PythonInfo): return False - return os.path.normcase(self.executable) == os.path.normcase(o.executable) + return self._py_ver == o._py_ver + + @property + def path(self) -> Path: + return self._py_ver.executable + + @property + def executable(self) -> Path: + return self._py_ver.interpreter + + @property + def version(self) -> Version: + return self._py_ver.version @property def major(self) -> int: - return self.version.major + return self._py_ver.major @property def minor(self) -> int: - return self.version.minor + return self._py_ver.minor @property def micro(self) -> int: - return self.version.micro + return self._py_ver.patch @property def version_tuple(self) -> tuple[int, ...]: return (self.major, self.minor, self.micro) - @cached_property + @property def is_32bit(self) -> bool: - return "32bit" in get_architecture(self.executable) + return "32bit" in self._py_ver.architecture def for_tag(self) -> str: return f"{self.major}{self.minor}" diff --git a/pdm/models/repositories.py b/pdm/models/repositories.py index b0ec717b26..a28dabcafd 100644 --- a/pdm/models/repositories.py +++ b/pdm/models/repositories.py @@ -425,7 +425,7 @@ def find_candidates( if key[0] != requirement.identify(): continue if not PySpecSet(info[1]).contains( - str(self.environment.interpreter.version) + str(self.environment.interpreter.version), True ): continue can = self.packages[key] diff --git a/pdm/project/core.py b/pdm/project/core.py index 37a595beb7..191b0eba22 100644 --- a/pdm/project/core.py +++ b/pdm/project/core.py @@ -11,8 +11,7 @@ from urllib.parse import urlparse import tomlkit -from pythonfinder import Finder -from pythonfinder.environment import PYENV_INSTALLED, PYENV_ROOT +from findpython import Finder from pdm import termui from pdm._types import Source @@ -47,6 +46,9 @@ from pdm.resolver.providers import BaseProvider +PYENV_ROOT = os.path.expanduser(os.getenv("PYENV_ROOT", "~/.pyenv")) + + class Project: """Core project class""" @@ -163,7 +165,7 @@ def python(self, value: PythonInfo) -> None: @property def python_executable(self) -> str: """For backward compatibility""" - return self.python.executable + return str(self.python.executable) def resolve_interpreter(self) -> PythonInfo: """Get the Python interpreter path.""" @@ -172,7 +174,7 @@ def resolve_interpreter(self) -> PythonInfo: saved_path = config["python.path"] try: python = PythonInfo.from_path(saved_path) - if self.python_requires.contains(str(python.version)): + if self.python_requires.contains(str(python.version), True): return python except (ValueError, FileNotFoundError): self.project_config.pop("python.path", None) @@ -191,7 +193,7 @@ def resolve_interpreter(self) -> PythonInfo: ) for py_version in self.find_interpreters(): - if self.python_requires.contains(str(py_version.version)): + if self.python_requires.contains(str(py_version.version), True): self.python = py_version return py_version @@ -576,7 +578,7 @@ def find_interpreters(self, python_spec: str | None = None) -> Iterable[PythonIn python: str | Path | None = None if not python_spec: - if config.get("python.use_pyenv", True) and PYENV_INSTALLED: + if config.get("python.use_pyenv", True) and os.path.exists(PYENV_ROOT): pyenv_shim = os.path.join(PYENV_ROOT, "shims", "python3") if os.name == "nt": pyenv_shim += ".bat" @@ -604,9 +606,9 @@ def find_interpreters(self, python_spec: str | None = None) -> Iterable[PythonIn yield PythonInfo.from_path(python) return args = [int(v) for v in python_spec.split(".") if v != ""] - finder = Finder() - for entry in finder.find_all_python_versions(*args): - yield PythonInfo.from_python_version(entry.py_version) + finder = Finder(resolve_symlinks=True) + for entry in finder.find_all(*args): + yield PythonInfo(entry) if not python_spec: this_python = getattr(sys, "_base_executable", sys.executable) yield PythonInfo.from_path(this_python) diff --git a/pyproject.toml b/pyproject.toml index d010a05e55..ead0e303a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ license = {text = "MIT"} dependencies = [ "blinker", "click>=7", + "findpython", "importlib-metadata; python_version < \"3.8\"", "installer~=0.3.0", "packaging", @@ -18,7 +19,6 @@ dependencies = [ "pip>=20.1", "platformdirs", "python-dotenv>=0.15", - "pythonfinder", "resolvelib>=0.8,<0.9", "shellingham>=1.3.2", "tomli>=1.1.0", @@ -82,7 +82,7 @@ doc = [ workflow = [ "parver>=0.3.1", "towncrier>=20", - "vendoring; python_version ~= \"3.8\"", + "vendoring; python_version >= \"3.8\"", "pycomplete~=0.3" ] diff --git a/tasks/patches/halo-multiple.patch b/tasks/patches/halo-multiple.patch index 2ffd552218..cbfda78cd6 100644 --- a/tasks/patches/halo-multiple.patch +++ b/tasks/patches/halo-multiple.patch @@ -1,33 +1,51 @@ diff --git a/pdm/_vendor/halo/_utils.py b/pdm/_vendor/halo/_utils.py -index 14a0d62..c8ddeaf 100644 +index 14a0d62..c0b5522 100644 --- a/pdm/_vendor/halo/_utils.py +++ b/pdm/_vendor/halo/_utils.py -@@ -9,11 +9,8 @@ try: +@@ -3,17 +3,13 @@ + """ + import codecs + import platform +-import six + try: + from shutil import get_terminal_size except ImportError: from backports.shutil_get_terminal_size import get_terminal_size --from pdm._vendor.colorama import init - from pdm._vendor.termcolor import colored +-from colorama import init + from termcolor import colored -init(autoreset=True) - def is_supported(): """Check whether operating system supports main symbols or not. +@@ -90,10 +86,7 @@ def is_text_type(text): + bool + Whether parameter is a string or not + """ +- if isinstance(text, six.text_type) or isinstance(text, six.string_types): +- return True +- +- return False ++ return isinstance(text, str) + + + def decode_utf_8_text(text): diff --git a/pdm/_vendor/halo/halo.py b/pdm/_vendor/halo/halo.py index 9bd32bb..4842d3c 100644 --- a/pdm/_vendor/halo/halo.py +++ b/pdm/_vendor/halo/halo.py @@ -12,7 +12,7 @@ import time - import pdm._vendor.halo.cursor as cursor + import halo.cursor as cursor --from pdm._vendor.log_symbols.symbols import LogSymbols -+from pdm._vendor.log_symbols.symbols import LogSymbols, is_supported - from pdm._vendor.spinners.spinners import Spinners +-from log_symbols.symbols import LogSymbols ++from log_symbols.symbols import LogSymbols, is_supported + from spinners.spinners import Spinners - from pdm._vendor.halo._utils import ( -@@ -20,7 +20,6 @@ from pdm._vendor.halo._utils import ( + from halo._utils import ( +@@ -20,7 +20,6 @@ from halo._utils import ( decode_utf_8_text, get_environment, get_terminal_columns, diff --git a/tasks/patches/log-symbols-support-utf8.patch b/tasks/patches/log-symbols-support-utf8.patch index 851b043772..1225e21ae4 100644 --- a/tasks/patches/log-symbols-support-utf8.patch +++ b/tasks/patches/log-symbols-support-utf8.patch @@ -12,7 +12,7 @@ index b7047fc..3ed2ef0 100644 +import sys from enum import Enum - from pdm._vendor.colorama import init, deinit, Fore + from colorama import init, deinit, Fore @@ -30,13 +33,17 @@ def is_supported(): boolean Whether operating system supports main symbols or not diff --git a/tests/cli/test_others.py b/tests/cli/test_others.py index 84c4396e63..baa0396a95 100644 --- a/tests/cli/test_others.py +++ b/tests/cli/test_others.py @@ -63,10 +63,10 @@ def test_info_command(project, invoke): assert project.root.as_posix() in result.output result = invoke(["info", "--python"], obj=project) - assert result.output.strip() == project.python.executable + assert result.output.strip() == str(project.python.executable) result = invoke(["info", "--where"], obj=project) - assert result.output.strip() == project.root.as_posix() + assert result.output.strip() == str(project.root) result = invoke(["info", "--env"], obj=project) assert result.exit_code == 0 @@ -80,7 +80,7 @@ def test_info_global_project(invoke, tmp_path): def test_global_project_other_location(invoke, project): result = invoke(["info", "-g", "-p", project.root.as_posix(), "--where"]) - assert result.stdout.strip() == project.root.as_posix() + assert result.stdout.strip() == str(project.root) def test_uncaught_error(invoke, mocker): diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py index 1f677443e5..a8d1641e4e 100644 --- a/tests/cli/test_run.py +++ b/tests/cli/test_run.py @@ -18,7 +18,7 @@ def test_pep582_launcher_for_python_interpreter(project, local_finder, invoke): env = os.environ.copy() env.update({"PYTHONPATH": PEP582_PATH}) output = subprocess.check_output( - [project.python.executable, str(project.root.joinpath("main.py"))], + [str(project.python.executable), str(project.root.joinpath("main.py"))], env=env, ) assert output.decode().strip() == "1" @@ -27,7 +27,9 @@ def test_pep582_launcher_for_python_interpreter(project, local_finder, invoke): def test_auto_isolate_site_packages(project, invoke): env = os.environ.copy() env.update({"PYTHONPATH": PEP582_PATH}) - proc = subprocess.run([project.python.executable, "-c", "import click"], env=env) + proc = subprocess.run( + [str(project.python.executable), "-c", "import click"], env=env + ) assert proc.returncode == 0 result = invoke(["run", "python", "-c", "import click"], obj=project) diff --git a/tests/cli/test_use.py b/tests/cli/test_use.py index 9989b6d739..9ce83a7a9f 100644 --- a/tests/cli/test_use.py +++ b/tests/cli/test_use.py @@ -12,11 +12,11 @@ def test_use_command(project, invoke): python = "python" if os.name == "nt" else "python3" - python_path = Path(shutil.which(python)).as_posix() + python_path = shutil.which(python) result = invoke(["use", "-f", python], obj=project) assert result.exit_code == 0 config_content = project.root.joinpath(".pdm.toml").read_text() - assert python_path in config_content + assert python_path.replace("\\", "\\\\") in config_content result = invoke(["use", "-f", python_path], obj=project) assert result.exit_code == 0 @@ -45,7 +45,7 @@ def test_use_wrapper_python(project): shim_path.chmod(0o755) actions.do_use(project, shim_path.as_posix()) - assert project.python.executable == sys.executable + assert project.python.executable == Path(sys.executable) @pytest.mark.skipif(os.name != "posix", reason="Run on POSIX platforms only") diff --git a/tests/test_integration.py b/tests/test_integration.py index 21f9a8ba21..6e410485b2 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -14,6 +14,10 @@ def test_basic_integration(python_version, core, tmp_path, invoke): invoke(["use", "-f", python_version], obj=project, strict=True) invoke(["init", "-n"], obj=project, strict=True) project.meta["name"] = "test-project" + project.meta[ + "requires-python" + ] = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" + project.write_pyproject() project._environment = None invoke(["add", "django", "-v"] + additional_args, obj=project, strict=True) with cd(project.root): diff --git a/tests/test_project.py b/tests/test_project.py index 49dff624dd..cae2bad9eb 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -4,6 +4,7 @@ from pathlib import Path import pytest +from packaging.version import parse from pdm.utils import cd @@ -13,7 +14,6 @@ def test_project_python_with_pyenv_support(project, mocker): del project.project_config["python.path"] project._python = None os.environ["PDM_IGNORE_SAVED_PYTHON"] = "1" - mocker.patch("pdm.project.core.PYENV_INSTALLED", True) mocker.patch("pdm.project.core.PYENV_ROOT", str(project.root)) pyenv_python = project.root / "shims/python" if os.name == "nt": @@ -21,14 +21,14 @@ def test_project_python_with_pyenv_support(project, mocker): pyenv_python.parent.mkdir() pyenv_python.touch() mocker.patch( - "pythonfinder.models.python.get_python_version", - return_value="3.8.0", + "findpython.python.PythonVersion._get_version", + return_value=parse("3.8.0"), ) mocker.patch( - "pdm.models.python.get_underlying_executable", return_value=sys.executable + "findpython.python.PythonVersion._get_interpreter", return_value=sys.executable ) assert Path(project.python.path) == pyenv_python - assert project.python.executable == Path(sys.executable).as_posix() + assert project.python.executable == Path(sys.executable) # Clean cache project._python = None @@ -129,7 +129,7 @@ def test_project_use_venv(project): project.project_config["python.use_venv"] = True env = project.environment assert ( - Path(env.interpreter.executable) + env.interpreter.executable == project.root / "venv" / scripts / f"python{suffix}" ) assert env.is_global @@ -167,10 +167,9 @@ def test_ignore_saved_python(project): suffix = ".exe" if os.name == "nt" else "" venv.create(project.root / "venv") os.environ["PDM_IGNORE_SAVED_PYTHON"] = "1" - assert Path(project.python.executable) != project.project_config["python.path"] + assert project.python.executable != project.project_config["python.path"] assert ( - Path(project.python.executable) - == project.root / "venv" / scripts / f"python{suffix}" + project.python.executable == project.root / "venv" / scripts / f"python{suffix}" ) @@ -202,7 +201,7 @@ def test_global_python_path_config(project_no_init): project_no_init.global_config["python.path"] = sys.executable # Recreate the project to clean cached properties p = project_no_init.core.create_project(project_no_init.root) - assert os.path.normcase(p.python.executable) == os.path.normcase(sys.executable) + assert p.python.executable == Path(sys.executable) assert "python.path" not in p.project_config @@ -210,6 +209,4 @@ def test_global_python_path_config(project_no_init): 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 os.path.normcase(project_no_init.python.executable) == os.path.normcase( - sys.executable - ) + assert project_no_init.python.executable == Path(sys.executable)