|
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