Skip to content

Warn when two packages write to the same module#13437

Merged
konstin merged 2 commits intomainfrom
konsti/warn-on-module-conflicts
Aug 8, 2025
Merged

Warn when two packages write to the same module#13437
konstin merged 2 commits intomainfrom
konsti/warn-on-module-conflicts

Conversation

@konstin
Copy link
Copy Markdown
Member

@konstin konstin commented May 13, 2025

We regularly get confusing bug reports where a package sometimes works and sometimes doesn't and it's not clear to the user why. Ultimately, it turns out that two packages contain the same module and there is a race condition when installing the two packages. Usually, it's one of the opencv-python distributions, but recently it's been z3, too. These error are completely inscrutable to users.

We now warn for top-level modules (pattern: <identifier>/__init__.py) that collide in a single installation, naming the offending wheels. Checking for __init__.py excludes namespace packages.

Test script:

uv venv -q && cargo run -q --profile fast-build pip install --no-progress --link-mode clone opencv-python opencv-contrib-python --no-build --no-deps
uv venv -q && cargo run -q --profile fast-build pip install --no-progress --link-mode copy opencv-python opencv-contrib-python --no-build --no-deps
uv venv -q && cargo run -q --profile fast-build pip install --no-progress --link-mode hardlink opencv-python opencv-contrib-python --no-build --no-deps
uv venv -q && cargo run -q --profile fast-build pip install --no-progress --link-mode symlink opencv-python opencv-contrib-python --no-build --no-deps

We currently only catch conflicts in a single installation. Should we prime the lock database with the site-packages contents, and would that carry overhead?

@konstin konstin requested a review from charliermarsh May 13, 2025 20:55
@konstin konstin added the bug Something isn't working label May 13, 2025
Comment on lines +23 to +33
modules: Mutex<FxHashMap<OsString, WheelFilename>>,
}

