From dd0e207bfa83907ff5b6fb7ce982df3f61763206 Mon Sep 17 00:00:00 2001 From: Ryan Leary Date: Sat, 14 Dec 2024 13:54:34 -0500 Subject: [PATCH 1/7] feat: add uv license command --- Cargo.lock | 280 ++++++------ crates/uv-cli/src/lib.rs | 128 ++++++ .../src/dependency_metadata.rs | 7 + crates/uv-distribution/src/metadata/mod.rs | 6 + crates/uv-distribution/src/source/mod.rs | 6 +- .../src/metadata/metadata_resolver.rs | 20 + .../src/metadata/pyproject_toml.rs | 13 + crates/uv-resolver/src/lib.rs | 2 +- crates/uv-resolver/src/lock/license.rs | 429 ++++++++++++++++++ crates/uv-resolver/src/lock/mod.rs | 82 +++- crates/uv/src/commands/mod.rs | 1 + crates/uv/src/commands/pip/show.rs | 9 + crates/uv/src/commands/project/license.rs | 251 ++++++++++ crates/uv/src/commands/project/mod.rs | 1 + crates/uv/src/lib.rs | 34 ++ crates/uv/src/settings.rs | 62 ++- 16 files changed, 1180 insertions(+), 151 deletions(-) create mode 100644 crates/uv-resolver/src/lock/license.rs create mode 100644 crates/uv/src/commands/project/license.rs diff --git a/Cargo.lock b/Cargo.lock index a0fc17c8a64a..c17cbcb0d506 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,9 +89,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.95" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "arrayref" @@ -235,9 +235,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "axoasset" -version = "1.2.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ba1098cfaa17f0973d2b766ee07bedb3e81a29b35c8d8b26de5074e37011443" +checksum = "489aa74cfacfaf4cabcb0c2bb63da396b2a5aa70937050bc9dc0233bd7c9b85c" dependencies = [ "camino", "image", @@ -247,7 +247,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "thiserror 2.0.9", + "thiserror 1.0.69", "url", "walkdir", ] @@ -289,7 +289,7 @@ dependencies = [ "self-replace", "serde", "tempfile", - "thiserror 2.0.9", + "thiserror 2.0.7", "tokio", "url", ] @@ -370,9 +370,9 @@ checksum = "2721c3c5a6f0e7f7e607125d963fedeb765f545f67adc9d71ed934693881eb42" [[package]] name = "bstr" -version = "1.11.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8" +checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" dependencies = [ "memchr", "regex-automata 0.4.9", @@ -410,9 +410,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.21.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" +checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" [[package]] name = "byteorder" @@ -502,9 +502,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.5" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" +checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" dependencies = [ "jobserver", "libc", @@ -585,9 +585,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.40" +version = "4.5.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac2e663e3e3bed2d32d065a8404024dad306e699a04263ec59919529f803aee9" +checksum = "d9647a559c112175f17cf724dc72d3645680a883c58481332779192b0d8e7a01" dependencies = [ "clap", ] @@ -667,9 +667,9 @@ checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "colored" -version = "2.2.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" dependencies = [ "lazy_static", "windows-sys 0.48.0", @@ -692,15 +692,15 @@ checksum = "e57e3272f0190c3f1584272d613719ba5fc7df7f4942fe542e63d949cf3a649b" [[package]] name = "console" -version = "0.15.10" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode", + "lazy_static", "libc", - "once_cell", - "unicode-width 0.2.0", - "windows-sys 0.59.0", + "unicode-width 0.1.14", + "windows-sys 0.52.0", ] [[package]] @@ -775,9 +775,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.6" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -794,9 +794,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crunchy" @@ -975,9 +975,9 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "encode_unicode" -version = "1.0.0" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" @@ -997,12 +997,6 @@ dependencies = [ "encoding_rs", ] -[[package]] -name = "env_home" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" - [[package]] name = "equivalent" version = "1.0.1" @@ -1063,9 +1057,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" [[package]] name = "fdeflate" @@ -1110,12 +1104,6 @@ name = "float-cmp" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" - -[[package]] -name = "float-cmp" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" dependencies = [ "num-traits", ] @@ -1128,9 +1116,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.4" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" [[package]] name = "fontconfig-parser" @@ -1445,11 +1433,11 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "home" -version = "0.5.11" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1527,9 +1515,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.5.2" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" dependencies = [ "bytes", "futures-channel", @@ -1548,9 +1536,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.5" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", "http", @@ -1970,9 +1958,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.169" +version = "0.2.167" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" [[package]] name = "libmimalloc-sys" @@ -1992,14 +1980,14 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.6.0", "libc", - "redox_syscall 0.5.8", + "redox_syscall 0.5.7", ] [[package]] name = "libz-rs-sys" -version = "0.4.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90e19106f1b2c93f1fa6cdeec2e56facbf2e403559c1e1c0ddcc6d46e979cdf" +checksum = "39cc71ac688c22a9f5730a38171ac94795c071ac81d1a0ab5537f6ef164fff30" dependencies = [ "zlib-rs", ] @@ -2170,9 +2158,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.8.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ "adler2", "simd-adler32", @@ -2291,9 +2279,9 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "object" -version = "0.36.7" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] @@ -2392,7 +2380,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.8", + "redox_syscall 0.5.7", "smallvec", "windows-targets 0.52.6", ] @@ -2428,7 +2416,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" dependencies = [ "memchr", - "thiserror 2.0.9", + "thiserror 2.0.7", "ucd-trie", ] @@ -2534,9 +2522,9 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "platform-info" -version = "2.0.5" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7539aeb3fdd8cb4f6a331307cf71a1039cee75e94e8a71725b9484f4a0d9451a" +checksum = "91077ffd05d058d70d79eefcd7d7f6aac34980860a7519960f7913b6563a8c3a" dependencies = [ "libc", "winapi", @@ -2544,9 +2532,9 @@ dependencies = [ [[package]] name = "png" -version = "0.17.16" +version = "0.17.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +checksum = "b67582bd5b65bdff614270e2ea89a1cf15bef71245cc1e5f7ea126977144211d" dependencies = [ "bitflags 1.3.2", "crc32fast", @@ -2581,13 +2569,13 @@ dependencies = [ [[package]] name = "predicates" -version = "3.1.3" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" dependencies = [ "anstyle", "difflib", - "float-cmp 0.10.0", + "float-cmp", "normalize-line-endings", "predicates-core", "regex", @@ -2595,15 +2583,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.9" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" +checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" [[package]] name = "predicates-tree" -version = "1.0.12" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" dependencies = [ "predicates-core", "termtree", @@ -2691,7 +2679,7 @@ dependencies = [ "log", "priority-queue", "rustc-hash", - "thiserror 2.0.9", + "thiserror 2.0.7", "version-ranges", ] @@ -2708,7 +2696,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.9", + "thiserror 2.0.7", "tokio", "tracing", ] @@ -2727,7 +2715,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.9", + "thiserror 2.0.7", "tinyvec", "tracing", "web-time", @@ -2735,9 +2723,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.9" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904" +checksum = "7d5a626c6807713b15cac82a6acaccd6043c9a5408c24baae07611fec3f243da" dependencies = [ "cfg_aliases", "libc", @@ -2749,9 +2737,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.38" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -2847,9 +2835,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.8" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ "bitflags 2.6.0", ] @@ -3185,9 +3173,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.20" +version = "0.23.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" +checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1" dependencies = [ "once_cell", "ring", @@ -3220,9 +3208,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" dependencies = [ "web-time", ] @@ -3337,9 +3325,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "security-framework" -version = "3.1.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81d3f8c9bfcc3cbb6b0179eb57042d75b1582bdc65c3cb95f3fa999509c03cbc" +checksum = "e1415a607e92bec364ea2cf9264646dcce0f91e6d65281bd6f2819cca3bf39c8" dependencies = [ "bitflags 2.6.0", "core-foundation", @@ -3350,9 +3338,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.13.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1863fd3768cd83c56a7f60faa4dc0d403f1b6df0a38c3c25f44b7894e45370d5" +checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" dependencies = [ "core-foundation-sys", "libc", @@ -3371,9 +3359,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.24" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" @@ -3419,9 +3407,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.134" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", @@ -3593,7 +3581,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" dependencies = [ - "float-cmp 0.9.0", + "float-cmp", ] [[package]] @@ -3641,7 +3629,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "639abcebc15fdc2df179f37d6f5463d660c1c79cd552c12343a4600827a04bce" dependencies = [ - "float-cmp 0.9.0", + "float-cmp", "rgb", ] @@ -3725,9 +3713,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.13.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc12939a1c9b9d391e0b7135f72fd30508b73450753e28341fed159317582a77" +checksum = "4ff4a4048091358129767b8a200d6927f58876c8b5ea16fb7b0222d43b79bfa8" [[package]] name = "temp-env" @@ -3773,9 +3761,9 @@ dependencies = [ [[package]] name = "termtree" -version = "0.5.1" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "test-case" @@ -3853,11 +3841,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.9" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" +checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767" dependencies = [ - "thiserror-impl 2.0.9", + "thiserror-impl 2.0.7", ] [[package]] @@ -3873,9 +3861,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.9" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" +checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36" dependencies = [ "proc-macro2", "quote", @@ -3959,9 +3947,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.8.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] @@ -4251,15 +4239,15 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicase" -version = "2.8.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" [[package]] name = "unicode-bidi" -version = "0.3.18" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" [[package]] name = "unicode-bidi-mirroring" @@ -4451,7 +4439,7 @@ dependencies = [ "tar", "tempfile", "textwrap", - "thiserror 2.0.9", + "thiserror 2.0.7", "tokio", "toml", "toml_edit", @@ -4576,7 +4564,7 @@ dependencies = [ "spdx", "tar", "tempfile", - "thiserror 2.0.9", + "thiserror 2.0.7", "toml", "tracing", "uv-distribution-filename", @@ -4608,7 +4596,7 @@ dependencies = [ "serde", "serde_json", "tempfile", - "thiserror 2.0.9", + "thiserror 2.0.7", "tokio", "toml_edit", "tracing", @@ -4657,7 +4645,7 @@ dependencies = [ "globwalk", "schemars", "serde", - "thiserror 2.0.9", + "thiserror 2.0.7", "toml", "tracing", ] @@ -4725,7 +4713,7 @@ dependencies = [ "serde", "serde_json", "sys-info", - "thiserror 2.0.9", + "thiserror 2.0.7", "tl", "tokio", "tokio-util", @@ -4764,7 +4752,7 @@ dependencies = [ "serde", "serde-untagged", "serde_json", - "thiserror 2.0.9", + "thiserror 2.0.7", "tracing", "url", "uv-auth", @@ -4847,7 +4835,7 @@ dependencies = [ "futures", "itertools 0.13.0", "rustc-hash", - "thiserror 2.0.9", + "thiserror 2.0.7", "tokio", "tracing", "uv-build-backend", @@ -4887,7 +4875,7 @@ dependencies = [ "rustc-hash", "serde", "tempfile", - "thiserror 2.0.9", + "thiserror 2.0.7", "tokio", "tokio-util", "tracing", @@ -4922,7 +4910,7 @@ dependencies = [ "insta", "rkyv", "serde", - "thiserror 2.0.9", + "thiserror 2.0.7", "url", "uv-normalize", "uv-pep440", @@ -4944,7 +4932,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "thiserror 2.0.9", + "thiserror 2.0.7", "tracing", "url", "urlencoding", @@ -4976,7 +4964,7 @@ dependencies = [ "reqwest", "rustc-hash", "sha2", - "thiserror 2.0.9", + "thiserror 2.0.7", "tokio", "tokio-util", "tracing", @@ -5021,7 +5009,7 @@ dependencies = [ "reqwest", "reqwest-middleware", "serde", - "thiserror 2.0.9", + "thiserror 2.0.7", "tokio", "tracing", "url", @@ -5042,7 +5030,7 @@ dependencies = [ "regex", "regex-automata 0.4.9", "tempfile", - "thiserror 2.0.9", + "thiserror 2.0.7", "tracing", "walkdir", ] @@ -5072,7 +5060,7 @@ dependencies = [ "serde_json", "sha2", "tempfile", - "thiserror 2.0.9", + "thiserror 2.0.7", "tracing", "uv-cache-info", "uv-distribution-filename", @@ -5100,7 +5088,7 @@ dependencies = [ "rustc-hash", "same-file", "tempfile", - "thiserror 2.0.9", + "thiserror 2.0.7", "tokio", "tracing", "url", @@ -5141,7 +5129,7 @@ dependencies = [ "async_zip", "fs-err 3.0.0", "futures", - "thiserror 2.0.9", + "thiserror 2.0.7", "tokio", "tokio-util", "uv-distribution-filename", @@ -5202,7 +5190,7 @@ dependencies = [ "serde", "serde_json", "smallvec", - "thiserror 2.0.9", + "thiserror 2.0.7", "tracing", "tracing-test", "unicode-width 0.1.14", @@ -5235,7 +5223,7 @@ dependencies = [ "insta", "rustc-hash", "serde", - "thiserror 2.0.9", + "thiserror 2.0.7", ] [[package]] @@ -5256,7 +5244,7 @@ dependencies = [ "rustc-hash", "serde", "serde_json", - "thiserror 2.0.9", + "thiserror 2.0.7", "tokio", "tokio-util", "tracing", @@ -5289,7 +5277,7 @@ dependencies = [ "schemars", "serde", "serde-untagged", - "thiserror 2.0.9", + "thiserror 2.0.7", "toml", "toml_edit", "tracing", @@ -5331,7 +5319,7 @@ dependencies = [ "temp-env", "tempfile", "test-log", - "thiserror 2.0.9", + "thiserror 2.0.7", "tokio", "tokio-util", "tracing", @@ -5370,7 +5358,7 @@ dependencies = [ "futures", "rustc-hash", "serde", - "thiserror 2.0.9", + "thiserror 2.0.7", "toml", "tracing", "url", @@ -5408,7 +5396,7 @@ dependencies = [ "reqwest-middleware", "tempfile", "test-case", - "thiserror 2.0.9", + "thiserror 2.0.7", "tokio", "tracing", "unscanny", @@ -5445,7 +5433,7 @@ dependencies = [ "schemars", "serde", "textwrap", - "thiserror 2.0.9", + "thiserror 2.0.7", "tokio", "tokio-stream", "toml", @@ -5483,7 +5471,7 @@ dependencies = [ "indoc", "memchr", "serde", - "thiserror 2.0.9", + "thiserror 2.0.7", "toml", "uv-pep440", "uv-pep508", @@ -5504,7 +5492,7 @@ dependencies = [ "schemars", "serde", "textwrap", - "thiserror 2.0.9", + "thiserror 2.0.7", "toml", "tracing", "url", @@ -5561,7 +5549,7 @@ dependencies = [ "pathdiff", "self-replace", "serde", - "thiserror 2.0.9", + "thiserror 2.0.7", "toml", "toml_edit", "tracing", @@ -5588,7 +5576,7 @@ dependencies = [ "assert_cmd", "assert_fs", "fs-err 3.0.0", - "thiserror 2.0.9", + "thiserror 2.0.7", "uv-fs", "which", "zip", @@ -5600,7 +5588,7 @@ version = "0.0.1" dependencies = [ "anyhow", "rustc-hash", - "thiserror 2.0.9", + "thiserror 2.0.7", "url", "uv-cache", "uv-configuration", @@ -5627,7 +5615,7 @@ dependencies = [ "itertools 0.13.0", "pathdiff", "self-replace", - "thiserror 2.0.9", + "thiserror 2.0.7", "tracing", "uv-fs", "uv-platform-tags", @@ -5663,7 +5651,7 @@ dependencies = [ "schemars", "serde", "tempfile", - "thiserror 2.0.9", + "thiserror 2.0.7", "tokio", "toml", "toml_edit", @@ -5869,12 +5857,12 @@ checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" [[package]] name = "which" -version = "7.0.1" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb4a9e33648339dc1642b0e36e21b3385e6148e289226f657c809dee59df5028" +checksum = "c9cad3279ade7346b96e38731a641d7343dd6a53d55083dd54eadfa5a1b38c6b" dependencies = [ "either", - "env_home", + "home", "regex", "rustix", "winsafe 0.0.19", @@ -6420,9 +6408,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.4.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aada01553a9312bad4b9569035a1f12b05e5ec9770a1a4b323757356928944f8" +checksum = "2ca4a9dc6566c9224cc161dedc5577bd81f4a9ee0f9fbe80592756d096b07ee5" [[package]] name = "zstd" diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index bddc6a5f7a37..09ad3d31e7b3 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -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 @@ -3463,6 +3465,132 @@ pub struct TreeArgs { pub python: Option>, } + +#[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, + + /// Exclude dependencies from the specified dependency group. + /// + /// May be provided multiple times. + #[arg(long)] + pub no_group: Vec, + + /// 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, + + /// 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, + + /// 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, + + /// 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>, +} + #[derive(Args)] #[allow(clippy::struct_excessive_bools)] pub struct ExportArgs { diff --git a/crates/uv-distribution-types/src/dependency_metadata.rs b/crates/uv-distribution-types/src/dependency_metadata.rs index 5d5b69ffd51d..4991833ea779 100644 --- a/crates/uv-distribution-types/src/dependency_metadata.rs +++ b/crates/uv-distribution-types/src/dependency_metadata.rs @@ -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 @@ -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(), }) } } @@ -109,4 +114,6 @@ pub struct StaticMetadata { pub requires_python: Option, #[serde(default)] pub provides_extras: Vec, + pub classifiers: Option>, + pub license: Option, } diff --git a/crates/uv-distribution/src/metadata/mod.rs b/crates/uv-distribution/src/metadata/mod.rs index f5dac8830f3b..47bf1e6a9742 100644 --- a/crates/uv-distribution/src/metadata/mod.rs +++ b/crates/uv-distribution/src/metadata/mod.rs @@ -50,6 +50,8 @@ pub struct Metadata { pub requires_python: Option, pub provides_extras: Vec, pub dependency_groups: BTreeMap>, + pub license: Option, + pub classifiers: Option>, } impl Metadata { @@ -67,6 +69,8 @@ impl Metadata { requires_python: metadata.requires_python, provides_extras: metadata.provides_extras, dependency_groups: BTreeMap::default(), + license: metadata.license, + classifiers: metadata.classifiers, } } @@ -109,6 +113,8 @@ impl Metadata { requires_python: metadata.requires_python, provides_extras, dependency_groups, + license: metadata.license, + classifiers: metadata.classifiers }) } } diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 772c3d384442..30e3abed70c2 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -37,7 +37,7 @@ 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; @@ -2448,6 +2448,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 { @@ -2456,6 +2457,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, }) } diff --git a/crates/uv-pypi-types/src/metadata/metadata_resolver.rs b/crates/uv-pypi-types/src/metadata/metadata_resolver.rs index 2ca03ddf6111..738e446fc585 100644 --- a/crates/uv-pypi-types/src/metadata/metadata_resolver.rs +++ b/crates/uv-pypi-types/src/metadata/metadata_resolver.rs @@ -29,6 +29,10 @@ pub struct ResolutionMetadata { pub requires_dist: Vec>, pub requires_python: Option, pub provides_extras: Vec, + #[serde(default)] + pub classifiers: Option>, + #[serde(default)] + pub license: Option, } /// From @@ -68,6 +72,11 @@ impl ResolutionMetadata { } }) .collect::>(); + let classifiers = Some(headers + .get_all_values("Classifier") + .collect::>()); + let license = headers + .get_first_value("License"); Ok(Self { name, @@ -75,6 +84,8 @@ impl ResolutionMetadata { requires_dist, requires_python, provides_extras, + classifiers, + license, }) } @@ -141,6 +152,11 @@ impl ResolutionMetadata { } }) .collect::>(); + let classifiers = Some(headers + .get_all_values("Classifiers") + .collect::>()); + let license = headers + .get_first_value("License"); Ok(Self { name, @@ -148,6 +164,8 @@ impl ResolutionMetadata { requires_dist, requires_python, provides_extras, + classifiers, + license, }) } @@ -231,4 +249,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 } diff --git a/crates/uv-pypi-types/src/metadata/pyproject_toml.rs b/crates/uv-pypi-types/src/metadata/pyproject_toml.rs index 08122727c221..22bc56d51a01 100644 --- a/crates/uv-pypi-types/src/metadata/pyproject_toml.rs +++ b/crates/uv-pypi-types/src/metadata/pyproject_toml.rs @@ -92,6 +92,12 @@ pub(crate) fn parse_pyproject_toml( ); provides_extras.push(extra); } + let classifiers = Some(project + .classifiers + .unwrap_or_default() + .into_iter() + .collect::>()); + Ok(ResolutionMetadata { name, @@ -99,6 +105,8 @@ pub(crate) fn parse_pyproject_toml( requires_dist, requires_python, provides_extras, + classifiers, + license: None // TODO(RL): come back }) } @@ -142,6 +150,9 @@ struct Project { /// Specifies which fields listed by PEP 621 were intentionally unspecified /// so another tool can/will provide such metadata dynamically. dynamic: Option>, + // Specifies zero or more "Trove Classifiers" to describe the project. + classifiers: Option>, + // TODO(RL): handle license field properly } #[derive(Deserialize, Debug)] @@ -153,6 +164,7 @@ struct PyprojectTomlWire { dependencies: Option>, optional_dependencies: Option>>, dynamic: Option>, + classifiers: Option>, } impl TryFrom for Project { @@ -167,6 +179,7 @@ impl TryFrom for Project { dependencies: wire.dependencies, optional_dependencies: wire.optional_dependencies, dynamic: wire.dynamic, + classifiers: wire.classifiers }) } } diff --git a/crates/uv-resolver/src/lib.rs b/crates/uv-resolver/src/lib.rs index 629539bace9e..786f444ef42d 100644 --- a/crates/uv-resolver/src/lib.rs +++ b/crates/uv-resolver/src/lib.rs @@ -6,7 +6,7 @@ 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, + ResolverManifest, SatisfiesResult, LicenseDisplay, TreeDisplay, VERSION, }; pub use manifest::Manifest; pub use options::{Flexibility, Options, OptionsBuilder}; diff --git a/crates/uv-resolver/src/lock/license.rs b/crates/uv-resolver/src/lock/license.rs new file mode 100644 index 000000000000..7a9043f0ad85 --- /dev/null +++ b/crates/uv-resolver/src/lock/license.rs @@ -0,0 +1,429 @@ +use std::borrow::Cow; +use std::collections::VecDeque; + +use itertools::Itertools; +use owo_colors::OwoColorize; +use petgraph::graph::{EdgeIndex, NodeIndex}; +use petgraph::prelude::EdgeRef; +use petgraph::Direction; +use rustc_hash::{FxHashMap, FxHashSet}; + +use uv_configuration::DevGroupsManifest; +use uv_normalize::{ExtraName, GroupName}; +use uv_pypi_types::ResolverMarkerEnvironment; + +use crate::lock::{Dependency, PackageId}; +use crate::{Lock, PackageMap}; + +#[derive(Debug)] +pub struct LicenseDisplay<'env> { + /// The constructed dependency graph. + graph: petgraph::graph::Graph<&'env PackageId, Edge<'env>, petgraph::Directed>, + /// The packages considered as roots of the dependency tree. + roots: Vec, + /// The discovered license data for each dependency + license: &'env PackageMap, + /// Maximum display depth of the dependency tree. + depth: usize, +} + +impl<'env> LicenseDisplay<'env> { + /// Create a new [`DisplayDependencyGraph`] for the set of installed packages. + pub fn new( + lock: &'env Lock, + markers: Option<&'env ResolverMarkerEnvironment>, + license: &'env PackageMap, + direct_only: bool, + // packages: &[PackageName], + dev: &DevGroupsManifest, + ) -> Self { + let depth = if direct_only { 1 } else { 255 }; + // Identify the workspace members. + let members: FxHashSet<&PackageId> = if lock.members().is_empty() { + lock.root().into_iter().map(|package| &package.id).collect() + } else { + lock.packages + .iter() + .filter_map(|package| { + if lock.members().contains(&package.id.name) { + Some(&package.id) + } else { + None + } + }) + .collect() + }; + + // Create a graph. + let mut graph = petgraph::graph::Graph::<&PackageId, Edge, petgraph::Directed>::new(); + + // Create the complete graph. + let mut inverse = FxHashMap::default(); + for package in &lock.packages { + // Insert the package into the graph. + let package_node = if let Some(index) = inverse.get(&package.id) { + *index + } else { + let index = graph.add_node(&package.id); + inverse.insert(&package.id, index); + index + }; + + if dev.prod() { + for dependency in &package.dependencies { + if markers.is_some_and(|markers| { + !dependency.complexified_marker.evaluate_no_extras(markers) + }) { + continue; + } + + // Insert the dependency into the graph. + let dependency_node = if let Some(index) = inverse.get(&dependency.package_id) { + *index + } else { + let index = graph.add_node(&dependency.package_id); + inverse.insert(&dependency.package_id, index); + index + }; + + // Add an edge between the package and the dependency. + graph.add_edge( + package_node, + dependency_node, + Edge::Prod(Cow::Borrowed(dependency)), + ); + } + } + + if dev.prod() { + for (extra, dependencies) in &package.optional_dependencies { + for dependency in dependencies { + if markers.is_some_and(|markers| { + !dependency.complexified_marker.evaluate_no_extras(markers) + }) { + continue; + } + + // Insert the dependency into the graph. + let dependency_node = + if let Some(index) = inverse.get(&dependency.package_id) { + *index + } else { + let index = graph.add_node(&dependency.package_id); + inverse.insert(&dependency.package_id, index); + index + }; + + // Add an edge between the package and the dependency. + graph.add_edge( + package_node, + dependency_node, + Edge::Optional(extra, Cow::Borrowed(dependency)), + ); + } + } + } + + for (group, dependencies) in &package.dependency_groups { + if dev.contains(group) { + for dependency in dependencies { + if markers.is_some_and(|markers| { + !dependency.complexified_marker.evaluate_no_extras(markers) + }) { + continue; + } + + // Insert the dependency into the graph. + let dependency_node = + if let Some(index) = inverse.get(&dependency.package_id) { + *index + } else { + let index = graph.add_node(&dependency.package_id); + inverse.insert(&dependency.package_id, index); + index + }; + + // Add an edge between the package and the dependency. + graph.add_edge( + package_node, + dependency_node, + Edge::Dev(group, Cow::Borrowed(dependency)), + ); + } + } + } + } + + // Filter the graph to remove any unreachable nodes. + { + let mut reachable = graph + .node_indices() + .filter(|index| members.contains(graph[*index])) + .collect::>(); + let mut stack = reachable.iter().copied().collect::>(); + while let Some(node) = stack.pop_front() { + for edge in graph.edges_directed(node, Direction::Outgoing) { + if reachable.insert(edge.target()) { + stack.push_back(edge.target()); + } + } + } + + // Remove the unreachable nodes from the graph. + graph.retain_nodes(|_, index| reachable.contains(&index)); + } + + + // // Filter the graph to those nodes reachable from the target packages. + // if !packages.is_empty() { + // let mut reachable = graph + // .node_indices() + // .filter(|index| packages.contains(&graph[*index].name)) + // .collect::>(); + // let mut stack = reachable.iter().copied().collect::>(); + // while let Some(node) = stack.pop_front() { + // for edge in graph.edges_directed(node, Direction::Outgoing) { + // if reachable.insert(edge.target()) { + // stack.push_back(edge.target()); + // } + // } + // } + + // // Remove the unreachable nodes from the graph. + // graph.retain_nodes(|_, index| reachable.contains(&index)); + // } + + // Compute the list of roots. + let roots = { + let mut edges = vec![]; + + // Remove any cycles. + let feedback_set: Vec = petgraph::algo::greedy_feedback_arc_set(&graph) + .map(|e| e.id()) + .collect(); + for edge_id in feedback_set { + if let Some((source, target)) = graph.edge_endpoints(edge_id) { + if let Some(weight) = graph.remove_edge(edge_id) { + edges.push((source, target, weight)); + } + } + } + + // Find the root nodes. + let mut roots = graph + .node_indices() + .filter(|index| { + graph + .edges_directed(*index, Direction::Incoming) + .next() + .is_none() + }) + .collect::>(); + + // Sort the roots. + roots.sort_by_key(|index| &graph[*index]); + + // Re-add the removed edges. + for (source, target, weight) in edges { + graph.add_edge(source, target, weight); + } + + roots + }; + + Self { + graph, + roots, + license, + depth, + } + } + + /// Perform a depth-first traversal of the given package and its dependencies. + fn visit( + &'env self, + cursor: Cursor, + visited: &mut FxHashMap<&'env PackageId, Vec<&'env PackageId>>, + path: &mut Vec<&'env PackageId>, + ) -> Vec { + let unknown_license = String::from("Unknown License"); + // Short-circuit if the current path is longer than the provided depth. + if path.len() > self.depth { + return Vec::new(); + } + + let package_id = self.graph[cursor.node()]; + let edge = cursor.edge().map(|edge_id| &self.graph[edge_id]); + + if visited.contains_key(&package_id) { + return vec![]; + } + + let line = { + let mut line = format!( + "{}: {},", + format!("{}", package_id.name).bold().green(), + package_id.version + ); + + if let Some(edge) = edge { + let extras = &edge.dependency().extra; + if !extras.is_empty() { + line.push('['); + line.push_str(extras.iter().join(", ").as_str()); + line.push(']'); + } + } + + line.push(' '); + line.push_str(self.license.get(package_id).unwrap_or_else(|| &unknown_license)); + + if let Some(edge) = edge { + match edge { + Edge::Prod(_) => {} + Edge::Optional(extra, _) => { + line.push_str(&format!(" (extra: {extra})")); + } + Edge::Dev(group, _) => { + line.push_str(&format!(" (group: {group})")); + } + } + } + + line + }; + + // Skip the traversal if: + // 1. The package is in the current traversal path (i.e., a dependency cycle). + // 2. The package has been visited and de-duplication is enabled (default). + if let Some(requirements) = visited.get(package_id) { + if requirements.is_empty() { + return vec![line] + } + } + + let mut dependencies = self + .graph + .edges_directed(cursor.node(), Direction::Outgoing) + .map(|edge| { + let node = edge.target(); + Cursor::new(node, edge.id()) + }) + .collect::>(); + dependencies.sort_by_key(|node| { + let package_id = self.graph[node.node()]; + let edge = node + .edge() + .map(|edge_id| &self.graph[edge_id]) + .map(Edge::kind); + (edge, package_id) + }); + + let mut lines = vec![line]; + + // Keep track of the dependency path to avoid cycles. + visited.insert( + package_id, + dependencies + .iter() + .map(|node| self.graph[node.node()]) + .collect(), + ); + path.push(package_id); + + for (_index, dep) in dependencies.iter().enumerate() { + for (_visited_index, visited_line) in self.visit(*dep, visited, path).iter().enumerate() + { + lines.push(format!("{visited_line}")); + } + } + + path.pop(); + + lines + } + + /// Depth-first traverse the nodes to render the tree. + fn render(&self) -> Vec { + let mut path = Vec::new(); + let mut lines = Vec::with_capacity(self.graph.node_count()); + let mut visited = + FxHashMap::with_capacity_and_hasher(self.graph.node_count(), rustc_hash::FxBuildHasher); + + for node in &self.roots { + path.clear(); + lines.extend(self.visit(Cursor::root(*node), &mut visited, &mut path)); + } + + lines + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] +enum Edge<'env> { + Prod(Cow<'env, Dependency>), + Optional(&'env ExtraName, Cow<'env, Dependency>), + Dev(&'env GroupName, Cow<'env, Dependency>), +} + +impl<'env> Edge<'env> { + fn dependency(&self) -> &Dependency { + match self { + Self::Prod(dependency) => dependency, + Self::Optional(_, dependency) => dependency, + Self::Dev(_, dependency) => dependency, + } + } + + fn kind(&self) -> EdgeKind<'env> { + match self { + Self::Prod(_) => EdgeKind::Prod, + Self::Optional(extra, _) => EdgeKind::Optional(extra), + Self::Dev(group, _) => EdgeKind::Dev(group), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] +enum EdgeKind<'env> { + Prod, + Optional(&'env ExtraName), + Dev(&'env GroupName), +} + +/// A node in the dependency graph along with the edge that led to it, or `None` for root nodes. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] +struct Cursor(NodeIndex, Option); + +impl Cursor { + /// Create a [`Cursor`] representing a node in the dependency tree. + fn new(node: NodeIndex, edge: EdgeIndex) -> Self { + Self(node, Some(edge)) + } + + /// Create a [`Cursor`] representing a root node in the dependency tree. + fn root(node: NodeIndex) -> Self { + Self(node, None) + } + + /// Return the [`NodeIndex`] of the node. + fn node(&self) -> NodeIndex { + self.0 + } + + /// Return the [`EdgeIndex`] of the edge that led to the node, if any. + fn edge(&self) -> Option { + self.1 + } +} + +impl std::fmt::Display for LicenseDisplay<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + + for line in self.render() { + writeln!(f, "{line}")?; + } + + Ok(()) + } +} diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 49f95bb7909e..ed0ce0a12e7e 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -13,6 +13,7 @@ use petgraph::visit::EdgeRef; use rustc_hash::{FxHashMap, FxHashSet}; use serde::Serializer; use toml_edit::{value, Array, ArrayOfTables, InlineTable, Item, Table, Value}; +use tracing::debug; use url::Url; use uv_cache_key::RepositoryUrl; @@ -38,10 +39,12 @@ use uv_pypi_types::{ Requirement, RequirementSource, }; use uv_types::{BuildContext, HashStrategy}; -use uv_workspace::WorkspaceMember; +use uv_workspace::{Workspace, WorkspaceMember}; use crate::fork_strategy::ForkStrategy; pub use crate::lock::installable::Installable; +pub use crate::lock::license::LicenseDisplay; + pub use crate::lock::map::PackageMap; pub use crate::lock::requirements_txt::RequirementsTxtExport; pub use crate::lock::tree::TreeDisplay; @@ -54,6 +57,7 @@ use crate::{ }; mod installable; +mod license; mod map; mod requirements_txt; mod tree; @@ -2251,6 +2255,82 @@ impl Package { &self.id.name } + fn get_license_string(&self, license_meta: &Option, classifiers: &Vec::) -> Option { + if let Some(license_txt) = license_meta { + if !license_txt.is_empty() { + return license_meta.clone() + } + } + let license_prefix = "License ::"; + let license_osi_prefix = "License :: OSI Approved ::"; + let classifier_license = Some(classifiers + .iter() + .filter_map(|c| { + if !c.starts_with(license_prefix) { + None // filter this classifier out if it's not License-related + } else { + if c.starts_with(license_osi_prefix) { + Some(c[license_osi_prefix.len()+1..].to_string()) // remove the License & OSI-approved prefixes + } else{ + Some(c[license_prefix.len()+1..].to_string()) // remove the License prefix + } + } + }) + .collect::>() + .join(", ")) + .filter(|s| !s.is_empty()); + classifier_license + } + + pub async fn license( + &self, + workspace: &Workspace, + tags: &Tags, + database: &DistributionDatabase<'_, Context>, + ) -> Option { + // parse license information from classifiers + // it is possible that the classifiers field isn't set yet because of the source + // of the package. the package may be populated from the lock file OR the resolver. + // in the case of the former, the package data is incomplete and we must fetch + // the additional data ourselves. + let mut classifiers: Option> = None; + let mut license_meta: Option = None; + if classifiers.is_none() || license_meta.is_none() { // TODO(RL): need a smarter check here + // Get the metadata for the distribution (see above for explanation of tags/capabilities). + let dist = self.to_dist( + workspace.install_path(), + TagPolicy::Preferred(tags), + &BuildOptions::default(), + ); + + if let Ok(generated_dist) = dist { + let hasher = HashStrategy::None; + + if let Ok(meta) = database + .get_or_build_wheel_metadata(&generated_dist, hasher.get(&generated_dist)) + .await + { + classifiers = meta.metadata.classifiers.clone(); + license_meta = meta.metadata.license.clone(); + println!("{} :: {:?}", self.name(), license_meta); + } else { + debug!("package metadata lookup failed"); + return None + } + } else { + debug!("package.to_dist failed"); + return None + } + }; + + if let Some(classifiers) = classifiers { + let license_string = self.get_license_string(&license_meta, &classifiers); + license_string + } else { + None + } + } + /// Returns the [`Version`] of the package. pub fn version(&self) -> &Version { &self.id.version diff --git a/crates/uv/src/commands/mod.rs b/crates/uv/src/commands/mod.rs index a3fb800b7249..87e94213370b 100644 --- a/crates/uv/src/commands/mod.rs +++ b/crates/uv/src/commands/mod.rs @@ -24,6 +24,7 @@ pub(crate) use pip::uninstall::pip_uninstall; pub(crate) use project::add::add; pub(crate) use project::export::export; pub(crate) use project::init::{init, InitKind, InitProjectKind}; +pub(crate) use project::license::license; pub(crate) use project::lock::lock; pub(crate) use project::remove::remove; pub(crate) use project::run::{run, RunCommand}; diff --git a/crates/uv/src/commands/pip/show.rs b/crates/uv/src/commands/pip/show.rs index 5c03f6d04676..3c86b7aae7b0 100644 --- a/crates/uv/src/commands/pip/show.rs +++ b/crates/uv/src/commands/pip/show.rs @@ -187,6 +187,15 @@ pub(crate) fn pip_show( )?; } } + if let Ok(meta) = distribution.metadata() { + if let Some(classifiers) = meta.classifiers { + if classifiers.is_empty() { + writeln!(printer.stdout(), "Classifiers:")?; + } else { + writeln!(printer.stdout(), "Classifiers: {}", classifiers.join(", "))?; + } + } + } // If requests, show the list of installed files. if files { diff --git a/crates/uv/src/commands/project/license.rs b/crates/uv/src/commands/project/license.rs new file mode 100644 index 000000000000..e59d15e4f72f --- /dev/null +++ b/crates/uv/src/commands/project/license.rs @@ -0,0 +1,251 @@ +use std::path::Path; + +use anyhow::{Error, Result}; + +use futures::StreamExt; +use uv_cache::{Cache, Refresh}; +use uv_cache_info::Timestamp; +use uv_client::{Connectivity, FlatIndexClient, RegistryClientBuilder}; +use uv_configuration::{ + Concurrency, Constraints, DevGroupsSpecification, LowerBound, PreviewMode, TargetTriple, + TrustedHost, +}; +use uv_dispatch::{BuildDispatch, SharedState}; +use uv_distribution::DistributionDatabase; +use uv_distribution_types::Index; +use uv_python::{ + PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest, PythonVersion, +}; +use uv_resolver::{FlatIndex, LicenseDisplay, PackageMap}; +use uv_settings::PythonInstallMirrors; +use uv_types::{BuildIsolation, HashStrategy}; +use uv_workspace::{DiscoveryOptions, Workspace}; + +use crate::commands::pip::loggers::DefaultResolveLogger; +use crate::commands::pip::resolution_markers; +use crate::commands::project::lock::{do_safe_lock, LockMode}; +use crate::commands::project::{ + default_dependency_groups, DependencyGroupsTarget, ProjectError, ProjectInterpreter, +}; +use crate::commands::{diagnostics, ExitStatus}; +use crate::printer::Printer; +use crate::settings::ResolverSettings; + +/// Run a command. +#[allow(clippy::fn_params_excessive_bools)] +pub(crate) async fn license( + project_dir: &Path, + dev: DevGroupsSpecification, + locked: bool, + frozen: bool, + universal: bool, + direct_only: bool, + python_version: Option, + python_platform: Option, + python: Option, + install_mirrors: PythonInstallMirrors, + settings: ResolverSettings, + python_preference: PythonPreference, + python_downloads: PythonDownloads, + connectivity: Connectivity, + concurrency: Concurrency, + native_tls: bool, + allow_insecure_host: &[TrustedHost], + no_config: bool, + cache: &Cache, + printer: Printer, + preview: PreviewMode, +) -> Result { + // Find the project requirements. + let workspace = Workspace::discover(project_dir, &DiscoveryOptions::default()).await?; + + // Validate that any referenced dependency groups are defined in the workspace. + if !frozen { + let target = DependencyGroupsTarget::Workspace(&workspace); + target.validate(&dev)?; + } + + // Determine the default groups to include. + let defaults = default_dependency_groups(workspace.pyproject_toml())?; + + // Find an interpreter for the project, unless `--frozen` and `--universal` are both set. + let interpreter = if frozen && universal { + None + } else { + Some( + ProjectInterpreter::discover( + &workspace, + project_dir, + python.as_deref().map(PythonRequest::parse), + python_preference, + python_downloads, + connectivity, + native_tls, + allow_insecure_host, + &install_mirrors, + no_config, + cache, + printer, + ) + .await? + .into_interpreter(), + ) + }; + + // Determine the lock mode. + let mode = if frozen { + LockMode::Frozen + } else if locked { + LockMode::Locked(interpreter.as_ref().unwrap()) + } else { + LockMode::Write(interpreter.as_ref().unwrap()) + }; + + // Initialize any shared state. + let state = SharedState::default(); + let bounds = LowerBound::Allow; + + // Update the lockfile, if necessary. + let lock = match do_safe_lock( + mode, + (&workspace).into(), + settings.as_ref(), + bounds, + &state, + Box::new(DefaultResolveLogger), + connectivity, + concurrency, + native_tls, + allow_insecure_host, + cache, + printer, + preview, + ) + .await + { + Ok(result) => result.into_lock(), + Err(ProjectError::Operation(err)) => { + return diagnostics::OperationDiagnostic::default() + .report(err) + .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())) + } + Err(err) => return Err(err.into()), + }; + + // Determine the markers to use for resolution. + let markers = (!universal).then(|| { + resolution_markers( + python_version.as_ref(), + python_platform.as_ref(), + interpreter.as_ref().unwrap(), + ) + }); + + let ResolverSettings { + index_locations, + index_strategy, + keyring_provider, + resolution: _, + prerelease: _, + fork_strategy: _, + dependency_metadata, + config_setting, + no_build_isolation, + no_build_isolation_package, + exclude_newer, + link_mode, + upgrade: _, + build_options, + sources, + } = settings; + + // Initialize the registry client. + let client: uv_client::RegistryClient = + RegistryClientBuilder::new(cache.clone().with_refresh(Refresh::All(Timestamp::now()))) + .native_tls(native_tls) + .connectivity(connectivity) + .keyring(keyring_provider) + .allow_insecure_host(allow_insecure_host.to_vec()) + .build(); + let environment; + let build_isolation = if no_build_isolation { + environment = PythonEnvironment::from_interpreter(interpreter.as_ref().unwrap().clone()); + BuildIsolation::Shared(&environment) + } else if no_build_isolation_package.is_empty() { + BuildIsolation::Isolated + } else { + environment = PythonEnvironment::from_interpreter(interpreter.as_ref().unwrap().clone()); + BuildIsolation::SharedPackage(&environment, no_build_isolation_package.as_ref()) + }; + + // TODO(charlie): These are all default values. We should consider whether we want to make them + // optional on the downstream APIs. + let build_hasher = HashStrategy::default(); + + // Resolve the flat indexes from `--find-links`. + let flat_index = { + let client = FlatIndexClient::new(&client, cache); + let entries = client + .fetch(index_locations.flat_indexes().map(Index::url)) + .await?; + FlatIndex::from_entries(entries, None, &build_hasher, &build_options) + }; + + // Create a build dispatch. + let build_dispatch = BuildDispatch::new( + &client, + cache, + Constraints::default(), + interpreter.as_ref().unwrap(), + &index_locations, + &flat_index, + &dependency_metadata, + state.clone(), + index_strategy.clone(), + &config_setting, + build_isolation, + link_mode, + &build_options, + &build_hasher, + exclude_newer, + bounds, + sources, + concurrency, + preview, + ); + let database = DistributionDatabase::new(&client, &build_dispatch, concurrency.downloads); + + let mut licenses = PackageMap::default(); + + let interpret = interpreter.as_ref().expect("need an interpreter").tags()?; + let ws = &workspace; + let db = &database; + let mut fetches = futures::stream::iter(lock.packages()) + .map(|package| async move { + let license = package.license(&ws.clone(), interpret, &db).await; + Ok::, Error>(Some((package, license))) + }) + .buffer_unordered(concurrency.downloads); + while let Some(entry) = fetches.next().await.transpose()? { + let Some((package, license)) = entry else { + continue; + }; + match license { + Some(license) => licenses.insert(package.clone(), license), + None => continue, + }; + } + + // Render the license information. + let display = LicenseDisplay::new( + &lock, + markers.as_ref(), + &licenses, + direct_only, + &dev.with_defaults(defaults), + ); + + print!("{display}"); + + Ok(ExitStatus::Success) +} diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index d7807c29a614..50d4cab0e1ce 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -61,6 +61,7 @@ pub(crate) mod remove; pub(crate) mod run; pub(crate) mod sync; pub(crate) mod tree; +pub(crate) mod license; #[derive(thiserror::Error, Debug)] pub(crate) enum ProjectError { diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 9e7be8ce3796..a88996b6b06f 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1629,6 +1629,40 @@ async fn run_project( )) .await } + ProjectCommand::License(args) => { + // Resolve the settings from the command-line arguments and workspace configuration. + let args = settings::LicenseSettings::resolve(args, filesystem); + show_settings!(args); + + // Initialize the cache. + let cache = cache.init()?; + + commands::license( + project_dir, + args.dev, + args.locked, + args.frozen, + args.universal, + args.direct_only, + args.python_version, + args.python_platform, + args.python, + args.install_mirrors, + args.resolver, + globals.python_preference, + globals.python_downloads, + globals.connectivity, + globals.concurrency, + globals.native_tls, + &globals.allow_insecure_host, + no_config, + &cache, + printer, + globals.preview, + ) + .await + } + ProjectCommand::Tree(args) => { // Resolve the settings from the command-line arguments and workspace configuration. let args = settings::TreeSettings::resolve(args, filesystem); diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index fff0dabb6e1b..dbe9f7fb8ebf 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -13,8 +13,8 @@ use uv_cli::{ ToolUpgradeArgs, }; use uv_cli::{ - AddArgs, ColorChoice, ExternalCommand, GlobalArgs, InitArgs, ListFormat, LockArgs, Maybe, - PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs, + AddArgs, ColorChoice, ExternalCommand, GlobalArgs, InitArgs, LicenseArgs, ListFormat, LockArgs, + Maybe, PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs, PipSyncArgs, PipTreeArgs, PipUninstallArgs, PythonFindArgs, PythonInstallArgs, PythonListArgs, PythonPinArgs, PythonUninstallArgs, RemoveArgs, RunArgs, SyncArgs, ToolDirArgs, ToolInstallArgs, ToolListArgs, ToolRunArgs, ToolUninstallArgs, TreeArgs, VenvArgs, @@ -1339,6 +1339,64 @@ impl TreeSettings { } } +/// The resolved settings to use for a `tree` invocation. +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone)] +pub(crate) struct LicenseSettings { + pub(crate) dev: DevGroupsSpecification, + pub(crate) locked: bool, + pub(crate) frozen: bool, + pub(crate) universal: bool, + pub(crate) direct_only: bool, + pub(crate) python_version: Option, + pub(crate) python_platform: Option, + pub(crate) python: Option, + pub(crate) install_mirrors: PythonInstallMirrors, + pub(crate) resolver: ResolverSettings, +} + +impl LicenseSettings { + /// Resolve the [`LicenseSettings`] from the CLI and workspace configuration. + pub(crate) fn resolve(args: LicenseArgs, filesystem: Option) -> Self { + let LicenseArgs { + universal, + dev, + only_dev, + no_dev, + group, + no_group, + only_group, + all_groups, + direct_deps_only, + locked, + frozen, + build, + resolver, + python_version, + python_platform, + python, + } = args; + let install_mirrors = filesystem + .clone() + .map(|fs| fs.install_mirrors.clone()) + .unwrap_or_default(); + Self { + dev: DevGroupsSpecification::from_args( + dev, no_dev, only_dev, group, no_group, only_group, all_groups, + ), + locked, + frozen, + universal, + direct_only: direct_deps_only, + python_version, + python_platform, + python: python.and_then(Maybe::into_option), + resolver: ResolverSettings::combine(resolver_options(resolver, build), filesystem), + install_mirrors, + } + } +} + /// The resolved settings to use for an `export` invocation. #[allow(clippy::struct_excessive_bools, dead_code)] #[derive(Debug, Clone)] From 1b6cac3e04817a66cf91233a8089c402356a1c0e Mon Sep 17 00:00:00 2001 From: Ryan Leary Date: Fri, 3 Jan 2025 11:24:24 -0500 Subject: [PATCH 2/7] fix: update insta tests --- crates/uv/tests/it/help.rs | 30 +++++++++++++++---------- crates/uv/tests/it/pip_show.rs | 41 +++++++++++++++++++++------------- 2 files changed, 43 insertions(+), 28 deletions(-) diff --git a/crates/uv/tests/it/help.rs b/crates/uv/tests/it/help.rs index 31e3e02d40cc..4957d16b8bdf 100644 --- a/crates/uv/tests/it/help.rs +++ b/crates/uv/tests/it/help.rs @@ -5,7 +5,7 @@ fn help() { let context = TestContext::new_with_versions(&[]); // The `uv help` command should show the long help message - uv_snapshot!(context.filters(), context.help(), @r###" + uv_snapshot!(context.filters(), context.help(), @r#" success: true exit_code: 0 ----- stdout ----- @@ -22,6 +22,7 @@ fn help() { lock Update the project's lockfile export Export the project's lockfile to an alternate format tree Display the project's dependency tree + license Display the project's license information tool Run and install commands provided by Python packages python Manage Python versions and installations pip Manage Python packages with a pip-compatible interface @@ -79,14 +80,14 @@ fn help() { ----- stderr ----- - "###); + "#); } #[test] fn help_flag() { let context = TestContext::new_with_versions(&[]); - uv_snapshot!(context.filters(), context.command().arg("--help"), @r###" + uv_snapshot!(context.filters(), context.command().arg("--help"), @r#" success: true exit_code: 0 ----- stdout ----- @@ -103,6 +104,7 @@ fn help_flag() { lock Update the project's lockfile export Export the project's lockfile to an alternate format tree Display the project's dependency tree + license Display the project's license information tool Run and install commands provided by Python packages python Manage Python versions and installations pip Manage Python packages with a pip-compatible interface @@ -158,14 +160,14 @@ fn help_flag() { Use `uv help` for more details. ----- stderr ----- - "###); + "#); } #[test] fn help_short_flag() { let context = TestContext::new_with_versions(&[]); - uv_snapshot!(context.filters(), context.command().arg("-h"), @r###" + uv_snapshot!(context.filters(), context.command().arg("-h"), @r#" success: true exit_code: 0 ----- stdout ----- @@ -182,6 +184,7 @@ fn help_short_flag() { lock Update the project's lockfile export Export the project's lockfile to an alternate format tree Display the project's dependency tree + license Display the project's license information tool Run and install commands provided by Python packages python Manage Python versions and installations pip Manage Python packages with a pip-compatible interface @@ -237,7 +240,7 @@ fn help_short_flag() { Use `uv help` for more details. ----- stderr ----- - "###); + "#); } #[test] @@ -832,7 +835,7 @@ fn help_flag_subsubcommand() { fn help_unknown_subcommand() { let context = TestContext::new_with_versions(&[]); - uv_snapshot!(context.filters(), context.help().arg("foobar"), @r###" + uv_snapshot!(context.filters(), context.help().arg("foobar"), @r" success: false exit_code: 2 ----- stdout ----- @@ -847,6 +850,7 @@ fn help_unknown_subcommand() { lock export tree + license tool python pip @@ -857,7 +861,7 @@ fn help_unknown_subcommand() { self version generate-shell-completion - "###); + "); uv_snapshot!(context.filters(), context.help().arg("foo").arg("bar"), @r###" success: false @@ -911,7 +915,7 @@ fn help_unknown_subsubcommand() { fn help_with_global_option() { let context = TestContext::new_with_versions(&[]); - uv_snapshot!(context.filters(), context.help().arg("--no-cache"), @r###" + uv_snapshot!(context.filters(), context.help().arg("--no-cache"), @r#" success: true exit_code: 0 ----- stdout ----- @@ -928,6 +932,7 @@ fn help_with_global_option() { lock Update the project's lockfile export Export the project's lockfile to an alternate format tree Display the project's dependency tree + license Display the project's license information tool Run and install commands provided by Python packages python Manage Python versions and installations pip Manage Python packages with a pip-compatible interface @@ -985,7 +990,7 @@ fn help_with_global_option() { ----- stderr ----- - "###); + "#); } #[test] @@ -1027,7 +1032,7 @@ fn help_with_no_pager() { // We can't really test whether the --no-pager option works with a snapshot test. // It's still nice to have a test for the option to confirm the option exists. - uv_snapshot!(context.filters(), context.help().arg("--no-pager"), @r###" + uv_snapshot!(context.filters(), context.help().arg("--no-pager"), @r#" success: true exit_code: 0 ----- stdout ----- @@ -1044,6 +1049,7 @@ fn help_with_no_pager() { lock Update the project's lockfile export Export the project's lockfile to an alternate format tree Display the project's dependency tree + license Display the project's license information tool Run and install commands provided by Python packages python Manage Python versions and installations pip Manage Python packages with a pip-compatible interface @@ -1101,5 +1107,5 @@ fn help_with_no_pager() { ----- stderr ----- - "###); + "#); } diff --git a/crates/uv/tests/it/pip_show.rs b/crates/uv/tests/it/pip_show.rs index 31b4c02cbbca..07023b19e371 100644 --- a/crates/uv/tests/it/pip_show.rs +++ b/crates/uv/tests/it/pip_show.rs @@ -55,7 +55,7 @@ fn show_requires_multiple() -> Result<()> { context.assert_command("import requests").success(); uv_snapshot!(context.filters(), context.pip_show() - .arg("requests"), @r###" + .arg("requests"), @r" success: true exit_code: 0 ----- stdout ----- @@ -64,9 +64,10 @@ fn show_requires_multiple() -> Result<()> { Location: [SITE_PACKAGES]/ Requires: certifi, charset-normalizer, idna, urllib3 Required-by: + Classifiers: Development Status :: 5 - Production/Stable, Environment :: Web Environment, Intended Audience :: Developers, License :: OSI Approved :: Apache Software License, Natural Language :: English, Operating System :: OS Independent, Programming Language :: Python, Programming Language :: Python :: 3, Programming Language :: Python :: 3.7, Programming Language :: Python :: 3.8, Programming Language :: Python :: 3.9, Programming Language :: Python :: 3.10, Programming Language :: Python :: 3.11, Programming Language :: Python :: 3 :: Only, Programming Language :: Python :: Implementation :: CPython, Programming Language :: Python :: Implementation :: PyPy, Topic :: Internet :: WWW/HTTP, Topic :: Software Development :: Libraries ----- stderr ----- - "### + " ); Ok(()) @@ -106,7 +107,7 @@ fn show_python_version_marker() -> Result<()> { } uv_snapshot!(filters, context.pip_show() - .arg("click"), @r###" + .arg("click"), @r" success: true exit_code: 0 ----- stdout ----- @@ -115,9 +116,10 @@ fn show_python_version_marker() -> Result<()> { Location: [SITE_PACKAGES]/ Requires: Required-by: + Classifiers: Development Status :: 5 - Production/Stable, Intended Audience :: Developers, License :: OSI Approved :: BSD License, Operating System :: OS Independent, Programming Language :: Python ----- stderr ----- - "### + " ); Ok(()) @@ -150,7 +152,7 @@ fn show_found_single_package() -> Result<()> { context.assert_command("import markupsafe").success(); uv_snapshot!(context.filters(), context.pip_show() - .arg("markupsafe"), @r###" + .arg("markupsafe"), @r" success: true exit_code: 0 ----- stdout ----- @@ -159,9 +161,10 @@ fn show_found_single_package() -> Result<()> { Location: [SITE_PACKAGES]/ Requires: Required-by: + Classifiers: Development Status :: 5 - Production/Stable, Environment :: Web Environment, Intended Audience :: Developers, License :: OSI Approved :: BSD License, Operating System :: OS Independent, Programming Language :: Python, Topic :: Internet :: WWW/HTTP :: Dynamic Content, Topic :: Text Processing :: Markup :: HTML ----- stderr ----- - "### + " ); Ok(()) @@ -200,7 +203,7 @@ fn show_found_multiple_packages() -> Result<()> { uv_snapshot!(context.filters(), context.pip_show() .arg("markupsafe") - .arg("pip"), @r###" + .arg("pip"), @r" success: true exit_code: 0 ----- stdout ----- @@ -209,15 +212,17 @@ fn show_found_multiple_packages() -> Result<()> { Location: [SITE_PACKAGES]/ Requires: Required-by: + Classifiers: Development Status :: 5 - Production/Stable, Environment :: Web Environment, Intended Audience :: Developers, License :: OSI Approved :: BSD License, Operating System :: OS Independent, Programming Language :: Python, Topic :: Internet :: WWW/HTTP :: Dynamic Content, Topic :: Text Processing :: Markup :: HTML --- Name: pip Version: 21.3.1 Location: [SITE_PACKAGES]/ Requires: Required-by: + Classifiers: Development Status :: 5 - Production/Stable, Intended Audience :: Developers, License :: OSI Approved :: MIT License, Topic :: Software Development :: Build Tools, Programming Language :: Python, Programming Language :: Python :: 3, Programming Language :: Python :: 3 :: Only, Programming Language :: Python :: 3.6, Programming Language :: Python :: 3.7, Programming Language :: Python :: 3.8, Programming Language :: Python :: 3.9, Programming Language :: Python :: 3.10, Programming Language :: Python :: Implementation :: CPython, Programming Language :: Python :: Implementation :: PyPy ----- stderr ----- - "### + " ); Ok(()) @@ -257,7 +262,7 @@ fn show_found_one_out_of_three() -> Result<()> { uv_snapshot!(context.filters(), context.pip_show() .arg("markupsafe") .arg("flask") - .arg("django"), @r###" + .arg("django"), @r" success: true exit_code: 0 ----- stdout ----- @@ -266,10 +271,11 @@ fn show_found_one_out_of_three() -> Result<()> { Location: [SITE_PACKAGES]/ Requires: Required-by: + Classifiers: Development Status :: 5 - Production/Stable, Environment :: Web Environment, Intended Audience :: Developers, License :: OSI Approved :: BSD License, Operating System :: OS Independent, Programming Language :: Python, Topic :: Internet :: WWW/HTTP :: Dynamic Content, Topic :: Text Processing :: Markup :: HTML ----- stderr ----- warning: Package(s) not found for: django, flask - "### + " ); Ok(()) @@ -386,7 +392,7 @@ fn show_editable() -> Result<()> { .success(); uv_snapshot!(context.filters(), context.pip_show() - .arg("poetry-editable"), @r###" + .arg("poetry-editable"), @r" success: true exit_code: 0 ----- stdout ----- @@ -396,9 +402,10 @@ fn show_editable() -> Result<()> { Editable project location: [WORKSPACE]/scripts/packages/poetry_editable Requires: anyio Required-by: + Classifiers: Programming Language :: Python :: 3, Programming Language :: Python :: 3.10, Programming Language :: Python :: 3.11, Programming Language :: Python :: 3.12 ----- stderr ----- - "### + " ); Ok(()) @@ -442,7 +449,7 @@ fn show_required_by_multiple() -> Result<()> { // idna is required by anyio and requests uv_snapshot!(context.filters(), context.pip_show() - .arg("idna"), @r###" + .arg("idna"), @r" success: true exit_code: 0 ----- stdout ----- @@ -451,9 +458,10 @@ fn show_required_by_multiple() -> Result<()> { Location: [SITE_PACKAGES]/ Requires: Required-by: anyio, requests + Classifiers: Development Status :: 5 - Production/Stable, Intended Audience :: Developers, Intended Audience :: System Administrators, License :: OSI Approved :: BSD License, Operating System :: OS Independent, Programming Language :: Python, Programming Language :: Python :: 3, Programming Language :: Python :: 3 :: Only, Programming Language :: Python :: 3.5, Programming Language :: Python :: 3.6, Programming Language :: Python :: 3.7, Programming Language :: Python :: 3.8, Programming Language :: Python :: 3.9, Programming Language :: Python :: 3.10, Programming Language :: Python :: 3.11, Programming Language :: Python :: 3.12, Programming Language :: Python :: Implementation :: CPython, Programming Language :: Python :: Implementation :: PyPy, Topic :: Internet :: Name Service (DNS), Topic :: Software Development :: Libraries :: Python Modules, Topic :: Utilities ----- stderr ----- - "### + " ); Ok(()) @@ -485,7 +493,7 @@ fn show_files() { // Windows has a different files order. #[cfg(not(windows))] - uv_snapshot!(context.filters(), context.pip_show().arg("requests").arg("--files"), @r#" + uv_snapshot!(context.filters(), context.pip_show().arg("requests").arg("--files"), @r" success: true exit_code: 0 ----- stdout ----- @@ -494,6 +502,7 @@ fn show_files() { Location: [SITE_PACKAGES]/ Requires: certifi, charset-normalizer, idna, urllib3 Required-by: + Classifiers: Development Status :: 5 - Production/Stable, Environment :: Web Environment, Intended Audience :: Developers, License :: OSI Approved :: Apache Software License, Natural Language :: English, Operating System :: OS Independent, Programming Language :: Python, Programming Language :: Python :: 3, Programming Language :: Python :: 3.7, Programming Language :: Python :: 3.8, Programming Language :: Python :: 3.9, Programming Language :: Python :: 3.10, Programming Language :: Python :: 3.11, Programming Language :: Python :: 3 :: Only, Programming Language :: Python :: Implementation :: CPython, Programming Language :: Python :: Implementation :: PyPy, Topic :: Internet :: WWW/HTTP, Topic :: Software Development :: Libraries Files: requests-2.31.0.dist-info/INSTALLER requests-2.31.0.dist-info/LICENSE @@ -522,5 +531,5 @@ fn show_files() { requests/utils.py ----- stderr ----- - "#); + "); } From 8cb729465e55e7caf8335c8b2357bcb3ba4b776c Mon Sep 17 00:00:00 2001 From: Ryan Leary Date: Fri, 3 Jan 2025 11:38:23 -0500 Subject: [PATCH 3/7] fix: cargo fmt --- crates/uv-cli/src/lib.rs | 1 - crates/uv-distribution/src/metadata/mod.rs | 6 +-- crates/uv-distribution/src/source/mod.rs | 4 +- .../src/metadata/metadata_resolver.rs | 14 ++---- .../src/metadata/pyproject_toml.rs | 17 +++---- crates/uv-resolver/src/lib.rs | 4 +- crates/uv-resolver/src/lock/license.rs | 10 ++-- crates/uv-resolver/src/lock/mod.rs | 47 +++++++++++-------- crates/uv/src/commands/project/mod.rs | 2 +- 9 files changed, 55 insertions(+), 50 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 09ad3d31e7b3..ab3d2e028205 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -3465,7 +3465,6 @@ pub struct TreeArgs { pub python: Option>, } - #[derive(Args)] #[allow(clippy::struct_excessive_bools)] pub struct LicenseArgs { diff --git a/crates/uv-distribution/src/metadata/mod.rs b/crates/uv-distribution/src/metadata/mod.rs index 47bf1e6a9742..dd1a73629c00 100644 --- a/crates/uv-distribution/src/metadata/mod.rs +++ b/crates/uv-distribution/src/metadata/mod.rs @@ -50,8 +50,8 @@ pub struct Metadata { pub requires_python: Option, pub provides_extras: Vec, pub dependency_groups: BTreeMap>, - pub license: Option, pub classifiers: Option>, + pub license: Option, } impl Metadata { @@ -69,8 +69,8 @@ impl Metadata { requires_python: metadata.requires_python, provides_extras: metadata.provides_extras, dependency_groups: BTreeMap::default(), - license: metadata.license, classifiers: metadata.classifiers, + license: metadata.license, } } @@ -113,8 +113,8 @@ impl Metadata { requires_python: metadata.requires_python, provides_extras, dependency_groups, + classifiers: metadata.classifiers, license: metadata.license, - classifiers: metadata.classifiers }) } } diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 30e3abed70c2..8a988fe3e97b 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -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, Metadata23, RequiresTxt, ResolutionMetadata}; +use uv_pypi_types::{ + HashAlgorithm, HashDigest, Metadata12, Metadata23, RequiresTxt, ResolutionMetadata, +}; use uv_types::{BuildContext, BuildStack, SourceBuildTrait}; use zip::ZipArchive; diff --git a/crates/uv-pypi-types/src/metadata/metadata_resolver.rs b/crates/uv-pypi-types/src/metadata/metadata_resolver.rs index 738e446fc585..db18449d8c68 100644 --- a/crates/uv-pypi-types/src/metadata/metadata_resolver.rs +++ b/crates/uv-pypi-types/src/metadata/metadata_resolver.rs @@ -72,11 +72,8 @@ impl ResolutionMetadata { } }) .collect::>(); - let classifiers = Some(headers - .get_all_values("Classifier") - .collect::>()); - let license = headers - .get_first_value("License"); + let classifiers = Some(headers.get_all_values("Classifier").collect::>()); + let license = headers.get_first_value("License"); Ok(Self { name, @@ -152,11 +149,8 @@ impl ResolutionMetadata { } }) .collect::>(); - let classifiers = Some(headers - .get_all_values("Classifiers") - .collect::>()); - let license = headers - .get_first_value("License"); + let classifiers = Some(headers.get_all_values("Classifiers").collect::>()); + let license = headers.get_first_value("License"); Ok(Self { name, diff --git a/crates/uv-pypi-types/src/metadata/pyproject_toml.rs b/crates/uv-pypi-types/src/metadata/pyproject_toml.rs index 22bc56d51a01..195e57209f89 100644 --- a/crates/uv-pypi-types/src/metadata/pyproject_toml.rs +++ b/crates/uv-pypi-types/src/metadata/pyproject_toml.rs @@ -92,12 +92,13 @@ pub(crate) fn parse_pyproject_toml( ); provides_extras.push(extra); } - let classifiers = Some(project - .classifiers - .unwrap_or_default() - .into_iter() - .collect::>()); - + let classifiers = Some( + project + .classifiers + .unwrap_or_default() + .into_iter() + .collect::>(), + ); Ok(ResolutionMetadata { name, @@ -106,7 +107,7 @@ pub(crate) fn parse_pyproject_toml( requires_python, provides_extras, classifiers, - license: None // TODO(RL): come back + license: None, // TODO(RL): come back }) } @@ -179,7 +180,7 @@ impl TryFrom for Project { dependencies: wire.dependencies, optional_dependencies: wire.optional_dependencies, dynamic: wire.dynamic, - classifiers: wire.classifiers + classifiers: wire.classifiers, }) } } diff --git a/crates/uv-resolver/src/lib.rs b/crates/uv-resolver/src/lib.rs index 786f444ef42d..ca21f4629c0f 100644 --- a/crates/uv-resolver/src/lib.rs +++ b/crates/uv-resolver/src/lib.rs @@ -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, LicenseDisplay, TreeDisplay, VERSION, + Installable, LicenseDisplay, Lock, LockError, LockVersion, Package, PackageMap, + RequirementsTxtExport, ResolverManifest, SatisfiesResult, TreeDisplay, VERSION, }; pub use manifest::Manifest; pub use options::{Flexibility, Options, OptionsBuilder}; diff --git a/crates/uv-resolver/src/lock/license.rs b/crates/uv-resolver/src/lock/license.rs index 7a9043f0ad85..cd62eef4775d 100644 --- a/crates/uv-resolver/src/lock/license.rs +++ b/crates/uv-resolver/src/lock/license.rs @@ -173,7 +173,6 @@ impl<'env> LicenseDisplay<'env> { graph.retain_nodes(|_, index| reachable.contains(&index)); } - // // Filter the graph to those nodes reachable from the target packages. // if !packages.is_empty() { // let mut reachable = graph @@ -276,7 +275,11 @@ impl<'env> LicenseDisplay<'env> { } line.push(' '); - line.push_str(self.license.get(package_id).unwrap_or_else(|| &unknown_license)); + line.push_str( + self.license + .get(package_id) + .unwrap_or_else(|| &unknown_license), + ); if let Some(edge) = edge { match edge { @@ -298,7 +301,7 @@ impl<'env> LicenseDisplay<'env> { // 2. The package has been visited and de-duplication is enabled (default). if let Some(requirements) = visited.get(package_id) { if requirements.is_empty() { - return vec![line] + return vec![line]; } } @@ -419,7 +422,6 @@ impl Cursor { impl std::fmt::Display for LicenseDisplay<'_> { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - for line in self.render() { writeln!(f, "{line}")?; } diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index ed0ce0a12e7e..a71ff66cb2a9 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -2255,30 +2255,36 @@ impl Package { &self.id.name } - fn get_license_string(&self, license_meta: &Option, classifiers: &Vec::) -> Option { + fn get_license_string( + &self, + license_meta: &Option, + classifiers: &Vec, + ) -> Option { if let Some(license_txt) = license_meta { if !license_txt.is_empty() { - return license_meta.clone() + return license_meta.clone(); } } let license_prefix = "License ::"; let license_osi_prefix = "License :: OSI Approved ::"; - let classifier_license = Some(classifiers - .iter() - .filter_map(|c| { - if !c.starts_with(license_prefix) { - None // filter this classifier out if it's not License-related - } else { - if c.starts_with(license_osi_prefix) { - Some(c[license_osi_prefix.len()+1..].to_string()) // remove the License & OSI-approved prefixes - } else{ - Some(c[license_prefix.len()+1..].to_string()) // remove the License prefix + let classifier_license = Some( + classifiers + .iter() + .filter_map(|c| { + if !c.starts_with(license_prefix) { + None // filter this classifier out if it's not License-related + } else { + if c.starts_with(license_osi_prefix) { + Some(c[license_osi_prefix.len() + 1..].to_string()) // remove the License & OSI-approved prefixes + } else { + Some(c[license_prefix.len() + 1..].to_string()) // remove the License prefix + } } - } - }) - .collect::>() - .join(", ")) - .filter(|s| !s.is_empty()); + }) + .collect::>() + .join(", "), + ) + .filter(|s| !s.is_empty()); classifier_license } @@ -2295,7 +2301,8 @@ impl Package { // the additional data ourselves. let mut classifiers: Option> = None; let mut license_meta: Option = None; - if classifiers.is_none() || license_meta.is_none() { // TODO(RL): need a smarter check here + if classifiers.is_none() || license_meta.is_none() { + // TODO(RL): need a smarter check here // Get the metadata for the distribution (see above for explanation of tags/capabilities). let dist = self.to_dist( workspace.install_path(), @@ -2315,11 +2322,11 @@ impl Package { println!("{} :: {:?}", self.name(), license_meta); } else { debug!("package metadata lookup failed"); - return None + return None; } } else { debug!("package.to_dist failed"); - return None + return None; } }; diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 50d4cab0e1ce..8b186d985624 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -55,13 +55,13 @@ pub(crate) mod environment; pub(crate) mod export; pub(crate) mod init; mod install_target; +pub(crate) mod license; pub(crate) mod lock; mod lock_target; pub(crate) mod remove; pub(crate) mod run; pub(crate) mod sync; pub(crate) mod tree; -pub(crate) mod license; #[derive(thiserror::Error, Debug)] pub(crate) enum ProjectError { From 2519cfb785a4aab95f892c5b95b269ef6b9b4ec7 Mon Sep 17 00:00:00 2001 From: Ryan Leary Date: Fri, 3 Jan 2025 12:29:30 -0500 Subject: [PATCH 4/7] fix: address clippy recommendations --- crates/uv-resolver/src/lock/license.rs | 13 +-- crates/uv-resolver/src/lock/mod.rs | 119 +++++++++++----------- crates/uv/src/commands/project/license.rs | 5 +- crates/uv/src/lib.rs | 4 +- 4 files changed, 70 insertions(+), 71 deletions(-) diff --git a/crates/uv-resolver/src/lock/license.rs b/crates/uv-resolver/src/lock/license.rs index cd62eef4775d..c5935bb9b310 100644 --- a/crates/uv-resolver/src/lock/license.rs +++ b/crates/uv-resolver/src/lock/license.rs @@ -275,11 +275,7 @@ impl<'env> LicenseDisplay<'env> { } line.push(' '); - line.push_str( - self.license - .get(package_id) - .unwrap_or_else(|| &unknown_license), - ); + line.push_str(self.license.get(package_id).unwrap_or(&unknown_license)); if let Some(edge) = edge { match edge { @@ -334,10 +330,9 @@ impl<'env> LicenseDisplay<'env> { ); path.push(package_id); - for (_index, dep) in dependencies.iter().enumerate() { - for (_visited_index, visited_line) in self.visit(*dep, visited, path).iter().enumerate() - { - lines.push(format!("{visited_line}")); + for dep in &dependencies { + for visited_line in &self.visit(*dep, visited, path) { + lines.push(visited_line.to_string()); } } diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index a71ff66cb2a9..75c125d91c75 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -2256,36 +2256,50 @@ impl Package { } fn get_license_string( - &self, - license_meta: &Option, - classifiers: &Vec, + license_meta: Option<&String>, + classifiers: Option<&Vec>, ) -> Option { + // first we'll try trove classifiers + let trove_license = if let Some(classifiers) = classifiers { + let license_prefix = "License ::"; + let license_osi_prefix = "License :: OSI Approved ::"; + Some( + classifiers + .iter() + .filter_map(|c| { + if !c.starts_with(license_prefix) { + None // filter this classifier out if it's not License-related + } else { + if c.starts_with(license_osi_prefix) { + Some(c[license_osi_prefix.len() + 1..].to_string()) + // remove the License & OSI-approved prefixes + } else { + Some(c[license_prefix.len() + 1..].to_string()) // remove the License prefix + } + } + }) + .collect::>() + .join(", "), + ) + .filter(|s| !s.is_empty()) + } else { + None + }; + + // trove_license is none if there were no classifiers that specify the license + if trove_license.is_some() { + return trove_license; + } + + // we did not successfully find and parse a license from a trove classifier + // try the license field if let Some(license_txt) = license_meta { if !license_txt.is_empty() { - return license_meta.clone(); + return Some(license_txt.clone()); } } - let license_prefix = "License ::"; - let license_osi_prefix = "License :: OSI Approved ::"; - let classifier_license = Some( - classifiers - .iter() - .filter_map(|c| { - if !c.starts_with(license_prefix) { - None // filter this classifier out if it's not License-related - } else { - if c.starts_with(license_osi_prefix) { - Some(c[license_osi_prefix.len() + 1..].to_string()) // remove the License & OSI-approved prefixes - } else { - Some(c[license_prefix.len() + 1..].to_string()) // remove the License prefix - } - } - }) - .collect::>() - .join(", "), - ) - .filter(|s| !s.is_empty()); - classifier_license + + None } pub async fn license( @@ -2299,43 +2313,32 @@ impl Package { // of the package. the package may be populated from the lock file OR the resolver. // in the case of the former, the package data is incomplete and we must fetch // the additional data ourselves. - let mut classifiers: Option> = None; - let mut license_meta: Option = None; - if classifiers.is_none() || license_meta.is_none() { - // TODO(RL): need a smarter check here - // Get the metadata for the distribution (see above for explanation of tags/capabilities). - let dist = self.to_dist( - workspace.install_path(), - TagPolicy::Preferred(tags), - &BuildOptions::default(), - ); - if let Ok(generated_dist) = dist { - let hasher = HashStrategy::None; + // TODO(RL): need a smarter check here + // Get the metadata for the distribution (see above for explanation of tags/capabilities). + let dist = self.to_dist( + workspace.install_path(), + TagPolicy::Preferred(tags), + &BuildOptions::default(), + ); - if let Ok(meta) = database - .get_or_build_wheel_metadata(&generated_dist, hasher.get(&generated_dist)) - .await - { - classifiers = meta.metadata.classifiers.clone(); - license_meta = meta.metadata.license.clone(); - println!("{} :: {:?}", self.name(), license_meta); - } else { - debug!("package metadata lookup failed"); - return None; - } - } else { - debug!("package.to_dist failed"); - return None; - } - }; + if let Ok(generated_dist) = dist { + let hasher = HashStrategy::None; - if let Some(classifiers) = classifiers { - let license_string = self.get_license_string(&license_meta, &classifiers); - license_string - } else { - None + if let Ok(meta) = database + .get_or_build_wheel_metadata(&generated_dist, hasher.get(&generated_dist)) + .await + { + return Package::get_license_string( + meta.metadata.license.as_ref(), + meta.metadata.classifiers.as_ref(), + ); + } + debug!("package metadata lookup failed"); + return None; } + debug!("package.to_dist failed"); + None } /// Returns the [`Version`] of the package. diff --git a/crates/uv/src/commands/project/license.rs b/crates/uv/src/commands/project/license.rs index e59d15e4f72f..fa713e79d58a 100644 --- a/crates/uv/src/commands/project/license.rs +++ b/crates/uv/src/commands/project/license.rs @@ -1,3 +1,4 @@ +use anstream::print; use std::path::Path; use anyhow::{Error, Result}; @@ -201,7 +202,7 @@ pub(crate) async fn license( &flat_index, &dependency_metadata, state.clone(), - index_strategy.clone(), + index_strategy, &config_setting, build_isolation, link_mode, @@ -222,7 +223,7 @@ pub(crate) async fn license( let db = &database; let mut fetches = futures::stream::iter(lock.packages()) .map(|package| async move { - let license = package.license(&ws.clone(), interpret, &db).await; + let license = package.license(&ws.clone(), interpret, db).await; Ok::, Error>(Some((package, license))) }) .buffer_unordered(concurrency.downloads); diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index a88996b6b06f..7c03aa67e03f 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1637,7 +1637,7 @@ async fn run_project( // Initialize the cache. let cache = cache.init()?; - commands::license( + Box::pin(commands::license( project_dir, args.dev, args.locked, @@ -1659,7 +1659,7 @@ async fn run_project( &cache, printer, globals.preview, - ) + )) .await } From 9c391fc1d59b1dcecc44437351dd7edd8285a274 Mon Sep 17 00:00:00 2001 From: Ryan Leary Date: Fri, 3 Jan 2025 12:30:13 -0500 Subject: [PATCH 5/7] fix: update clippy test --- crates/uv/tests/it/help.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/uv/tests/it/help.rs b/crates/uv/tests/it/help.rs index 4957d16b8bdf..5036eaff351f 100644 --- a/crates/uv/tests/it/help.rs +++ b/crates/uv/tests/it/help.rs @@ -863,7 +863,7 @@ fn help_unknown_subcommand() { generate-shell-completion "); - uv_snapshot!(context.filters(), context.help().arg("foo").arg("bar"), @r###" + uv_snapshot!(context.filters(), context.help().arg("foo").arg("bar"), @r" success: false exit_code: 2 ----- stdout ----- @@ -878,6 +878,7 @@ fn help_unknown_subcommand() { lock export tree + license tool python pip @@ -888,7 +889,7 @@ fn help_unknown_subcommand() { self version generate-shell-completion - "###); + "); } #[test] From f6d4e3a3ca9c483feb459e7ba87fafc00058f711 Mon Sep 17 00:00:00 2001 From: Ryan Leary Date: Fri, 3 Jan 2025 12:48:23 -0500 Subject: [PATCH 6/7] fix: add initial itests and dev artifacts --- crates/uv/tests/it/common/mod.rs | 8 + crates/uv/tests/it/license.rs | 152 +++++++++++ crates/uv/tests/it/main.rs | 3 + docs/reference/cli.md | 423 +++++++++++++++++++++++++++++++ uv.schema.json | 15 ++ 5 files changed, 601 insertions(+) create mode 100644 crates/uv/tests/it/license.rs diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index b34a6faaf585..7c044980556d 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -796,6 +796,14 @@ impl TestContext { command } + /// Create a `uv license` command with options shared across scenarios. + pub fn license(&self) -> Command { + let mut command = self.new_command(); + command.arg("license"); + self.add_shared_args(&mut command, false); + command + } + /// Create a `uv cache clean` command. pub fn clean(&self) -> Command { let mut command = self.new_command(); diff --git a/crates/uv/tests/it/license.rs b/crates/uv/tests/it/license.rs new file mode 100644 index 000000000000..ecfa623b2032 --- /dev/null +++ b/crates/uv/tests/it/license.rs @@ -0,0 +1,152 @@ +use anyhow::Result; +use assert_cmd::assert::OutputAssertExt; +use assert_fs::prelude::*; +use indoc::formatdoc; +use url::Url; + +use crate::common::{uv_snapshot, TestContext}; + +#[test] +fn project_with_no_license() -> 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" + requires-python = ">=3.12" + "#, + )?; + + uv_snapshot!(context.filters(), context.license().arg("--universal"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + project: 0.1.0, Unknown License + + ----- stderr ----- + Resolved 1 package in [TIME] + "### + ); + + // `uv tree` should update the lockfile + let lock = context.read("uv.lock"); + assert!(!lock.is_empty()); + + Ok(()) +} + +#[test] +fn project_with_trove_license() -> 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" + requires-python = ">=3.12" + classifiers = [ + "License :: Other/Proprietary License" + ] + "#, + )?; + + uv_snapshot!(context.filters(), context.license().arg("--universal"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + project: 0.1.0, Other/Proprietary License + + ----- stderr ----- + Resolved 1 package in [TIME] + "### + ); + + // `uv tree` should update the lockfile + let lock = context.read("uv.lock"); + assert!(!lock.is_empty()); + + Ok(()) +} + +#[test] +fn project_with_trove_osi_license() -> 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" + requires-python = ">=3.12" + classifiers = [ + "License :: OSI Approved" + ] + "#, + )?; + + uv_snapshot!(context.filters(), context.license().arg("--universal"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + project: 0.1.0, OSI Approved + + ----- stderr ----- + Resolved 1 package in [TIME] + "### + ); + + // `uv tree` should update the lockfile + let lock = context.read("uv.lock"); + assert!(!lock.is_empty()); + + Ok(()) +} + +#[test] +fn nested_dependencies() -> 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" + requires-python = ">=3.12" + dependencies = [ + "scikit-learn==1.4.1.post1" + ] + classifiers = [ + "License :: OSI Approved :: MIT License" + ] + "#, + )?; + + uv_snapshot!(context.filters(), context.license().arg("--universal"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + project: 0.1.0, MIT License + scikit-learn: 1.4.1.post1, BSD License + joblib: 1.3.2, BSD License + numpy: 1.26.4, BSD License + scipy: 1.12.0, BSD License + threadpoolctl: 3.4.0, BSD License + + ----- stderr ----- + Resolved 6 packages in [TIME] + "### + ); + + // `uv tree` should update the lockfile + let lock = context.read("uv.lock"); + assert!(!lock.is_empty()); + + Ok(()) +} diff --git a/crates/uv/tests/it/main.rs b/crates/uv/tests/it/main.rs index 1768dd5e7a4f..0776250c9c57 100644 --- a/crates/uv/tests/it/main.rs +++ b/crates/uv/tests/it/main.rs @@ -31,6 +31,9 @@ mod help; #[cfg(all(feature = "python", feature = "pypi"))] mod init; +#[cfg(all(feature = "python", feature = "pypi"))] +mod license; + #[cfg(all(feature = "python", feature = "pypi"))] mod lock; diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 602c77c9224f..7da32fb895b5 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -28,6 +28,8 @@ uv [OPTIONS]
uv tree

Display the project’s dependency tree

+
uv license

Display the project’s license information

+
uv tool

Run and install commands provided by Python packages

uv python

Manage Python versions and installations

@@ -2965,6 +2967,427 @@ uv tree [OPTIONS]
+## uv license + +Display the project's license information + +

Usage

+ +``` +uv license [OPTIONS] +``` + +

Options

+ +
--all-groups

Include dependencies from all dependency groups.

+ +

--no-group can be used to exclude specific groups.

+ +
--allow-insecure-host allow-insecure-host

Allow insecure connections to a host.

+ +

Can be provided multiple times.

+ +

Expects to receive either a hostname (e.g., localhost), a host-port pair (e.g., localhost:8080), or a URL (e.g., https://localhost).

+ +

WARNING: Hosts included in this list will not be verified against the system’s certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.

+ +

May also be set with the UV_INSECURE_HOST environment variable.

+
--cache-dir cache-dir

Path to the cache directory.

+ +

Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

+ +

To view the location of the cache directory, run uv cache dir.

+ +

May also be set with the UV_CACHE_DIR environment variable.

+
--color color-choice

Control colors in output

+ +

[default: auto]

+

Possible values:

+ +
    +
  • auto: Enables colored output only when the output is going to a terminal or TTY with support
  • + +
  • always: Enables colored output regardless of the detected environment
  • + +
  • never: Disables colored output
  • +
+
--config-file config-file

The path to a uv.toml file to use for configuration.

+ +

While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.

+ +

May also be set with the UV_CONFIG_FILE environment variable.

+
--config-setting, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+ +
--default-index default-index

The URL of the default package index (by default: <https://pypi.org/simple>).

+ +

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

+ +

The index given by this flag is given lower priority than all other indexes specified via the --index flag.

+ +

May also be set with the UV_DEFAULT_INDEX environment variable.

+
--direct-deps-only

Display only direct dependencies (default false)

+ +
--directory directory

Change to the given directory prior to running the command.

+ +

Relative paths are resolved with the given directory as the base.

+ +

See --project to only change the project root directory.

+ +
--exclude-newer exclude-newer

Limit candidate packages to those that were uploaded prior to the given date.

+ +

Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system’s configured time zone.

+ +

May also be set with the UV_EXCLUDE_NEWER environment variable.

+
--extra-index-url extra-index-url

(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

+ +

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

+ +

All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.

+ +

May also be set with the UV_EXTRA_INDEX_URL environment variable.

+
--find-links, -f find-links

Locations to search for candidate distributions, in addition to those found in the registry indexes.

+ +

If a path, the target must be a directory that contains packages as wheel files (.whl) or source distributions (e.g., .tar.gz or .zip) at the top level.

+ +

If a URL, the page must contain a flat list of links to package files adhering to the formats described above.

+ +

May also be set with the UV_FIND_LINKS environment variable.

+
--fork-strategy fork-strategy

The strategy to use when selecting multiple versions of a given package across Python versions and platforms.

+ +

By default, uv will optimize for selecting the latest version of each package for each supported Python version (requires-python), while minimizing the number of selected versions across platforms.

+ +

Under fewest, uv will minimize the number of selected versions for each package, preferring older versions that are compatible with a wider range of supported Python versions or platforms.

+ +

May also be set with the UV_FORK_STRATEGY environment variable.

+

Possible values:

+ +
    +
  • fewest: Optimize for selecting the fewest number of versions for each package. Older versions may be preferred if they are compatible with a wider range of supported Python versions or platforms
  • + +
  • requires-python: Optimize for selecting latest supported version of each package, for each supported Python version
  • +
+
--frozen

Display the requirements without locking the project.

+ +

If the lockfile is missing, uv will exit with an error.

+ +

May also be set with the UV_FROZEN environment variable.

+
--group group

Include dependencies from the specified dependency group.

+ +

May be provided multiple times.

+ +
--help, -h

Display the concise help for this command

+ +
--index index

The URLs to use when resolving dependencies, in addition to the default index.

+ +

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

+ +

All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

+ +

May also be set with the UV_INDEX environment variable.

+
--index-strategy index-strategy

The strategy to use when resolving against multiple index URLs.

+ +

By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

+ +

May also be set with the UV_INDEX_STRATEGY environment variable.

+

Possible values:

+ +
    +
  • first-index: Only use results from the first index that returns a match for a given package name
  • + +
  • unsafe-first-match: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next
  • + +
  • unsafe-best-match: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index
  • +
+
--index-url, -i index-url

(Deprecated: use --default-index instead) The URL of the Python package index (by default: <https://pypi.org/simple>).

+ +

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

+ +

The index given by this flag is given lower priority than all other indexes specified via the --extra-index-url flag.

+ +

May also be set with the UV_INDEX_URL environment variable.

+
--keyring-provider keyring-provider

Attempt to use keyring for authentication for index URLs.

+ +

At present, only --keyring-provider subprocess is supported, which configures uv to use the keyring CLI to handle authentication.

+ +

Defaults to disabled.

+ +

May also be set with the UV_KEYRING_PROVIDER environment variable.

+

Possible values:

+ +
    +
  • disabled: Do not use keyring for credential lookup
  • + +
  • subprocess: Use the keyring command for credential lookup
  • +
+
--link-mode link-mode

The method to use when installing packages from the global cache.

+ +

This option is only used when building source distributions.

+ +

Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.

+ +

May also be set with the UV_LINK_MODE environment variable.

+

Possible values:

+ +
    +
  • clone: Clone (i.e., copy-on-write) packages from the wheel into the site-packages directory
  • + +
  • copy: Copy packages from the wheel into the site-packages directory
  • + +
  • hardlink: Hard link packages from the wheel into the site-packages directory
  • + +
  • symlink: Symbolically link packages from the wheel into the site-packages directory
  • +
+
--locked

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.

+ +

May also be set with the UV_LOCKED environment variable.

+
--native-tls

Whether to load TLS certificates from the platform’s native certificate store.

+ +

By default, uv loads certificates from the bundled webpki-roots crate. The webpki-roots are a reliable set of trust roots from Mozilla, and including them in uv improves portability and performance (especially on macOS).

+ +

However, in some cases, you may want to use the platform’s native certificate store, especially if you’re relying on a corporate trust root (e.g., for a mandatory proxy) that’s included in your system’s certificate store.

+ +

May also be set with the UV_NATIVE_TLS environment variable.

+
--no-binary

Don’t install pre-built wheels.

+ +

The given packages will be built and installed from source. The resolver will still use pre-built wheels to extract package metadata, if available.

+ +
--no-binary-package no-binary-package

Don’t install pre-built wheels for a specific package

+ +
--no-build

Don’t build source distributions.

+ +

When enabled, resolving will not run arbitrary Python code. The cached wheels of already-built source distributions will be reused, but operations that require building distributions will exit with an error.

+ +
--no-build-isolation

Disable isolation when building source distributions.

+ +

Assumes that build dependencies specified by PEP 518 are already installed.

+ +

May also be set with the UV_NO_BUILD_ISOLATION environment variable.

+
--no-build-isolation-package no-build-isolation-package

Disable isolation when building source distributions for a specific package.

+ +

Assumes that the packages’ build dependencies specified by PEP 518 are already installed.

+ +
--no-build-package no-build-package

Don’t build source distributions for a specific package

+ +
--no-cache, -n

Avoid reading from or writing to the cache, instead using a temporary directory for the duration of the operation

+ +

May also be set with the UV_NO_CACHE environment variable.

+
--no-config

Avoid discovering configuration files (pyproject.toml, uv.toml).

+ +

Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.

+ +

May also be set with the UV_NO_CONFIG environment variable.

+
--no-dev

Omit the development dependency group.

+ +

This option is an alias for --no-group dev.

+ +
--no-group no-group

Exclude dependencies from the specified dependency group.

+ +

May be provided multiple times.

+ +
--no-index

Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those provided via --find-links

+ +
--no-progress

Hide all progress outputs.

+ +

For example, spinners or progress bars.

+ +

May also be set with the UV_NO_PROGRESS environment variable.

+
--no-python-downloads

Disable automatic downloads of Python.

+ +
--no-sources

Ignore the tool.uv.sources table when resolving dependencies. Used to lock against the standards-compliant, publishable package metadata, as opposed to using any local or Git sources

+ +
--offline

Disable network access.

+ +

When disabled, uv will only use locally cached data and locally available files.

+ +

May also be set with the UV_OFFLINE environment variable.

+
--only-dev

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.

+ +
--only-group only-group

Only include dependencies from the specified dependency group.

+ +

May be provided multiple times.

+ +

The project itself will also be omitted.

+ +
--prerelease prerelease

The strategy to use when considering pre-release versions.

+ +

By default, uv will accept pre-releases for packages that only publish pre-releases, along with first-party requirements that contain an explicit pre-release marker in the declared specifiers (if-necessary-or-explicit).

+ +

May also be set with the UV_PRERELEASE environment variable.

+

Possible values:

+ +
    +
  • disallow: Disallow all pre-release versions
  • + +
  • allow: Allow all pre-release versions
  • + +
  • if-necessary: Allow pre-release versions if all versions of a package are pre-release
  • + +
  • explicit: Allow pre-release versions for first-party packages with explicit pre-release markers in their version requirements
  • + +
  • if-necessary-or-explicit: Allow pre-release versions if all versions of a package are pre-release, or if the package has an explicit pre-release marker in its version requirements
  • +
+
--project project

Run the command within the given project directory.

+ +

All pyproject.toml, uv.toml, and .python-version files will be discovered by walking up the directory tree from the project root, as will the project’s virtual environment (.venv).

+ +

Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.

+ +

See --directory to change the working directory entirely.

+ +

This setting has no effect when used in the uv pip interface.

+ +
--python, -p python

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 python for details on Python discovery and supported request formats.

+ +

May also be set with the UV_PYTHON environment variable.

+
--python-platform python-platform

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.

+ +

Possible values:

+ +
    +
  • windows: An alias for x86_64-pc-windows-msvc, the default target for Windows
  • + +
  • linux: An alias for x86_64-unknown-linux-gnu, the default target for Linux
  • + +
  • macos: An alias for aarch64-apple-darwin, the default target for macOS
  • + +
  • x86_64-pc-windows-msvc: A 64-bit x86 Windows target
  • + +
  • i686-pc-windows-msvc: A 32-bit x86 Windows target
  • + +
  • x86_64-unknown-linux-gnu: An x86 Linux target. Equivalent to x86_64-manylinux_2_17
  • + +
  • aarch64-apple-darwin: An ARM-based macOS target, as seen on Apple Silicon devices
  • + +
  • x86_64-apple-darwin: An x86 macOS target
  • + +
  • aarch64-unknown-linux-gnu: An ARM64 Linux target. Equivalent to aarch64-manylinux_2_17
  • + +
  • aarch64-unknown-linux-musl: An ARM64 Linux target
  • + +
  • x86_64-unknown-linux-musl: An x86_64 Linux target
  • + +
  • x86_64-manylinux2014: An x86_64 target for the manylinux2014 platform. Equivalent to x86_64-manylinux_2_17
  • + +
  • x86_64-manylinux_2_17: An x86_64 target for the manylinux_2_17 platform
  • + +
  • x86_64-manylinux_2_28: An x86_64 target for the manylinux_2_28 platform
  • + +
  • x86_64-manylinux_2_31: An x86_64 target for the manylinux_2_31 platform
  • + +
  • x86_64-manylinux_2_32: An x86_64 target for the manylinux_2_32 platform
  • + +
  • x86_64-manylinux_2_33: An x86_64 target for the manylinux_2_33 platform
  • + +
  • x86_64-manylinux_2_34: An x86_64 target for the manylinux_2_34 platform
  • + +
  • x86_64-manylinux_2_35: An x86_64 target for the manylinux_2_35 platform
  • + +
  • x86_64-manylinux_2_36: An x86_64 target for the manylinux_2_36 platform
  • + +
  • x86_64-manylinux_2_37: An x86_64 target for the manylinux_2_37 platform
  • + +
  • x86_64-manylinux_2_38: An x86_64 target for the manylinux_2_38 platform
  • + +
  • x86_64-manylinux_2_39: An x86_64 target for the manylinux_2_39 platform
  • + +
  • x86_64-manylinux_2_40: An x86_64 target for the manylinux_2_40 platform
  • + +
  • aarch64-manylinux2014: An ARM64 target for the manylinux2014 platform. Equivalent to aarch64-manylinux_2_17
  • + +
  • aarch64-manylinux_2_17: An ARM64 target for the manylinux_2_17 platform
  • + +
  • aarch64-manylinux_2_28: An ARM64 target for the manylinux_2_28 platform
  • + +
  • aarch64-manylinux_2_31: An ARM64 target for the manylinux_2_31 platform
  • + +
  • aarch64-manylinux_2_32: An ARM64 target for the manylinux_2_32 platform
  • + +
  • aarch64-manylinux_2_33: An ARM64 target for the manylinux_2_33 platform
  • + +
  • aarch64-manylinux_2_34: An ARM64 target for the manylinux_2_34 platform
  • + +
  • aarch64-manylinux_2_35: An ARM64 target for the manylinux_2_35 platform
  • + +
  • aarch64-manylinux_2_36: An ARM64 target for the manylinux_2_36 platform
  • + +
  • aarch64-manylinux_2_37: An ARM64 target for the manylinux_2_37 platform
  • + +
  • aarch64-manylinux_2_38: An ARM64 target for the manylinux_2_38 platform
  • + +
  • aarch64-manylinux_2_39: An ARM64 target for the manylinux_2_39 platform
  • + +
  • aarch64-manylinux_2_40: An ARM64 target for the manylinux_2_40 platform
  • +
+
--python-preference python-preference

Whether to prefer uv-managed or system Python installations.

+ +

By default, uv prefers using Python versions it manages. However, it will use system Python installations if a uv-managed Python is not installed. This option allows prioritizing or ignoring system Python installations.

+ +

May also be set with the UV_PYTHON_PREFERENCE environment variable.

+

Possible values:

+ +
    +
  • only-managed: Only use managed Python installations; never use system Python installations
  • + +
  • managed: Prefer managed Python installations over system Python installations
  • + +
  • system: Prefer system Python installations over managed Python installations
  • + +
  • only-system: Only use system Python installations; never use managed Python installations
  • +
+
--python-version python-version

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.

+ +
--quiet, -q

Do not print any output

+ +
--resolution resolution

The strategy to use when selecting between the different compatible versions for a given package requirement.

+ +

By default, uv will use the latest compatible version of each package (highest).

+ +

May also be set with the UV_RESOLUTION environment variable.

+

Possible values:

+ +
    +
  • highest: Resolve the highest compatible version of each package
  • + +
  • lowest: Resolve the lowest compatible version of each package
  • + +
  • lowest-direct: Resolve the lowest compatible version of any direct dependencies, and the highest compatible version of any transitive dependencies
  • +
+
--universal

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.

+ +
--upgrade, -U

Allow package upgrades, ignoring pinned versions in any existing output file. Implies --refresh

+ +
--upgrade-package, -P upgrade-package

Allow upgrades for a specific package, ignoring pinned versions in any existing output file. Implies --refresh-package

+ +
--verbose, -v

Use verbose output.

+ +

You can configure fine-grained logging using the RUST_LOG environment variable. (<https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives>)

+ +
--version, -V

Display the uv version

+ +
+ ## uv tool Run and install commands provided by Python packages diff --git a/uv.schema.json b/uv.schema.json index fcba78967b0d..3043c023f1a4 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -1749,6 +1749,21 @@ "name" ], "properties": { + "classifiers": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "license": { + "type": [ + "string", + "null" + ] + }, "name": { "$ref": "#/definitions/PackageName" }, From 213daf42aee369f84ee5659490877bd958d5b564 Mon Sep 17 00:00:00 2001 From: Ryan Leary Date: Fri, 3 Jan 2025 13:08:52 -0500 Subject: [PATCH 7/7] fix: add more test coverage --- crates/uv/tests/it/license.rs | 399 +++++++++++++++++++++++++++++++++- 1 file changed, 398 insertions(+), 1 deletion(-) diff --git a/crates/uv/tests/it/license.rs b/crates/uv/tests/it/license.rs index ecfa623b2032..58558d3198e8 100644 --- a/crates/uv/tests/it/license.rs +++ b/crates/uv/tests/it/license.rs @@ -1,5 +1,4 @@ use anyhow::Result; -use assert_cmd::assert::OutputAssertExt; use assert_fs::prelude::*; use indoc::formatdoc; use url::Url; @@ -150,3 +149,401 @@ fn nested_dependencies() -> Result<()> { Ok(()) } + +#[test] +fn nested_platform_dependencies() -> 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" + requires-python = ">=3.12" + dependencies = [ + "jupyter-client" + ] + "#, + )?; + + uv_snapshot!(context.filters(), context.license().arg("--python-platform").arg("linux"), @r" + success: true + exit_code: 0 + ----- stdout ----- + project: 0.1.0, Unknown License + jupyter-client: 8.6.1, BSD License + jupyter-core: 5.7.2, BSD License + platformdirs: 4.2.0, MIT License + traitlets: 5.14.2, BSD License + python-dateutil: 2.9.0.post0, BSD License, Apache Software License + six: 1.16.0, MIT License + pyzmq: 25.1.2, GNU Library or Lesser General Public License (LGPL), BSD License + tornado: 6.4, Apache Software License + + ----- stderr ----- + Resolved 12 packages in [TIME] + " + ); + + uv_snapshot!(context.filters(), context.license().arg("--universal"), @r" + success: true + exit_code: 0 + ----- stdout ----- + project: 0.1.0, Unknown License + jupyter-client: 8.6.1, BSD License + jupyter-core: 5.7.2, BSD License + platformdirs: 4.2.0, MIT License + pywin32: 306, Python Software Foundation License + traitlets: 5.14.2, BSD License + python-dateutil: 2.9.0.post0, BSD License, Apache Software License + six: 1.16.0, MIT License + pyzmq: 25.1.2, GNU Library or Lesser General Public License (LGPL), BSD License + cffi: 1.16.0, MIT License + pycparser: 2.21, BSD License + tornado: 6.4, Apache Software License + + ----- stderr ----- + Resolved 12 packages in [TIME] + " + ); + + // `uv tree` should update the lockfile + let lock = context.read("uv.lock"); + assert!(!lock.is_empty()); + + Ok(()) +} + +#[test] +fn frozen() -> 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" + requires-python = ">=3.12" + dependencies = ["anyio"] + "#, + )?; + + uv_snapshot!(context.filters(), context.license().arg("--universal"), @r" + success: true + exit_code: 0 + ----- stdout ----- + project: 0.1.0, Unknown License + anyio: 4.3.0, MIT License + idna: 3.6, BSD License + sniffio: 1.3.1, MIT License, Apache Software License + + ----- stderr ----- + Resolved 4 packages in [TIME] + " + ); + + // `uv tree` should update the lockfile + let lock = context.read("uv.lock"); + assert!(!lock.is_empty()); + + // Update the project dependencies. + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + "#, + )?; + + // Running with `--frozen` should show the stale tree. + uv_snapshot!(context.filters(), context.license().arg("--frozen"), @r" + success: true + exit_code: 0 + ----- stdout ----- + project: 0.1.0, Unknown License + anyio: 4.3.0, MIT License + idna: 3.6, BSD License + sniffio: 1.3.1, MIT License, Apache Software License + + ----- stderr ----- + " + ); + + Ok(()) +} + +#[test] +fn platform_dependencies() -> 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" + requires-python = ">=3.12" + dependencies = [ + "black" + ] + "#, + )?; + + // When `--universal` is _not_ provided, `colorama` should _not_ be included. + #[cfg(not(windows))] + uv_snapshot!(context.filters(), context.license(), @r" + success: true + exit_code: 0 + ----- stdout ----- + project: 0.1.0, Unknown License + black: 24.3.0, MIT License + click: 8.1.7, BSD License + mypy-extensions: 1.0.0, MIT License + packaging: 24.0, Apache Software License, BSD License + pathspec: 0.12.1, Mozilla Public License 2.0 (MPL 2.0) + platformdirs: 4.2.0, MIT License + + ----- stderr ----- + Resolved 8 packages in [TIME] + "); + + // Unless `--python-platform` is set to `windows`, in which case it should be included. + uv_snapshot!(context.filters(), context.license().arg("--python-platform").arg("windows"), @r" + success: true + exit_code: 0 + ----- stdout ----- + project: 0.1.0, Unknown License + black: 24.3.0, MIT License + click: 8.1.7, BSD License + colorama: 0.4.6, BSD License + mypy-extensions: 1.0.0, MIT License + packaging: 24.0, Apache Software License, BSD License + pathspec: 0.12.1, Mozilla Public License 2.0 (MPL 2.0) + platformdirs: 4.2.0, MIT License + + ----- stderr ----- + Resolved 8 packages in [TIME] + "); + + // When `--universal` is _not_ provided, should include `colorama`, even though it's only + // included on Windows. + uv_snapshot!(context.filters(), context.license().arg("--universal"), @r" + success: true + exit_code: 0 + ----- stdout ----- + project: 0.1.0, Unknown License + black: 24.3.0, MIT License + click: 8.1.7, BSD License + colorama: 0.4.6, BSD License + mypy-extensions: 1.0.0, MIT License + packaging: 24.0, Apache Software License, BSD License + pathspec: 0.12.1, Mozilla Public License 2.0 (MPL 2.0) + platformdirs: 4.2.0, MIT License + + ----- stderr ----- + Resolved 8 packages in [TIME] + " + ); + + // `uv tree` should update the lockfile + let lock = context.read("uv.lock"); + assert!(!lock.is_empty()); + + Ok(()) +} + +#[test] +fn repeated_dependencies() -> 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" + requires-python = ">=3.12" + dependencies = [ + "anyio < 2 ; sys_platform == 'win32'", + "anyio > 2 ; sys_platform == 'linux'", + ] + "#, + )?; + + // Should include both versions of `anyio`, which have different dependencies. + uv_snapshot!(context.filters(), context.license().arg("--universal"), @r" + success: true + exit_code: 0 + ----- stdout ----- + project: 0.1.0, Unknown License + anyio: 1.4.0, MIT License + async-generator: 1.10, MIT License, Apache Software License + idna: 3.6, BSD License + sniffio: 1.3.1, MIT License, Apache Software License + anyio: 4.3.0, MIT License + + ----- stderr ----- + Resolved 6 packages in [TIME] + " + ); + + // `uv tree` should update the lockfile + let lock = context.read("uv.lock"); + assert!(!lock.is_empty()); + + Ok(()) +} + +/// In this case, a package is included twice at the same version, but pointing to different direct +/// URLs. +#[test] +fn repeated_version() -> Result<()> { + let context = TestContext::new("3.12"); + + let v1 = context.temp_dir.child("v1"); + fs_err::create_dir_all(&v1)?; + let pyproject_toml = v1.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "dependency" + version = "0.0.1" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0"] + "#, + )?; + + let v2 = context.temp_dir.child("v2"); + fs_err::create_dir_all(&v2)?; + let pyproject_toml = v2.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "dependency" + version = "0.0.1" + requires-python = ">=3.12" + dependencies = ["anyio==3.0.0"] + "#, + )?; + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(&formatdoc! { + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + "dependency @ {} ; sys_platform == 'darwin'", + "dependency @ {} ; sys_platform != 'darwin'", + ] + "#, + Url::from_file_path(context.temp_dir.join("v1")).unwrap(), + Url::from_file_path(context.temp_dir.join("v2")).unwrap(), + })?; + + uv_snapshot!(context.filters(), context.license().arg("--universal"), @r" + success: true + exit_code: 0 + ----- stdout ----- + project: 0.1.0, Unknown License + dependency: 0.0.1, Unknown License + anyio: 3.7.0, MIT License + idna: 3.6, BSD License + sniffio: 1.3.1, MIT License, Apache Software License + dependency: 0.0.1, Unknown License + anyio: 3.0.0, MIT License + + ----- stderr ----- + Resolved 7 packages in [TIME] + " + ); + + // `uv tree` should update the lockfile + let lock = context.read("uv.lock"); + assert!(!lock.is_empty()); + + Ok(()) +} + +#[test] +fn workspace_dev() -> 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" + requires-python = ">=3.12" + dependencies = ["anyio"] + + [dependency-groups] + dev = ["child"] + + [tool.uv.workspace] + members = ["child"] + + [tool.uv.sources] + child = { workspace = true } + "#, + )?; + + let child = context.temp_dir.child("child"); + let pyproject_toml = child.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "child" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + "#, + )?; + + uv_snapshot!(context.filters(), context.license().arg("--universal"), @r" + success: true + exit_code: 0 + ----- stdout ----- + project: 0.1.0, Unknown License + anyio: 4.3.0, MIT License + idna: 3.6, BSD License + sniffio: 1.3.1, MIT License, Apache Software License + child: 0.1.0, Unknown License (group: dev) + iniconfig: 2.0.0, MIT License + + ----- stderr ----- + Resolved 6 packages in [TIME] + " + ); + + // Under `--no-dev`, the member should still be included, since we show the entire workspace. + // But it shouldn't be considered a dependency of the root. + uv_snapshot!(context.filters(), context.license().arg("--universal").arg("--no-dev"), @r" + success: true + exit_code: 0 + ----- stdout ----- + child: 0.1.0, Unknown License + iniconfig: 2.0.0, MIT License + project: 0.1.0, Unknown License + anyio: 4.3.0, MIT License + idna: 3.6, BSD License + sniffio: 1.3.1, MIT License, Apache Software License + + ----- stderr ----- + Resolved 6 packages in [TIME] + " + ); + + // `uv tree` should update the lockfile + let lock = context.read("uv.lock"); + assert!(!lock.is_empty()); + + Ok(()) +}