From 26cc6f59979beb2ff6c4e593f8b85e05155d3351 Mon Sep 17 00:00:00 2001 From: konstin Date: Thu, 19 Sep 2024 14:30:07 +0200 Subject: [PATCH] Add `uv publish` cli and configuration --- Cargo.lock | 1 + crates/uv-cli/src/lib.rs | 65 ++++++++++++++ crates/uv-settings/Cargo.toml | 1 + crates/uv-settings/src/combine.rs | 2 + crates/uv-settings/src/settings.rs | 24 +++++- crates/uv/src/lib.rs | 28 ++++++ crates/uv/src/settings.rs | 75 +++++++++++++++- crates/uv/tests/help.rs | 7 ++ docs/configuration/environment.md | 8 ++ docs/guides/publish.md | 20 +++-- docs/reference/cli.md | 134 +++++++++++++++++++++++++++++ docs/reference/settings.md | 26 ++++++ uv.schema.json | 8 ++ 13 files changed, 386 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 71edd66e8d69..cf26eae9c296 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5154,6 +5154,7 @@ dependencies = [ "thiserror", "toml", "tracing", + "url", "uv-cache-info", "uv-configuration", "uv-fs", diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index e9515a9e1262..374e5092b997 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -10,6 +10,7 @@ use clap::{Args, Parser, Subcommand}; use distribution_types::{FlatIndexLocation, IndexUrl}; use pep508_rs::Requirement; use pypi_types::VerbatimParsedUrl; +use url::Url; use uv_cache::CacheArgs; use uv_configuration::{ ConfigSettingEntry, ExportFormat, IndexStrategy, KeyringProviderType, PackageNameSpecifier, @@ -367,6 +368,8 @@ pub enum Commands { after_long_help = "" )] Build(BuildArgs), + /// Upload distributions to an index. + Publish(PublishArgs), /// Manage uv's cache. #[command( after_help = "Use `uv help cache` for more details.", @@ -4287,3 +4290,65 @@ pub struct DisplayTreeArgs { #[arg(long, alias = "reverse")] pub invert: bool, } + +#[derive(Args, Debug)] +pub struct PublishArgs { + /// The paths to the files to uploads, as glob expressions. + #[arg(default_value = "dist/*")] + pub files: Vec, + + /// The URL to the upload endpoint. Note: This is usually not the same as the index URL. + /// + /// The default value is publish URL for PyPI (). + #[arg(long, env = "UV_PUBLISH_URL")] + pub publish_url: Option, + + /// The username for the upload. + #[arg(short, long, env = "UV_PUBLISH_USERNAME")] + pub username: Option, + + /// The password for the upload. + #[arg(short, long, env = "UV_PUBLISH_PASSWORD")] + pub password: Option, + + /// The token for the upload. + /// + /// Using a token is equivalent to using `__token__` as username and using the token as + /// password. + #[arg( + short, + long, + env = "UV_PUBLISH_TOKEN", + conflicts_with = "username", + conflicts_with = "password" + )] + pub token: Option, + + /// Attempt to use `keyring` for authentication for remote requirements files. + /// + /// At present, only `--keyring-provider subprocess` is supported, which configures uv to + /// use the `keyring` CLI to handle authentication. + /// + /// Defaults to `disabled`. + #[arg(long, value_enum, env = "UV_KEYRING_PROVIDER")] + pub keyring_provider: Option, + + /// 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. + #[arg( + long, + alias = "trusted-host", + env = "UV_INSECURE_HOST", + value_delimiter = ' ', + value_parser = parse_insecure_host, + )] + pub allow_insecure_host: Option>>, +} diff --git a/crates/uv-settings/Cargo.toml b/crates/uv-settings/Cargo.toml index 6be4b49b758f..1473eb0c75f0 100644 --- a/crates/uv-settings/Cargo.toml +++ b/crates/uv-settings/Cargo.toml @@ -36,6 +36,7 @@ textwrap = { workspace = true } thiserror = { workspace = true } toml = { workspace = true } tracing = { workspace = true } +url = { workspace = true } [package.metadata.cargo-shear] ignored = ["uv-options-metadata", "clap"] diff --git a/crates/uv-settings/src/combine.rs b/crates/uv-settings/src/combine.rs index f7c1e8af6581..a4e53e6451f3 100644 --- a/crates/uv-settings/src/combine.rs +++ b/crates/uv-settings/src/combine.rs @@ -1,5 +1,6 @@ use std::num::NonZeroUsize; use std::path::PathBuf; +use url::Url; use distribution_types::IndexUrl; use install_wheel_rs::linker::LinkMode; @@ -71,6 +72,7 @@ impl_combine_or!(AnnotationStyle); impl_combine_or!(ExcludeNewer); impl_combine_or!(IndexStrategy); impl_combine_or!(IndexUrl); +impl_combine_or!(Url); impl_combine_or!(KeyringProviderType); impl_combine_or!(LinkMode); impl_combine_or!(NonZeroUsize); diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index 55b1f7a945d8..9206e4fe5177 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -1,11 +1,11 @@ use std::{fmt::Debug, num::NonZeroUsize, path::PathBuf}; -use serde::{Deserialize, Serialize}; - use distribution_types::{FlatIndexLocation, IndexUrl, StaticMetadata}; use install_wheel_rs::linker::LinkMode; use pep508_rs::Requirement; use pypi_types::{SupportedEnvironments, VerbatimParsedUrl}; +use serde::{Deserialize, Serialize}; +use url::Url; use uv_cache_info::CacheKey; use uv_configuration::{ ConfigSettings, IndexStrategy, KeyringProviderType, PackageNameSpecifier, TargetTriple, @@ -40,6 +40,8 @@ pub struct Options { pub globals: GlobalOptions, #[serde(flatten)] pub top_level: ResolverInstallerOptions, + #[serde(flatten)] + pub publish: PublishOptions, #[option_group] pub pip: Option, @@ -1472,3 +1474,21 @@ impl From for ResolverInstallerOptions { } } } + +#[derive( + Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, CombineOptions, OptionsMetadata, +)] +#[serde(rename_all = "kebab-case")] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct PublishOptions { + /// The URL for publishing packages to the Python package index (by default: + /// ). + #[option( + default = "\"https://upload.pypi.org/legacy\"", + value_type = "str", + example = r#" + publish-url = "https://test.pypi.org/simple" + "# + )] + pub publish_url: Option, +} diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 80b788ec2578..565219041a6d 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -31,6 +31,7 @@ use crate::printer::Printer; use crate::settings::{ CacheSettings, GlobalSettings, PipCheckSettings, PipCompileSettings, PipFreezeSettings, PipInstallSettings, PipListSettings, PipShowSettings, PipSyncSettings, PipUninstallSettings, + PublishSettings, }; #[cfg(target_os = "windows")] @@ -1063,6 +1064,33 @@ async fn run(cli: Cli) -> Result { commands::python_dir()?; Ok(ExitStatus::Success) } + Commands::Publish(args) => { + show_settings!(args); + // Resolve the settings from the command-line arguments and workspace configuration. + let PublishSettings { + files, + username, + password, + publish_url, + keyring_provider, + allow_insecure_host, + } = PublishSettings::resolve(args, filesystem); + + todo!( + "{:?}", + ( + files, + publish_url, + keyring_provider, + allow_insecure_host, + username, + password, + globals.connectivity, + globals.native_tls, + printer, + ) + ) + } } } diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index c7d1eb626825..d1d7556f308c 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -8,10 +8,11 @@ use distribution_types::{DependencyMetadata, IndexLocations}; use install_wheel_rs::linker::LinkMode; use pep508_rs::{ExtraName, RequirementOrigin}; use pypi_types::{Requirement, SupportedEnvironments}; +use url::Url; use uv_cache::{CacheArgs, Refresh}; use uv_cli::{ options::{flag, resolver_installer_options, resolver_options}, - BuildArgs, ExportArgs, ToolUpgradeArgs, + BuildArgs, ExportArgs, PublishArgs, ToolUpgradeArgs, }; use uv_cli::{ AddArgs, ColorChoice, ExternalCommand, GlobalArgs, InitArgs, ListFormat, LockArgs, Maybe, @@ -30,7 +31,8 @@ use uv_normalize::PackageName; use uv_python::{Prefix, PythonDownloads, PythonPreference, PythonVersion, Target}; use uv_resolver::{AnnotationStyle, DependencyMode, ExcludeNewer, PrereleaseMode, ResolutionMode}; use uv_settings::{ - Combine, FilesystemOptions, Options, PipOptions, ResolverInstallerOptions, ResolverOptions, + Combine, FilesystemOptions, Options, PipOptions, PublishOptions, ResolverInstallerOptions, + ResolverOptions, }; use uv_warnings::warn_user_once; use uv_workspace::pyproject::DependencyType; @@ -38,6 +40,9 @@ use uv_workspace::pyproject::DependencyType; use crate::commands::ToolRunCommand; use crate::commands::{pip::operations::Modifications, InitProjectKind}; +/// The default publish URL. +const PYPI_PUBLISH_URL: &str = "https://upload.pypi.org/legacy/"; + /// The resolved global settings to use for any invocation of the CLI. #[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] @@ -2419,6 +2424,72 @@ impl<'a> From> for InstallerSettingsRef<'a> { } } +/// The resolved settings to use for an invocation of the `uv publish` CLI. +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone)] +pub(crate) struct PublishSettings { + // CLI only, see [`PublishArgs`] for docs. + pub(crate) files: Vec, + pub(crate) username: Option, + pub(crate) password: Option, + + // Both CLI and configuration. + pub(crate) publish_url: Url, + pub(crate) keyring_provider: KeyringProviderType, + pub(crate) allow_insecure_host: Vec, +} + +impl PublishSettings { + /// Resolve the [`crate::settings::PublishSettings`] from the CLI and filesystem configuration. + pub(crate) fn resolve(args: PublishArgs, filesystem: Option) -> Self { + let Options { + publish, top_level, .. + } = filesystem + .map(FilesystemOptions::into_options) + .unwrap_or_default(); + + let PublishOptions { publish_url } = publish; + let ResolverInstallerOptions { + keyring_provider, + allow_insecure_host, + .. + } = top_level; + + // Tokens are encoded in the same way as username/password + let (username, password) = if let Some(token) = args.token { + (Some("__token__".to_string()), Some(token)) + } else { + (args.username, args.password) + }; + + Self { + files: args.files, + username, + password, + publish_url: args + .publish_url + .combine(publish_url) + // TODO(reviewer): This is different from how it's done anywhere else, but i haven't + // figured out what the ergonomic pattern is? + .unwrap_or(Url::parse(PYPI_PUBLISH_URL).unwrap()), + keyring_provider: args + .keyring_provider + .combine(keyring_provider) + .unwrap_or_default(), + allow_insecure_host: args + .allow_insecure_host + .map(|allow_insecure_host| { + allow_insecure_host + .into_iter() + .filter_map(Maybe::into_option) + .collect() + }) + .combine(allow_insecure_host) + .unwrap_or_default(), + } + } +} + // Environment variables that are not exposed as CLI arguments. mod env { pub(super) const CONCURRENT_DOWNLOADS: (&str, &str) = diff --git a/crates/uv/tests/help.rs b/crates/uv/tests/help.rs index 516408dcdad5..c4b9369b721b 100644 --- a/crates/uv/tests/help.rs +++ b/crates/uv/tests/help.rs @@ -29,6 +29,7 @@ fn help() { pip Manage Python packages with a pip-compatible interface venv Create a virtual environment build Build Python packages into source distributions and wheels + publish Upload distributions to an index cache Manage uv's cache version Display uv's version generate-shell-completion Generate shell completion @@ -94,6 +95,7 @@ fn help_flag() { pip Manage Python packages with a pip-compatible interface venv Create a virtual environment build Build Python packages into source distributions and wheels + publish Upload distributions to an index cache Manage uv's cache version Display uv's version help Display documentation for a command @@ -157,6 +159,7 @@ fn help_short_flag() { pip Manage Python packages with a pip-compatible interface venv Create a virtual environment build Build Python packages into source distributions and wheels + publish Upload distributions to an index cache Manage uv's cache version Display uv's version help Display documentation for a command @@ -637,6 +640,7 @@ fn help_unknown_subcommand() { pip venv build + publish cache version generate-shell-completion @@ -662,6 +666,7 @@ fn help_unknown_subcommand() { pip venv build + publish cache version generate-shell-completion @@ -714,6 +719,7 @@ fn help_with_global_option() { pip Manage Python packages with a pip-compatible interface venv Create a virtual environment build Build Python packages into source distributions and wheels + publish Upload distributions to an index cache Manage uv's cache version Display uv's version generate-shell-completion Generate shell completion @@ -815,6 +821,7 @@ fn help_with_no_pager() { pip Manage Python packages with a pip-compatible interface venv Create a virtual environment build Build Python packages into source distributions and wheels + publish Upload distributions to an index cache Manage uv's cache version Display uv's version generate-shell-completion Generate shell completion diff --git a/docs/configuration/environment.md b/docs/configuration/environment.md index 9cb1142db78c..9e110637d1b2 100644 --- a/docs/configuration/environment.md +++ b/docs/configuration/environment.md @@ -63,6 +63,14 @@ uv accepts the following command-line arguments as environment variables: `--no-python-downloads` option. Whether uv should allow Python downloads. - `UV_COMPILE_BYTECODE`: Equivalent to the `--compile-bytecode` command-line argument. If set, uv will compile Python source files to bytecode after installation. +- `UV_PUBLISH_URL`: Equivalent to the `--publish-url` command-line argument. The URL of the upload + endpoint of the index to use with `uv publish`. +- `UV_PUBLISH_TOKEN`: Equivalent to the `--token` command-line argument in `uv publish`. If set, uv + will use this token (with the username `__token__`) for publishing. +- `UV_PUBLISH_USERNAME`: Equivalent to the `--username` command-line argument in `uv publish`. If + set, uv will use this username for publishing. +- `UV_PUBLISH_PASSWORD`: Equivalent to the `--password` command-line argument in `uv publish`. If + set, uv will use this password for publishing. In each case, the corresponding command-line argument takes precedence over an environment variable. diff --git a/docs/guides/publish.md b/docs/guides/publish.md index 537d088eb4b0..cae0acb0a92c 100644 --- a/docs/guides/publish.md +++ b/docs/guides/publish.md @@ -1,10 +1,7 @@ # Publishing a package -uv supports building Python packages into source and binary distributions via `uv build`. - -As uv does not yet have a dedicated command for publishing packages, you can use the PyPA tool -[`twine`](https://github.com/pypa/twine) to upload your package to a package registry, which can be -invoked via `uvx`. +uv supports building Python packages into source and binary distributions via `uv build` and +uploading them to a registry with `uv publish`. ## Preparing your project for packaging @@ -32,15 +29,20 @@ Alternatively, `uv build ` will build the package in the specified director ## Publishing your package -Publish your package with `twine`: +Publish your package with `uv publish`: ```console -$ uvx twine upload dist/* +$ uv publish ``` -!!! tip +Set a PyPI token with `--token` or `UV_PUBLISH_TOKEN`, or set a username with `--username` or +`UV_PUBLISH_USERNAME` and password with `--password` or `UV_PUBLISH_PASSWORD`. + +!!! note - To provide credentials, use the `TWINE_USERNAME` and `TWINE_PASSWORD` environment variables. + PyPI does not support publishing with username and password anymore, instead you need to + generate a token. Using a token is equivalent to setting `--username __token__` and using the + token as password. ## Installing your package diff --git a/docs/reference/cli.md b/docs/reference/cli.md index a7ee29fd4982..7719471cd557 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -38,6 +38,8 @@ uv [OPTIONS]
uv build

Build Python packages into source distributions and wheels

+
uv publish

Upload distributions to an index

+
uv cache

Manage uv’s cache

uv version

Display uv’s version

@@ -6520,6 +6522,138 @@ uv build [OPTIONS] [SRC]
+## uv publish + +Upload distributions to an index + +

Usage

+ +``` +uv publish [OPTIONS] [FILES]... +``` + +

Arguments

+ +
FILES

The paths to the files to uploads, as glob expressions

+ +
+ +

Options

+ +
--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 $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

+ +

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.

+
--help, -h

Display the concise help for this command

+ +
--keyring-provider keyring-provider

Attempt to use keyring for authentication for remote requirements files.

+ +

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
  • +
+
--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-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-progress

Hide all progress outputs.

+ +

For example, spinners or progress bars.

+ +
--no-python-downloads

Disable automatic downloads of Python.

+ +
--offline

Disable network access.

+ +

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

+ +
--password, -p password

The password for the upload

+ +

May also be set with the UV_PUBLISH_PASSWORD environment variable.

+
--publish-url publish-url

The URL to the upload endpoint. Note: This is usually not the same as the index URL.

+ +

The default value is publish URL for PyPI (<https://upload.pypi.org/legacy/>).

+ +

May also be set with the UV_PUBLISH_URL environment variable.

+
--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
  • +
+
--quiet, -q

Do not print any output

+ +
--token, -t token

The token for the upload.

+ +

Using a token is equivalent to using __token__ as username and using the token as password.

+ +

May also be set with the UV_PUBLISH_TOKEN environment variable.

+
--username, -u username

The username for the upload

+ +

May also be set with the UV_PUBLISH_USERNAME environment variable.

+
--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 cache Manage uv's cache diff --git a/docs/reference/settings.md b/docs/reference/settings.md index 97718c304788..5afa2e2ab87e 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -1077,6 +1077,32 @@ Whether to enable experimental, preview features. --- +### [`publish-url`](#publish-url) {: #publish-url } + +The URL for publishing packages to the Python package index (by default: +). + +**Default value**: `"https://upload.pypi.org/legacy"` + +**Type**: `str` + +**Example usage**: + +=== "pyproject.toml" + + ```toml + [tool.uv] + publish-url = "https://test.pypi.org/simple" + ``` +=== "uv.toml" + + ```toml + + publish-url = "https://test.pypi.org/simple" + ``` + +--- + ### [`python-downloads`](#python-downloads) {: #python-downloads } Whether to allow Python downloads. diff --git a/uv.schema.json b/uv.schema.json index 15cdc4729ca4..eac3570681d6 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -330,6 +330,14 @@ "null" ] }, + "publish-url": { + "description": "The URL for publishing packages to the Python package index (by default: ).", + "type": [ + "string", + "null" + ], + "format": "uri" + }, "python-downloads": { "description": "Whether to allow Python downloads.", "anyOf": [