Skip to content

Commit f18c876

Browse files
authored
Convert pythonfinder from pydantic to vanilla dataclasses (#157)
* Initial pass at converting off pydantic * Correction * Cosnsitent API change * Try to normalize on using pathlib basics and storing string paths in dataclass to compare against. * Lessons fromt testing this code against the pipenv integrations--simplifying assumptions. * Port over latest changes from integration testing with pipenv -- completely remove the posix path conversions. * Upgrade from deprecated CI module. * Address PR findings * Add news fragment
1 parent dc7bd85 commit f18c876

File tree

11 files changed

+318
-425
lines changed

11 files changed

+318
-425
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
os: [ubuntu-latest, macOS-latest, windows-latest]
1818

1919
steps:
20-
- uses: actions/checkout@v3
20+
- uses: actions/checkout@v4
2121
with:
2222
submodules: true
2323
- uses: actions/setup-python@v4

news/157.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* Convert away from pydantic to reduce complexity; simplify the path manipulation logics to use pathlib.

pyproject.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ dynamic = ["version"]
3535
requires-python = ">=3.8"
3636
dependencies = [
3737
"packaging>=22.0",
38-
"pydantic>=1.10.7,<2",
3938
]
4039

4140
[project.optional-dependencies]
@@ -136,7 +135,6 @@ unfixable = [
136135

137136
[tool.ruff.flake8-type-checking]
138137
runtime-evaluated-base-classes = [
139-
"pydantic.BaseModel",
140138
"pythonfinder.models.common.FinderBaseModel",
141139
"pythonfinder.models.mixins.PathEntry",
142140
]

src/pythonfinder/environment.py

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,14 @@
22

33
import os
44
import platform
5-
import re
65
import shutil
76
import sys
8-
9-
10-
def possibly_convert_to_windows_style_path(path):
11-
if not isinstance(path, str):
12-
path = str(path)
13-
# Check if the path is in Unix-style (Git Bash)
14-
if os.name != "nt":
15-
return path
16-
if os.path.exists(path):
17-
return path
18-
match = re.match(r"[/\\]([a-zA-Z])[/\\](.*)", path)
19-
if match is None:
20-
return path
21-
drive, rest_of_path = match.groups()
22-
rest_of_path = rest_of_path.replace("/", "\\")
23-
revised_path = f"{drive.upper()}:\\{rest_of_path}"
24-
if os.path.exists(revised_path):
25-
return revised_path
26-
return path
27-
7+
from pathlib import Path
288

299
PYENV_ROOT = os.path.expanduser(
3010
os.path.expandvars(os.environ.get("PYENV_ROOT", "~/.pyenv"))
3111
)
32-
PYENV_ROOT = possibly_convert_to_windows_style_path(PYENV_ROOT)
12+
PYENV_ROOT = Path(PYENV_ROOT)
3313
PYENV_INSTALLED = shutil.which("pyenv") is not None
3414
ASDF_DATA_DIR = os.path.expanduser(
3515
os.path.expandvars(os.environ.get("ASDF_DATA_DIR", "~/.asdf"))

src/pythonfinder/models/common.py

Lines changed: 0 additions & 26 deletions
This file was deleted.

src/pythonfinder/models/mixins.py

Lines changed: 57 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
from __future__ import annotations
22

3+
import dataclasses
34
import os
45
from collections import defaultdict
5-
from pathlib import Path
6+
from dataclasses import field
67
from typing import (
78
TYPE_CHECKING,
89
Any,
9-
Dict,
1010
Generator,
1111
Iterator,
12-
Optional,
1312
)
1413

15-
from pydantic import BaseModel, Field, validator
16-
1714
from ..exceptions import InvalidPythonVersion
1815
from ..utils import (
1916
KNOWN_EXTS,
@@ -25,51 +22,47 @@
2522
)
2623

2724
if TYPE_CHECKING:
28-
from pythonfinder.models.python import PythonVersion
29-
30-
31-
class PathEntry(BaseModel):
32-
is_root: bool = Field(default=False, order=False)
33-
name: Optional[str] = None
34-
path: Optional[Path] = None
35-
children_ref: Optional[Any] = Field(default_factory=lambda: dict())
36-
only_python: Optional[bool] = False
37-
py_version_ref: Optional[Any] = None
38-
pythons_ref: Optional[Dict[Any, Any]] = defaultdict(lambda: None)
39-
is_dir_ref: Optional[bool] = None
40-
is_executable_ref: Optional[bool] = None
41-
is_python_ref: Optional[bool] = None
42-
43-
class Config:
44-
validate_assignment = True
45-
arbitrary_types_allowed = True
46-
allow_mutation = True
47-
include_private_attributes = True
48-
49-
@validator("children", pre=True, always=True, check_fields=False)
50-
def set_children(cls, v, values, **kwargs):
51-
path = values.get("path")
52-
if path:
53-
values["name"] = path.name
54-
return v or cls()._gen_children()
25+
from pathlib import Path
26+
27+
from .python import PythonVersion
28+
29+
30+
@dataclasses.dataclass(unsafe_hash=True)
31+
class PathEntry:
32+
is_root: bool = False
33+
name: str | None = None
34+
path: Path | None = None
35+
children_ref: dict[str, Any] = field(default_factory=dict)
36+
only_python: bool | None = False
37+
py_version_ref: Any | None = None
38+
pythons_ref: dict[str, Any] | None = field(
39+
default_factory=lambda: defaultdict(lambda: None)
40+
)
41+
is_dir_ref: bool | None = None
42+
is_executable_ref: bool | None = None
43+
is_python_ref: bool | None = None
44+
45+
def __post_init__(self):
46+
if not self.children_ref:
47+
self._gen_children()
5548

5649
def __str__(self) -> str:
57-
return f"{self.path.as_posix()}"
50+
return f"{self.path}"
5851

5952
def __lt__(self, other) -> bool:
60-
return self.path.as_posix() < other.path.as_posix()
53+
return self.path < other.path
6154

6255
def __lte__(self, other) -> bool:
63-
return self.path.as_posix() <= other.path.as_posix()
56+
return self.path <= other.path
6457

6558
def __gt__(self, other) -> bool:
66-
return self.path.as_posix() > other.path.as_posix()
59+
return self.path > other.path
6760

6861
def __gte__(self, other) -> bool:
69-
return self.path.as_posix() >= other.path.as_posix()
62+
return self.path >= other.path
7063

7164
def __eq__(self, other) -> bool:
72-
return self.path.as_posix() == other.path.as_posix()
65+
return self.path == other.path
7366

7467
def which(self, name) -> PathEntry | None:
7568
"""Search in this path for an executable.
@@ -87,9 +80,9 @@ def which(self, name) -> PathEntry | None:
8780
if self.path is not None:
8881
found = next(
8982
(
90-
children[(self.path / child).as_posix()]
83+
children[(self.path / child)]
9184
for child in valid_names
92-
if (self.path / child).as_posix() in children
85+
if (self.path / child) in children
9386
),
9487
None,
9588
)
@@ -210,7 +203,7 @@ def pythons(self) -> dict[str | Path, PathEntry]:
210203
if not self.pythons_ref:
211204
self.pythons_ref = defaultdict(PathEntry)
212205
for python in self._iter_pythons():
213-
python_path = python.path.as_posix()
206+
python_path = python.path
214207
self.pythons_ref[python_path] = python
215208
return self.pythons_ref
216209

@@ -295,17 +288,10 @@ def version_matcher(py_version):
295288
if self.is_python and self.as_python and version_matcher(self.py_version):
296289
return self
297290

298-
matching_pythons = [
299-
[entry, entry.as_python.version_sort]
300-
for entry in self._iter_pythons()
301-
if (
302-
entry is not None
303-
and entry.as_python is not None
304-
and version_matcher(entry.py_version)
305-
)
306-
]
307-
results = sorted(matching_pythons, key=lambda r: (r[1], r[0]), reverse=True)
308-
return next(iter(r[0] for r in results if r is not None), None)
291+
for entry in self._iter_pythons():
292+
if entry is not None and entry.as_python is not None:
293+
if version_matcher(entry.as_python):
294+
return entry
309295

310296
def _filter_children(self) -> Iterator[Path]:
311297
if not os.access(str(self.path), os.R_OK):
@@ -316,39 +302,26 @@ def _filter_children(self) -> Iterator[Path]:
316302
children = self.path.iterdir()
317303
return children
318304

319-
def _gen_children(self) -> Iterator:
320-
pass_name = self.name != self.path.name
321-
pass_args = {"is_root": False, "only_python": self.only_python}
322-
if pass_name:
323-
if self.name is not None and isinstance(self.name, str):
324-
pass_args["name"] = self.name
325-
elif self.path is not None and isinstance(self.path.name, str):
326-
pass_args["name"] = self.path.name
327-
328-
if not self.is_dir:
329-
yield (self.path.as_posix(), self)
330-
elif self.is_root:
331-
for child in self._filter_children():
332-
if self.only_python:
333-
try:
334-
entry = PathEntry.create(path=child, **pass_args)
335-
except (InvalidPythonVersion, ValueError):
336-
continue
337-
else:
338-
try:
339-
entry = PathEntry.create(path=child, **pass_args)
340-
except (InvalidPythonVersion, ValueError):
341-
continue
342-
yield (child.as_posix(), entry)
343-
return
305+
def _gen_children(self):
306+
if self.is_dir and self.is_root and self.path is not None:
307+
# Assuming _filter_children returns an iterator over child paths
308+
for child_path in self._filter_children():
309+
pass_name = self.name != self.path.name
310+
pass_args = {"is_root": False, "only_python": self.only_python}
311+
if pass_name:
312+
if self.name is not None and isinstance(self.name, str):
313+
pass_args["name"] = self.name
314+
elif self.path is not None and isinstance(self.path.name, str):
315+
pass_args["name"] = self.path.name
316+
317+
try:
318+
entry = PathEntry.create(path=child_path, **pass_args)
319+
self.children_ref[child_path] = entry
320+
except (InvalidPythonVersion, ValueError):
321+
continue # Or handle as needed
344322

345323
@property
346324
def children(self) -> dict[str, PathEntry]:
347-
children = getattr(self, "children_ref", {})
348-
if not children:
349-
for child_key, child_val in self._gen_children():
350-
children[child_key] = child_val
351-
self.children_ref = children
352325
return self.children_ref
353326

354327
@classmethod
@@ -360,7 +333,7 @@ def create(
360333
pythons: dict[str, PythonVersion] | None = None,
361334
name: str | None = None,
362335
) -> PathEntry:
363-
"""Helper method for creating new :class:`pythonfinder.models.PathEntry` instances.
336+
"""Helper method for creating new :class:`PathEntry` instances.
364337
365338
:param str path: Path to the specified location.
366339
:param bool is_root: Whether this is a root from the environment PATH variable, defaults to False
@@ -390,7 +363,7 @@ def create(
390363
child_creation_args["name"] = _new.name
391364
for pth, python in pythons.items():
392365
pth = ensure_path(pth)
393-
children[pth.as_posix()] = PathEntry(
366+
children[str(path)] = PathEntry(
394367
py_version=python, path=pth, **child_creation_args
395368
)
396369
_new.children_ref = children

0 commit comments

Comments
 (0)