|
6 | 6 | installation reproducibility).
|
7 | 7 | """
|
8 | 8 |
|
| 9 | +import datetime |
| 10 | +import json |
9 | 11 | import os
|
10 | 12 | from dataclasses import dataclass, field
|
11 | 13 | from pathlib import Path
|
12 | 14 | from typing import Any, Dict, List, Optional, Set, Union
|
13 | 15 |
|
| 16 | +from pipenv.utils import err |
| 17 | +from pipenv.utils.locking import atomic_open_for_write |
14 | 18 | from pipenv.utils.toml import tomlkit_value_to_python
|
15 | 19 | from pipenv.vendor import tomlkit
|
16 | 20 |
|
@@ -40,6 +44,122 @@ class PylockFile:
|
40 | 44 | path: Path
|
41 | 45 | data: Dict[str, Any] = field(default_factory=dict)
|
42 | 46 |
|
| 47 | + @classmethod |
| 48 | + def from_lockfile( |
| 49 | + cls, lockfile_path: Union[str, Path], pylock_path: Union[str, Path] = None |
| 50 | + ) -> "PylockFile": |
| 51 | + """Create a PylockFile from a Pipfile.lock file. |
| 52 | +
|
| 53 | + Args: |
| 54 | + lockfile_path: Path to the Pipfile.lock file |
| 55 | + pylock_path: Path to save the pylock.toml file, defaults to pylock.toml in the same directory |
| 56 | +
|
| 57 | + Returns: |
| 58 | + A PylockFile instance |
| 59 | +
|
| 60 | + Raises: |
| 61 | + FileNotFoundError: If the Pipfile.lock file doesn't exist |
| 62 | + ValueError: If the Pipfile.lock file is invalid |
| 63 | + """ |
| 64 | + if isinstance(lockfile_path, str): |
| 65 | + lockfile_path = Path(lockfile_path) |
| 66 | + |
| 67 | + if not lockfile_path.exists(): |
| 68 | + raise FileNotFoundError(f"Pipfile.lock not found: {lockfile_path}") |
| 69 | + |
| 70 | + if pylock_path is None: |
| 71 | + pylock_path = lockfile_path.parent / "pylock.toml" |
| 72 | + elif isinstance(pylock_path, str): |
| 73 | + pylock_path = Path(pylock_path) |
| 74 | + |
| 75 | + try: |
| 76 | + with open(lockfile_path, encoding="utf-8") as f: |
| 77 | + lockfile_data = json.load(f) |
| 78 | + except Exception as e: |
| 79 | + raise ValueError(f"Invalid Pipfile.lock file: {e}") |
| 80 | + |
| 81 | + # Create the basic pylock.toml structure |
| 82 | + pylock_data = { |
| 83 | + "lock-version": "1.0", |
| 84 | + "environments": [], |
| 85 | + "extras": [], |
| 86 | + "dependency-groups": [], |
| 87 | + "default-groups": [], |
| 88 | + "created-by": "pipenv", |
| 89 | + "packages": [], |
| 90 | + } |
| 91 | + |
| 92 | + # Add Python version requirement if present |
| 93 | + meta = lockfile_data.get("_meta", {}) |
| 94 | + requires = meta.get("requires", {}) |
| 95 | + if "python_version" in requires: |
| 96 | + pylock_data["requires-python"] = f">={requires['python_version']}" |
| 97 | + elif "python_full_version" in requires: |
| 98 | + pylock_data["requires-python"] = f"=={requires['python_full_version']}" |
| 99 | + |
| 100 | + # Add sources |
| 101 | + sources = meta.get("sources", []) |
| 102 | + if sources: |
| 103 | + pylock_data["sources"] = sources |
| 104 | + |
| 105 | + # Process packages |
| 106 | + for section in ["default", "develop"]: |
| 107 | + packages = lockfile_data.get(section, {}) |
| 108 | + for name, package_data in packages.items(): |
| 109 | + package = {"name": name} |
| 110 | + |
| 111 | + # Add version if present |
| 112 | + if "version" in package_data: |
| 113 | + version = package_data["version"] |
| 114 | + if version.startswith("=="): |
| 115 | + package["version"] = version[2:] |
| 116 | + else: |
| 117 | + package["version"] = version |
| 118 | + |
| 119 | + # Add markers if present |
| 120 | + if "markers" in package_data: |
| 121 | + # For develop packages, add dependency_groups marker |
| 122 | + if section == "develop": |
| 123 | + if "markers" in package_data: |
| 124 | + package["marker"] = ( |
| 125 | + f"dependency_groups in ('dev', 'test') and ({package_data['markers']})" |
| 126 | + ) |
| 127 | + else: |
| 128 | + package["marker"] = "dependency_groups in ('dev', 'test')" |
| 129 | + else: |
| 130 | + package["marker"] = package_data["markers"] |
| 131 | + elif section == "develop": |
| 132 | + package["marker"] = "dependency_groups in ('dev', 'test')" |
| 133 | + |
| 134 | + # Add hashes if present |
| 135 | + if "hashes" in package_data: |
| 136 | + wheels = [] |
| 137 | + for hash_value in package_data["hashes"]: |
| 138 | + if hash_value.startswith("sha256:"): |
| 139 | + hash_value = hash_value[7:] # Remove "sha256:" prefix |
| 140 | + wheel = { |
| 141 | + "name": f"{name}-{package.get('version', '0.0.0')}-py3-none-any.whl", |
| 142 | + "hashes": {"sha256": hash_value}, |
| 143 | + } |
| 144 | + wheels.append(wheel) |
| 145 | + if wheels: |
| 146 | + package["wheels"] = wheels |
| 147 | + |
| 148 | + pylock_data["packages"].append(package) |
| 149 | + |
| 150 | + # Add tool.pipenv section with metadata |
| 151 | + pylock_data["tool"] = { |
| 152 | + "pipenv": { |
| 153 | + "generated_from": "Pipfile.lock", |
| 154 | + "generation_date": datetime.datetime.now( |
| 155 | + datetime.timezone.utc |
| 156 | + ).isoformat(), |
| 157 | + } |
| 158 | + } |
| 159 | + |
| 160 | + instance = cls(path=pylock_path, data=pylock_data) |
| 161 | + return instance |
| 162 | + |
43 | 163 | @classmethod
|
44 | 164 | def from_path(cls, path: Union[str, Path]) -> "PylockFile":
|
45 | 165 | """Load a pylock.toml file from the given path.
|
@@ -80,6 +200,74 @@ def from_path(cls, path: Union[str, Path]) -> "PylockFile":
|
80 | 200 |
|
81 | 201 | return cls(path=path, data=tomlkit_value_to_python(data))
|
82 | 202 |
|
| 203 | + def write(self) -> None: |
| 204 | + """Write the pylock.toml file to disk. |
| 205 | +
|
| 206 | + Raises: |
| 207 | + OSError: If there is an error writing the file |
| 208 | + """ |
| 209 | + try: |
| 210 | + # Convert the data to a TOML document |
| 211 | + doc = tomlkit.document() |
| 212 | + |
| 213 | + # Add top-level keys in a specific order for readability |
| 214 | + for key in [ |
| 215 | + "lock-version", |
| 216 | + "environments", |
| 217 | + "requires-python", |
| 218 | + "extras", |
| 219 | + "dependency-groups", |
| 220 | + "default-groups", |
| 221 | + "created-by", |
| 222 | + ]: |
| 223 | + if key in self.data: |
| 224 | + doc[key] = self.data[key] |
| 225 | + |
| 226 | + # Add packages |
| 227 | + if "packages" in self.data: |
| 228 | + doc["packages"] = [] |
| 229 | + for package in self.data["packages"]: |
| 230 | + pkg_table = tomlkit.table() |
| 231 | + for k, v in package.items(): |
| 232 | + if k not in {"wheels", "sdist"}: |
| 233 | + pkg_table[k] = v |
| 234 | + |
| 235 | + # Add wheels as an array of tables |
| 236 | + if "wheels" in package: |
| 237 | + pkg_table["wheels"] = [] |
| 238 | + for wheel in package["wheels"]: |
| 239 | + wheel_table = tomlkit.inline_table() |
| 240 | + for k, v in wheel.items(): |
| 241 | + wheel_table[k] = v |
| 242 | + pkg_table["wheels"].append(wheel_table) |
| 243 | + |
| 244 | + # Add sdist as a table |
| 245 | + if "sdist" in package: |
| 246 | + sdist_table = tomlkit.table() |
| 247 | + for k, v in package["sdist"].items(): |
| 248 | + sdist_table[k] = v |
| 249 | + pkg_table["sdist"] = sdist_table |
| 250 | + |
| 251 | + doc["packages"].append(pkg_table) |
| 252 | + |
| 253 | + # Add tool section |
| 254 | + if "tool" in self.data: |
| 255 | + tool_table = tomlkit.table() |
| 256 | + for tool_name, tool_data in self.data["tool"].items(): |
| 257 | + tool_section = tomlkit.table() |
| 258 | + for k, v in tool_data.items(): |
| 259 | + tool_section[k] = v |
| 260 | + tool_table[tool_name] = tool_section |
| 261 | + doc["tool"] = tool_table |
| 262 | + |
| 263 | + # Write the document to the file |
| 264 | + with atomic_open_for_write(self.path, encoding="utf-8") as f: |
| 265 | + f.write(tomlkit.dumps(doc)) |
| 266 | + |
| 267 | + except Exception as e: |
| 268 | + err.print(f"[bold red]Error writing pylock.toml: {e}[/bold red]") |
| 269 | + raise OSError(f"Error writing pylock.toml: {e}") |
| 270 | + |
83 | 271 | @property
|
84 | 272 | def lock_version(self) -> str:
|
85 | 273 | """Get the lock-version."""
|
|
0 commit comments