Skip to content

Commit

Permalink
Try to normalize on using pathlib basics and storing string paths in …
Browse files Browse the repository at this point in the history
…dataclass to compare against.
  • Loading branch information
matteius committed Jan 24, 2024
1 parent 5dc59cd commit c3746ef
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 133 deletions.
24 changes: 2 additions & 22 deletions src/pythonfinder/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,14 @@

import os
import platform
import re
import shutil
import sys


def possibly_convert_to_windows_style_path(path):
if not isinstance(path, str):
path = str(path)
# Check if the path is in Unix-style (Git Bash)
if os.name != "nt":
return path
if os.path.exists(path):
return path
match = re.match(r"[/\\]([a-zA-Z])[/\\](.*)", path)
if match is None:
return path
drive, rest_of_path = match.groups()
rest_of_path = rest_of_path.replace("/", "\\")
revised_path = f"{drive.upper()}:\\{rest_of_path}"
if os.path.exists(revised_path):
return revised_path
return path

from pathlib import Path

PYENV_ROOT = os.path.expanduser(
os.path.expandvars(os.environ.get("PYENV_ROOT", "~/.pyenv"))
)
PYENV_ROOT = possibly_convert_to_windows_style_path(PYENV_ROOT)
PYENV_ROOT = Path(PYENV_ROOT)
PYENV_INSTALLED = shutil.which("pyenv") is not None
ASDF_DATA_DIR = os.path.expanduser(
os.path.expandvars(os.environ.get("ASDF_DATA_DIR", "~/.asdf"))
Expand Down
141 changes: 74 additions & 67 deletions src/pythonfinder/models/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
dedup,
ensure_path,
is_in_path,
normalize_path,
parse_asdf_version_order,
parse_pyenv_version_order,
resolve_path,
split_version_and_name,
)
from .mixins import PathEntry
Expand Down Expand Up @@ -84,7 +84,7 @@ def __post_init__(self):
self.python_version_dict = defaultdict(list)
self.pyenv_finder = self.pyenv_finder or None
self.asdf_finder = self.asdf_finder or None
self.path_order = self.path_order or []
self.path_order = [str(p) for p in self.path_order] or []
self.finders_dict = self.finders_dict or {}

# The part with 'paths' seems to be setting up 'executables'
Expand All @@ -107,11 +107,11 @@ def finders(self) -> list[str]:

@staticmethod
def check_for_pyenv():
return PYENV_INSTALLED or os.path.exists(normalize_path(PYENV_ROOT))
return PYENV_INSTALLED or os.path.exists(resolve_path(PYENV_ROOT))

@staticmethod
def check_for_asdf():
return ASDF_INSTALLED or os.path.exists(normalize_path(ASDF_DATA_DIR))
return ASDF_INSTALLED or os.path.exists(resolve_path(ASDF_DATA_DIR))

@property
def executables(self) -> list[PathEntry]:
Expand Down Expand Up @@ -155,57 +155,59 @@ def version_dict(self) -> DefaultDict[tuple, list[PathEntry]]:
self.version_dict_tracking[version].append(entry)
return self.version_dict_tracking

def _handle_virtualenv_and_system_paths(self):
venv = os.environ.get("VIRTUAL_ENV")
if venv:
venv_path = Path(venv).resolve()
bin_dir = "Scripts" if os.name == "nt" else "bin"
venv_bin_path = venv_path / bin_dir
if venv_bin_path.exists() and (self.system or self.global_search):
self.path_order = [str(venv_bin_path), *self.path_order]
self.paths[str(venv_bin_path)] = self.get_path(venv_bin_path)

if self.system:
syspath_bin = Path(sys.executable).resolve().parent
if (syspath_bin / bin_dir).exists():
syspath_bin = syspath_bin / bin_dir
if str(syspath_bin) not in self.path_order:
self.path_order = [str(syspath_bin), *self.path_order]
self.paths[str(syspath_bin)] = PathEntry.create(
path=syspath_bin, is_root=True, only_python=False
)

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)
path_order += os.environ["PATH"].split(os.pathsep)
path_order = list(dedup(path_order))
path_instances = [ensure_path(p.strip('"')) for p in path_order]
path_instances = [
Path(p.strip('"')).resolve()
for p in path_order
if exists_and_is_accessible(Path(p.strip('"')).resolve())
]

# Update paths with PathEntry objects
self.paths.update(
{
p.as_posix(): PathEntry.create(
path=p.absolute(), is_root=True, only_python=self.only_python
str(p): PathEntry.create(
path=p, is_root=True, only_python=self.only_python
)
for p in path_instances
if exists_and_is_accessible(p)
}
)
self.path_order = [
p.as_posix() for p in path_instances if exists_and_is_accessible(p)
]
#: slice in pyenv
if self.check_for_pyenv() and "pyenv" not in self.finders:
self._setup_pyenv()
#: slice in asdf
if self.check_for_asdf() and "asdf" not in self.finders:
self._setup_asdf()
venv = os.environ.get("VIRTUAL_ENV")
if venv:
venv = ensure_path(venv)
if os.name == "nt":
bin_dir = "Scripts"
else:
bin_dir = "bin"
if venv and (self.system or self.global_search):
path_order = [(venv / bin_dir).as_posix(), *self.path_order]
self.path_order = path_order
self.paths[venv] = self.get_path(venv.joinpath(bin_dir))
if self.system:
syspath = Path(sys.executable)
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]
self.paths[syspath_bin] = PathEntry.create(
path=syspath_bin, is_root=True, only_python=False
)
self.path_order = path_order

