diff --git a/pyproject.toml b/pyproject.toml index a4dacb0..897a582 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,6 @@ dynamic = ["version"] requires-python = ">=3.8" dependencies = [ "packaging>=22.0", - "pydantic>=1.10.7,<2", ] [project.optional-dependencies] @@ -136,7 +135,6 @@ unfixable = [ [tool.ruff.flake8-type-checking] runtime-evaluated-base-classes = [ - "pydantic.BaseModel", "pythonfinder.models.common.FinderBaseModel", "pythonfinder.models.mixins.PathEntry", ] diff --git a/src/pythonfinder/models/common.py b/src/pythonfinder/models/common.py deleted file mode 100644 index 4c439c9..0000000 --- a/src/pythonfinder/models/common.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import annotations - -from pydantic import BaseModel, Extra - - -class FinderBaseModel(BaseModel): - def __setattr__(self, name, value): - private_attributes = { - field_name - for field_name in self.__annotations__ - if field_name.startswith("_") - } - - if name in private_attributes or name in self.__fields__: - return object.__setattr__(self, name, value) - - if self.__config__.extra is not Extra.allow and name not in self.__fields__: - raise ValueError(f'"{self.__class__.__name__}" object has no field "{name}"') - - object.__setattr__(self, name, value) - - class Config: - validate_assignment = True - arbitrary_types_allowed = True - allow_mutation = True - include_private_attributes = False diff --git a/src/pythonfinder/models/mixins.py b/src/pythonfinder/models/mixins.py index 58ce99a..47e7c7a 100644 --- a/src/pythonfinder/models/mixins.py +++ b/src/pythonfinder/models/mixins.py @@ -1,19 +1,16 @@ from __future__ import annotations +import dataclasses import os from collections import defaultdict -from pathlib import Path +from dataclasses import field from typing import ( TYPE_CHECKING, Any, - Dict, Generator, Iterator, - Optional, ) -from pydantic import BaseModel, Field, validator - from ..exceptions import InvalidPythonVersion from ..utils import ( KNOWN_EXTS, @@ -25,33 +22,37 @@ ) if TYPE_CHECKING: + from pathlib import Path + from pythonfinder.models.python import PythonVersion -class PathEntry(BaseModel): - is_root: bool = Field(default=False, order=False) - name: Optional[str] = None - path: Optional[Path] = None - children_ref: Optional[Any] = Field(default_factory=lambda: dict()) - only_python: Optional[bool] = False - py_version_ref: Optional[Any] = None - pythons_ref: Optional[Dict[Any, Any]] = defaultdict(lambda: None) - is_dir_ref: Optional[bool] = None - is_executable_ref: Optional[bool] = None - is_python_ref: Optional[bool] = None - - class Config: - validate_assignment = True - arbitrary_types_allowed = True - allow_mutation = True - include_private_attributes = True - - @validator("children", pre=True, always=True, check_fields=False) - def set_children(cls, v, values, **kwargs): - path = values.get("path") - if path: - values["name"] = path.name - return v or cls()._gen_children() +@dataclasses.dataclass(unsafe_hash=True) +class PathEntry: + is_root: bool = False + name: str | None = None + path: Path | None = None + children_ref: dict[str, Any] | None = field(default_factory=dict) + only_python: bool | None = False + py_version_ref: Any | None = None + pythons_ref: dict[str, Any] | None = field( + default_factory=lambda: defaultdict(lambda: None) + ) + is_dir_ref: bool | None = None + is_executable_ref: bool | None = None + is_python_ref: bool | None = None + + def __post_init__(self): + # If path is set, use its name for the name field + if self.path and not self.name: + self.name = self.path.name + + # Ensure children are properly set + self.children_ref = self.set_children(self.children_ref) + + def set_children(self, children): + # If children are not provided, generate them + return children or self._gen_children() def __str__(self) -> str: return f"{self.path.as_posix()}" @@ -360,7 +361,7 @@ def create( pythons: dict[str, PythonVersion] | None = None, name: str | None = None, ) -> PathEntry: - """Helper method for creating new :class:`pythonfinder.models.PathEntry` instances. + """Helper method for creating new :class:`PathEntry` instances. :param str path: Path to the specified location. :param bool is_root: Whether this is a root from the environment PATH variable, defaults to False diff --git a/src/pythonfinder/models/path.py b/src/pythonfinder/models/path.py index 107c9e6..bfd2f66 100644 --- a/src/pythonfinder/models/path.py +++ b/src/pythonfinder/models/path.py @@ -1,27 +1,22 @@ from __future__ import annotations +import dataclasses import errno import operator import os import sys from collections import ChainMap, defaultdict +from dataclasses import field from functools import cached_property from itertools import chain from pathlib import Path from typing import ( Any, DefaultDict, - Dict, Generator, Iterator, - List, - Optional, - Tuple, - Union, ) -from pydantic import Field, root_validator - from ..environment import ( ASDF_DATA_DIR, ASDF_INSTALLED, @@ -37,7 +32,6 @@ parse_pyenv_version_order, split_version_and_name, ) -from .common import FinderBaseModel from .mixins import PathEntry from .python import PythonFinder @@ -52,38 +46,32 @@ def exists_and_is_accessible(path): raise -class SystemPath(FinderBaseModel): +@dataclasses.dataclass(unsafe_hash=True) +class SystemPath: global_search: bool = True - paths: Dict[str, Union[PythonFinder, PathEntry]] = Field( + paths: dict[str, 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() + executables_tracking: list[PathEntry] = field(default_factory=list) + python_executables_tracking: dict[str, PathEntry] = field( + default_factory=dict, init=False ) - path_order: List[str] = Field(default_factory=lambda: list()) - python_version_dict: Dict[Tuple, Any] = Field( + path_order: list[str] = field(default_factory=list) + python_version_dict: dict[tuple, Any] = field( default_factory=lambda: defaultdict(list) ) - version_dict_tracking: Dict[Tuple, List[PathEntry]] = Field( + 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 + pyenv_finder: PythonFinder | None = None + asdf_finder: PythonFinder | None = None system: bool = False ignore_unsupported: bool = False - finders_dict: Dict[str, PythonFinder] = Field(default_factory=lambda: dict()) - - class Config: - validate_assignment = True - arbitrary_types_allowed = True - allow_mutation = True - include_private_attributes = True - keep_untouched = (cached_property,) + finders_dict: dict[str, PythonFinder] = field(default_factory=dict) - def __init__(self, **data): - super().__init__(**data) + def __post_init__(self): + # Initialize python_executables_tracking python_executables = {} for child in self.paths.values(): if child.pythons: @@ -93,24 +81,21 @@ def __init__(self, **data): python_executables.update(dict(finder.pythons)) self.python_executables_tracking = python_executables - @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") - if paths: - values["executables"] = [ + 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.finders_dict = self.finders_dict or {} + + # The part with 'paths' seems to be setting up 'executables' + if self.paths: + self.executables_tracking = [ p for p in ChainMap( - *(child.children_ref.values() for child in paths.values()) + *(child.children_ref.values() for child in self.paths.values()) ) if p.is_executable ] - return values def _register_finder(self, finder_name, finder): if finder_name not in self.finders_dict: diff --git a/src/pythonfinder/models/python.py b/src/pythonfinder/models/python.py index c5e0345..f0ef5f1 100644 --- a/src/pythonfinder/models/python.py +++ b/src/pythonfinder/models/python.py @@ -1,10 +1,13 @@ from __future__ import annotations +import dataclasses import logging import os import platform import sys from collections import defaultdict +from dataclasses import field +from functools import cached_property from pathlib import Path, WindowsPath from typing import ( Any, @@ -14,12 +17,9 @@ Iterator, List, Optional, - Tuple, - Union, ) from packaging.version import Version -from pydantic import Field, validator from ..environment import ASDF_DATA_DIR, PYENV_ROOT, SYSTEM_ARCH from ..exceptions import InvalidPythonVersion @@ -34,35 +34,25 @@ parse_pyenv_version_order, parse_python_version, ) -from .common import FinderBaseModel from .mixins import PathEntry logger = logging.getLogger(__name__) +@dataclasses.dataclass class PythonFinder(PathEntry): - root: Path - # should come before versions, because its value is used in versions's default initializer. - #: Whether to ignore any paths which raise exceptions and are not actually python + root: Path = field(default_factory=Path) ignore_unsupported: bool = True - #: Glob path for python versions off of the root directory version_glob_path: str = "versions/*" - #: The function to use to sort version order when returning an ordered version set sort_function: Optional[Callable] = None - #: The root locations used for discovery - roots: Dict = Field(default_factory=lambda: defaultdict()) - #: List of paths discovered during search - paths: List = Field(default_factory=lambda: list()) - #: Versions discovered in the specified paths - _versions: Dict = Field(default_factory=lambda: defaultdict()) - pythons_ref: Dict = Field(default_factory=lambda: defaultdict()) - - class Config: - validate_assignment = True - arbitrary_types_allowed = True - allow_mutation = True - include_private_attributes = True - # keep_untouched = (cached_property,) + roots: Dict = field(default_factory=lambda: defaultdict()) + paths: Optional[List[PathEntry]] = field(default_factory=list) + _versions: Dict = field(default_factory=lambda: defaultdict()) + pythons_ref: Dict = field(default_factory=lambda: defaultdict()) + + def __post_init__(self): + # Ensuring that paths are set correctly + self.paths = self.get_paths(self.paths) @property def version_paths(self) -> Any: @@ -157,7 +147,7 @@ def _iter_versions(self) -> Iterator[tuple[Path, PathEntry, tuple]]: ) yield (base_path, entry, version_tuple) - @property + @cached_property def versions(self) -> DefaultDict[tuple, PathEntry]: if not self._versions: for _, entry, version_tuple in self._iter_versions(): @@ -174,15 +164,16 @@ def _iter_pythons(self) -> Iterator: else: yield self.versions[version_tuple] - @validator("paths", pre=True, always=True) - def get_paths(cls, v) -> list[PathEntry]: - if v is not None: - return v + def get_paths(self, paths) -> list[PathEntry]: + # If paths are provided, use them + if paths is not None: + return paths - _paths = [base for _, base in cls._iter_version_bases()] + # Otherwise, generate paths using _iter_version_bases + _paths = [base for _, base in self._iter_version_bases()] return _paths - @property + @cached_property def pythons(self) -> dict: if not self.pythons_ref: from .path import PathEntry @@ -312,27 +303,21 @@ def which(self, name) -> PathEntry | None: return non_empty_match -class PythonVersion(FinderBaseModel): +@dataclasses.dataclass +class PythonVersion: major: int = 0 - minor: Optional[int] = None - patch: Optional[int] = None + minor: int | None = None + patch: int | None = None is_prerelease: bool = False is_postrelease: bool = False is_devrelease: bool = False is_debug: bool = False - version: Optional[Version] = None - architecture: Optional[str] = None - comes_from: Optional["PathEntry"] = None - executable: Optional[Union[str, WindowsPath, Path]] = None - company: Optional[str] = None - name: Optional[str] = None - - class Config: - validate_assignment = True - arbitrary_types_allowed = True - allow_mutation = True - include_private_attributes = True - # keep_untouched = (cached_property,) + version: Version | None = None + architecture: str | None = None + comes_from: PathEntry | None = None + executable: str | WindowsPath | Path | None = None + company: str | None = None + name: str | None = None def __getattribute__(self, key): result = super().__getattribute__(key) @@ -620,17 +605,11 @@ def create(cls, **kwargs) -> PythonVersion: return cls(**kwargs) -class VersionMap(FinderBaseModel): +@dataclasses.dataclass +class VersionMap: versions: DefaultDict[ - Tuple[int, Optional[int], Optional[int], bool, bool, bool], List[PathEntry] - ] = defaultdict(list) - - class Config: - validate_assignment = True - arbitrary_types_allowed = True - allow_mutation = True - include_private_attributes = True - # keep_untouched = (cached_property,) + tuple[int, int | None, int | None, bool, bool, bool], list[PathEntry] + ] = field(default_factory=lambda: defaultdict(list)) def add_entry(self, entry) -> None: version = entry.as_python diff --git a/src/pythonfinder/pythonfinder.py b/src/pythonfinder/pythonfinder.py index 6477cdf..beac9b0 100644 --- a/src/pythonfinder/pythonfinder.py +++ b/src/pythonfinder/pythonfinder.py @@ -1,39 +1,30 @@ from __future__ import annotations +import dataclasses import operator -from collections.abc import Iterable -from typing import Any, Optional +from typing import Any, Iterable from .environment import set_asdf_paths, set_pyenv_paths from .exceptions import InvalidPythonVersion -from .models.common import FinderBaseModel from .models.path import PathEntry, SystemPath from .models.python import PythonVersion from .utils import version_re -class Finder(FinderBaseModel): - path_prepend: Optional[str] = None +@dataclasses.dataclass(unsafe_hash=True) +class Finder: + path_prepend: str | None = None system: bool = False global_search: bool = True ignore_unsupported: bool = True sort_by_path: bool = False - system_path: Optional[SystemPath] = None + system_path: SystemPath | None = dataclasses.field(default=None, init=False) - def __init__(self, **data) -> None: - super().__init__(**data) + def __post_init__(self): self.system_path = self.create_system_path() - @property - def __hash__(self) -> int: - return hash( - (self.path_prepend, self.system, self.global_search, self.ignore_unsupported) - ) - - def __eq__(self, other) -> bool: - return self.__hash__ == other.__hash__ - def create_system_path(self) -> SystemPath: + # Implementation of set_asdf_paths and set_pyenv_paths might need to be adapted. set_asdf_paths() set_pyenv_paths() return SystemPath.create(