Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: Add uv license command #10292

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
280 changes: 134 additions & 146 deletions Cargo.lock

Large diffs are not rendered by default.

127 changes: 127 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,8 @@ pub enum ProjectCommand {
Export(ExportArgs),
/// Display the project's dependency tree.
Tree(TreeArgs),
/// Display the project's license information.
License(LicenseArgs),
}

/// A re-implementation of `Option`, used to avoid Clap's automatic `Option` flattening in
Expand Down Expand Up @@ -3463,6 +3465,131 @@ pub struct TreeArgs {
pub python: Option<Maybe<String>>,
}

#[derive(Args)]
#[allow(clippy::struct_excessive_bools)]
pub struct LicenseArgs {
/// Show full list of platform-independent dependency licenses.
///
/// Shows resolved package versions for all Python versions and platforms,
/// rather than filtering to those that are relevant for the current
/// environment.
///
/// Multiple versions may be shown for a each package.
#[arg(long)]
pub universal: bool,

/// Include the development dependency group.
///
/// Development dependencies are defined via `dependency-groups.dev` or
/// `tool.uv.dev-dependencies` in a `pyproject.toml`.
///
/// This option is an alias for `--group dev`.
#[arg(long, overrides_with("no_dev"), hide = true)]
pub dev: bool,

/// Only include the development dependency group.
///
/// Omit other dependencies. The project itself will also be omitted.
///
/// This option is an alias for `--only-group dev`.
#[arg(long, conflicts_with("no_dev"))]
pub only_dev: bool,

/// Omit the development dependency group.
///
/// This option is an alias for `--no-group dev`.
#[arg(long, overrides_with("dev"))]
pub no_dev: bool,

/// Include dependencies from the specified dependency group.
///
/// May be provided multiple times.
#[arg(long, conflicts_with("only_group"))]
pub group: Vec<GroupName>,

/// Exclude dependencies from the specified dependency group.
///
/// May be provided multiple times.
#[arg(long)]
pub no_group: Vec<GroupName>,

/// Only include dependencies from the specified dependency group.
///
/// May be provided multiple times.
///
/// The project itself will also be omitted.
#[arg(long, conflicts_with("group"))]
pub only_group: Vec<GroupName>,

/// Include dependencies from all dependency groups.
///
/// `--no-group` can be used to exclude specific groups.
#[arg(long, conflicts_with_all = [ "group", "only_group" ])]
pub all_groups: bool,

/// Display only direct dependencies (default false)
#[arg(long)]
pub direct_deps_only: bool,

/// Assert that the `uv.lock` will remain unchanged.
///
/// Requires that the lockfile is up-to-date. If the lockfile is missing or
/// needs to be updated, uv will exit with an error.
#[arg(long, env = EnvVars::UV_LOCKED, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with = "frozen")]
pub locked: bool,

/// Display the requirements without locking the project.
///
/// If the lockfile is missing, uv will exit with an error.
#[arg(long, env = EnvVars::UV_FROZEN, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with = "locked")]
pub frozen: bool,

#[command(flatten)]
pub build: BuildOptionsArgs,

#[command(flatten)]
pub resolver: ResolverArgs,

/// The Python version to use when filtering the tree.
///
/// For example, pass `--python-version 3.10` to display the dependencies
/// that would be included when installing on Python 3.10.
///
/// Defaults to the version of the discovered Python interpreter.
#[arg(long, conflicts_with = "universal")]
pub python_version: Option<PythonVersion>,

/// The platform to use when filtering the tree.
///
/// For example, pass `--platform windows` to display the dependencies that
/// would be included when installing on Windows.
///
/// Represented as a "target triple", a string that describes the target
/// platform in terms of its CPU, vendor, and operating system name, like
/// `x86_64-unknown-linux-gnu` or `aarch64-apple-darwin`.
#[arg(long, conflicts_with = "universal")]
pub python_platform: Option<TargetTriple>,

/// The Python interpreter to use for locking and filtering.
///
/// By default, the tree is filtered to match the platform as reported by
/// the Python interpreter. Use `--universal` to display the tree for all
/// platforms, or use `--python-version` or `--python-platform` to override
/// a subset of markers.
///
/// See `uv help python` for details on Python discovery and supported
/// request formats.
#[arg(
long,
short,
env = EnvVars::UV_PYTHON,
verbatim_doc_comment,
help_heading = "Python options",
value_parser = parse_maybe_string,
)]
pub python: Option<Maybe<String>>,
}

