Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert from pydantic to vanilla dataclasses (includes pythonfinder 2.1.0) #6065

Merged
merged 27 commits into from
Jan 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4de1487
Try changes from https://github.com/sarugaku/pythonfinder/pull/157
matteius Jan 22, 2024
1b133bb
Try changes from https://github.com/sarugaku/pythonfinder/pull/157
matteius Jan 22, 2024
a0d19f8
Consistent API change
matteius Jan 22, 2024
b285868
Try to normalize on using pathlib basics and storing string paths in…
matteius Jan 24, 2024
844796a
Corrected import
matteius Jan 24, 2024
d867857
Simplify some of the pythonfinder mechanics.
matteius Jan 26, 2024
f7fb0f3
simplify test assertion for mac os
matteius Jan 26, 2024
877e486
simplify test assertion for mac os
matteius Jan 26, 2024
1a6b0aa
simplify test assertion for mac os
matteius Jan 26, 2024
500a62e
Use resolve instead of absolute to follow symlinks
matteius Jan 26, 2024
3071bf9
Update the path logic again, this time try samefile.
matteius Jan 26, 2024
ccbe1c9
iterate on this last test failure
matteius Jan 26, 2024
c5c8bd5
Restrict CI to the failing test for a moment
matteius Jan 26, 2024
df8a14d
Iterate on this awful predicament with the test passing locally in wi…
matteius Jan 26, 2024
b05cd14
Iterate on this awful predicament with the test passing locally in wi…
matteius Jan 26, 2024
9c2bc3c
Consider the angle that the test is the one that is wrong.
matteius Jan 26, 2024
3816a1f
Iterate on this awful predicament with the test passing locally in wi…
matteius Jan 26, 2024
3afcb8a
ensure we at least search from the root path (its currently None)
matteius Jan 26, 2024
3939b49
ensure we at least search from the root path (its currently None)
matteius Jan 26, 2024
3a5a4fd
Completely remove posix path conversions.
matteius Jan 27, 2024
bda8968
Unrestrict test runner now
matteius Jan 27, 2024
aa0f5cc
restrict down to the one failing test now
matteius Jan 27, 2024
2f3acf0
Unrestrict the test runner again
matteius Jan 27, 2024
6100752
Round 2 of pydantic conversions
matteius Jan 27, 2024
e676b99
Vendor in pythonfinder 2.1.0
matteius Jan 28, 2024
af3a1b5
Add news fragment
matteius Jan 28, 2024
099983b
Merge branch 'pilot-pythnonfinder-no-pydantic' of github.com:pypa/pip…
matteius Jan 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/6065.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Removal of pydantic from pythonfinder and pipenv; reduced complexity of pythonfinder pathlib usage (avoid posix conversions).
10 changes: 0 additions & 10 deletions pipenv/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,6 @@