# Update path_order to use absolute paths
self.path_order = [str(p) for p in path_instances]

# Handle virtual environment and system paths
self._handle_virtualenv_and_system_paths()

return self

def _get_last_instance(self, path) -> int:
reversed_paths = reversed(self.path_order)
paths = [normalize_path(p) for p in reversed_paths]
normalized_target = normalize_path(path)
paths = [resolve_path(p) for p in reversed_paths]
normalized_target = resolve_path(path)
last_instance = next(iter(p for p in paths if normalized_target in p), None)
if last_instance is None:
raise ValueError(f"No instance found on path for target: {path!s}")
Expand All @@ -222,7 +224,7 @@ def _slice_in_paths(self, start_idx, paths) -> SystemPath:
else:
before_path = self.path_order[: start_idx + 1]
after_path = self.path_order[start_idx + 2 :]
path_order = before_path + [p.as_posix() for p in paths] + after_path
path_order = before_path + [str(p) for p in paths] + after_path
self.path_order = path_order
return self

Expand All @@ -231,23 +233,23 @@ def _remove_shims(self):
new_order = []
for current_path in path_copy:
if not current_path.endswith("shims"):
normalized = normalize_path(current_path)
normalized = resolve_path(current_path)
new_order.append(normalized)
new_order = [ensure_path(p).as_posix() for p in new_order]
self.path_order = new_order

def _remove_path(self, path) -> SystemPath:
path_copy = [p for p in reversed(self.path_order[:])]
new_order = []
target = normalize_path(path)
path_map = {normalize_path(pth): pth for pth in self.paths.keys()}
target = resolve_path(path)
path_map = {resolve_path(pth): pth for pth in self.paths.keys()}
if target in path_map:
del self.paths[path_map[target]]
for current_path in path_copy:
normalized = normalize_path(current_path)
normalized = resolve_path(current_path)
if normalized != target:
new_order.append(normalized)
new_order = [ensure_path(p).as_posix() for p in reversed(new_order)]
new_order = [str(p) for p in reversed(new_order)]
self.path_order = new_order
return self

Expand All @@ -256,28 +258,31 @@ def _setup_asdf(self) -> SystemPath:
return self

os_path = os.environ["PATH"].split(os.pathsep)
asdf_data_dir = Path(ASDF_DATA_DIR)
asdf_finder = PythonFinder.create(
root=ASDF_DATA_DIR,
root=asdf_data_dir,
ignore_unsupported=True,
sort_function=parse_asdf_version_order,
version_glob_path="installs/python/*",
)
asdf_index = None
try:
asdf_index = self._get_last_instance(ASDF_DATA_DIR)
asdf_index = self._get_last_instance(asdf_data_dir)
except ValueError:
asdf_index = 0 if is_in_path(next(iter(os_path), ""), ASDF_DATA_DIR) else -1
asdf_index = 0 if is_in_path(next(iter(os_path), ""), asdf_data_dir) else -1
if asdf_index is None:
# we are in a virtualenv without global pyenv on the path, so we should
# not write pyenv to the path here
return self
# * These are the root paths for the finder
_ = [p for p in asdf_finder.roots]
self._slice_in_paths(asdf_index, [asdf_finder.root])
self.paths[asdf_finder.root] = asdf_finder
self.paths.update(asdf_finder.roots)
self._slice_in_paths(asdf_index, [str(asdf_finder.root)])
self.paths[str(asdf_finder.root)] = asdf_finder
self.paths.update(
{str(root): asdf_finder.roots[root] for root in asdf_finder.roots}
)
self.asdf_finder = asdf_finder
self._remove_path(normalize_path(os.path.join(ASDF_DATA_DIR, "shims")))
self._remove_path(asdf_data_dir / "shims")
self._register_finder("asdf", asdf_finder)
return self

Expand All @@ -286,26 +291,28 @@ def _setup_pyenv(self) -> SystemPath:
return self

os_path = os.environ["PATH"].split(os.pathsep)

