Skip to content

Commit 4ffafc2

Browse files
authored
Refactor pythonfinder for improved efficiency and PEP 514 support (#6360)
* Complete python finder 3.x rewrite (with new tests-and updated docs). * Support py command on windows as well * PR feedback from testing * More conversions to pathlib to help with the pythonfinder integration * Fix issue related to python bug 35144 now that we dropped python3.8 * fix ruff
1 parent 2989262 commit 4ffafc2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+3398
-2651
lines changed

news/6360.feature.rst

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Refactor pythonfinder for improved efficiency and PEP 514 support
2+
3+
## Summary
4+
This PR completely refactors the pythonfinder module to improve efficiency, reduce logical errors, and fix support for PEP 514 (Python registration in the Windows registry). The refactoring replaces the complex object hierarchy with a more modular, composition-based approach that is easier to maintain and extend.
5+
6+
## Motivation
7+
The original pythonfinder implementation had several issues:
8+
* Complex object wrapping with paths as objects, leading to excessive recursion
9+
* Tight coupling between classes making the code difficult to follow and maintain
10+
* Broken Windows registry support (PEP 514)
11+
* Performance issues due to redundant path scanning and inefficient caching
12+
13+
## Changes
14+
* **Architecture**: Replaced inheritance-heavy design with a composition-based approach using specialized finders
15+
* **Data Model**: Simplified the data model with a clean ``PythonInfo`` dataclass
16+
* **Windows Support**: Implemented proper PEP 514 support for Windows registry
17+
* **Performance**: Improved caching and reduced redundant operations
18+
* **Error Handling**: Added more specific exceptions and better error handling
19+
20+
## Features
21+
The refactored implementation continues to support all required features:
22+
* System and user PATH searches
23+
* pyenv installations
24+
* asdf installations
25+
* Windows registry (PEP 514) - now working correctly

pipenv/__init__.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
import os
33
import sys
44
import warnings
5+
from pathlib import Path
56

67
# This has to come before imports of pipenv
7-
PIPENV_ROOT = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
8-
PIP_ROOT = os.sep.join([PIPENV_ROOT, "patched", "pip"])
9-
sys.path.insert(0, PIPENV_ROOT)
8+
PIPENV_ROOT = Path(__file__).resolve().parent.absolute()
9+
PIP_ROOT = str(PIPENV_ROOT / "patched" / "pip")
10+
sys.path.insert(0, str(PIPENV_ROOT))
1011
sys.path.insert(0, PIP_ROOT)
1112

1213
# Load patched pip instead of system pip
@@ -15,9 +16,10 @@
1516

1617
def _ensure_modules():
1718
# Ensure when pip gets invoked it uses our patched version
19+
location = Path(__file__).parent / "patched" / "pip" / "__init__.py"
1820
spec = importlib.util.spec_from_file_location(
1921
"pip",
20-
location=os.path.join(os.path.dirname(__file__), "patched", "pip", "__init__.py"),
22+
location=str(location),
2123
)
2224
pip = importlib.util.module_from_spec(spec)
2325
sys.modules["pip"] = pip

pipenv/cli/command.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import sys
3+
from pathlib import Path
34

45
from pipenv import environments
56
from pipenv.__version__ import __version__
@@ -98,8 +99,8 @@ def cli(
9899

99100
if man:
100101
if system_which("man"):
101-
path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "pipenv.1")
102-
os.execle(system_which("man"), "man", path, os.environ)
102+
path = Path(__file__).parent.parent / "pipenv.1"
103+
os.execle(system_which("man"), "man", str(path), os.environ)
103104
return 0
104105
else:
105106
err.print(
@@ -393,8 +394,8 @@ def shell(state, fancy=False, shell_args=None, anyway=False, quiet=False):
393394
# Use fancy mode for Windows or pwsh on *nix.
394395
if (
395396
os.name == "nt"
396-
or (os.environ.get("PIPENV_SHELL") or "").split(os.path.sep)[-1] == "pwsh"
397-
or (os.environ.get("SHELL") or "").split(os.path.sep)[-1] == "pwsh"
397+
or Path(os.environ.get("PIPENV_SHELL") or "").name == "pwsh"
398+
or Path(os.environ.get("SHELL") or "").name == "pwsh"
398399
):
399400
fancy = True
400401
do_shell(
@@ -624,7 +625,7 @@ def run_open(state, module, *args, **kwargs):
624625
console.print("Module not found!", style="red")
625626
sys.exit(1)
626627
if "__init__.py" in c.stdout:
627-
p = os.path.dirname(c.stdout.strip().rstrip("cdo"))
628+
p = Path(c.stdout.strip().rstrip("cdo")).parent
628629
else:
629630
p = c.stdout.strip().rstrip("cdo")
630631
console.print(f"Opening {p!r} in your EDITOR.", style="bold")

pipenv/cli/options.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import os
21
import re
2+
from pathlib import Path
33

44
from pipenv.project import Project
55
from pipenv.utils import console, err
@@ -460,7 +460,8 @@ def validate_python_path(ctx, param, value):
460460
# the path or an absolute path. To report errors as early as possible
461461
# we'll report absolute paths which do not exist:
462462
if isinstance(value, (str, bytes)):
463-
if os.path.isabs(value) and not os.path.isfile(value):
463+
path = Path(value)
464+
if path.is_absolute() and not path.is_file():
464465
raise BadParameter(f"Expected Python at path {value} does not exist")
465466
return value
466467

pipenv/environment.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def python_version(self) -> str | None:
113113
@property
114114
def python_info(self) -> dict[str, str]:
115115
include_dir = self.prefix / "include"
116-
if not os.path.exists(include_dir):
116+
if not include_dir.exists():
117117
include_dirs = self.get_include_path()
118118
if include_dirs:
119119
include_path = include_dirs.get(
@@ -130,15 +130,17 @@ def python_info(self) -> dict[str, str]:
130130
return {}
131131

132132
def _replace_parent_version(self, path: str, replace_version: str) -> str:
133-
if not os.path.exists(path):
134-
base, leaf = os.path.split(path)
135-
base, parent = os.path.split(base)
136-
leaf = os.path.join(parent, leaf).replace(
133+
path_obj = Path(path)
134+
if not path_obj.exists():
135+
parent = path_obj.parent
136+
grandparent = parent.parent
137+
leaf = f"{parent.name}/{path_obj.name}"
138+
leaf = leaf.replace(
137139
replace_version,
138140
self.python_info.get("py_version_short", get_python_version()),
139141
)
140-
return os.path.join(base, leaf)
141-
return path
142+
return str(grandparent / leaf)
143+
return str(path_obj)
142144

143145
@cached_property
144146
def install_scheme(self):

pipenv/environments.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import glob
22
import os
3-
import pathlib
43
import re
54
import sys
5+
from pathlib import Path
66

77
from pipenv.patched.pip._vendor.platformdirs import user_cache_dir
88
from pipenv.utils.fileutils import normalize_drive
@@ -53,18 +53,18 @@ def get_from_env(arg, prefix="PIPENV", check_for_negation=True, default=None):
5353
def normalize_pipfile_path(p):
5454
if p is None:
5555
return None
56-
loc = pathlib.Path(p)
56+
loc = Path(p)
5757
try:
5858
loc = loc.resolve()
5959
except OSError:
6060
loc = loc.absolute()
6161
# Recase the path properly on Windows. From https://stackoverflow.com/a/35229734/5043728
6262
if os.name == "nt":
6363
matches = glob.glob(re.sub(r"([^:/\\])(?=[/\\]|$)", r"[\1]", str(loc)))
64-
path_str = matches and matches[0] or str(loc)
64+
path = Path(matches[0] if matches else str(loc))
6565
else:
66-
path_str = str(loc)
67-
return normalize_drive(os.path.abspath(path_str))
66+
path = loc
67+
return normalize_drive(str(path.absolute()))
6868

6969

7070
# HACK: Prevent invalid shebangs with Homebrew-installed Python:
@@ -433,8 +433,8 @@ def is_in_virtualenv() -> bool:
433433
:rtype: bool
434434
"""
435435

436-
pipenv_active = os.environ.get("PIPENV_ACTIVE")
437-
virtual_env = bool(os.environ.get("VIRTUAL_ENV"))
436+
pipenv_active = is_env_truthy("PIPENV_ACTIVE")
437+
virtual_env = bool(os.getenv("VIRTUAL_ENV"))
438438
ignore_virtualenvs = bool(get_from_env("IGNORE_VIRTUALENVS"))
439439
return virtual_env and not (pipenv_active or ignore_virtualenvs)
440440

pipenv/help.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def get_pipenv_diagnostics(project):
3232
finder = pythonfinder.Finder(system=False, global_search=True)
3333
python_paths = finder.find_all_python_versions()
3434
for python in python_paths:
35-
print(f" - `{python.py_version.version}`: `{python.path}`")
35+
print(f" - `{python.version_str}`: `{python.path}`")
3636

3737
print("")
3838
print("PEP 508 Information:")

0 commit comments

Comments
 (0)