diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index d749825ef5000..c932bfeb2a663 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -1737,9 +1737,12 @@ pub struct PipOptions { /// (i.e., when each file was uploaded to the package index), not the release date of the /// package version. /// - /// Accepts a superset of [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html) (e.g., - /// `2006-12-02T02:07:43Z`). A full timestamp is required to ensure that the resolver will - /// behave consistently across timezones. + /// Accepts RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`), a "friendly" duration (e.g., + /// `24 hours`, `1 week`, `30 days`), or an ISO 8601 duration (e.g., `PT24H`, `P7D`, `P30D`). + /// + /// Durations do not respect semantics of the local time zone and are always resolved to a fixed + /// number of seconds assuming that a day is 24 hours (e.g., DST transitions are ignored). + /// Calendar units such as months and years are not allowed. #[option( default = "None", value_type = "str", @@ -1750,8 +1753,16 @@ pub struct PipOptions { pub exclude_newer: Option, /// Limit candidate packages for specific packages to those that were uploaded prior to the given date. /// - /// Accepts package-date pairs in a dictionary format. Set a package to `false` to exempt it - /// from the global [`exclude-newer`](#exclude-newer) constraint entirely. + /// Accepts a dictionary format of `PACKAGE = "DATE"` pairs, where `DATE` is an RFC 3339 + /// timestamp (e.g., `2006-12-02T02:07:43Z`), a "friendly" duration (e.g., `24 hours`, `1 week`, + /// `30 days`), or a ISO 8601 duration (e.g., `PT24H`, `P7D`, `P30D`). + /// + /// Durations do not respect semantics of the local time zone and are always resolved to a fixed + /// number of seconds assuming that a day is 24 hours (e.g., DST transitions are ignored). + /// Calendar units such as months and years are not allowed. + /// + /// Set a package to `false` to exempt it from the global [`exclude-newer`](#exclude-newer) + /// constraint entirely. #[option( default = "None", value_type = "dict", diff --git a/crates/uv/tests/it/main.rs b/crates/uv/tests/it/main.rs index 003c83a665bd2..9c118b78731b3 100644 --- a/crates/uv/tests/it/main.rs +++ b/crates/uv/tests/it/main.rs @@ -71,6 +71,9 @@ mod pip_compile_scenarios; #[cfg(all(feature = "test-python", feature = "test-pypi"))] mod pip_freeze; +#[cfg(all(feature = "test-python", feature = "test-pypi"))] +mod pip_exclude_newer_relative; + #[cfg(all(feature = "test-python", feature = "test-pypi"))] mod pip_install; diff --git a/crates/uv/tests/it/pip_exclude_newer_relative.rs b/crates/uv/tests/it/pip_exclude_newer_relative.rs new file mode 100644 index 0000000000000..a77311f1845e9 --- /dev/null +++ b/crates/uv/tests/it/pip_exclude_newer_relative.rs @@ -0,0 +1,123 @@ +use anyhow::Result; +use assert_fs::fixture::{FileWriteStr, PathChild}; +use uv_static::EnvVars; + +use uv_test::uv_snapshot; + +/// Install with relative `exclude-newer` values from the `uv pip` CLI. +/// +/// Uses idna which has releases at: +/// - 3.6: 2023-11-25 +/// - 3.7: 2024-04-11 +#[test] +fn pip_install_exclude_newer_relative() { + let context = uv_test::test_context!("3.12"); + let current_timestamp = "2024-05-01T00:00:00Z"; + + // 3 weeks before 2024-05-01 is 2024-04-10, which is before idna 3.7. + uv_snapshot!(context.filters(), context + .pip_install() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, current_timestamp) + .arg("--exclude-newer") + .arg("3 weeks") + .arg("idna"), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + idna==3.6 + "); + + // A package-specific span can relax the global cutoff for that package. + uv_snapshot!(context.filters(), context + .pip_install() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, current_timestamp) + .arg("--exclude-newer") + .arg("3 weeks") + .arg("--exclude-newer-package") + .arg("idna=2 weeks") + .arg("--upgrade") + .arg("idna"), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + - idna==3.6 + + idna==3.7 + "); +} + +/// Install with relative `exclude-newer` values from `[tool.uv.pip]`. +/// +/// Uses idna which has releases at: +/// - 3.6: 2023-11-25 +/// - 3.7: 2024-04-11 +#[test] +fn pip_install_exclude_newer_relative_config() -> Result<()> { + let context = uv_test::test_context!("3.12"); + let current_timestamp = "2024-05-01T00:00:00Z"; + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + + pyproject_toml.write_str( + r#" + [tool.uv.pip] + exclude-newer = "3 weeks" + "#, + )?; + + uv_snapshot!(context.filters(), context + .pip_install() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, current_timestamp) + .arg("idna"), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + idna==3.6 + "); + + pyproject_toml.write_str( + r#" + [tool.uv.pip] + exclude-newer = "3 weeks" + exclude-newer-package = { idna = "2 weeks" } + "#, + )?; + + uv_snapshot!(context.filters(), context + .pip_install() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, current_timestamp) + .arg("--upgrade") + .arg("idna"), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + - idna==3.6 + + idna==3.7 + "); + + Ok(()) +} diff --git a/uv.schema.json b/uv.schema.json index 3146d4c478626..acd6713e6aadb 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -1263,7 +1263,7 @@ "type": ["boolean", "null"] }, "exclude-newer": { - "description": "Limit candidate packages to those that were uploaded prior to a given point in time.\n\nThe date is compared against the upload time of each individual distribution artifact\n(i.e., when each file was uploaded to the package index), not the release date of the\npackage version.\n\nAccepts a superset of [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html) (e.g.,\n`2006-12-02T02:07:43Z`). A full timestamp is required to ensure that the resolver will\nbehave consistently across timezones.", + "description": "Limit candidate packages to those that were uploaded prior to a given point in time.\n\nThe date is compared against the upload time of each individual distribution artifact\n(i.e., when each file was uploaded to the package index), not the release date of the\npackage version.\n\nAccepts RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`), a \"friendly\" duration (e.g.,\n`24 hours`, `1 week`, `30 days`), or an ISO 8601 duration (e.g., `PT24H`, `P7D`, `P30D`).\n\nDurations do not respect semantics of the local time zone and are always resolved to a fixed\nnumber of seconds assuming that a day is 24 hours (e.g., DST transitions are ignored).\nCalendar units such as months and years are not allowed.", "anyOf": [ { "$ref": "#/definitions/ExcludeNewerValue" @@ -1274,7 +1274,7 @@ ] }, "exclude-newer-package": { - "description": "Limit candidate packages for specific packages to those that were uploaded prior to the given date.\n\nAccepts package-date pairs in a dictionary format. Set a package to `false` to exempt it\nfrom the global [`exclude-newer`](#exclude-newer) constraint entirely.", + "description": "Limit candidate packages for specific packages to those that were uploaded prior to the given date.\n\nAccepts a dictionary format of `PACKAGE = \"DATE\"` pairs, where `DATE` is an RFC 3339\ntimestamp (e.g., `2006-12-02T02:07:43Z`), a \"friendly\" duration (e.g., `24 hours`, `1 week`,\n`30 days`), or a ISO 8601 duration (e.g., `PT24H`, `P7D`, `P30D`).\n\nDurations do not respect semantics of the local time zone and are always resolved to a fixed\nnumber of seconds assuming that a day is 24 hours (e.g., DST transitions are ignored).\nCalendar units such as months and years are not allowed.\n\nSet a package to `false` to exempt it from the global [`exclude-newer`](#exclude-newer)\nconstraint entirely.", "anyOf": [ { "$ref": "#/definitions/ExcludeNewerPackage"