Skip to content

Commit 103482e

Browse files
authored
feat: editable pypi packages (#581)
Adds the `editable` field to pypi packages to indicate they are installed in editable mode.
1 parent d8e96a4 commit 103482e

File tree

7 files changed

+113
-2
lines changed

7 files changed

+113
-2
lines changed

crates/rattler_lock/src/lib.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ pub use channel::Channel;
8989
pub use conda::{CondaPackageData, ConversionError};
9090
pub use hash::PackageHashes;
9191
pub use parse::ParseCondaLockError;
92-
pub use pypi::{PypiPackageData, PypiPackageEnvironmentData};
92+
pub use pypi::{PypiPackageData, PypiPackageEnvironmentData, PypiSourceTreeHashable};
9393
pub use url_or_path::UrlOrPath;
9494

9595
/// The name of the default environment in a [`LockFile`]. This is the environment name that is used
@@ -560,6 +560,11 @@ impl PypiPackage {
560560
pub fn satisfies(&self, spec: &Requirement) -> bool {
561561
self.package_data().satisfies(spec)
562562
}
563+
564+
/// Returns true if this package should be installed in "editable" mode.
565+
pub fn is_editable(&self) -> bool {
566+
self.package_data().editable
567+
}
563568
}
564569

565570
/// A helper struct to group package and environment data together.

crates/rattler_lock/src/parse/v3.rs

+1
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ pub fn parse_v3_or_lower(document: serde_yaml::Value) -> Result<LockFile, ParseC
185185
requires_python: pkg.requires_python,
186186
url_or_path: UrlOrPath::Url(pkg.url),
187187
hash: pkg.hash,
188+
editable: false,
188189
})
189190
.0;
190191
EnvironmentPackageData::Pypi(

crates/rattler_lock/src/pypi.rs

+77
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
use crate::{PackageHashes, UrlOrPath};
22
use pep440_rs::VersionSpecifiers;
33
use pep508_rs::{ExtraName, PackageName, Requirement};
4+
use rattler_digest::{digest::Digest, Sha256};
45
use serde::{Deserialize, Serialize};
56
use serde_with::{serde_as, skip_serializing_none};
67
use std::cmp::Ordering;
78
use std::collections::BTreeSet;
9+
use std::fs;
10+
use std::path::Path;
811

912
/// A pinned Pypi package
1013
#[serde_as]
@@ -31,6 +34,10 @@ pub struct PypiPackageData {
3134

3235
/// The python version that this package requires.
3336
pub requires_python: Option<VersionSpecifiers>,
37+
38+
/// Whether the projects should be installed in editable mode or not.
39+
#[serde(default, skip_serializing_if = "should_skip_serializing_editable")]
40+
pub editable: bool,
3441
}
3542

3643
/// Additional runtime configuration of a package. Multiple environments/platforms might refer to
@@ -77,3 +84,73 @@ impl PypiPackageData {
7784
true
7885
}
7986
}
87+
88+
/// Used in `skip_serializing_if` to skip serializing the `editable` field if it is `false`.
89+
fn should_skip_serializing_editable(editable: &bool) -> bool {
90+
!*editable
91+
}
92+
93+
/// A struct that wraps the hashable part of a source package.
94+
///
95+
/// This struct the relevant parts of a source package that are used to compute a [`PackageHashes`].
96+
pub struct PypiSourceTreeHashable {
97+
/// The contents of an optional pyproject.toml file.
98+
pub pyproject_toml: Option<String>,
99+
100+
/// The contents of an optional setup.py file.
101+
pub setup_py: Option<String>,
102+
103+
/// The contents of an optional setup.cfg file.
104+
pub setup_cfg: Option<String>,
105+
}
106+
107+
fn ignore_not_found<C>(result: std::io::Result<C>) -> std::io::Result<Option<C>> {
108+
match result {
109+
Ok(content) => Ok(Some(content)),
110+
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
111+
Err(err) => Err(err),
112+
}
113+
}
114+
115+
/// Ensure that line endings are normalized to `\n` this ensures that if files are checked out on
116+
/// windows through git they still have the same hash as on linux.
117+
fn normalize_file_contents(contents: &str) -> String {
118+
contents.replace("\r\n", "\n")
119+
}
120+
121+
impl PypiSourceTreeHashable {
122+
/// Creates a new [`PypiSourceTreeHashable`] from a directory containing a source package.
123+
pub fn from_directory(directory: impl AsRef<Path>) -> std::io::Result<Self> {
124+
let directory = directory.as_ref();
125+
126+
let pyproject_toml =
127+
ignore_not_found(fs::read_to_string(directory.join("pyproject.toml")))?;
128+
let setup_py = ignore_not_found(fs::read_to_string(directory.join("setup.py")))?;
129+
let setup_cfg = ignore_not_found(fs::read_to_string(directory.join("setup.cfg")))?;
130+
131+
Ok(Self {
132+
pyproject_toml: pyproject_toml.as_deref().map(normalize_file_contents),
133+
setup_py: setup_py.as_deref().map(normalize_file_contents),
134+
setup_cfg: setup_cfg.as_deref().map(normalize_file_contents),
135+
})
136+
}
137+
138+
/// Determine the [`PackageHashes`] of this source package.
139+
pub fn hash(&self) -> PackageHashes {
140+
let mut hasher = Sha256::new();
141+
142+
if let Some(pyproject_toml) = &self.pyproject_toml {
143+
hasher.update(pyproject_toml.as_bytes());
144+
}
145+
146+
if let Some(setup_py) = &self.setup_py {
147+
hasher.update(setup_py.as_bytes());
148+
}
149+
150+
if let Some(setup_cfg) = &self.setup_cfg {
151+
hasher.update(setup_cfg.as_bytes());
152+
}
153+
154+
PackageHashes::Sha256(hasher.finalize())
155+
}
156+
}

crates/rattler_lock/src/snapshots/rattler_lock__test__v4__path-based-lock.yml.snap

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
source: crates/rattler_lock/src/lib.rs
3-
assertion_line: 597
3+
assertion_line: 602
44
expression: conda_lock
55
---
66
version: 4
@@ -251,6 +251,7 @@ packages:
251251
name: minimal-project
252252
version: "0.1"
253253
path: "./minimal_project"
254+
editable: true
254255
- kind: conda
255256
name: openssl
256257
version: 3.2.1

py-rattler/rattler/lock/pypi.py

+20
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,26 @@ def requires_python(self) -> Optional[str]:
151151
"""
152152
return self._data.requires_python
153153

154+
@property
155+
def is_editable(self) -> bool:
156+
"""
157+
Whether the package should be installed in editable mode or not.
158+
159+
Examples
160+
--------
161+
```python
162+
>>> from rattler import LockFile, Platform
163+
>>> lock_file = LockFile.from_path("../test-data/test.lock")
164+
>>> env = lock_file.default_environment()
165+
>>> pypi_packages = env.pypi_packages()
166+
>>> data = pypi_packages[Platform("osx-arm64")][0][0]
167+
>>> data.is_editable
168+
False
169+
>>>
170+
```
171+
"""
172+
return self._data.is_editable
173+
154174
@classmethod
155175
def _from_py_pypi_package_data(cls, pkg_data: PyPypiPackageData) -> PypiPackageData:
156176
"""

py-rattler/src/lock/mod.rs

+6
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,12 @@ impl PyPypiPackageData {
422422
self.inner.url_or_path.to_string()
423423
}
424424

425+
/// Whether the package is installed in editable mode or not.
426+
#[getter]
427+
pub fn is_editable(&self) -> bool {
428+
self.inner.editable
429+
}
430+
425431
/// Hashes of the file pointed to by `url`.
426432
#[getter]
427433
pub fn hash(&self) -> Option<PyPackageHashes> {

test-data/conda-lock/v4/path-based-lock.yml

+1
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ packages:
246246
name: minimal-project
247247
version: '0.1'
248248
path: ./minimal_project
249+
editable: true
249250
- kind: conda
250251
name: openssl
251252
version: 3.2.1

0 commit comments

Comments
 (0)