From 4cf007d8626212dd6d8bdcc1b5d13a54dbffeb1a Mon Sep 17 00:00:00 2001 From: Luca BRUNO Date: Mon, 16 Sep 2024 11:47:53 +0200 Subject: [PATCH 1/7] Warn when trying to `uv run` a package without build configuration This enhances `uv run` logic in order to detect and warn if it is trying to run a packaged script without any `build-system` configuration in the project manifest. --- crates/uv-workspace/src/pyproject.rs | 48 ++++++++++++++++++++++----- crates/uv/src/commands/project/run.rs | 6 ++++ crates/uv/tests/run.rs | 42 +++++++++++++++++++++++ 3 files changed, 88 insertions(+), 8 deletions(-) diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index dc0609caa63b..990f230c9b91 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -44,7 +44,7 @@ pub struct PyProjectToml { #[serde(skip)] pub raw: String, - /// Used to determine whether a `build-system` is present. + /// Used to determine whether a `build-system` section is present. #[serde(default, skip_serializing)] build_system: Option, } @@ -70,18 +70,43 @@ impl PyProjectToml { /// non-package ("virtual") project. pub fn is_package(&self) -> bool { // If `tool.uv.package` is set, defer to that explicit setting. - if let Some(is_package) = self - .tool - .as_ref() - .and_then(|tool| tool.uv.as_ref()) - .and_then(|uv| uv.package) - { + if let Some(is_package) = self.is_uv_package() { return is_package; } // Otherwise, a project is assumed to be a package if `build-system` is present. self.build_system.is_some() } + + /// Returns whether the project should be considered a packaged script. + /// + /// This detects if a project is declared as a uv package, or if it has + /// a scripts-related section defined. + pub fn is_packaged_script(&self) -> bool { + if let Some(is_package) = self.is_uv_package() { + return is_package; + }; + + if let Some(ref project) = self.project { + let is_script = project.gui_scripts.is_some() || project.scripts.is_some(); + return is_script; + }; + + false + } + + /// Returns whether the project has a `build-system` section. + pub fn has_build_system(&self) -> bool { + self.build_system.is_some() + } + + /// Returns the value of `tool.uv.package`, if explicitly set. + fn is_uv_package(&self) -> Option { + self.tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.package) + } } // Ignore raw document in comparison. @@ -102,7 +127,7 @@ impl AsRef<[u8]> for PyProjectToml { /// PEP 621 project metadata (`project`). /// /// See . -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(rename_all = "kebab-case")] pub struct Project { /// The name of the project @@ -113,6 +138,13 @@ pub struct Project { pub requires_python: Option, /// The optional dependencies of the project. pub optional_dependencies: Option>>, + + /// Used to determine whether a `gui-scripts` section is present. + #[serde(default, skip_serializing)] + pub(crate) gui_scripts: Option, + /// Used to determine whether a `scripts` section is present. + #[serde(default, skip_serializing)] + pub(crate) scripts: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 110c2a618fac..1a637a3f0e93 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -451,6 +451,12 @@ pub(crate) async fn run( .await? }; + if project.workspace().pyproject_toml().is_packaged_script() + && !project.workspace().pyproject_toml().has_build_system() + { + warn_user!("No `build-system` section found for the package; consider adding one to correctly build the project"); + } + if no_sync { debug!("Skipping environment synchronization due to `--no-sync`"); } else { diff --git a/crates/uv/tests/run.rs b/crates/uv/tests/run.rs index c88749b4b539..a6895be82531 100644 --- a/crates/uv/tests/run.rs +++ b/crates/uv/tests/run.rs @@ -1950,3 +1950,45 @@ fn run_invalid_project_table() -> Result<()> { Ok(()) } + +#[test] +fn run_script_without_build_system() -> Result<()> { + // Check warning message for https://github.com/astral-sh/uv/issues/6998. + + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [project.scripts] + entry = "foo:custom_entry" + "# + })?; + + let test_script = context.temp_dir.child("src/__init__.py"); + test_script.write_str(indoc! { r#" + def custom_entry(): + print!("Hello") + "# + })?; + + uv_snapshot!(context.filters(), context.run().arg("entry"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: No `build-system` section found for the package; consider adding one to correctly build the project + Resolved 1 package in [TIME] + Audited in [TIME] + error: Failed to spawn: `entry` + Caused by: No such file or directory (os error 2) + "###); + + Ok(()) +} From 2e976e78b3f1bc900f2e1f6dd3e6cd4c2eca0f29 Mon Sep 17 00:00:00 2001 From: Luca BRUNO Date: Mon, 16 Sep 2024 14:08:45 +0200 Subject: [PATCH 2/7] misc fixes from review --- crates/uv-workspace/src/pyproject.rs | 8 ++++---- crates/uv/tests/run.rs | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 990f230c9b91..0d2be109f0d9 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -70,7 +70,7 @@ impl PyProjectToml { /// non-package ("virtual") project. pub fn is_package(&self) -> bool { // If `tool.uv.package` is set, defer to that explicit setting. - if let Some(is_package) = self.is_uv_package() { + if let Some(is_package) = self.uv_package() { return is_package; } @@ -78,12 +78,12 @@ impl PyProjectToml { self.build_system.is_some() } - /// Returns whether the project should be considered a packaged script. + /// Returns whether the project should be considered a package with scripts. /// /// This detects if a project is declared as a uv package, or if it has /// a scripts-related section defined. pub fn is_packaged_script(&self) -> bool { - if let Some(is_package) = self.is_uv_package() { + if let Some(is_package) = self.uv_package() { return is_package; }; @@ -101,7 +101,7 @@ impl PyProjectToml { } /// Returns the value of `tool.uv.package`, if explicitly set. - fn is_uv_package(&self) -> Option { + fn uv_package(&self) -> Option { self.tool .as_ref() .and_then(|tool| tool.uv.as_ref()) diff --git a/crates/uv/tests/run.rs b/crates/uv/tests/run.rs index a6895be82531..5cf9574d7f39 100644 --- a/crates/uv/tests/run.rs +++ b/crates/uv/tests/run.rs @@ -1952,9 +1952,8 @@ fn run_invalid_project_table() -> Result<()> { } #[test] +/// Check warning message for https://github.com/astral-sh/uv/issues/6998. fn run_script_without_build_system() -> Result<()> { - // Check warning message for https://github.com/astral-sh/uv/issues/6998. - let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); From 9e410d74c77a8be23e5a235e0ad00db8aa3cbd64 Mon Sep 17 00:00:00 2001 From: Luca BRUNO Date: Mon, 16 Sep 2024 15:47:03 +0200 Subject: [PATCH 3/7] pyproject: fix scripts heuristics --- crates/uv-workspace/src/pyproject.rs | 40 ++++++++------------------- crates/uv/src/commands/project/run.rs | 6 ++-- 2 files changed, 15 insertions(+), 31 deletions(-) diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 0d2be109f0d9..af1a5717fd04 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -70,7 +70,12 @@ impl PyProjectToml { /// non-package ("virtual") project. pub fn is_package(&self) -> bool { // If `tool.uv.package` is set, defer to that explicit setting. - if let Some(is_package) = self.uv_package() { + if let Some(is_package) = self + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.package) + { return is_package; } @@ -78,34 +83,13 @@ impl PyProjectToml { self.build_system.is_some() } - /// Returns whether the project should be considered a package with scripts. - /// - /// This detects if a project is declared as a uv package, or if it has - /// a scripts-related section defined. - pub fn is_packaged_script(&self) -> bool { - if let Some(is_package) = self.uv_package() { - return is_package; - }; - + /// Returns whether the project manifest contains any script table. + pub fn has_scripts(&self) -> bool { if let Some(ref project) = self.project { - let is_script = project.gui_scripts.is_some() || project.scripts.is_some(); - return is_script; - }; - - false - } - - /// Returns whether the project has a `build-system` section. - pub fn has_build_system(&self) -> bool { - self.build_system.is_some() - } - - /// Returns the value of `tool.uv.package`, if explicitly set. - fn uv_package(&self) -> Option { - self.tool - .as_ref() - .and_then(|tool| tool.uv.as_ref()) - .and_then(|uv| uv.package) + project.gui_scripts.is_some() || project.scripts.is_some() + } else { + false + } } } diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 1a637a3f0e93..8c3a28c2b157 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -451,10 +451,10 @@ pub(crate) async fn run( .await? }; - if project.workspace().pyproject_toml().is_packaged_script() - && !project.workspace().pyproject_toml().has_build_system() + if project.workspace().pyproject_toml().has_scripts() + && !project.workspace().pyproject_toml().is_package() { - warn_user!("No `build-system` section found for the package; consider adding one to correctly build the project"); + warn_user!("Skipping scripts installation because this project is not a package. To install them, consider setting `tool.uv.package = true` or configuring a custom `build-system`."); } if no_sync { From ad900bebd7c4bda3c96e88b02295773200eb9117 Mon Sep 17 00:00:00 2001 From: Luca BRUNO Date: Mon, 16 Sep 2024 15:58:45 +0200 Subject: [PATCH 4/7] uv: move warning from run to sync --- crates/uv/src/commands/project/run.rs | 6 ---- crates/uv/src/commands/project/sync.rs | 7 +++++ crates/uv/tests/run.rs | 41 -------------------------- crates/uv/tests/sync.rs | 41 ++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 47 deletions(-) diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 8c3a28c2b157..110c2a618fac 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -451,12 +451,6 @@ pub(crate) async fn run( .await? }; - if project.workspace().pyproject_toml().has_scripts() - && !project.workspace().pyproject_toml().is_package() - { - warn_user!("Skipping scripts installation because this project is not a package. To install them, consider setting `tool.uv.package = true` or configuring a custom `build-system`."); - } - if no_sync { debug!("Skipping environment synchronization due to `--no-sync`"); } else { diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 31ed1c1a88e8..7ba8f94be942 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -16,6 +16,7 @@ use uv_normalize::{PackageName, DEV_DEPENDENCIES}; use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest}; use uv_resolver::{FlatIndex, Lock}; use uv_types::{BuildIsolation, HashStrategy}; +use uv_warnings::warn_user; use uv_workspace::{DiscoveryOptions, InstallTarget, MemberDiscovery, VirtualProject, Workspace}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger}; @@ -74,6 +75,12 @@ pub(crate) async fn sync( InstallTarget::from(&project) }; + if project.workspace().pyproject_toml().has_scripts() + && !project.workspace().pyproject_toml().is_package() + { + warn_user!("Skipping scripts installation because this project is not a package. To install them, consider setting `tool.uv.package = true` or configuring a custom `build-system`."); + } + // Discover or create the virtual environment. let venv = project::get_or_init_environment( target.workspace(), diff --git a/crates/uv/tests/run.rs b/crates/uv/tests/run.rs index 5cf9574d7f39..c88749b4b539 100644 --- a/crates/uv/tests/run.rs +++ b/crates/uv/tests/run.rs @@ -1950,44 +1950,3 @@ fn run_invalid_project_table() -> Result<()> { Ok(()) } - -#[test] -/// Check warning message for https://github.com/astral-sh/uv/issues/6998. -fn run_script_without_build_system() -> Result<()> { - let context = TestContext::new("3.12"); - - let pyproject_toml = context.temp_dir.child("pyproject.toml"); - pyproject_toml.write_str(indoc! { r#" - [project] - name = "foo" - version = "0.1.0" - requires-python = ">=3.12" - dependencies = [] - - [project.scripts] - entry = "foo:custom_entry" - "# - })?; - - let test_script = context.temp_dir.child("src/__init__.py"); - test_script.write_str(indoc! { r#" - def custom_entry(): - print!("Hello") - "# - })?; - - uv_snapshot!(context.filters(), context.run().arg("entry"), @r###" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - warning: No `build-system` section found for the package; consider adding one to correctly build the project - Resolved 1 package in [TIME] - Audited in [TIME] - error: Failed to spawn: `entry` - Caused by: No such file or directory (os error 2) - "###); - - Ok(()) -} diff --git a/crates/uv/tests/sync.rs b/crates/uv/tests/sync.rs index bff1a3def208..623c2d11013b 100644 --- a/crates/uv/tests/sync.rs +++ b/crates/uv/tests/sync.rs @@ -2328,3 +2328,44 @@ fn transitive_dev() -> Result<()> { Ok(()) } + +#[test] +/// Check warning message for https://github.com/astral-sh/uv/issues/6998. +fn sync_scripts_without_build_system() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "foo" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [project.scripts] + entry = "foo:custom_entry" + "#, + )?; + + let test_script = context.temp_dir.child("src/__init__.py"); + test_script.write_str( + r#" + def custom_entry(): + print!("Hello") + "#, + )?; + + uv_snapshot!(context.filters(), context.sync(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: Skipping scripts installation because this project is not a package. To install them, consider setting `tool.uv.package = true` or configuring a custom `build-system`. + Resolved 1 package in [TIME] + Audited in [TIME] + "###); + + Ok(()) +} From 5161c52015689938a48ed54184405201cd272b50 Mon Sep 17 00:00:00 2001 From: Luca BRUNO Date: Mon, 16 Sep 2024 16:42:21 +0200 Subject: [PATCH 5/7] improve wording and tests --- crates/uv/src/commands/project/sync.rs | 4 +- crates/uv/tests/run.rs | 41 +++++++++++++++++++ crates/uv/tests/sync.rs | 54 +++++++++++++++++++++++++- 3 files changed, 96 insertions(+), 3 deletions(-) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 7ba8f94be942..f2d1c6b662c9 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -75,10 +75,12 @@ pub(crate) async fn sync( InstallTarget::from(&project) }; + // TODO(lucab): improve warning content + // https://github.com/astral-sh/uv/issues/7428 if project.workspace().pyproject_toml().has_scripts() && !project.workspace().pyproject_toml().is_package() { - warn_user!("Skipping scripts installation because this project is not a package. To install them, consider setting `tool.uv.package = true` or configuring a custom `build-system`."); + warn_user!("Skipping installation of entry points (`project.scripts`) because this project is not packaged; to install entry points, set `tool.uv.package = true` or define a `build-system`"); } // Discover or create the virtual environment. diff --git a/crates/uv/tests/run.rs b/crates/uv/tests/run.rs index c88749b4b539..2e51437e1fed 100644 --- a/crates/uv/tests/run.rs +++ b/crates/uv/tests/run.rs @@ -1950,3 +1950,44 @@ fn run_invalid_project_table() -> Result<()> { Ok(()) } + +#[test] +fn run_script_without_build_system() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [project.scripts] + entry = "foo:custom_entry" + "# + })?; + + let test_script = context.temp_dir.child("src/__init__.py"); + test_script.write_str(indoc! { r#" + def custom_entry(): + print!("Hello") + "# + })?; + + // TODO(lucab): this should match `entry` and warn + // https://github.com/astral-sh/uv/issues/7428 + uv_snapshot!(context.filters(), context.run().arg("entry"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Audited in [TIME] + error: Failed to spawn: `entry` + Caused by: No such file or directory (os error 2) + "###); + + Ok(()) +} diff --git a/crates/uv/tests/sync.rs b/crates/uv/tests/sync.rs index 623c2d11013b..ffc00f41ecce 100644 --- a/crates/uv/tests/sync.rs +++ b/crates/uv/tests/sync.rs @@ -2330,7 +2330,8 @@ fn transitive_dev() -> Result<()> { } #[test] -/// Check warning message for https://github.com/astral-sh/uv/issues/6998. +/// Check warning message for https://github.com/astral-sh/uv/issues/6998 +/// if no `build-system` section is defined. fn sync_scripts_without_build_system() -> Result<()> { let context = TestContext::new("3.12"); @@ -2362,7 +2363,56 @@ fn sync_scripts_without_build_system() -> Result<()> { ----- stdout ----- ----- stderr ----- - warning: Skipping scripts installation because this project is not a package. To install them, consider setting `tool.uv.package = true` or configuring a custom `build-system`. + warning: Skipping installation of entry points (`project.scripts`) because this project is not packaged; to install entry points, set `tool.uv.package = true` or define a `build-system` + Resolved 1 package in [TIME] + Audited in [TIME] + "###); + + Ok(()) +} + +#[test] +/// Check warning message for https://github.com/astral-sh/uv/issues/6998 +/// if the project is marked as `package = false`. +fn sync_scripts_project_not_packaged() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "foo" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [project.scripts] + entry = "foo:custom_entry" + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + + [tool.uv] + package = false + "#, + )?; + + let test_script = context.temp_dir.child("src/__init__.py"); + test_script.write_str( + r#" + def custom_entry(): + print!("Hello") + "#, + )?; + + uv_snapshot!(context.filters(), context.sync(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: Skipping installation of entry points (`project.scripts`) because this project is not packaged; to install entry points, set `tool.uv.package = true` or define a `build-system` Resolved 1 package in [TIME] Audited in [TIME] "###); From 76b29561d3750dbabfa6bf2f5994ab35a6f55903 Mon Sep 17 00:00:00 2001 From: Luca BRUNO Date: Mon, 16 Sep 2024 16:48:37 +0200 Subject: [PATCH 6/7] fix links syntax --- crates/uv/src/commands/project/sync.rs | 2 +- crates/uv/tests/run.rs | 2 +- crates/uv/tests/sync.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index f2d1c6b662c9..ce2449494d19 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -76,7 +76,7 @@ pub(crate) async fn sync( }; // TODO(lucab): improve warning content - // https://github.com/astral-sh/uv/issues/7428 + // if project.workspace().pyproject_toml().has_scripts() && !project.workspace().pyproject_toml().is_package() { diff --git a/crates/uv/tests/run.rs b/crates/uv/tests/run.rs index 2e51437e1fed..7f8482904d3c 100644 --- a/crates/uv/tests/run.rs +++ b/crates/uv/tests/run.rs @@ -1976,7 +1976,7 @@ fn run_script_without_build_system() -> Result<()> { })?; // TODO(lucab): this should match `entry` and warn - // https://github.com/astral-sh/uv/issues/7428 + // uv_snapshot!(context.filters(), context.run().arg("entry"), @r###" success: false exit_code: 2 diff --git a/crates/uv/tests/sync.rs b/crates/uv/tests/sync.rs index ffc00f41ecce..57bcc1fcabb5 100644 --- a/crates/uv/tests/sync.rs +++ b/crates/uv/tests/sync.rs @@ -2330,7 +2330,7 @@ fn transitive_dev() -> Result<()> { } #[test] -/// Check warning message for https://github.com/astral-sh/uv/issues/6998 +/// Check warning message for /// if no `build-system` section is defined. fn sync_scripts_without_build_system() -> Result<()> { let context = TestContext::new("3.12"); @@ -2372,7 +2372,7 @@ fn sync_scripts_without_build_system() -> Result<()> { } #[test] -/// Check warning message for https://github.com/astral-sh/uv/issues/6998 +/// Check warning message for /// if the project is marked as `package = false`. fn sync_scripts_project_not_packaged() -> Result<()> { let context = TestContext::new("3.12"); From 591e4a6dfd23e774a989005feba3e0fa54503a8f Mon Sep 17 00:00:00 2001 From: Luca BRUNO Date: Mon, 16 Sep 2024 17:16:15 +0200 Subject: [PATCH 7/7] scope test to unix targets --- crates/uv/tests/run.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/uv/tests/run.rs b/crates/uv/tests/run.rs index 7f8482904d3c..c84d92b159db 100644 --- a/crates/uv/tests/run.rs +++ b/crates/uv/tests/run.rs @@ -1952,6 +1952,7 @@ fn run_invalid_project_table() -> Result<()> { } #[test] +#[cfg(target_family = "unix")] fn run_script_without_build_system() -> Result<()> { let context = TestContext::new("3.12");