diff --git a/README.md b/README.md index 80be6c057..f2bc5c579 100644 --- a/README.md +++ b/README.md @@ -94,9 +94,10 @@ uv pip install "flask[dotenv]" # Install Flask with "dotenv" extra. To generate a set of locked dependencies: ```shell -uv pip compile pyproject.toml -o requirements.txt # Read a pyproject.toml file. -uv pip compile requirements.in -o requirements.txt # Read a requirements.in file. -echo flask | uv pip compile - -o requirements.txt # Read from stdin. +uv pip compile requirements.in -o requirements.txt # Read a requirements.in file. +uv pip compile pyproject.toml -o requirements.txt # Read a pyproject.toml file. +uv pip compile setup.py -o requirements.txt # Read a setup.py file. +echo flask | uv pip compile - -o requirements.txt # Read from stdin. uv pip freeze | uv pip compile - -o requirements.txt # Lock the current environment. ``` diff --git a/crates/uv-requirements/src/sources.rs b/crates/uv-requirements/src/sources.rs index 027b3818d..fd5b8d807 100644 --- a/crates/uv-requirements/src/sources.rs +++ b/crates/uv-requirements/src/sources.rs @@ -18,6 +18,10 @@ pub enum RequirementsSource { RequirementsTxt(PathBuf), /// Dependencies were provided via a `pyproject.toml` file (e.g., `pip-compile pyproject.toml`). PyprojectToml(PathBuf), + /// Dependencies were provided via a `setup.py` file (e.g., `pip-compile setup.py`). + SetupPy(PathBuf), + /// Dependencies were provided via a `setup.cfg` file (e.g., `pip-compile setup.cfg`). + SetupCfg(PathBuf), } impl RequirementsSource { @@ -26,6 +30,10 @@ impl RequirementsSource { pub fn from_requirements_file(path: PathBuf) -> Self { if path.ends_with("pyproject.toml") { Self::PyprojectToml(path) + } else if path.ends_with("setup.py") { + Self::SetupPy(path) + } else if path.ends_with("setup.cfg") { + Self::SetupCfg(path) } else { Self::RequirementsTxt(path) } @@ -74,16 +82,27 @@ impl RequirementsSource { Self::Package(name) } + + /// Returns `true` if the source allows extras to be specified. + pub fn allows_extras(&self) -> bool { + matches!( + self, + Self::PyprojectToml(_) | Self::SetupPy(_) | Self::SetupCfg(_) + ) + } } impl std::fmt::Display for RequirementsSource { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { + Self::Package(package) => write!(f, "{package}"), Self::Editable(path) => write!(f, "-e {path}"), - Self::RequirementsTxt(path) | Self::PyprojectToml(path) => { + Self::RequirementsTxt(path) + | Self::PyprojectToml(path) + | Self::SetupPy(path) + | Self::SetupCfg(path) => { write!(f, "{}", path.simplified_display()) } - Self::Package(package) => write!(f, "{package}"), } } } diff --git a/crates/uv-requirements/src/specification.rs b/crates/uv-requirements/src/specification.rs index 410b4a9ba..7eec89831 100644 --- a/crates/uv-requirements/src/specification.rs +++ b/crates/uv-requirements/src/specification.rs @@ -177,6 +177,28 @@ impl RequirementsSpecification { } } } + RequirementsSource::SetupPy(path) | RequirementsSource::SetupCfg(path) => { + let path = fs_err::canonicalize(path)?; + let source_tree = path.parent().ok_or_else(|| { + anyhow::anyhow!( + "The file `{}` appears to be a `setup.py` or `setup.cfg` file, which must be in a directory", + path.user_display() + ) + })?; + Self { + project: None, + requirements: vec![], + constraints: vec![], + overrides: vec![], + editables: vec![], + source_trees: vec![source_tree.to_path_buf()], + extras: FxHashSet::default(), + index_url: None, + extra_index_urls: vec![], + no_index: false, + find_links: vec![], + } + } }) } diff --git a/crates/uv/src/commands/pip_compile.rs b/crates/uv/src/commands/pip_compile.rs index cb4182fcc..11025e98f 100644 --- a/crates/uv/src/commands/pip_compile.rs +++ b/crates/uv/src/commands/pip_compile.rs @@ -80,14 +80,11 @@ pub(crate) async fn pip_compile( ) -> Result { let start = std::time::Instant::now(); - // If the user requests `extras` but does not provide a pyproject toml source - if !matches!(extras, ExtrasSpecification::None) - && !requirements - .iter() - .any(|source| matches!(source, RequirementsSource::PyprojectToml(_))) - { + // If the user requests `extras` but does not provide a valid source (e.g., a `pyproject.toml`), + // return an error. + if !extras.is_empty() && !requirements.iter().any(RequirementsSource::allows_extras) { return Err(anyhow!( - "Requesting extras requires a pyproject.toml input file." + "Requesting extras requires a `pyproject.toml`, `setup.cfg`, or `setup.py` file." )); } diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index fafc8066c..55f54beb9 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -510,6 +510,108 @@ setup( Ok(()) } +/// Compile a `setup.cfg` file. +#[test] +fn compile_setup_cfg() -> Result<()> { + let context = TestContext::new("3.12"); + + let setup_cfg = context.temp_dir.child("setup.cfg"); + setup_cfg.write_str( + r#"[options] +packages = find: +install_requires= + anyio + +[options.extras_require] +dev = + iniconfig; python_version >= "3.7" + mypy; python_version <= "3.8" +"#, + )?; + + let setup_py = context.temp_dir.child("setup.py"); + setup_py.write_str( + r#"# setup.py +from setuptools import setup + + +setup( + name="dummypkg", + description="A dummy package", +) +"#, + )?; + + uv_snapshot!(context.compile() + .arg("setup.cfg") + .arg("--extra") + .arg("dev"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z setup.cfg --extra dev + anyio==4.3.0 + idna==3.6 + # via anyio + iniconfig==2.0.0 + sniffio==1.3.1 + # via anyio + + ----- stderr ----- + Resolved 4 packages in [TIME] + "### + ); + + Ok(()) +} + +/// Compile a `setup.py` file. +#[test] +fn compile_setup_py() -> Result<()> { + let context = TestContext::new("3.12"); + + let setup_py = context.temp_dir.child("setup.py"); + setup_py.write_str( + r#"# setup.py +from setuptools import setup + + +setup( + name="dummypkg", + description="A dummy package", + install_requires=["anyio"], + extras_require={ + "dev": ["iniconfig; python_version >= '3.7'", "mypy; python_version <= '3.8'"], + }, +) +"#, + )?; + + uv_snapshot!(context.compile() + .arg("setup.py") + .arg("--extra") + .arg("dev"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z setup.py --extra dev + anyio==4.3.0 + idna==3.6 + # via anyio + iniconfig==2.0.0 + sniffio==1.3.1 + # via anyio + + ----- stderr ----- + Resolved 4 packages in [TIME] + "### + ); + + Ok(()) +} + /// Request multiple extras that do not exist as a dependency group in a `pyproject.toml` file. #[test] fn compile_pyproject_toml_extras_missing() -> Result<()> { @@ -564,7 +666,7 @@ fn compile_requirements_file_extra() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: Requesting extras requires a pyproject.toml input file. + error: Requesting extras requires a `pyproject.toml`, `setup.cfg`, or `setup.py` file. "### );