impl Locks {
/// Warn when a module exists in multiple packages.
fn warn_module_conflict(&self, module: &OsStr, filename: &WheelFilename) {
if let Some(existing) = self
.modules
.lock()
.unwrap()
.insert(module.to_os_string(), filename.clone())
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This should get <1000 hits so I've implemented the naive approach.

@cthoyt
Copy link
Copy Markdown
Contributor

cthoyt commented May 14, 2025

An example that's vexed me in the past:

  1. https://pypi.org/project/Flask-Bootstrap/ (original, unmaintained project)
  2. https://pypi.org/project/Bootstrap-Flask/ (maintained fork, uses same package name but
    different on PyPI)

both install to flask_bootstrap

@konstin konstin force-pushed the konsti/warn-on-module-conflicts branch from 1dce177 to b052c16 Compare May 19, 2025 08:29
@charliermarsh
Copy link
Copy Markdown
Member

I don't know that we should make this user-facing, to be honest. Isn't it going to trigger any time anyone installs Jupyter?

@konstin
Copy link
Copy Markdown
Member Author

konstin commented May 20, 2025

I strongly think this should be user-facing, I linked some of the issues this would have prevented above (we had another one just today: #13550), especially since this is otherwise non-deterministic, silent and almost impossible to figure out if you don't know what you're looking for.

It doesn't warn for uv pip install jupyter, is there a reason we would expect it to? Note that we're checking for an __init__.py, so this does not affect namespace packages that are allowed to interleave.

@atinary-bvollmer
Copy link
Copy Markdown

Hey @charliermarsh,

I'm the author of #13550. Thanks to @konstin I found the issue but I would strongly side with here that especially for less experienced users this can lead to very frustrating problems. A warning during the install would have most likely prevented it for me.

Thanks!

@konstin konstin force-pushed the konsti/warn-on-module-conflicts branch from b052c16 to 8bcf72a Compare May 27, 2025 20:08
@konstin
Copy link
Copy Markdown
Member Author

konstin commented May 27, 2025

Added a test case

@zanieb zanieb self-assigned this May 28, 2025
@konstin
Copy link
Copy Markdown
Member Author

konstin commented May 30, 2025

One place where this could fail is the Python 2 path = import("pkgutil").extend_path(__path__, name) hack, but I haven't seen that one in a while, and we can special case it if necessary.

@zanieb
Copy link
Copy Markdown
Member

zanieb commented May 30, 2025

(I'll review this)

@konstin konstin requested a review from zanieb June 5, 2025 10:08
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
warning: The module built_by_uv exists in two packages! This leads to a race condition and likely to a broken installation. Consider removing either built-by-uv (built_by_uv-0.1.0-py3-none-any.whl) or also-built-by-uv (also_built_by_uv-0.1.0-py3-none-any.whl).
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why do we show the wheel filenames here? Is that an important detail?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

For debugging, I usually need to go open the wheel and look inside of it, so I consider this a valuable detail. We should show at least the version though, otherwise we can't find where this is from.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I worry about this as a user-facing detail in the warning though, isn't the file name going to be in the verbose logs?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also, aren't the versions included in the install summary?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The error should by itself contain enough information to track down the problem, so I would like it to at least contain the versions.

Copy link
Copy Markdown
Member

@zanieb zanieb Jun 11, 2025

Choose a reason for hiding this comment

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

I'm okay with the versions, styled as we do elsewhere, e.g., foo (v1.0.0). I think the filenames sound like a developer-facing rather than user-facing detail.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The exclamation mark is a bit much, I don't think we do that anywhere else.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That's dropped in #13889

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I updated the error message

(wheel_a, &wheel_b)
};
warn_user!(
"The module {} exists in two packages! \
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I reworded this in #13889, curious for your thoughts

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Will this trigger if two packages include the same file?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, do we have a case where this is expected and valid to happen?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Isn't it a feature that two packages could contain the same shared module, and could just vendor it into their own wheel?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is it? That seems pretty problematic from a versioning perspective.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I haven't seen any vendoring cases where the vendored modules would overlap.

&& relative.components().next_back().unwrap().as_os_str() == "__init__.py"
{
// Modules must be UTF-8, but we can skip the conversion using OsStr.
locks.warn_module_conflict(relative.components().next().unwrap().as_os_str(), filename);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What does this look like for modules like foo.bar? Are we only going to show bar? Or are you only checking for the top-level modules because of relative.components.count() == 2?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That's the TODO above, we're only looking for foo/__init__.py rn and don't track foo/bar/__init__.py

@konstin konstin temporarily deployed to uv-test-publish June 11, 2025 19:35 — with GitHub Actions Inactive
@konstin konstin temporarily deployed to uv-test-publish June 11, 2025 20:43 — with GitHub Actions Inactive
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Jun 11, 2025

CodSpeed WallTime Performance Report

Merging #13437 will not alter performance

Comparing konsti/warn-on-module-conflicts (f18ca49) with main (8968d78)

Summary

✅ 3 untouched benchmarks

@MalteEbner
Copy link
Copy Markdown

MalteEbner commented Jun 13, 2025

Suggested solution: I think it should be possible for the user to define different packages under the same module name similar to the sources logic. Something similar to this:

[tool.uv.sources]
pillow = [
  { package = "pillow-simd>=9,<10", marker = "platform_machine == 'x86_64'"},
  { package = "pillow>=11,<12", marker = "platform_machine == 'aarch64'"},
]

@konstin

This comment was marked as outdated.

@konstin konstin force-pushed the konsti/warn-on-module-conflicts branch from dcefc54 to 6f9e320 Compare July 24, 2025 10:05
@konstin konstin temporarily deployed to uv-test-registries July 24, 2025 10:07 — with GitHub Actions Inactive
@konstin konstin temporarily deployed to uv-test-registries July 24, 2025 10:13 — with GitHub Actions Inactive
@konstin konstin requested a review from zanieb August 7, 2025 14:30
@konstin konstin force-pushed the konsti/warn-on-module-conflicts branch from d31f63c to f18ca49 Compare August 8, 2025 08:48
@konstin konstin enabled auto-merge (squash) August 8, 2025 08:48
@konstin konstin temporarily deployed to uv-test-registries August 8, 2025 08:50 — with GitHub Actions Inactive
@konstin konstin merged commit 1843c90 into main Aug 8, 2025
95 checks passed
@konstin konstin deleted the konsti/warn-on-module-conflicts branch August 8, 2025 09:01
@geofft
Copy link
Copy Markdown
Contributor

geofft commented Aug 8, 2025

I sort of thought I've seen pre-PEP-420 namespace packages recently enough. Debian Code Search for path:__init__.py extend_path shows a good amount of hits, though several are in vendored and possibly old packages. Protobuf has a google/__init__.py in its sources, but it doesn't seem to actually ship it in the wheel. The latest PyQt6 wheel does have a PyQt6/__init__.py, but I'm not sure if anything else installs into that namespace.

[edit: meant to write "pre-PEP-420", not "PEP-420"]

@konstin
Copy link
Copy Markdown
Member Author

konstin commented Aug 8, 2025

Valid PEP 420 packages that don't clash are not affected by this change.

konstin added a commit that referenced this pull request Jan 21, 2026
In the previous iteration, conflict detection was based on top level
modules. This would work if all namespace packages correctly omitted the
`__init__.py`, but e.g. the nvidia packages include an empty
`nvida/__init__.py`.

Instead, we track overlapping top level modules during installation and
perform conflict analysis after installation, recursing only as far as
necessary.

See #13437 for a list of reported
conflicts.

Before:
```
$ uv venv -c -q && uv pip install --preview nvidia-nvjitlink-cu12==12.8.93 nvidia-nvtx-cu12==12.8.90
  Resolved 2 packages in 0.99ms
  ░░░░░░░░░░░░░░░░░░░░ [0/2] Installing wheels...                                                    warning: The module `nvidia` is provided by more than one package, which causes an install race condition and can result in a broken module. Consider removing your dependency on either `nvidia-nvtx-cu12` (v12.8.90) or `nvidia-nvjitlink-cu12` (v12.8.93).
  Installed 2 packages in 3ms
   + nvidia-nvjitlink-cu12==12.8.93
   + nvidia-nvtx-cu12==12.8.90
```

After:
```
$ uv venv -c -q && cargo run -q pip install --preview nvidia-nvjitlink-cu12==12.8.93 nvidia-nvtx-cu12==12.8.90
  Resolved 2 packages in 3ms
  Installed 2 packages in 4ms
   + nvidia-nvjitlink-cu12==12.8.93
   + nvidia-nvtx-cu12==12.8.90
```

Still detected true positive:
```
$ uv venv -c -q && cargo run -q pip install --no-progress opencv-python opencv-contrib-python --no-build --no-deps --preview
  Resolved 2 packages in 5ms
warning: The file `cv2/__init__.pyi` is provided by more than one package, which causes an install race condition and can result in a broken module. Packages containing the file:
* opencv-contrib-python (opencv_contrib_python-4.13.0.90-cp37-abi3-manylinux_2_28_x86_64.whl)
* opencv-python (opencv_python-4.13.0.90-cp37-abi3-manylinux_2_28_x86_64.whl)
Installed 2 packages in 6ms
 + opencv-contrib-python==4.13.0.90
 + opencv-python==4.13.0.90
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants