From a40e6432f702ecb89db0f7026d34892e604beefe Mon Sep 17 00:00:00 2001 From: finswimmer Date: Tue, 2 May 2023 10:26:40 +0200 Subject: [PATCH] chore(pre-commit): replace several linter by ruff (#136) * chore(pre-commit): replace several linter by ruff * chore(pre-commit): update pre-commit hooks --- .pre-commit-config.yaml | 39 ++----- pyproject.toml | 47 ++++++++- setup.cfg | 35 ------- setup.py | 3 +- src/pythonfinder/__init__.py | 2 + src/pythonfinder/cli.py | 2 +- src/pythonfinder/models/__init__.py | 2 + src/pythonfinder/models/common.py | 8 +- src/pythonfinder/models/mixins.py | 95 +++++++++-------- src/pythonfinder/models/path.py | 153 ++++++++++++++++------------ src/pythonfinder/models/python.py | 126 +++++++++++++---------- src/pythonfinder/pythonfinder.py | 67 ++++++------ src/pythonfinder/utils.py | 44 ++++---- tests/conftest.py | 10 +- tests/test_python.py | 13 +-- tests/test_utils.py | 8 +- tests/testutils.py | 1 + 17 files changed, 326 insertions(+), 329 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b4274d8..beee232 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,49 +4,24 @@ exclude: > )$ repos: - - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 - hooks: - - id: pyupgrade - args: [ --py37-plus ] - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.9.0 + rev: v1.10.0 hooks: - id: python-use-type-annotations - - id: python-check-blanket-noqa - - - repo: https://github.com/asottile/yesqa - rev: v1.4.0 - hooks: - - id: yesqa - additional_dependencies: &flake8_deps - - flake8-bugbear - - flake8-type-checking - - repo: https://github.com/pycqa/isort - rev: 5.12.0 + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.0.263 hooks: - - id: isort - args: [--add-import, from __future__ import annotations] - exclude: | - (?x)( - ^docs/.+\.py$ - ) + - id: ruff + args: [ --exit-non-zero-on-fix ] - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 23.3.0 hooks: - id: black - - - repo: https://github.com/pycqa/flake8 - rev: 6.0.0 - hooks: - - id: flake8 - additional_dependencies: *flake8_deps diff --git a/pyproject.toml b/pyproject.toml index 9e8f3d4..eff4216 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,11 +9,48 @@ extend-exclude = ''' ) ''' -[tool.isort] -profile = "black" -atomic = true -filter_files = true -known_first_party = ["pythonfinder"] +[tool.ruff] +target-version = "py37" +fix = true +line-length = 90 +select = [ + "B", # flake8-bugbear + "E", # pycodestyle (flake8) + "F", # pyflakes (flake8) + "I", # isort + "PGH", # pygrep-hooks + "RUF", # Ruff u.a. yesqa + "TCH", # flake8-type-checking + "UP", # pyupgrade + "W", # pycodestyle (flake8) +] +ignore = [ + "B904", # Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling + "E501", # line too long (flake8) + "PGH003", # Use specific rule codes when ignoring type issues +] +unfixable = [ + "F841", # Local variable {x} is assigned to but never used (flake8) +] +[tool.ruff.per-file-ignores] +"__init__.py" = [ + "F401", # module imported but unused (flake8) + "F403", # ‘from module import *’ used; unable to detect undefined names (flake8) +] +"docs/*" = [ + "I", +] + +[tool.ruff.flake8-type-checking] +runtime-evaluated-base-classes = [ + "pydantic.BaseModel", + "pythonfinder.models.common.FinderBaseModel", + "pythonfinder.models.mixins.PathEntry", +] + +[tool.ruff.isort] +known-first-party = ["pythonfinder"] +required-imports = ["from __future__ import annotations"] [tool.towncrier] package = "pythonfinder" diff --git a/setup.cfg b/setup.cfg index c9fe1ed..9da917e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -84,41 +84,6 @@ filterwarnings = [bdist_wheel] universal = 1 -[flake8] -max-line-length = 90 -select = C,E,F,W,B -ignore = - # The default ignore list: - # E121,E123,E126,E226,E24,E704, - D203,F401,E123,E203,W503,E501,E402,F841,B950,TC,TC1 - # Our additions: - # E127: continuation line over-indented for visual indent - # E128: continuation line under-indented for visual indent - # E129: visually indented line with same indent as next logical line - # E222: multiple spaces after operator - # E231: missing whitespace after ',' - # E402: module level import not at top of file - # E501: line too long - # E231,E402,E501 -extend_exclude = - docs/source/*, - tests/*, - setup.py -max-complexity=16 - -[isort] -atomic = true -not_skip = __init__.py -skip = src/pythonfinder/_vendor -line_length = 90 -indent = ' ' -multi_line_output = 3 -known_third_party = cached_property,click,invoke,packaging,parver,pydantic,pytest,requests,setuptools,six,towncrier -known_first_party = pythonfinder,tests -combine_as_imports=True -include_trailing_comma = True -force_grid_wrap=0 - [mypy] ignore_missing_imports=true follow_imports=skip diff --git a/setup.py b/setup.py index 6ff9a59..3941cf6 100644 --- a/setup.py +++ b/setup.py @@ -3,9 +3,8 @@ import codecs import os import re -import sys -from setuptools import Command, find_packages, setup +from setuptools import find_packages, setup here = os.path.abspath(os.path.dirname(__file__)) diff --git a/src/pythonfinder/__init__.py b/src/pythonfinder/__init__.py index 66bd699..4aae7e7 100644 --- a/src/pythonfinder/__init__.py +++ b/src/pythonfinder/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from .exceptions import InvalidPythonVersion from .models import SystemPath from .pythonfinder import Finder diff --git a/src/pythonfinder/cli.py b/src/pythonfinder/cli.py index 1ba1e50..4b8b105 100644 --- a/src/pythonfinder/cli.py +++ b/src/pythonfinder/cli.py @@ -60,7 +60,7 @@ def cli( comes_from_path = getattr(comes_from, "path", found.path) else: comes_from_path = found.path - arch = getattr(py, "architecture", None) + click.secho("Found python at the following locations:", fg="green") click.secho( "{py.name!s}: {py.version!s} ({py.architecture!s}) @ {comes_from!s}".format( diff --git a/src/pythonfinder/models/__init__.py b/src/pythonfinder/models/__init__.py index bc92f89..be8d1e8 100644 --- a/src/pythonfinder/models/__init__.py +++ b/src/pythonfinder/models/__init__.py @@ -1,2 +1,4 @@ +from __future__ import annotations + from .path import SystemPath from .python import PythonVersion diff --git a/src/pythonfinder/models/common.py b/src/pythonfinder/models/common.py index a9a2d67..4c439c9 100644 --- a/src/pythonfinder/models/common.py +++ b/src/pythonfinder/models/common.py @@ -1,10 +1,14 @@ +from __future__ import annotations + from pydantic import BaseModel, Extra class FinderBaseModel(BaseModel): - def __setattr__(self, name, value): # noqa: C901 (ignore complexity) + def __setattr__(self, name, value): private_attributes = { - field_name for field_name in self.__annotations__ if field_name.startswith("_") + field_name + for field_name in self.__annotations__ + if field_name.startswith("_") } if name in private_attributes or name in self.__fields__: diff --git a/src/pythonfinder/models/mixins.py b/src/pythonfinder/models/mixins.py index 5292653..d92f768 100644 --- a/src/pythonfinder/models/mixins.py +++ b/src/pythonfinder/models/mixins.py @@ -1,32 +1,34 @@ -import operator +from __future__ import annotations + import os from collections import defaultdict from pathlib import Path from typing import ( + TYPE_CHECKING, Any, Dict, Generator, Iterator, - List, Optional, - Union, ) from pydantic import BaseModel, Field, validator +from ..environment import get_shim_paths from ..exceptions import InvalidPythonVersion from ..utils import ( KNOWN_EXTS, - expand_paths, - looks_like_python, - path_is_known_executable, ensure_path, + expand_paths, filter_pythons, is_in_path, + looks_like_python, normalize_path, + path_is_known_executable, ) -from ..environment import get_shim_paths +if TYPE_CHECKING: + from pythonfinder.models.python import PythonVersion class PathEntry(BaseModel): @@ -47,15 +49,15 @@ class Config: allow_mutation = True include_private_attributes = True - @validator('children', pre=True, always=True, check_fields=False) + @validator("children", pre=True, always=True, check_fields=False) def set_children(cls, v, values, **kwargs): - path = values.get('path') + path = values.get("path") if path: - values['name'] = path.name + values["name"] = path.name return v or cls()._gen_children() def __str__(self) -> str: - return "{0}".format(self.path.as_posix()) + return f"{self.path.as_posix()}" def __lt__(self, other) -> bool: return self.path.as_posix() < other.path.as_posix() @@ -69,7 +71,7 @@ def __gt__(self, other) -> bool: def __gte__(self, other) -> bool: return self.path.as_posix() >= other.path.as_posix() - def which(self, name) -> Optional["PathEntry"]: + def which(self, name) -> PathEntry | None: """Search in this path for an executable. :param executable: The name of an executable to search for. @@ -78,8 +80,7 @@ def which(self, name) -> Optional["PathEntry"]: """ valid_names = [name] + [ - "{0}.{1}".format(name, ext).lower() if ext else "{0}".format(name).lower() - for ext in KNOWN_EXTS + f"{name}.{ext}".lower() if ext else f"{name}".lower() for ext in KNOWN_EXTS ] children = self.children found = None @@ -93,8 +94,9 @@ def which(self, name) -> Optional["PathEntry"]: None, ) return found + @property - def as_python(self) -> "PythonVersion": + def as_python(self) -> PythonVersion: py_version = None if self.py_version_ref: return self.py_version_ref @@ -102,9 +104,7 @@ def as_python(self) -> "PythonVersion": from .python import PythonVersion try: - py_version = PythonVersion.from_path( - path=self, name=self.name - ) + py_version = PythonVersion.from_path(path=self, name=self.name) except (ValueError, InvalidPythonVersion): pass self.py_version_ref = py_version @@ -174,9 +174,7 @@ def get_py_version(self): from .python import PythonVersion try: - py_version = PythonVersion.from_path( - path=self, name=self.name - ) + py_version = PythonVersion.from_path(path=self, name=self.name) except (InvalidPythonVersion, ValueError): py_version = None except Exception: @@ -186,7 +184,7 @@ def get_py_version(self): return None @property - def py_version(self) -> Optional["PythonVersion"]: + def py_version(self) -> PythonVersion | None: if not self.py_version_ref: py_version = self.get_py_version() self.py_version_ref = py_version @@ -208,7 +206,7 @@ def _iter_pythons(self) -> Iterator: yield self @property - def pythons(self) -> Dict[Union[str, Path], "PathEntry"]: + def pythons(self) -> dict[str | Path, PathEntry]: if not self.pythons_ref: self.pythons_ref = defaultdict(PathEntry) for python in self._iter_pythons(): @@ -217,8 +215,7 @@ def pythons(self) -> Dict[Union[str, Path], "PathEntry"]: return self.pythons_ref def __iter__(self) -> Iterator: - for entry in self.children.values(): - yield entry + yield from self.children.values() def __next__(self) -> Generator: return next(iter(self)) @@ -228,14 +225,14 @@ def next(self) -> Generator: def find_all_python_versions( self, - major: Optional[Union[str, int]] = None, - minor: Optional[int] = None, - patch: Optional[int] = None, - pre: Optional[bool] = None, - dev: Optional[bool] = None, - arch: Optional[str] = None, - name: Optional[str] = None, - ) -> List["PathEntry"]: + major: str | int | None = None, + minor: int | None = None, + patch: int | None = None, + pre: bool | None = None, + dev: bool | None = None, + arch: str | None = None, + name: str | None = None, + ) -> list[PathEntry]: """Search for a specific python version on the path. Return all copies :param major: Major python version to search for. @@ -268,14 +265,14 @@ def version_sort(path_entry): def find_python_version( self, - major: Optional[Union[str, int]] = None, - minor: Optional[int] = None, - patch: Optional[int] = None, - pre: Optional[bool] = None, - dev: Optional[bool] = None, - arch: Optional[str] = None, - name: Optional[str] = None, - ) -> Optional["PathEntry"]: + major: str | int | None = None, + minor: int | None = None, + patch: int | None = None, + pre: bool | None = None, + dev: bool | None = None, + arch: str | None = None, + name: str | None = None, + ) -> PathEntry | None: """Search or self for the specified Python version and return the first match. :param major: Major version number. @@ -288,8 +285,11 @@ def find_python_version( :param str name: The name of a python version, e.g. ``anaconda3-5.3.0`` :returns: A :class:`~pythonfinder.models.PathEntry` instance matching the version requested. """ + def version_matcher(py_version): - return py_version.matches(major, minor, patch, pre, dev, arch, python_name=name) + return py_version.matches( + major, minor, patch, pre, dev, arch, python_name=name + ) if not self.is_dir: if self.is_python and self.as_python and version_matcher(self.py_version): @@ -346,7 +346,7 @@ def _gen_children(self) -> Iterator: return @property - def children(self) -> Dict[str, "PathEntry"]: + def children(self) -> dict[str, PathEntry]: children = getattr(self, "children_ref", {}) if not children: for child_key, child_val in self._gen_children(): @@ -357,12 +357,12 @@ def children(self) -> Dict[str, "PathEntry"]: @classmethod def create( cls, - path: Union[str, Path], + path: str | Path, is_root: bool = False, only_python: bool = False, - pythons: Optional[Dict[str, "PythonVersion"]] = None, - name: Optional[str] = None, - ) -> "PathEntry": + pythons: dict[str, PythonVersion] | None = None, + name: str | None = None, + ) -> PathEntry: """Helper method for creating new :class:`pythonfinder.models.PathEntry` instances. :param str path: Path to the specified location. @@ -401,4 +401,3 @@ def create( ) _new.children_ref = children return _new - diff --git a/src/pythonfinder/models/path.py b/src/pythonfinder/models/path.py index 8c72de2..45e8d89 100644 --- a/src/pythonfinder/models/path.py +++ b/src/pythonfinder/models/path.py @@ -1,16 +1,27 @@ +from __future__ import annotations + import errno import operator import os -from pathlib import Path import sys -from collections import defaultdict, ChainMap +from collections import ChainMap, defaultdict from itertools import chain -from typing import Any, Dict, DefaultDict, List, Generator, Iterator, Optional, Tuple, Union +from pathlib import Path +from typing import ( + Any, + DefaultDict, + Dict, + Generator, + Iterator, + List, + Optional, + Tuple, + Union, +) from cached_property import cached_property from pydantic import Field, root_validator -from .common import FinderBaseModel from ..environment import ( ASDF_DATA_DIR, ASDF_INSTALLED, @@ -27,8 +38,9 @@ parse_pyenv_version_order, split_version_and_name, ) +from .common import FinderBaseModel from .mixins import PathEntry -from .python import PythonFinder, PythonVersion +from .python import PythonFinder def exists_and_is_accessible(path): @@ -43,12 +55,20 @@ def exists_and_is_accessible(path): class SystemPath(FinderBaseModel): global_search: bool = True - paths: Dict[str, Union[PythonFinder, PathEntry]] = Field(default_factory=lambda: defaultdict(PathEntry)) + paths: Dict[str, Union[PythonFinder, PathEntry]] = Field( + default_factory=lambda: defaultdict(PathEntry) + ) executables_tracking: List[PathEntry] = Field(default_factory=lambda: list()) - python_executables_tracking: Dict[str, PathEntry] = Field(default_factory=lambda: dict()) + python_executables_tracking: Dict[str, PathEntry] = Field( + default_factory=lambda: dict() + ) path_order: List[str] = Field(default_factory=lambda: list()) - python_version_dict: Dict[Tuple, Any] = Field(default_factory=lambda: defaultdict(list)) - version_dict_tracking: Dict[Tuple, List[PathEntry]] = Field(default_factory=lambda: defaultdict(list)) + python_version_dict: Dict[Tuple, Any] = Field( + default_factory=lambda: defaultdict(list) + ) + version_dict_tracking: Dict[Tuple, List[PathEntry]] = Field( + default_factory=lambda: defaultdict(list) + ) only_python: bool = False pyenv_finder: Optional[PythonFinder] = None asdf_finder: Optional[PythonFinder] = None @@ -76,17 +96,19 @@ def __init__(self, **data): @root_validator(pre=True) def set_defaults(cls, values): - values['python_version_dict'] = defaultdict(list) - values['pyenv_finder'] = None - values['asdf_finder'] = None - values['path_order'] = [] - values['_finders'] = {} - values['paths'] = defaultdict(PathEntry) - paths = values.get('paths') + values["python_version_dict"] = defaultdict(list) + values["pyenv_finder"] = None + values["asdf_finder"] = None + values["path_order"] = [] + values["_finders"] = {} + values["paths"] = defaultdict(PathEntry) + paths = values.get("paths") if paths: - values['executables'] = [ + values["executables"] = [ p - for p in ChainMap(*(child.children_ref.values() for child in paths.values())) + for p in ChainMap( + *(child.children_ref.values() for child in paths.values()) + ) if p.is_executable ] return values @@ -97,7 +119,7 @@ def _register_finder(self, finder_name, finder): return self @property - def finders(self) -> List[str]: + def finders(self) -> list[str]: return [k for k in self.finders_dict.keys()] @staticmethod @@ -109,18 +131,20 @@ def check_for_asdf(): return ASDF_INSTALLED or os.path.exists(normalize_path(ASDF_DATA_DIR)) @property - def executables(self) -> List[PathEntry]: + def executables(self) -> list[PathEntry]: if self.executables_tracking: return self.executables_tracking self.executables_tracking = [ p - for p in chain(*(child.children_ref.values() for child in self.paths.values())) + for p in chain( + *(child.children_ref.values() for child in self.paths.values()) + ) if p.is_executable ] return self.executables_tracking @cached_property - def python_executables(self) -> Dict[str, PathEntry]: + def python_executables(self) -> dict[str, PathEntry]: python_executables = {} for child in self.paths.values(): if child.pythons: @@ -132,11 +156,9 @@ def python_executables(self) -> Dict[str, PathEntry]: return self.python_executables_tracking @cached_property - def version_dict(self) -> DefaultDict[Tuple, List[PathEntry]]: - self.version_dict_tracking = defaultdict( - list - ) - for finder_name, finder in self.finders_dict.items(): + def version_dict(self) -> DefaultDict[tuple, list[PathEntry]]: + self.version_dict_tracking = defaultdict(list) + for _finder_name, finder in self.finders_dict.items(): for version, entry in finder.versions.items(): if entry not in self.version_dict_tracking[version] and entry.is_python: self.version_dict_tracking[version].append(entry) @@ -150,7 +172,7 @@ def version_dict(self) -> DefaultDict[Tuple, List[PathEntry]]: self.version_dict_tracking[version].append(entry) return self.version_dict_tracking - def _run_setup(self) -> "SystemPath": + def _run_setup(self) -> SystemPath: path_order = self.path_order[:] if self.global_search and "PATH" in os.environ: path_order = path_order + os.environ["PATH"].split(os.pathsep) @@ -188,7 +210,7 @@ def _run_setup(self) -> "SystemPath": bin_dir = "bin" if venv and (self.system or self.global_search): p = ensure_path(venv) - path_order = [(p / bin_dir).as_posix()] + self.path_order + path_order = [(p / bin_dir).as_posix(), *self.path_order] self.path_order = path_order self.paths[p] = self.get_path(p.joinpath(bin_dir)) if self.system: @@ -196,7 +218,7 @@ def _run_setup(self) -> "SystemPath": syspath_bin = syspath.parent if syspath_bin.name != bin_dir and syspath_bin.joinpath(bin_dir).exists(): syspath_bin = syspath_bin / bin_dir - path_order = [syspath_bin.as_posix()] + self.path_order + path_order = [syspath_bin.as_posix(), *self.path_order] self.paths[syspath_bin] = PathEntry.create( path=syspath_bin, is_root=True, only_python=False ) @@ -209,11 +231,11 @@ def _get_last_instance(self, path) -> int: normalized_target = normalize_path(path) last_instance = next(iter(p for p in paths if normalized_target in p), None) if last_instance is None: - raise ValueError("No instance found on path for target: {0!s}".format(path)) + raise ValueError(f"No instance found on path for target: {path!s}") path_index = self.path_order.index(last_instance) return path_index - def _slice_in_paths(self, start_idx, paths) -> "SystemPath": + def _slice_in_paths(self, start_idx, paths) -> SystemPath: before_path = [] after_path = [] if start_idx == 0: @@ -227,7 +249,7 @@ def _slice_in_paths(self, start_idx, paths) -> "SystemPath": self.path_order = path_order return self - def _remove_path(self, path) -> "SystemPath": + def _remove_path(self, path) -> SystemPath: path_copy = [p for p in reversed(self.path_order[:])] new_order = [] target = normalize_path(path) @@ -242,7 +264,7 @@ def _remove_path(self, path) -> "SystemPath": self.path_order = new_order return self - def _setup_asdf(self) -> "SystemPath": + def _setup_asdf(self) -> SystemPath: if "asdf" in self.finders and self.asdf_finder is not None: return self @@ -272,7 +294,7 @@ def _setup_asdf(self) -> "SystemPath": self._register_finder("asdf", asdf_finder) return self - def _setup_pyenv(self) -> "SystemPath": + def _setup_pyenv(self) -> SystemPath: if "pyenv" in self.finders and self.pyenv_finder is not None: return self @@ -303,7 +325,7 @@ def _setup_pyenv(self) -> "SystemPath": self._register_finder("pyenv", pyenv_finder) return self - def get_path(self, path) -> Union[PythonFinder, PathEntry]: + def get_path(self, path) -> PythonFinder | PathEntry: if path is None: raise TypeError("A path must be provided in order to generate a path entry.") path = ensure_path(path) @@ -316,10 +338,10 @@ def get_path(self, path) -> Union[PythonFinder, PathEntry]: ) self.paths[path.as_posix()] = _path if not _path: - raise ValueError("Path not found or generated: {0!r}".format(path)) + raise ValueError(f"Path not found or generated: {path!r}") return _path - def _get_paths(self) -> Generator[Union[PythonFinder, PathEntry], None, None]: + def _get_paths(self) -> Generator[PythonFinder | PathEntry, None, None]: for path in self.path_order: try: entry = self.get_path(path) @@ -329,11 +351,11 @@ def _get_paths(self) -> Generator[Union[PythonFinder, PathEntry], None, None]: yield entry @cached_property - def path_entries(self) -> List[Union[PythonFinder, PathEntry]]: + def path_entries(self) -> list[PythonFinder | PathEntry]: paths = list(self._get_paths()) return paths - def find_all(self, executable) -> List[Union["PathEntry", PythonFinder]]: + def find_all(self, executable) -> list[PathEntry | PythonFinder]: """ Search the path for an executable. Return all copies. @@ -346,7 +368,7 @@ def find_all(self, executable) -> List[Union["PathEntry", PythonFinder]]: filtered = (sub_which(self.get_path(k)) for k in self.path_order) return list(filtered) - def which(self, executable) -> Union["PathEntry", None]: + def which(self, executable) -> PathEntry | None: """ Search for an executable on the path. @@ -385,22 +407,24 @@ def version_sort_key(entry): def find_all_python_versions( self, - major: Optional[Union[str, int]] = None, - minor: Optional[int] = None, - patch: Optional[int] = None, - pre: Optional[bool] = None, - dev: Optional[bool] = None, - arch: Optional[str] = None, - name: Optional[str] = None, - ) -> List["PathEntry"]: - + major: str | int | None = None, + minor: int | None = None, + patch: int | None = None, + pre: bool | None = None, + dev: bool | None = None, + arch: str | None = None, + name: str | None = None, + ) -> list[PathEntry]: def sub_finder(obj): return obj.find_all_python_versions(major, minor, patch, pre, dev, arch, name) alternate_sub_finder = None if major and not (minor or patch or pre or dev or arch or name): + def alternate_sub_finder(obj): - return obj.find_all_python_versions(None, None, None, None, None, None, major) + return obj.find_all_python_versions( + None, None, None, None, None, None, major + ) values = list(self.get_pythons(sub_finder)) if not values and alternate_sub_finder is not None: @@ -410,16 +434,15 @@ def alternate_sub_finder(obj): def find_python_version( self, - major: Optional[Union[str, int]] = None, - minor: Optional[Union[str, int]] = None, - patch: Optional[Union[str, int]] = None, - pre: Optional[bool] = None, - dev: Optional[bool] = None, - arch: Optional[str] = None, - name: Optional[str] = None, + major: str | int | None = None, + minor: str | int | None = None, + patch: str | int | None = None, + pre: bool | None = None, + dev: bool | None = None, + arch: str | None = None, + name: str | None = None, sort_by_path: bool = False, - ) -> "PathEntry": - + ) -> PathEntry: def sub_finder(obj): return obj.find_python_version(major, minor, patch, pre, dev, arch, name) @@ -458,12 +481,12 @@ def alternate_sub_finder(obj): @classmethod def create( cls, - path: Optional[str] = None, + path: str | None = None, system: bool = False, only_python: bool = False, global_search: bool = True, ignore_unsupported: bool = True, - ) -> "SystemPath": + ) -> SystemPath: """Create a new :class:`pythonfinder.models.SystemPath` instance. :param path: Search path to prepend when searching, defaults to None @@ -494,8 +517,10 @@ def create( ) } ) - paths = [path] + paths - paths = [p for p in paths if not any(is_in_path(p, shim) for shim in get_shim_paths())] + paths = [path, *paths] + paths = [ + p for p in paths if not any(is_in_path(p, shim) for shim in get_shim_paths()) + ] _path_objects = [ensure_path(p.strip('"')) for p in paths] path_entries.update( { diff --git a/src/pythonfinder/models/python.py b/src/pythonfinder/models/python.py index dc6332e..8ed796f 100644 --- a/src/pythonfinder/models/python.py +++ b/src/pythonfinder/models/python.py @@ -1,15 +1,26 @@ +from __future__ import annotations + import logging import os import platform import sys from collections import defaultdict from pathlib import Path, WindowsPath -from typing import Any, Callable, DefaultDict, Dict, List, Optional, Tuple, Union, Generator, Iterator +from typing import ( + Any, + Callable, + DefaultDict, + Dict, + Iterator, + List, + Optional, + Tuple, + Union, +) from packaging.version import Version from pydantic import Field, validator -from .common import FinderBaseModel from ..environment import ASDF_DATA_DIR, PYENV_ROOT, SYSTEM_ARCH from ..exceptions import InvalidPythonVersion from ..utils import ( @@ -22,11 +33,10 @@ parse_asdf_version_order, parse_pyenv_version_order, parse_python_version, - unnest, ) +from .common import FinderBaseModel from .mixins import PathEntry - logger = logging.getLogger(__name__) @@ -68,7 +78,7 @@ def is_pyenv(self) -> bool: def is_asdf(self) -> bool: return is_in_path(str(self.root), ASDF_DATA_DIR) - def get_version_order(self) -> List[Path]: + def get_version_order(self) -> list[Path]: version_paths = [ p for p in self.root.glob(self.version_glob_path) @@ -101,11 +111,11 @@ def get_bin_dir(self, base) -> Path: return base / "bin" @classmethod - def version_from_bin_dir(cls, entry) -> Optional[PathEntry]: + def version_from_bin_dir(cls, entry) -> PathEntry | None: py_version = next(iter(entry.find_all_python_versions()), None) return py_version - def _iter_version_bases(self) -> Iterator[Tuple[Path, PathEntry]]: + def _iter_version_bases(self) -> Iterator[tuple[Path, PathEntry]]: for p in self.get_version_order(): bin_dir = self.get_bin_dir(p) if bin_dir.exists() and bin_dir.is_dir(): @@ -115,7 +125,7 @@ def _iter_version_bases(self) -> Iterator[Tuple[Path, PathEntry]]: self.roots[p] = entry yield (p, entry) - def _iter_versions(self) -> Iterator[Tuple[Path, PathEntry, Tuple]]: + def _iter_versions(self) -> Iterator[tuple[Path, PathEntry, tuple]]: for base_path, entry in self._iter_version_bases(): version = None version_entry = None @@ -150,7 +160,7 @@ def _iter_versions(self) -> Iterator[Tuple[Path, PathEntry, Tuple]]: yield (base_path, entry, version_tuple) @property - def versions(self) -> DefaultDict[Tuple, PathEntry]: + def versions(self) -> DefaultDict[tuple, PathEntry]: if not self._versions: for _, entry, version_tuple in self._iter_versions(): self._versions[version_tuple] = entry @@ -167,7 +177,7 @@ def _iter_pythons(self) -> Iterator: yield self.versions[version_tuple] @validator("paths", pre=True, always=True) - def get_paths(cls, v) -> List[PathEntry]: + def get_paths(cls, v) -> list[PathEntry]: if v is not None: return v @@ -175,7 +185,7 @@ def get_paths(cls, v) -> List[PathEntry]: return _paths @property - def pythons(self) -> Dict: + def pythons(self) -> dict: if not self.pythons_ref: from .path import PathEntry @@ -193,7 +203,9 @@ def get_pythons(self) -> DefaultDict[str, PathEntry]: return self.pythons @classmethod - def create(cls, root, sort_function, version_glob_path=None, ignore_unsupported=True) -> "PythonFinder": + def create( + cls, root, sort_function, version_glob_path=None, ignore_unsupported=True + ) -> PythonFinder: root = ensure_path(root) if not version_glob_path: version_glob_path = "versions/*" @@ -207,14 +219,14 @@ def create(cls, root, sort_function, version_glob_path=None, ignore_unsupported= def find_all_python_versions( self, - major: Optional[Union[str, int]] = None, - minor: Optional[int] = None, - patch: Optional[int] = None, - pre: Optional[bool] = None, - dev: Optional[bool] = None, - arch: Optional[str] = None, - name: Optional[str] = None, - ) -> List[PathEntry]: + major: str | int | None = None, + minor: int | None = None, + patch: int | None = None, + pre: bool | None = None, + dev: bool | None = None, + arch: str | None = None, + name: str | None = None, + ) -> list[PathEntry]: """Search for a specific python version on the path. Return all copies :param major: Major python version to search for. @@ -253,14 +265,14 @@ def version_sort(py): def find_python_version( self, - major: Optional[Union[str, int]] = None, - minor: Optional[int] = None, - patch: Optional[int] = None, - pre: Optional[bool] = None, - dev: Optional[bool] = None, - arch: Optional[str] = None, - name: Optional[str] = None, - ) -> Optional[PathEntry]: + major: str | int | None = None, + minor: int | None = None, + patch: int | None = None, + pre: bool | None = None, + dev: bool | None = None, + arch: str | None = None, + name: str | None = None, + ) -> PathEntry | None: """Search or self for the specified Python version and return the first match. :param major: Major version number. @@ -275,7 +287,7 @@ def find_python_version( """ def sub_finder(obj): - return getattr(obj, "find_python_version")(major, minor, patch, pre, dev, arch, name) + return obj.find_python_version(major, minor, patch, pre, dev, arch, name) def version_sort(path_entry): return path_entry.as_python.version_sort @@ -289,8 +301,7 @@ def version_sort(path_entry): paths = sorted(list(unnested), key=version_sort, reverse=True) return next(iter(p for p in paths if p is not None), None) - - def which(self, name) -> Optional[PathEntry]: + def which(self, name) -> PathEntry | None: """Search in this path for an executable. :param executable: The name of an executable to search for. @@ -313,7 +324,7 @@ class PythonVersion(FinderBaseModel): is_debug: bool = False version: Optional[Version] = None architecture: Optional[str] = None - comes_from: Optional['PathEntry'] = None + comes_from: Optional["PathEntry"] = None executable: Optional[Union[str, WindowsPath, Path]] = None company: Optional[str] = None name: Optional[str] = None @@ -325,9 +336,8 @@ class Config: include_private_attributes = True # keep_untouched = (cached_property,) - def __getattribute__(self, key): - result = super(PythonVersion, self).__getattribute__(key) + result = super().__getattribute__(key) if key in ["minor", "patch"] and result is None: executable = None if self.executable: @@ -340,7 +350,7 @@ def __getattribute__(self, key): instance_dict = self.parse_executable(executable) for k in instance_dict.keys(): try: - super(PythonVersion, self).__getattribute__(k) + super().__getattribute__(k) except AttributeError: continue else: @@ -349,7 +359,7 @@ def __getattribute__(self, key): return result @property - def version_sort(self) -> Tuple[int, int, Optional[int], int, int]: + def version_sort(self) -> tuple[int, int, int | None, int, int]: """ A tuple for sorting against other instances of the same class. @@ -379,7 +389,7 @@ def version_sort(self) -> Tuple[int, int, Optional[int], int, int]: ) @property - def version_tuple(self) -> Tuple[int, int, int, bool, bool, bool]: + def version_tuple(self) -> tuple[int, int, int, bool, bool, bool]: """ Provides a version tuple for using as a dictionary key. @@ -397,20 +407,20 @@ def version_tuple(self) -> Tuple[int, int, int, bool, bool, bool]: def matches( self, - major: Optional[int] = None, - minor: Optional[int] = None, - patch: Optional[int] = None, + major: int | None = None, + minor: int | None = None, + patch: int | None = None, pre: bool = False, dev: bool = False, - arch: Optional[str] = None, + arch: str | None = None, debug: bool = False, - python_name: Optional[str] = None, + python_name: str | None = None, ) -> bool: result = False if arch: own_arch = self.get_architecture() if arch.isdigit(): - arch = "{0}bit".format(arch) + arch = f"{arch}bit" if ( (major is None or self.major == major) and (minor is None or self.minor == minor) @@ -428,16 +438,16 @@ def matches( result = True return result - def as_major(self) -> "PythonVersion": + def as_major(self) -> PythonVersion: self.minor = None self.patch = None return self - def as_minor(self) -> "PythonVersion": + def as_minor(self) -> PythonVersion: self.patch = None return self - def as_dict(self) -> Dict[str, Union[int, bool, Version, None]]: + def as_dict(self) -> dict[str, int | bool | Version | None]: return { "major": self.major, "minor": self.minor, @@ -468,7 +478,7 @@ def update_metadata(self, metadata) -> None: setattr(self, key, metadata[key]) @classmethod - def parse(cls, version) -> Dict[str, Union[str, int, Version]]: + def parse(cls, version) -> dict[str, str | int | Version]: """ Parse a valid version string into a dictionary @@ -502,7 +512,9 @@ def get_architecture(self) -> str: return self.architecture @classmethod - def from_path(cls, path, name=None, ignore_unsupported=True, company=None) -> "PythonVersion": + def from_path( + cls, path, name=None, ignore_unsupported=True, company=None + ) -> PythonVersion: """ Parses a python version from a system path. @@ -548,7 +560,7 @@ def from_path(cls, path, name=None, ignore_unsupported=True, company=None) -> "P return cls(**instance_dict) @classmethod - def parse_executable(cls, path) -> Dict[str, Optional[Union[str, int, Version]]]: + def parse_executable(cls, path) -> dict[str, str | int | Version | None]: result_dict = {} result_version = None if path is None: @@ -567,7 +579,9 @@ def parse_executable(cls, path) -> Dict[str, Optional[Union[str, int, Version]]] return result_dict @classmethod - def from_windows_launcher(cls, launcher_entry, name=None, company=None) -> "PythonVersion": + def from_windows_launcher( + cls, launcher_entry, name=None, company=None + ) -> PythonVersion: """Create a new PythonVersion instance from a Windows Launcher Entry :param launcher_entry: A python launcher environment object. @@ -601,15 +615,17 @@ def from_windows_launcher(cls, launcher_entry, name=None, company=None) -> "Pyth return py_version @classmethod - def create(cls, **kwargs) -> "PythonVersion": + def create(cls, **kwargs) -> PythonVersion: if "architecture" in kwargs: if kwargs["architecture"].isdigit(): - kwargs["architecture"] = "{0}bit".format(kwargs["architecture"]) + kwargs["architecture"] = "{}bit".format(kwargs["architecture"]) return cls(**kwargs) class VersionMap(FinderBaseModel): - versions: DefaultDict[Tuple[int, Optional[int], Optional[int], bool, bool, bool], List[PathEntry]] = defaultdict(list) + versions: DefaultDict[ + Tuple[int, Optional[int], Optional[int], bool, bool, bool], List[PathEntry] + ] = defaultdict(list) class Config: validate_assignment = True @@ -632,9 +648,7 @@ def merge(self, target) -> None: self.versions[version] = entries else: current_entries = { - p.path - for p in self.versions[version] - if version in self.versions + p.path for p in self.versions[version] if version in self.versions } new_entries = {p.path for p in entries} new_entries -= current_entries diff --git a/src/pythonfinder/pythonfinder.py b/src/pythonfinder/pythonfinder.py index 9682b63..48bfd27 100644 --- a/src/pythonfinder/pythonfinder.py +++ b/src/pythonfinder/pythonfinder.py @@ -1,18 +1,16 @@ -import operator -import os -from typing import Any, Dict, List, Optional, Union +from __future__ import annotations -from pydantic import BaseModel +import operator +from typing import Any, Optional from .exceptions import InvalidPythonVersion -from .utils import Iterable, version_re +from .models.common import FinderBaseModel from .models.path import PathEntry, SystemPath from .models.python import PythonVersion -from .models.common import FinderBaseModel +from .utils import Iterable, version_re class Finder(FinderBaseModel): - path_prepend: Optional[str] = None system: bool = False global_search: bool = True @@ -41,20 +39,19 @@ def create_system_path(self) -> SystemPath: ignore_unsupported=self.ignore_unsupported, ) - def which(self, exe) -> Optional[PathEntry]: + def which(self, exe) -> PathEntry | None: return self.system_path.which(exe) @classmethod def parse_major( cls, - major: Optional[str], - minor: Optional[int]=None, - patch: Optional[int]=None, - pre: Optional[bool]=None, - dev: Optional[bool]=None, - arch: Optional[str]=None, - ) -> Dict[str, Any]: - + major: str | None, + minor: int | None = None, + patch: int | None = None, + pre: bool | None = None, + dev: bool | None = None, + arch: str | None = None, + ) -> dict[str, Any]: major_is_str = major and isinstance(major, str) is_num = ( major @@ -70,7 +67,7 @@ def parse_major( ) name = None if major and major_has_arch: - orig_string = "{0!s}".format(major) + orig_string = f"{major!s}" major, _, arch = major.rpartition("-") if arch: arch = arch.lower().lstrip("x").replace("bit", "") @@ -78,12 +75,12 @@ def parse_major( major = orig_string arch = None else: - arch = "{0}bit".format(arch) + arch = f"{arch}bit" try: version_dict = PythonVersion.parse(major) except (ValueError, InvalidPythonVersion): if name is None: - name = "{0!s}".format(major) + name = f"{major!s}" major = None version_dict = {} elif major and major[0].isalpha(): @@ -125,15 +122,15 @@ def parse_major( def find_python_version( self, - major: Optional[Union[str, int]] = None, - minor: Optional[int] = None, - patch: Optional[int] = None, - pre: Optional[bool] = None, - dev: Optional[bool] = None, - arch: Optional[str] = None, - name: Optional[str] = None, + major: str | int | None = None, + minor: int | None = None, + patch: int | None = None, + pre: bool | None = None, + dev: bool | None = None, + arch: str | None = None, + name: str | None = None, sort_by_path: bool = False, - ) -> Optional[PathEntry]: + ) -> PathEntry | None: """ Find the python version which corresponds most closely to the version requested. @@ -184,14 +181,14 @@ def find_python_version( def find_all_python_versions( self, - major: Optional[Union[str, int]] = None, - minor: Optional[int] = None, - patch: Optional[int] = None, - pre: Optional[bool] = None, - dev: Optional[bool] = None, - arch: Optional[str] = None, - name: Optional[str] = None, - ) -> List[PathEntry]: + major: str | int | None = None, + minor: int | None = None, + patch: int | None = None, + pre: bool | None = None, + dev: bool | None = None, + arch: str | None = None, + name: str | None = None, + ) -> list[PathEntry]: version_sort = operator.attrgetter("as_python.version_sort") python_version_dict = getattr(self.system_path, "python_version_dict", {}) if python_version_dict: diff --git a/src/pythonfinder/utils.py b/src/pythonfinder/utils.py index b85cd57..2916fc7 100644 --- a/src/pythonfinder/utils.py +++ b/src/pythonfinder/utils.py @@ -1,23 +1,21 @@ from __future__ import annotations -import io + import itertools import os import re import subprocess +from builtins import TimeoutError from collections import OrderedDict from collections.abc import Iterable, Sequence from fnmatch import fnmatch -from threading import Timer from pathlib import Path -from builtins import TimeoutError -from typing import Any, Dict, Iterator, List, Optional, Union +from typing import Any, Iterator -from packaging.version import Version, InvalidVersion +from packaging.version import InvalidVersion, Version -from .environment import PYENV_ROOT, SUBPROCESS_TIMEOUT +from .environment import PYENV_ROOT from .exceptions import InvalidPythonVersion - version_re_str = ( r"(?P\d+)(?:\.(?P\d+))?(?:\.(?P(?<=\.)[0-9]+))?\.?" r"(?:(?P[abc]|rc|dev)(?:(?P\d+(?:\.\d+)*))?)" @@ -46,12 +44,12 @@ filter(None, os.environ.get("PATHEXT", "").split(os.pathsep)) ) PY_MATCH_STR = ( - r"((?P{0})(?:\d?(?:\.\d[cpm]{{0,3}}))?(?:-?[\d\.]+)*(?!w))".format( + r"((?P{})(?:\d?(?:\.\d[cpm]{{,3}}))?(?:-?[\d\.]+)*(?!w))".format( "|".join(PYTHON_IMPLEMENTATIONS) ) ) -EXE_MATCH_STR = r"{0}(?:\.(?P{1}))?".format(PY_MATCH_STR, "|".join(KNOWN_EXTS)) -RE_MATCHER = re.compile(r"({0}|{1})".format(version_re_str, PY_MATCH_STR)) +EXE_MATCH_STR = r"{}(?:\.(?P{}))?".format(PY_MATCH_STR, "|".join(KNOWN_EXTS)) +RE_MATCHER = re.compile(rf"({version_re_str}|{PY_MATCH_STR})") EXE_MATCHER = re.compile(EXE_MATCH_STR) RULES_BASE = [ "*{0}", @@ -66,9 +64,7 @@ MATCH_RULES = [] for rule in RULES: - MATCH_RULES.extend( - ["{0}.{1}".format(rule, ext) if ext else "{0}".format(rule) for ext in KNOWN_EXTS] - ) + MATCH_RULES.extend([f"{rule}.{ext}" if ext else f"{rule}" for ext in KNOWN_EXTS]) def get_python_version(path) -> str: @@ -86,7 +82,7 @@ def get_python_version(path) -> str: "shell": False, } c = subprocess.Popen(version_cmd, **subprocess_kwargs) - timer = Timer(SUBPROCESS_TIMEOUT, c.kill) + try: out, _ = c.communicate() except (SystemExit, KeyboardInterrupt, TimeoutError): @@ -100,7 +96,7 @@ def get_python_version(path) -> str: return out.strip() -def parse_python_version(version_str: str) -> Dict[str, Union[str, int, Version]]: +def parse_python_version(version_str: str) -> dict[str, str | int | Version]: from packaging.version import parse as parse_version is_debug = False @@ -130,7 +126,7 @@ def parse_python_version(version_str: str) -> Dict[str, Union[str, int, Version] pre = "" if v_dict.get("prerel") and v_dict.get("prerelversion"): pre = v_dict.pop("prerel") - pre = "{0}{1}".format(pre, v_dict.pop("prerelversion")) + pre = "{}{}".format(pre, v_dict.pop("prerelversion")) v_dict["pre"] = pre keys = ["major", "minor", "patch", "pre", "postdev", "post", "dev"] values = [v_dict.get(val) for val in keys] @@ -202,7 +198,7 @@ def path_is_python(path: Path) -> bool: return path_is_executable(path) and looks_like_python(path.name) -def guess_company(path: str) -> Optional[str]: +def guess_company(path: str) -> str | None: """Given a path to python, guess the company who created it :param str path: The path to guess about @@ -230,7 +226,7 @@ def path_is_pythoncore(path: str) -> bool: return False -def ensure_path(path: Union[Path, str]) -> Path: +def ensure_path(path: Path | str) -> Path: """ Given a path (either a string or a Path object), expand variables and return a Path object. @@ -259,7 +255,7 @@ def normalize_path(path: str) -> str: ) -def filter_pythons(path: Union[str, Path]) -> Union[Iterable, Path]: +def filter_pythons(path: str | Path) -> Iterable | Path: """Return all valid pythons in a given path""" if not isinstance(path, Path): path = Path(str(path)) @@ -285,20 +281,20 @@ def unnest(item) -> Iterable[Any]: yield target -def parse_pyenv_version_order(filename="version") -> List[str]: +def parse_pyenv_version_order(filename="version") -> list[str]: version_order_file = normalize_path(os.path.join(PYENV_ROOT, filename)) if os.path.exists(version_order_file) and os.path.isfile(version_order_file): - with io.open(version_order_file, encoding="utf-8") as fh: + with open(version_order_file, encoding="utf-8") as fh: contents = fh.read() version_order = [v for v in contents.splitlines()] return version_order return [] -def parse_asdf_version_order(filename: str=".tool-versions") -> List[str]: +def parse_asdf_version_order(filename: str = ".tool-versions") -> list[str]: version_order_file = normalize_path(os.path.join("~", filename)) if os.path.exists(version_order_file) and os.path.isfile(version_order_file): - with io.open(version_order_file, encoding="utf-8") as fh: + with open(version_order_file, encoding="utf-8") as fh: contents = fh.read() python_section = next( iter(line for line in contents.splitlines() if line.startswith("python")), @@ -335,7 +331,7 @@ def split_version_and_name( major = major name = None else: - name = "{0!s}".format(major) + name = f"{major!s}" major = None return (major, minor, patch, name) diff --git a/tests/conftest.py b/tests/conftest.py index 0e1ed53..0dadf5d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,12 +7,9 @@ from collections import namedtuple from pathlib import Path -import click -import click.testing import pytest import pythonfinder -from pythonfinder import environment from .testutils import ( cd, @@ -103,7 +100,6 @@ def no_pyenv_root_envvar(monkeypatch): @pytest.fixture def isolated_envdir(create_tmpdir): - runner = click.testing.CliRunner() fake_root_path = create_tmpdir() fake_root = fake_root_path.as_posix() set_write_bit(fake_root) @@ -157,7 +153,7 @@ def setup_plugin(name): this = Path(__file__).absolute().parent plugin_dir = this / "test_artifacts" / name plugin_uri = plugin_dir.as_uri() - if not "file:///" in plugin_uri and "file:/" in plugin_uri: + if "file:///" not in plugin_uri and "file:/" in plugin_uri: plugin_uri = plugin_uri.replace("file:/", "file:///") out = subprocess.check_output(["git", "clone", plugin_uri, Path(target).as_posix()]) print(out, file=sys.stderr) @@ -230,7 +226,7 @@ def setup_asdf(home_dir): @pytest.fixture def setup_pythons(isolated_envdir, monkeypatch): - with monkeypatch.context() as m: + with monkeypatch.context(): setup_plugin("asdf") setup_plugin("pyenv") asdf_dict = setup_asdf(isolated_envdir) @@ -306,7 +302,7 @@ def get_windows_python_versions(): versions = [] for line in out.splitlines(): line = line.strip() - if line and not "Installed Pythons found" in line: + if line and "Installed Pythons found" not in line: version, path = line.split("\t") version = version.strip().lstrip("-") path = normalize_path(path.strip().strip('"')) diff --git a/tests/test_python.py b/tests/test_python.py index 92b1c03..ee4a7a7 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -1,24 +1,15 @@ from __future__ import annotations import functools -import importlib import os import sys + import pytest from packaging.version import Version -import pythonfinder -from pythonfinder import utils, environment -from pythonfinder.pythonfinder import Finder +from pythonfinder import utils from pythonfinder.models.python import PythonFinder, PythonVersion -from .testutils import ( - is_in_ospath, - normalize_path, - normalized_match, - print_python_versions, -) - @pytest.mark.skipif(sys.version_info < (3,), reason="Must run on Python 3") def test_python_versions(monkeypatch, special_character_python): diff --git a/tests/test_utils.py b/tests/test_utils.py index 7d21afe..5908480 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -17,13 +17,7 @@ def _get_python_versions(): finder = Finder(global_search=True, system=False, ignore_unsupported=True) pythons = finder.find_all_python_versions() - for v in pythons: - py = v.py_version - comes_from = getattr(py, "comes_from", None) - if comes_from is not None: - comes_from_path = getattr(comes_from, "path", v.path) - else: - comes_from_path = v.path + return sorted(list(pythons)) diff --git a/tests/testutils.py b/tests/testutils.py index 46bbf52..ac14581 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -9,6 +9,7 @@ import warnings from contextlib import contextmanager from pathlib import Path +from typing import AnyStr import click