pyenv_root = Path(PYENV_ROOT)
pyenv_finder = PythonFinder.create(
root=PYENV_ROOT,
root=pyenv_root,
sort_function=parse_pyenv_version_order,
version_glob_path="versions/*",
ignore_unsupported=self.ignore_unsupported,
)
try:
pyenv_index = self._get_last_instance(PYENV_ROOT)
pyenv_index = self._get_last_instance(pyenv_root)
except ValueError:
pyenv_index = 0 if is_in_path(next(iter(os_path), ""), PYENV_ROOT) else -1
pyenv_index = 0 if is_in_path(next(iter(os_path), ""), pyenv_root) else -1
if pyenv_index is None:
# we are in a virtualenv without global pyenv on the path, so we should
# not write pyenv to the path here
return self
# * These are the root paths for the finder
_ = [p for p in pyenv_finder.roots]
self._slice_in_paths(pyenv_index, [pyenv_finder.root])
self.paths[pyenv_finder.root] = pyenv_finder
self.paths.update(pyenv_finder.roots)
self._slice_in_paths(pyenv_index, [str(pyenv_finder.root)])
self.paths[str(pyenv_finder.root)] = pyenv_finder
self.paths.update(
{str(root): pyenv_finder.roots[root] for root in pyenv_finder.roots}
)
self.pyenv_finder = pyenv_finder
self._remove_shims()
self._register_finder("pyenv", pyenv_finder)
Expand All @@ -314,15 +321,15 @@ def _setup_pyenv(self) -> SystemPath:
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)
_path = self.paths.get(path)
path_str = path if isinstance(path, str) else str(path.absolute())
_path = self.paths.get(path_str)
if not _path:
_path = self.paths.get(path.as_posix())
if not _path and path.as_posix() in self.path_order and path.exists():
_path = self.paths.get(path_str)
if not _path and path_str in self.path_order and path.exists():
_path = PathEntry.create(
path=path.absolute(), is_root=True, only_python=self.only_python
)
self.paths[path.as_posix()] = _path
self.paths[path_str] = _path
if not _path:
raise ValueError(f"Path not found or generated: {path!r}")
return _path
Expand Down Expand Up @@ -490,7 +497,7 @@ def create(
if global_search:
if "PATH" in os.environ:
paths = os.environ["PATH"].split(os.pathsep)
path_order = []
path_order = [str(path)]
if path:
path_order = [path]
path_instance = ensure_path(path)
Expand Down
2 changes: 1 addition & 1 deletion src/pythonfinder/pythonfinder.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ def find_python_version(
dev=dev,
arch=arch,
name=name,
sort_by_path=self.sort_by_path,
sort_by_path=sort_by_path,
)

def find_all_python_versions(
Expand Down
37 changes: 22 additions & 15 deletions src/pythonfinder/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from packaging.version import InvalidVersion, Version

from .environment import PYENV_ROOT, possibly_convert_to_windows_style_path
from .environment import PYENV_ROOT
from .exceptions import InvalidPythonVersion

version_re_str = (
Expand Down Expand Up @@ -234,19 +234,27 @@ def ensure_path(path: Path | str) -> Path:
:type path: str or :class:`~pathlib.Path`
:return: A fully expanded Path object.
"""
path = possibly_convert_to_windows_style_path(path)
if isinstance(path, Path):
return path
path = Path(os.path.expandvars(path))
return path.absolute()
return path.resolve()
# Expand environment variables and user tilde in the path
expanded_path = os.path.expandvars(os.path.expanduser(path))
return Path(expanded_path).resolve()


def normalize_path(path: str) -> str:
return os.path.normpath(
os.path.normcase(
os.path.abspath(os.path.expandvars(os.path.expanduser(str(path))))
)
)
def resolve_path(path: Path | str) -> Path:
"""
Resolves the path to an absolute path, expanding user variables and environment variables.
"""
# Convert to Path object if it's a string
if isinstance(path, str):
path = Path(path)

# Expand user and variables
path = path.expanduser()
path = Path(os.path.expandvars(str(path)))

# Resolve to absolute path
return path.resolve()


def filter_pythons(path: str | Path) -> Iterable | Path:
Expand Down Expand Up @@ -276,7 +284,7 @@ def unnest(item) -> Iterable[Any]:


def parse_pyenv_version_order(filename="version") -> list[str]:
version_order_file = normalize_path(os.path.join(PYENV_ROOT, filename))
version_order_file = resolve_path(os.path.join(PYENV_ROOT, filename))
if os.path.exists(version_order_file) and os.path.isfile(version_order_file):
with open(version_order_file, encoding="utf-8") as fh:
contents = fh.read()
Expand All @@ -286,7 +294,7 @@ def parse_pyenv_version_order(filename="version") -> list[str]:


def parse_asdf_version_order(filename: str = ".tool-versions") -> list[str]:
version_order_file = normalize_path(os.path.join("~", filename))
version_order_file = resolve_path(os.path.join("~", filename))
if os.path.exists(version_order_file) and os.path.isfile(version_order_file):
with open(version_order_file, encoding="utf-8") as fh:
contents = fh.read()
Expand Down Expand Up @@ -330,9 +338,8 @@ def split_version_and_name(
return (major, minor, patch, name)


# TODO: Reimplement in vistir
def is_in_path(path, parent):
return normalize_path(str(path)).startswith(normalize_path(str(parent)))
return resolve_path(str(path)).startswith(resolve_path(str(parent)))


def expand_paths(path, only_python=True) -> Iterator:
Expand Down
Loading

0 comments on commit c3746ef

Please sign in to comment.