|
69 | 69 | system_which,
|
70 | 70 | )
|
71 | 71 | from pipenv.utils.toml import cleanup_toml, convert_toml_outline_tables
|
| 72 | +from pipenv.vendor.tomlkit.toml_document import TOMLDocument |
72 | 73 | from pipenv.vendor import plette, tomlkit
|
73 | 74 |
|
74 | 75 | try:
|
@@ -138,6 +139,53 @@ class SourceNotFound(KeyError):
|
138 | 139 | pass
|
139 | 140 |
|
140 | 141 |
|
| 142 | +class PipfileReader: |
| 143 | + _instance = None |
| 144 | + _instances = {} # path -> instance mapping |
| 145 | + |
| 146 | + def __new__(cls, pipfile_path: str | Path): |
| 147 | + path_key = str(Path(pipfile_path).resolve()) |
| 148 | + if path_key not in cls._instances: |
| 149 | + cls._instances[path_key] = super().__new__(cls) |
| 150 | + return cls._instances[path_key] |
| 151 | + |
| 152 | + def __init__(self, pipfile_path: str | Path): |
| 153 | + # Only initialize if this is a new instance |
| 154 | + if not hasattr(self, "pipfile_path"): # Check if already initialized |
| 155 | + self.pipfile_path = Path(pipfile_path) |
| 156 | + self._cached_mtime = None |
| 157 | + self._cached_content = None |
| 158 | + |
| 159 | + def _is_cache_valid(self) -> bool: |
| 160 | + """Check if the cached content is still valid based on mtime""" |
| 161 | + if self._cached_mtime is None or self._cached_content is None: |
| 162 | + return False |
| 163 | + |
| 164 | + current_mtime = os.path.getmtime(self.pipfile_path) |
| 165 | + return current_mtime == self._cached_mtime |
| 166 | + |
| 167 | + def read_pipfile(self) -> str: |
| 168 | + """Read the raw contents of the Pipfile""" |
| 169 | + return self.pipfile_path.read_text() |
| 170 | + |
| 171 | + def _parse_pipfile(self, contents: str) -> TOMLDocument | TPipfile: |
| 172 | + """Parse the TOML contents""" |
| 173 | + return tomlkit.parse(contents) |
| 174 | + |
| 175 | + @property |
| 176 | + def parsed_pipfile(self) -> TOMLDocument | TPipfile: |
| 177 | + """Parse Pipfile into a TOMLFile with caching based on mtime""" |
| 178 | + if self._is_cache_valid(): |
| 179 | + return self._cached_content |
| 180 | + |
| 181 | + # Cache is invalid or doesn't exist, reload the file |
| 182 | + contents = self.read_pipfile() |
| 183 | + self._cached_content = self._parse_pipfile(contents) |
| 184 | + self._cached_mtime = os.path.getmtime(self.pipfile_path) |
| 185 | + |
| 186 | + return self._cached_content |
| 187 | + |
| 188 | + |
141 | 189 | class Project:
|
142 | 190 | """docstring for Project"""
|
143 | 191 |
|
@@ -208,6 +256,8 @@ def __init__(self, python_version=None, chdir=True):
|
208 | 256 | default_sources_toml += f"\n\n[[source]]\n{tomlkit.dumps(pip_conf_index)}"
|
209 | 257 | plette.pipfiles.DEFAULT_SOURCE_TOML = default_sources_toml
|
210 | 258 |
|
| 259 | + self._pipfile_reader = PipfileReader(self.pipfile_location) |
| 260 | + |
211 | 261 | # Hack to skip this during pipenv run, or -r.
|
212 | 262 | if ("run" not in sys.argv) and chdir:
|
213 | 263 | with contextlib.suppress(TypeError, AttributeError):
|
@@ -666,49 +716,7 @@ def requirements_location(self) -> str | None:
|
666 | 716 | @property
|
667 | 717 | def parsed_pipfile(self) -> tomlkit.toml_document.TOMLDocument | TPipfile:
|
668 | 718 | """Parse Pipfile into a TOMLFile"""
|
669 |
| - contents = self.read_pipfile() |
670 |
| - return self._parse_pipfile(contents) |
671 |
| - |
672 |
| - def read_pipfile(self) -> str: |
673 |
| - # Open the pipfile, read it into memory. |
674 |
| - if not self.pipfile_exists: |
675 |
| - return "" |
676 |
| - with open(self.pipfile_location) as f: |
677 |
| - contents = f.read() |
678 |
| - self._pipfile_newlines = preferred_newlines(f) |
679 |
| - |
680 |
| - return contents |
681 |
| - |
682 |
| - def _parse_pipfile( |
683 |
| - self, contents: str |
684 |
| - ) -> tomlkit.toml_document.TOMLDocument | TPipfile: |
685 |
| - try: |
686 |
| - return tomlkit.parse(contents) |
687 |
| - except Exception: |
688 |
| - # We lose comments here, but it's for the best.) |
689 |
| - # Fallback to toml parser, for large files. |
690 |
| - return toml.loads(contents) |
691 |
| - |
692 |
| - def _read_pyproject(self) -> None: |
693 |
| - pyproject = self.path_to("pyproject.toml") |
694 |
| - if os.path.exists(pyproject): |
695 |
| - self._pyproject = toml.load(pyproject) |
696 |
| - build_system = self._pyproject.get("build-system", None) |
697 |
| - if not os.path.exists(self.path_to("setup.py")): |
698 |
| - if not build_system or not build_system.get("requires"): |
699 |
| - build_system = { |
700 |
| - "requires": ["setuptools>=40.8.0", "wheel"], |
701 |
| - "build-backend": get_default_pyproject_backend(), |
702 |
| - } |
703 |
| - self._build_system = build_system |
704 |
| - |
705 |
| - @property |
706 |
| - def build_requires(self) -> list[str]: |
707 |
| - return self._build_system.get("requires", ["setuptools>=40.8.0", "wheel"]) |
708 |
| - |
709 |
| - @property |
710 |
| - def build_backend(self) -> str: |
711 |
| - return self._build_system.get("build-backend", get_default_pyproject_backend()) |
| 719 | + return self._pipfile_reader.parsed_pipfile |
712 | 720 |
|
713 | 721 | @property
|
714 | 722 | def settings(self) -> tomlkit.items.Table | dict[str, str | bool]:
|
@@ -828,8 +836,7 @@ def dev_packages(self):
|
828 | 836 | def pipfile_is_empty(self):
|
829 | 837 | if not self.pipfile_exists:
|
830 | 838 | return True
|
831 |
| - |
832 |
| - if not self.read_pipfile(): |
| 839 | + if not self.pipfile_exists: |
833 | 840 | return True
|
834 | 841 |
|
835 | 842 | return False
|
|
0 commit comments