Use lockfile versions as resolution preferences (#3921)
## Summary Ensures that we avoid upgrading packages unless `--upgrade` or similar is passed. For now, the resolver only respects these for registry distributions. Closes https://github.com/astral-sh/uv/issues/3918.
This commit is contained in:
@@ -52,6 +52,11 @@ impl Lock {
|
||||
Lock::try_from(wire)
|
||||
}
|
||||
|
||||
/// Returns the [`Distribution`] entries in this lock.
|
||||
pub fn distributions(&self) -> &[Distribution] {
|
||||
&self.distributions
|
||||
}
|
||||
|
||||
pub fn to_resolution(
|
||||
&self,
|
||||
marker_env: &MarkerEnvironment,
|
||||
@@ -202,7 +207,7 @@ impl TryFrom<LockWire> for Lock {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub(crate) struct Distribution {
|
||||
pub struct Distribution {
|
||||
#[serde(flatten)]
|
||||
pub(crate) id: DistributionId,
|
||||
#[serde(default)]
|
||||
|
||||
@@ -86,6 +86,16 @@ impl Preference {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`Preference`] from a locked distribution.
|
||||
pub fn from_lock(dist: &crate::lock::Distribution) -> Self {
|
||||
Self {
|
||||
name: dist.id.name.clone(),
|
||||
version: dist.id.version.clone(),
|
||||
marker: None,
|
||||
hashes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the [`PackageName`] of the package for this [`Preference`].
|
||||
pub fn name(&self) -> &PackageName {
|
||||
&self.name
|
||||
|
||||
+70
-2
@@ -372,8 +372,6 @@ pub(crate) struct PipCompileArgs {
|
||||
#[arg(long, env = "UV_CUSTOM_COMPILE_COMMAND")]
|
||||
pub(crate) custom_compile_command: Option<String>,
|
||||
|
||||
/// Run offline, i.e., without accessing the network.
|
||||
|
||||
/// Refresh all cached data.
|
||||
#[arg(long, conflicts_with("offline"), overrides_with("no_refresh"))]
|
||||
pub(crate) refresh: bool,
|
||||
@@ -1786,6 +1784,33 @@ pub(crate) struct RunArgs {
|
||||
#[arg(long)]
|
||||
pub(crate) with: Vec<String>,
|
||||
|
||||
/// Refresh all cached data.
|
||||
#[arg(long, conflicts_with("offline"), overrides_with("no_refresh"))]
|
||||
pub(crate) refresh: bool,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
conflicts_with("offline"),
|
||||
overrides_with("refresh"),
|
||||
hide = true
|
||||
)]
|
||||
pub(crate) no_refresh: bool,
|
||||
|
||||
/// Refresh cached data for a specific package.
|
||||
#[arg(long)]
|
||||
pub(crate) refresh_package: Vec<PackageName>,
|
||||
|
||||
/// Allow package upgrades, ignoring pinned versions in the existing lockfile.
|
||||
#[arg(long, short = 'U', overrides_with("no_upgrade"))]
|
||||
pub(crate) upgrade: bool,
|
||||
|
||||
#[arg(long, overrides_with("upgrade"), hide = true)]
|
||||
pub(crate) no_upgrade: bool,
|
||||
|
||||
/// Allow upgrades for a specific package, ignoring pinned versions in the existing lockfile.
|
||||
#[arg(long, short = 'P')]
|
||||
pub(crate) upgrade_package: Vec<PackageName>,
|
||||
|
||||
/// The Python interpreter to use to build the run environment.
|
||||
///
|
||||
/// By default, `uv` uses the virtual environment in the current working directory or any parent
|
||||
@@ -1822,6 +1847,22 @@ pub(crate) struct SyncArgs {
|
||||
#[arg(long, overrides_with("all_extras"), hide = true)]
|
||||
pub(crate) no_all_extras: bool,
|
||||
|
||||
/// Refresh all cached data.
|
||||
#[arg(long, conflicts_with("offline"), overrides_with("no_refresh"))]
|
||||
pub(crate) refresh: bool,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
conflicts_with("offline"),
|
||||
overrides_with("refresh"),
|
||||
hide = true
|
||||
)]
|
||||
pub(crate) no_refresh: bool,
|
||||
|
||||
/// Refresh cached data for a specific package.
|
||||
#[arg(long)]
|
||||
pub(crate) refresh_package: Vec<PackageName>,
|
||||
|
||||
/// The Python interpreter to use to build the run environment.
|
||||
///
|
||||
/// By default, `uv` uses the virtual environment in the current working directory or any parent
|
||||
@@ -1840,6 +1881,33 @@ pub(crate) struct SyncArgs {
|
||||
#[derive(Args)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub(crate) struct LockArgs {
|
||||
/// Refresh all cached data.
|
||||
#[arg(long, conflicts_with("offline"), overrides_with("no_refresh"))]
|
||||
pub(crate) refresh: bool,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
conflicts_with("offline"),
|
||||
overrides_with("refresh"),
|
||||
hide = true
|
||||
)]
|
||||
pub(crate) no_refresh: bool,
|
||||
|
||||
/// Refresh cached data for a specific package.
|
||||
#[arg(long)]
|
||||
pub(crate) refresh_package: Vec<PackageName>,
|
||||
|
||||
/// Allow package upgrades, ignoring pinned versions in the existing lockfile.
|
||||
#[arg(long, short = 'U', overrides_with("no_upgrade"))]
|
||||
pub(crate) upgrade: bool,
|
||||
|
||||
#[arg(long, overrides_with("upgrade"), hide = true)]
|
||||
pub(crate) no_upgrade: bool,
|
||||
|
||||
/// Allow upgrades for a specific package, ignoring pinned versions in the existing lockfile.
|
||||
#[arg(long, short = 'P')]
|
||||
pub(crate) upgrade_package: Vec<PackageName>,
|
||||
|
||||
/// The Python interpreter to use to build the run environment.
|
||||
///
|
||||
/// By default, `uv` uses the virtual environment in the current working directory or any parent
|
||||
|
||||
@@ -12,7 +12,7 @@ use uv_configuration::{
|
||||
use uv_dispatch::BuildDispatch;
|
||||
use uv_interpreter::PythonEnvironment;
|
||||
use uv_requirements::ProjectWorkspace;
|
||||
use uv_resolver::{ExcludeNewer, FlatIndex, InMemoryIndex, Lock, OptionsBuilder};
|
||||
use uv_resolver::{ExcludeNewer, FlatIndex, InMemoryIndex, Lock, OptionsBuilder, Preference};
|
||||
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight};
|
||||
use uv_warnings::warn_user;
|
||||
|
||||
@@ -23,6 +23,7 @@ use crate::printer::Printer;
|
||||
/// Resolve the project requirements into a lockfile.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn lock(
|
||||
upgrade: Upgrade,
|
||||
exclude_newer: Option<ExcludeNewer>,
|
||||
preview: PreviewMode,
|
||||
cache: &Cache,
|
||||
@@ -39,7 +40,7 @@ pub(crate) async fn lock(
|
||||
let venv = project::init_environment(&project, preview, cache, printer)?;
|
||||
|
||||
// Perform the lock operation.
|
||||
match do_lock(&project, &venv, exclude_newer, cache, printer).await {
|
||||
match do_lock(&project, &venv, upgrade, exclude_newer, cache, printer).await {
|
||||
Ok(_) => Ok(ExitStatus::Success),
|
||||
Err(ProjectError::Operation(pip::operations::Error::Resolve(
|
||||
uv_resolver::ResolveError::NoSolution(err),
|
||||
@@ -57,6 +58,7 @@ pub(crate) async fn lock(
|
||||
pub(super) async fn do_lock(
|
||||
project: &ProjectWorkspace,
|
||||
venv: &PythonEnvironment,
|
||||
upgrade: Upgrade,
|
||||
exclude_newer: Option<ExcludeNewer>,
|
||||
cache: &Cache,
|
||||
printer: Printer,
|
||||
@@ -96,14 +98,34 @@ pub(super) async fn do_lock(
|
||||
let link_mode = LinkMode::default();
|
||||
let no_binary = NoBinary::default();
|
||||
let no_build = NoBuild::default();
|
||||
let preferences = Vec::default();
|
||||
let reinstall = Reinstall::default();
|
||||
let setup_py = SetupPyStrategy::default();
|
||||
let upgrade = Upgrade::default();
|
||||
|
||||
let hasher = HashStrategy::Generate;
|
||||
let options = OptionsBuilder::new().exclude_newer(exclude_newer).build();
|
||||
|
||||
// If an existing lockfile exists, build up a set of preferences.
|
||||
let lockfile = project.workspace().root().join("uv.lock");
|
||||
let lock = match fs_err::tokio::read_to_string(&lockfile).await {
|
||||
Ok(encoded) => match toml::from_str::<Lock>(&encoded) {
|
||||
Ok(lock) => Some(lock),
|
||||
Err(err) => {
|
||||
eprint!("Failed to parse lockfile; ignoring locked requirements: {err}");
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
let preferences: Vec<Preference> = lock
|
||||
.map(|lock| {
|
||||
lock.distributions()
|
||||
.iter()
|
||||
.map(Preference::from_lock)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// Create a build dispatch.
|
||||
let build_dispatch = BuildDispatch::new(
|
||||
&client,
|
||||
|
||||
@@ -9,7 +9,7 @@ use tracing::debug;
|
||||
|
||||
use uv_cache::Cache;
|
||||
use uv_client::Connectivity;
|
||||
use uv_configuration::{ExtrasSpecification, PreviewMode};
|
||||
use uv_configuration::{ExtrasSpecification, PreviewMode, Upgrade};
|
||||
use uv_interpreter::{PythonEnvironment, SystemPython};
|
||||
use uv_requirements::{ProjectWorkspace, RequirementsSource};
|
||||
use uv_resolver::ExcludeNewer;
|
||||
@@ -26,6 +26,7 @@ pub(crate) async fn run(
|
||||
mut args: Vec<OsString>,
|
||||
requirements: Vec<RequirementsSource>,
|
||||
python: Option<String>,
|
||||
upgrade: Upgrade,
|
||||
exclude_newer: Option<ExcludeNewer>,
|
||||
isolated: bool,
|
||||
preview: PreviewMode,
|
||||
@@ -47,7 +48,8 @@ pub(crate) async fn run(
|
||||
let venv = project::init_environment(&project, preview, cache, printer)?;
|
||||
|
||||
// Lock and sync the environment.
|
||||
let lock = project::lock::do_lock(&project, &venv, exclude_newer, cache, printer).await?;
|
||||
let lock =
|
||||
project::lock::do_lock(&project, &venv, upgrade, exclude_newer, cache, printer).await?;
|
||||
project::sync::do_sync(&project, &venv, &lock, extras, cache, printer).await?;
|
||||
|
||||
Some(venv)
|
||||
|
||||
+12
-4
@@ -550,7 +550,7 @@ async fn run() -> Result<ExitStatus> {
|
||||
let args = settings::RunSettings::resolve(args, workspace);
|
||||
|
||||
// Initialize the cache.
|
||||
let cache = cache.init()?;
|
||||
let cache = cache.init()?.with_refresh(args.refresh);
|
||||
|
||||
let requirements = args
|
||||
.with
|
||||
@@ -578,6 +578,7 @@ async fn run() -> Result<ExitStatus> {
|
||||
args.args,
|
||||
requirements,
|
||||
args.python,
|
||||
args.upgrade,
|
||||
args.exclude_newer,
|
||||
globals.isolated,
|
||||
globals.preview,
|
||||
@@ -592,7 +593,7 @@ async fn run() -> Result<ExitStatus> {
|
||||
let args = settings::SyncSettings::resolve(args, workspace);
|
||||
|
||||
// Initialize the cache.
|
||||
let cache = cache.init()?;
|
||||
let cache = cache.init()?.with_refresh(args.refresh);
|
||||
|
||||
commands::sync(args.extras, globals.preview, &cache, printer).await
|
||||
}
|
||||
@@ -601,9 +602,16 @@ async fn run() -> Result<ExitStatus> {
|
||||
let args = settings::LockSettings::resolve(args, workspace);
|
||||
|
||||
// Initialize the cache.
|
||||
let cache = cache.init()?;
|
||||
let cache = cache.init()?.with_refresh(args.refresh);
|
||||
|
||||
commands::lock(args.exclude_newer, globals.preview, &cache, printer).await
|
||||
commands::lock(
|
||||
args.upgrade,
|
||||
args.exclude_newer,
|
||||
globals.preview,
|
||||
&cache,
|
||||
printer,
|
||||
)
|
||||
.await
|
||||
}
|
||||
#[cfg(feature = "self-update")]
|
||||
Commands::Self_(SelfNamespace {
|
||||
|
||||
@@ -102,6 +102,8 @@ pub(crate) struct RunSettings {
|
||||
pub(crate) args: Vec<OsString>,
|
||||
pub(crate) with: Vec<String>,
|
||||
pub(crate) python: Option<String>,
|
||||
pub(crate) refresh: Refresh,
|
||||
pub(crate) upgrade: Upgrade,
|
||||
pub(crate) exclude_newer: Option<ExcludeNewer>,
|
||||
}
|
||||
|
||||
@@ -116,11 +118,19 @@ impl RunSettings {
|
||||
target,
|
||||
args,
|
||||
with,
|
||||
refresh,
|
||||
no_refresh,
|
||||
refresh_package,
|
||||
upgrade,
|
||||
no_upgrade,
|
||||
upgrade_package,
|
||||
python,
|
||||
exclude_newer,
|
||||
} = args;
|
||||
|
||||
Self {
|
||||
refresh: Refresh::from_args(flag(refresh, no_refresh), refresh_package),
|
||||
upgrade: Upgrade::from_args(flag(upgrade, no_upgrade), upgrade_package),
|
||||
extras: ExtrasSpecification::from_args(
|
||||
flag(all_extras, no_all_extras).unwrap_or_default(),
|
||||
extra.unwrap_or_default(),
|
||||
@@ -138,6 +148,7 @@ impl RunSettings {
|
||||
#[allow(clippy::struct_excessive_bools, dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct SyncSettings {
|
||||
pub(crate) refresh: Refresh,
|
||||
pub(crate) extras: ExtrasSpecification,
|
||||
pub(crate) python: Option<String>,
|
||||
}
|
||||
@@ -150,10 +161,14 @@ impl SyncSettings {
|
||||
extra,
|
||||
all_extras,
|
||||
no_all_extras,
|
||||
refresh,
|
||||
no_refresh,
|
||||
refresh_package,
|
||||
python,
|
||||
} = args;
|
||||
|
||||
Self {
|
||||
refresh: Refresh::from_args(flag(refresh, no_refresh), refresh_package),
|
||||
extras: ExtrasSpecification::from_args(
|
||||
flag(all_extras, no_all_extras).unwrap_or_default(),
|
||||
extra.unwrap_or_default(),
|
||||
@@ -167,6 +182,8 @@ impl SyncSettings {
|
||||
#[allow(clippy::struct_excessive_bools, dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct LockSettings {
|
||||
pub(crate) refresh: Refresh,
|
||||
pub(crate) upgrade: Upgrade,
|
||||
pub(crate) exclude_newer: Option<ExcludeNewer>,
|
||||
pub(crate) python: Option<String>,
|
||||
}
|
||||
@@ -176,11 +193,19 @@ impl LockSettings {
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub(crate) fn resolve(args: LockArgs, _workspace: Option<Workspace>) -> Self {
|
||||
let LockArgs {
|
||||
refresh,
|
||||
no_refresh,
|
||||
refresh_package,
|
||||
upgrade,
|
||||
no_upgrade,
|
||||
upgrade_package,
|
||||
exclude_newer,
|
||||
python,
|
||||
} = args;
|
||||
|
||||
Self {
|
||||
refresh: Refresh::from_args(flag(refresh, no_refresh), refresh_package),
|
||||
upgrade: Upgrade::from_args(flag(upgrade, no_upgrade), upgrade_package),
|
||||
exclude_newer,
|
||||
python,
|
||||
}
|
||||
|
||||
@@ -735,3 +735,183 @@ fn lock_extra() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Respect the locked version in an existing lockfile.
|
||||
#[test]
|
||||
fn lock_preference() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
dependencies = ["iniconfig<2"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv lock` is experimental and may change without warning.
|
||||
Resolved 2 packages in [TIME]
|
||||
"###);
|
||||
|
||||
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r###"
|
||||
version = 1
|
||||
|
||||
[[distribution]]
|
||||
name = "iniconfig"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
|
||||
[distribution.sdist]
|
||||
url = "https://files.pythonhosted.org/packages/23/a2/97899f6bd0e873fed3a7e67ae8d3a08b21799430fb4da15cfedf10d6e2c2/iniconfig-1.1.1.tar.gz"
|
||||
hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
|
||||
size = 8104
|
||||
|
||||
[[distribution.wheel]]
|
||||
url = "https://files.pythonhosted.org/packages/9b/dd/b3c12c6d707058fa947864b67f0c4e0c39ef8610988d7baea9578f3c48f3/iniconfig-1.1.1-py2.py3-none-any.whl"
|
||||
hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"
|
||||
size = 4990
|
||||
|
||||
[[distribution]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = "editable+file://[TEMP_DIR]/"
|
||||
|
||||
[distribution.sdist]
|
||||
url = "file://[TEMP_DIR]/"
|
||||
|
||||
[[distribution.dependencies]]
|
||||
name = "iniconfig"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
// Modify the `pyproject.toml` to loosen the requirement.
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
dependencies = ["iniconfig"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Ensure that the locked version is still respected.
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv lock` is experimental and may change without warning.
|
||||
Resolved 2 packages in [TIME]
|
||||
"###);
|
||||
|
||||
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r###"
|
||||
version = 1
|
||||
|
||||
[[distribution]]
|
||||
name = "iniconfig"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
|
||||
[distribution.sdist]
|
||||
url = "https://files.pythonhosted.org/packages/23/a2/97899f6bd0e873fed3a7e67ae8d3a08b21799430fb4da15cfedf10d6e2c2/iniconfig-1.1.1.tar.gz"
|
||||
hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
|
||||
size = 8104
|
||||
|
||||
[[distribution.wheel]]
|
||||
url = "https://files.pythonhosted.org/packages/9b/dd/b3c12c6d707058fa947864b67f0c4e0c39ef8610988d7baea9578f3c48f3/iniconfig-1.1.1-py2.py3-none-any.whl"
|
||||
hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"
|
||||
size = 4990
|
||||
|
||||
[[distribution]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = "editable+file://[TEMP_DIR]/"
|
||||
|
||||
[distribution.sdist]
|
||||
url = "file://[TEMP_DIR]/"
|
||||
|
||||
[[distribution.dependencies]]
|
||||
name = "iniconfig"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
// Run with `--upgrade`; ensure that `iniconfig` is upgraded.
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--upgrade"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv lock` is experimental and may change without warning.
|
||||
Resolved 2 packages in [TIME]
|
||||
"###);
|
||||
|
||||
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r###"
|
||||
version = 1
|
||||
|
||||
[[distribution]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
|
||||
[distribution.sdist]
|
||||
url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz"
|
||||
hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"
|
||||
size = 4646
|
||||
|
||||
[[distribution.wheel]]
|
||||
url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl"
|
||||
hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
|
||||
size = 5892
|
||||
|
||||
[[distribution]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = "editable+file://[TEMP_DIR]/"
|
||||
|
||||
[distribution.sdist]
|
||||
url = "file://[TEMP_DIR]/"
|
||||
|
||||
[[distribution.dependencies]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user