#[derive(Args)]
#[allow(clippy::struct_excessive_bools)]
pub struct ExportArgs {
Expand Down
7 changes: 7 additions & 0 deletions crates/uv-distribution-types/src/dependency_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,15 @@ impl DependencyMetadata {
return None;
};
debug!("Found dependency metadata entry for `{package}=={version}`",);

Some(ResolutionMetadata {
name: metadata.name.clone(),
version: version.clone(),
requires_dist: metadata.requires_dist.clone(),
requires_python: metadata.requires_python.clone(),
provides_extras: metadata.provides_extras.clone(),
classifiers: metadata.classifiers.clone(),
license: metadata.license.clone(),
})
} else {
// If no version was requested (i.e., it's a direct URL dependency), allow a single
Expand All @@ -70,6 +73,8 @@ impl DependencyMetadata {
requires_dist: metadata.requires_dist.clone(),
requires_python: metadata.requires_python.clone(),
provides_extras: metadata.provides_extras.clone(),
classifiers: metadata.classifiers.clone(),
license: metadata.license.clone(),
})
}
}
Expand Down Expand Up @@ -109,4 +114,6 @@ pub struct StaticMetadata {
pub requires_python: Option<VersionSpecifiers>,
#[serde(default)]
pub provides_extras: Vec<ExtraName>,
pub classifiers: Option<Vec<String>>,
pub license: Option<String>,
}
6 changes: 6 additions & 0 deletions crates/uv-distribution/src/metadata/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ pub struct Metadata {
pub requires_python: Option<VersionSpecifiers>,
pub provides_extras: Vec<ExtraName>,
pub dependency_groups: BTreeMap<GroupName, Vec<uv_pypi_types::Requirement>>,
pub classifiers: Option<Vec<String>>,
pub license: Option<String>,
}

impl Metadata {
Expand All @@ -67,6 +69,8 @@ impl Metadata {
requires_python: metadata.requires_python,
provides_extras: metadata.provides_extras,
dependency_groups: BTreeMap::default(),
classifiers: metadata.classifiers,
license: metadata.license,
}
}

Expand Down Expand Up @@ -109,6 +113,8 @@ impl Metadata {
requires_python: metadata.requires_python,
provides_extras,
dependency_groups,
classifiers: metadata.classifiers,
license: metadata.license,
})
}
}
Expand Down
8 changes: 7 additions & 1 deletion crates/uv-distribution/src/source/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ use uv_metadata::read_archive_metadata;
use uv_normalize::PackageName;
use uv_pep440::{release_specifiers_to_ranges, Version};
use uv_platform_tags::Tags;
use uv_pypi_types::{HashAlgorithm, HashDigest, Metadata12, RequiresTxt, ResolutionMetadata};
use uv_pypi_types::{
HashAlgorithm, HashDigest, Metadata12, Metadata23, RequiresTxt, ResolutionMetadata,
};
use uv_types::{BuildContext, BuildStack, SourceBuildTrait};
use zip::ZipArchive;

