Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial implementation of uv add and uv remove #4193

Merged
merged 22 commits into from
Jun 11, 2024

Conversation

ibraheemdev
Copy link
Member

@ibraheemdev ibraheemdev commented Jun 10, 2024

Summary

Basic implementation of uv add and uv remove that supports writing PEP508 requirements to project.dependencies.

First step for #3959 and #3960.

let upgrade = Upgrade::default();
let extras = ExtrasSpecification::default();
let exclude_newer = None;
let dev = false; // We only add regular dependencies currently.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is correct? Should dev dependencies ever be synced even if they weren't modified?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible that the dev dependencies could change by adding a non-dev dependency, because they're all locked together. (E.g., maybe the dep you add has a transitive dep on one of the dev deps, so now it's more constrained.) So, I guess we do have the include them? But it's not really right to sync them unconditionally. The same problem exists with extras. We shouldn't be syncing them unconditionally. We need to know which extras the user wants activated.

I'm wondering if add should imply lock, but not sync? Since locking is much simpler: we lock everything.

(I think there is a general problem here that exists as long as add implies lock or sync. (Rye has this problem too). Any arguments that are typically passed to lock or sync to guide resolution or installation now need to be passed to add too... So, e.g., we might need exclude-newer too.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made add and remove do an unconditional full sync for now.

let mut pyproject = PyProjectTomlMut::from_toml(project.current_project().pyproject_toml())?;
for req in requirements {
let req = Requirement::from(LenientRequirement::from_str(&req)?);
if pyproject.remove_dependency(&req)?.is_none() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if you uv remove anyio, without including the specifier?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove_dependency removes by package name, so that works.


let mut pyproject = PyProjectTomlMut::from_toml(project.current_project().pyproject_toml())?;
for req in requirements {
let req = Requirement::from(LenientRequirement::from_str(&req)?);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should enumerate some of the TODOs that aren't tackled yet. For example:

  • We should accept unnamed URLs, and resolve them to named requirements.
  • We need to figure out how this interacts with tool.uv.sources. When do we add data to tool.uv.sources rather than project.dependencies? \cc @konstin, you two should discuss this.
  • Editable requirements?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracking in #3959.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should just use pep508_rs::Requirement::from_str instead of LenientRequirement... We are a little inconsistent about it, but in general, LenientRequirement is really intended for third-party metadata over which the user has no control. We shouldn't let them add invalid requirements here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also applies to the modification code in crates/uv-distribution/src/pyproject_mut.rs. What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense to me, I removed the use of LenientRequirement.

pub(crate) struct RemoveArgs {
/// The packages to remove as PEP 508 requirement strings. e.g. 'flask==2.2.3'
#[arg(required = true)]
pub(crate) requirements: Vec<String>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this should be Requirement as long as we're parsing these directly. But, I think what you have here is probably better.

crates/uv/src/cli.rs Outdated Show resolved Hide resolved
let mut to_remove = None;
for (i, dep) in deps.iter().enumerate() {
if let Some(dep) = dep.as_str().and_then(|dep| parse_requirement(dep).ok()) {
if dep.name.as_ref().eq_ignore_ascii_case(req.name.as_ref()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if a dep is included multiple times? E.g.:

dependencies = [
  "flask > 2 ; python_version >= '3.6'",
  "flask < 2 ; python_version < '3.6'",
]

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm it would only remove the first occurrence then. I think Rye has this bug as well?

Copy link
Member Author

@ibraheemdev ibraheemdev Jun 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to remove all occurrences. Also changed uv remove to take package names, not full requirements, because otherwise uv remove flask>2 becomes more complicated. I think package names make more sense anyways? Unless we want to support e.g. removing unnamed requirements by URL?


match to_replace {
Some(i) => {
deps.replace(i, req.to_string());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What should happen if there are multiple occurrences? My guess is we replace them all.

Copy link
Member Author

@ibraheemdev ibraheemdev Jun 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace them all with the single new requirement? i.e. replace the first occurrence and remove the rest?

Copy link
Member Author

@ibraheemdev ibraheemdev Jun 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented it that way, I think that makes the most sense, although it might be good to warn/log because this whole scenario is a bit questionable.

Copy link
Member

@charliermarsh charliermarsh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few more comments. I am happy to re-review but also trust you to address them and merge when you think they're closed out.

@ibraheemdev ibraheemdev merged commit eefa9e6 into astral-sh:main Jun 11, 2024
47 checks passed
@zanieb zanieb added the preview Experimental behavior label Jun 11, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
preview Experimental behavior
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants