Skip to content

Commit

Permalink
Add uv publish cli and configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
konstin committed Sep 19, 2024
1 parent 593fca0 commit 26cc6f5
Show file tree
Hide file tree
Showing 13 changed files with 386 additions and 13 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

65 changes: 65 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -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<String>,

/// 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/>).
#[arg(long, env = "UV_PUBLISH_URL")]
pub publish_url: Option<Url>,

/// The username for the upload.
#[arg(short, long, env = "UV_PUBLISH_USERNAME")]
pub username: Option<String>,

/// The password for the upload.
#[arg(short, long, env = "UV_PUBLISH_PASSWORD")]
pub password: Option<String>,

/// 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<String>,

/// 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<KeyringProviderType>,

/// 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<Vec<Maybe<TrustedHost>>>,
}
1 change: 1 addition & 0 deletions crates/uv-settings/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
2 changes: 2 additions & 0 deletions crates/uv-settings/src/combine.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
Expand Down
24 changes: 22 additions & 2 deletions crates/uv-settings/src/settings.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<PipOptions>,

Expand Down Expand Up @@ -1472,3 +1474,21 @@ impl From<ToolOptions> 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:
/// <https://upload.pypi.org/legacy>).
#[option(
default = "\"https://upload.pypi.org/legacy\"",
value_type = "str",
example = r#"
publish-url = "https://test.pypi.org/simple"
"#
)]
pub publish_url: Option<Url>,
}
28 changes: 28 additions & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -1063,6 +1064,33 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
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,
)
)
}
}
}

Expand Down
75 changes: 73 additions & 2 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -30,14 +31,18 @@ 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;

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)]
Expand Down Expand Up @@ -2419,6 +2424,72 @@ impl<'a> From<ResolverInstallerSettingsRef<'a>> 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<String>,
pub(crate) username: Option<String>,
pub(crate) password: Option<String>,

// Both CLI and configuration.
pub(crate) publish_url: Url,
pub(crate) keyring_provider: KeyringProviderType,
pub(crate) allow_insecure_host: Vec<TrustedHost>,
}

impl PublishSettings {
/// Resolve the [`crate::settings::PublishSettings`] from the CLI and filesystem configuration.
pub(crate) fn resolve(args: PublishArgs, filesystem: Option<FilesystemOptions>) -> 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) =
Expand Down
7 changes: 7 additions & 0 deletions crates/uv/tests/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -637,6 +640,7 @@ fn help_unknown_subcommand() {
pip
venv
build
publish
cache
version
generate-shell-completion
Expand All @@ -662,6 +666,7 @@ fn help_unknown_subcommand() {
pip
venv
build
publish
cache
version
generate-shell-completion
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions docs/configuration/environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Loading

0 comments on commit 26cc6f5

Please sign in to comment.