Expand Down Expand Up @@ -2448,6 +2450,7 @@ async fn read_egg_info(

// Parse the metadata.
let metadata = Metadata12::parse_metadata(&content).map_err(Error::PkgInfo)?;
let metadata23 = Metadata23::parse(&content).map_err(Error::PkgInfo)?;

// Combine the sources.
Ok(ResolutionMetadata {
Expand All @@ -2456,6 +2459,9 @@ async fn read_egg_info(
requires_python: metadata.requires_python,
requires_dist: requires_txt.requires_dist,
provides_extras: requires_txt.provides_extras,
classifiers: Some(metadata23.classifiers),
// TODO(RL): collapse metadata23.license / metadata23.license_expression [pep639] / metadata23.license_files
license: metadata23.license,
})
}

Expand Down
14 changes: 14 additions & 0 deletions crates/uv-pypi-types/src/metadata/metadata_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ pub struct ResolutionMetadata {
pub requires_dist: Vec<Requirement<VerbatimParsedUrl>>,
pub requires_python: Option<VersionSpecifiers>,
pub provides_extras: Vec<ExtraName>,
#[serde(default)]
pub classifiers: Option<Vec<String>>,
#[serde(default)]
pub license: Option<String>,
}

/// From <https://github.com/PyO3/python-pkginfo-rs/blob/d719988323a0cfea86d4737116d7917f30e819e2/src/metadata.rs#LL78C2-L91C26>
Expand Down Expand Up @@ -68,13 +72,17 @@ impl ResolutionMetadata {
}
})
.collect::<Vec<_>>();
let classifiers = Some(headers.get_all_values("Classifier").collect::<Vec<_>>());
let license = headers.get_first_value("License");

Ok(Self {
name,
version,
requires_dist,
requires_python,
provides_extras,
classifiers,
license,
})
}

Expand Down Expand Up @@ -141,13 +149,17 @@ impl ResolutionMetadata {
}
})
.collect::<Vec<_>>();
let classifiers = Some(headers.get_all_values("Classifiers").collect::<Vec<_>>());
let license = headers.get_first_value("License");

Ok(Self {
name,
version,
requires_dist,
requires_python,
provides_extras,
classifiers,
license,
})
}

Expand Down Expand Up @@ -231,4 +243,6 @@ mod tests {
assert_eq!(meta.version, Version::new([1, 0]));
assert_eq!(meta.requires_dist, vec!["foo".parse().unwrap()]);
}

// TODO(RL): write test cases for checking classifier information
}
14 changes: 14 additions & 0 deletions crates/uv-pypi-types/src/metadata/pyproject_toml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,22 @@ pub(crate) fn parse_pyproject_toml(
);
provides_extras.push(extra);
}
let classifiers = Some(
project
.classifiers
.unwrap_or_default()
.into_iter()
.collect::<Vec<_>>(),
);

Ok(ResolutionMetadata {
name,
version,
requires_dist,
requires_python,
provides_extras,
classifiers,
license: None, // TODO(RL): come back
})
}

Expand Down Expand Up @@ -142,6 +151,9 @@ struct Project {
/// Specifies which fields listed by PEP 621 were intentionally unspecified
/// so another tool can/will provide such metadata dynamically.
dynamic: Option<Vec<String>>,
// Specifies zero or more "Trove Classifiers" to describe the project.
classifiers: Option<Vec<String>>,
// TODO(RL): handle license field properly
}

#[derive(Deserialize, Debug)]
Expand All @@ -153,6 +165,7 @@ struct PyprojectTomlWire {
dependencies: Option<Vec<String>>,
optional_dependencies: Option<IndexMap<ExtraName, Vec<String>>>,
dynamic: Option<Vec<String>>,
classifiers: Option<Vec<String>>,
}

impl TryFrom<PyprojectTomlWire> for Project {
Expand All @@ -167,6 +180,7 @@ impl TryFrom<PyprojectTomlWire> for Project {
dependencies: wire.dependencies,
optional_dependencies: wire.optional_dependencies,
dynamic: wire.dynamic,
classifiers: wire.classifiers,
})
}
}
Expand Down
4 changes: 2 additions & 2 deletions crates/uv-resolver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ pub use exclusions::Exclusions;
pub use flat_index::{FlatDistributions, FlatIndex};
pub use fork_strategy::ForkStrategy;
pub use lock::{
Installable, Lock, LockError, LockVersion, Package, PackageMap, RequirementsTxtExport,
ResolverManifest, SatisfiesResult, TreeDisplay, VERSION,
Installable, LicenseDisplay, Lock, LockError, LockVersion, Package, PackageMap,
RequirementsTxtExport, ResolverManifest, SatisfiesResult, TreeDisplay, VERSION,
};
pub use manifest::Manifest;
pub use options::{Flexibility, Options, OptionsBuilder};
Expand Down
Loading
Loading