def _ensure_modules():
# Can be removed when we drop pydantic
spec = importlib.util.spec_from_file_location(
"typing_extensions",
location=os.path.join(
os.path.dirname(__file__), "patched", "pip", "_vendor", "typing_extensions.py"
),
)
typing_extensions = importlib.util.module_from_spec(spec)
sys.modules["typing_extensions"] = typing_extensions
spec.loader.exec_module(typing_extensions)
# Ensure when pip gets invoked it uses our patched version
spec = importlib.util.spec_from_file_location(
"pip",
Expand Down
11 changes: 5 additions & 6 deletions pipenv/installers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@
import re
import sys
from abc import ABCMeta, abstractmethod
from dataclasses import dataclass, field
from typing import Optional

from pipenv.utils.processes import subprocess_run
from pipenv.utils.shell import find_windows_executable
from pipenv.vendor.pydantic import BaseModel


class Version(BaseModel):
@dataclass
class Version:
major: int
minor: int
patch: Optional[int] = None
patch: Optional[int] = field(default=None)

def __str__(self):
parts = [self.major, self.minor]
Expand All @@ -30,8 +31,6 @@ def parse(cls, name: str):
major = int(match.group(1))
minor = int(match.group(2))
patch = match.group(3)
# prerelease = match.group(4) # Not used
# prerelease_num = match.group(5)

if patch is not None:
patch = int(patch)
Expand All @@ -47,7 +46,7 @@ def cmpkey(self):
"""
return (self.major, self.minor, self.patch or 0)

def matches_minor(self, other):
def matches_minor(self, other: "Version"):
"""Check whether this version matches the other in (major, minor)."""
return (self.major, self.minor) == (other.major, other.minor)

Expand Down
28 changes: 15 additions & 13 deletions pipenv/utils/locking.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
import os
import stat
from contextlib import contextmanager, suppress
from dataclasses import dataclass, field
from json import JSONDecodeError
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import Dict, Iterator, List, Optional
from typing import Any, Dict, Iterator, List, Optional

from pipenv.patched.pip._internal.req.req_install import InstallRequirement
from pipenv.utils.dependencies import (
Expand All @@ -25,7 +26,6 @@
from pipenv.utils.requirements import normalize_name
from pipenv.utils.requirementslib import is_editable, is_vcs, merge_items
from pipenv.vendor.plette import lockfiles
from pipenv.vendor.pydantic import BaseModel, Field


def merge_markers(entry, markers):
Expand Down Expand Up @@ -235,22 +235,24 @@ def atomic_open_for_write(target, binary=False, newline=None, encoding=None) ->
os.rename(f.name, target) # No os.replace() on Python 2.


class Lockfile(BaseModel):
path: Path = Field(
@dataclass
class Lockfile:
lockfile: lockfiles.Lockfile
path: Path = field(
default_factory=lambda: Path(os.curdir).joinpath("Pipfile.lock").absolute()
)
_requirements: Optional[list] = Field(default_factory=list)
_dev_requirements: Optional[list] = Field(default_factory=list)
_requirements: Optional[List[Any]] = field(default_factory=list)
_dev_requirements: Optional[List[Any]] = field(default_factory=list)
projectfile: ProjectFile = None
lockfile: lockfiles.Lockfile
newlines: str = DEFAULT_NEWLINES

class Config:
validate_assignment = True
arbitrary_types_allowed = True
allow_mutation = True
include_private_attributes = True
# keep_untouched = (cached_property,)
def __post_init__(self):
if not self.path:
self.path = Path(os.curdir).absolute()
if not self.projectfile:
self.projectfile = self.load_projectfile(os.curdir, create=False)
if not self.lockfile:
self.lockfile = self.projectfile.model

@property
def section_keys(self):
Expand Down
11 changes: 6 additions & 5 deletions pipenv/utils/markers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import operator
import re
from collections.abc import Mapping, Set
from dataclasses import dataclass, fields
from functools import reduce
from typing import Optional

Expand All @@ -12,7 +13,6 @@
Specifier,
SpecifierSet,
)
from pipenv.vendor.pydantic import BaseModel

MAX_VERSIONS = {1: 7, 2: 7, 3: 11, 4: 0}
DEPRECATED_VERSIONS = ["3.0", "3.1", "3.2", "3.3"]
Expand All @@ -22,7 +22,8 @@ class RequirementError(Exception):
pass


class PipenvMarkers(BaseModel):
@dataclass
class PipenvMarkers:
os_name: Optional[str] = None
sys_platform: Optional[str] = None
platform_machine: Optional[str] = None
Expand All @@ -41,14 +42,14 @@ def make_marker(cls, marker_string):
marker = Marker(marker_string)
except InvalidMarker:
raise RequirementError(
"Invalid requirement: Invalid marker %r" % marker_string
f"Invalid requirement: Invalid marker {marker_string!r}"
)
return marker

@classmethod
def from_pipfile(cls, name, pipfile):
attr_fields = list(cls.__fields__)
found_keys = [k for k in pipfile if k in attr_fields]
attr_fields = list(fields(cls))
found_keys = [k.name for k in attr_fields if k.name in pipfile]
marker_strings = [f"{k} {pipfile[k]}" for k in found_keys]
if pipfile.get("markers"):
marker_strings.append(pipfile.get("markers"))
Expand Down
82 changes: 33 additions & 49 deletions pipenv/utils/pipfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import io
import itertools
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional

Expand All @@ -14,7 +15,6 @@
from pipenv.utils.toml import tomlkit_value_to_python
from pipenv.vendor import tomlkit
from pipenv.vendor.plette import pipfiles
from pipenv.vendor.pydantic import BaseModel, Field, validator

DEFAULT_NEWLINES = "\n"

Expand Down Expand Up @@ -149,10 +149,11 @@ def preferred_newlines(f):
return DEFAULT_NEWLINES


class ProjectFile(BaseModel):
@dataclass
class ProjectFile:
location: str
line_ending: str
model: Optional[Any] = Field(default_factory=lambda: {})
model: Optional[Any] = field(default_factory=dict)

@classmethod
def read(cls, location: str, model_cls, invalid_ok: bool = False) -> "ProjectFile":
Expand Down Expand Up @@ -263,32 +264,32 @@ def __getattribute__(self, key):
return super().__getattribute__(key)


class Pipfile(BaseModel):
@dataclass
class Pipfile:
oz123 marked this conversation as resolved.
Show resolved Hide resolved
path: Path
projectfile: ProjectFile
pipfile: Optional[PipfileLoader]
_pyproject: Optional[tomlkit.TOMLDocument] = tomlkit.document()
build_system: Optional[Dict] = {}
_requirements: Optional[List] = []
_dev_requirements: Optional[List] = []

class Config:
validate_assignment = True
arbitrary_types_allowed = True
allow_mutation = True
include_private_attributes = True
# keep_untouched = (cached_property,)

@validator("path", pre=True, always=True)
def _get_path(cls, v):
pipfile: Optional[PipfileLoader] = None
_pyproject: Optional[tomlkit.TOMLDocument] = field(default_factory=tomlkit.document)
build_system: Optional[Dict] = field(default_factory=dict)
_requirements: Optional[List] = field(default_factory=list)
_dev_requirements: Optional[List] = field(default_factory=list)

def __post_init__(self):
# Validators or equivalent logic here
self.path = self._get_path(self.path)
self.projectfile = self._get_projectfile(self.projectfile, {"path": self.path})
self.pipfile = self._get_pipfile(self.pipfile, {"projectfile": self.projectfile})

@staticmethod
def _get_path(v: Path) -> Path:
return v or Path(os.curdir).absolute()

@validator("projectfile", pre=True, always=True)
def _get_projectfile(cls, v, values):
return v or cls.load_projectfile(os.curdir, create=False)
@staticmethod
def _get_projectfile(v: ProjectFile, values: dict) -> ProjectFile:
return v or Pipfile.load_projectfile(os.curdir, create=False)

@validator("pipfile", pre=True, always=True)
def _get_pipfile(cls, v, values):
@staticmethod
def _get_pipfile(v: PipfileLoader, values: dict) -> PipfileLoader:
return v or values["projectfile"].model

@property
Expand Down Expand Up @@ -378,41 +379,24 @@ def read_projectfile(cls, path):
return pf

@classmethod
def load_projectfile(cls, path, create=False):
# type: (Text, bool) -> ProjectFile
"""Given a path, load or create the necessary pipfile.

:param Text path: Path to the project root or pipfile
:param bool create: Whether to create the pipfile if not found, defaults to True
:raises OSError: Thrown if the project root directory doesn't exist
:raises FileNotFoundError: Thrown if the pipfile doesn't exist and ``create=False``
:return: A project file instance for the supplied project
:rtype: :class:`~project.ProjectFile`
"""
def load_projectfile(cls, path: str, create: bool = False) -> ProjectFile:
"""..."""

if not path:
raise RuntimeError("Must pass a path to classmethod 'Pipfile.load'")
if not isinstance(path, Path):
path = Path(path).absolute()
pipfile_path = path if path.is_file() else path.joinpath("Pipfile")
project_path = pipfile_path.parent
if not project_path.exists():
raise FileNotFoundError("%s is not a valid project path!" % path)
raise RequirementError(f"{path} is not a valid project path!")
elif (not pipfile_path.exists() or not pipfile_path.is_file()) and not create:
raise RequirementError("%s is not a valid Pipfile" % pipfile_path)
return cls.read_projectfile(pipfile_path.as_posix())
raise RequirementError(f"{pipfile_path} is not a valid Pipfile")
return cls.read_projectfile(pipfile_path)

@classmethod
def load(cls, path, create=False):
# type: (Text, bool) -> Pipfile
"""Given a path, load or create the necessary pipfile.

:param Text path: Path to the project root or pipfile
:param bool create: Whether to create the pipfile if not found, defaults to True
:raises OSError: Thrown if the project root directory doesn't exist
:raises FileNotFoundError: Thrown if the pipfile doesn't exist and ``create=False``
:return: A pipfile instance pointing at the supplied project
:rtype:: class:`~pipfile.Pipfile`
"""
def load(cls, path: str, create: bool = False) -> "Pipfile":
"""..."""

projectfile = cls.load_projectfile(path, create=create)
pipfile = projectfile.model
Expand Down
16 changes: 12 additions & 4 deletions pipenv/utils/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from pipenv.utils.fileutils import normalize_drive, normalize_path
from pipenv.vendor import click
from pipenv.vendor.pythonfinder.utils import ensure_path
from pipenv.vendor.pythonfinder.utils import ensure_path, parse_python_version

from .constants import FALSE_VALUES, SCHEME_LIST, TRUE_VALUES
from .processes import subprocess_run
Expand Down Expand Up @@ -237,7 +237,7 @@ def find_python(finder, line=None):
:return: A path to python
:rtype: str
"""

print(line)
if line and not isinstance(line, str):
raise TypeError(f"Invalid python search type: expected string, received {line!r}")
if line:
Expand All @@ -258,12 +258,20 @@ def find_python(finder, line=None):
if not line:
result = next(iter(finder.find_all_python_versions()), None)
elif line and line[0].isdigit() or re.match(r"^\d+(\.\d+)*$", line):
result = finder.find_python_version(line)
version_info = parse_python_version(line)
result = finder.find_python_version(
major=version_info.get("major"),
minor=version_info.get("minor"),
patch=version_info.get("patch"),
pre=version_info.get("is_prerelease"),
dev=version_info.get("is_devrelease"),
sort_by_path=True,
)
else:
result = finder.find_python_version(name=line)
if not result:
result = finder.which(line)
if not result and not line.startswith("python"):
if not result and "python" not in line.lower():
line = f"python{line}"
result = find_python(finder, line)

Expand Down
3 changes: 2 additions & 1 deletion pipenv/utils/virtualenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,12 +347,13 @@ def find_a_system_python(line):

from pipenv.vendor.pythonfinder import Finder

finder = Finder(system=False, global_search=True)
finder = Finder(system=True, global_search=True)
if not line:
return next(iter(finder.find_all_python_versions()), None)
# Use the windows finder executable
if (line.startswith(("py ", "py.exe "))) and os.name == "nt":
line = line.split(" ", 1)[1].lstrip("-")
print(line)
python_entry = find_python(finder, line)
return python_entry

Expand Down
21 changes: 0 additions & 21 deletions pipenv/vendor/pydantic/LICENSE

This file was deleted.

Loading
Loading