astral-sh / uv

An extremely fast Python package and project manager, written in Rust.
https://docs.astral.sh/uv
Apache License 2.0
23.38k stars 671 forks source link

uv installs really old version of web3 in the presence of numpy and streamlit on 3.10, unlike pip #4372

Closed wakamex closed 4 months ago

wakamex commented 4 months ago

uv worked fine until a recent update to one of these packages (not uv itself, saw the same on 0.19) caused it to go into dependency resolution hell.

[project]
name = "project"
version = "1"
# with eth-typing, we get web3==3.16.3
dependencies = [
    "eth-typing",
    "numpy",
    "streamlit",
    "web3"
]
# === PIP FREEZE (pip-24.0) ===
# aiohttp==3.9.5
# aiosignal==1.3.1
# altair==5.3.0
# async-timeout==4.0.3
# attrs==23.2.0
# bitarray==2.9.2
# blinker==1.8.2
# cachetools==5.3.3
# certifi==2024.6.2
# charset-normalizer==3.3.2
# ckzg==1.0.2
# click==8.1.7
# cytoolz==0.12.3
# eth-account==0.11.2
# eth-hash==0.7.0
# eth-keyfile==0.8.1
# eth-keys==0.5.1
# eth-rlp==1.0.1
# eth-typing==4.3.1
# eth-utils==4.1.1
# eth_abi==5.1.0
# frozenlist==1.4.1
# gitdb==4.0.11
# GitPython==3.1.43
# hexbytes==0.3.1
# idna==3.7
# Jinja2==3.1.4
# jsonschema==4.22.0
# jsonschema-specifications==2023.12.1
# lru-dict==1.2.0
# markdown-it-py==3.0.0
# MarkupSafe==2.1.5
# mdurl==0.1.2
# multidict==6.0.5
# numpy==1.26.4
# packaging==24.1
# pandas==2.2.2
# parsimonious==0.10.0
# pillow==10.3.0
# project==1
# protobuf==4.25.3
# pyarrow==16.1.0
# pycryptodome==3.20.0
# pydeck==0.9.1
# Pygments==2.18.0
# python-dateutil==2.9.0.post0
# pytz==2024.1
# pyunormalize==15.1.0
# referencing==0.35.1
# regex==2024.5.15
# requests==2.32.3
# rich==13.7.1
# rlp==4.0.1
# rpds-py==0.18.1
# six==1.16.0
# smmap==5.0.1
# streamlit==1.35.0
# tenacity==8.4.1
# toml==0.10.2
# toolz==0.12.1
# tornado==6.4.1
# typing_extensions==4.12.2
# tzdata==2024.1
# urllib3==2.2.2
# watchdog==4.0.1
# web3==6.19.0
# websockets==12.0
# yarl==1.9.4
# === PIP FREEZE (pip-24.0) ===
# altair==4.2.2
# attrs==23.2.0
# blinker==1.8.2
# cachetools==5.3.3
# certifi==2024.6.2
# charset-normalizer==3.3.2
# click==8.1.7
# cytoolz==0.12.3
# entrypoints==0.4
# eth-hash==0.7.0
# eth-typing==4.3.1
# eth-utils==4.1.1
# ethereum-abi-utils==0.4.7
# ethereum-tester==0.1.0b3
# ethereum-utils==0.6.2
# gitdb==4.0.11
# gitpython==3.1.43
# idna==3.7
# importlib-metadata==7.1.0
# jinja2==3.1.4
# jsonschema==4.22.0
# jsonschema-specifications==2023.12.1
# markdown-it-py==3.0.0
# markupsafe==2.1.5
# mdurl==0.1.2
# numpy==2.0.0
# packaging==24.1
# pandas==2.2.2
# pillow==10.3.0
# -e file:///code/testrepo
# protobuf==3.20.3
# pyarrow==16.1.0
# pydeck==0.9.1
# pygments==2.18.0
# pylru==1.2.1
# pympler==1.0.1
# pysha3==1.0.2
# python-dateutil==2.9.0.post0
# pytz==2024.1
# referencing==0.35.1
# requests==2.32.3
# rich==13.7.1
# rlp==4.0.1
# rpds-py==0.18.1
# six==1.16.0
# smmap==5.0.1
# streamlit==1.22.0
# tenacity==8.4.1
# toml==0.10.2
# toolz==0.12.1
# tornado==6.4.1
# typing-extensions==4.12.2
# tzdata==2024.1
# tzlocal==5.2
# urllib3==2.2.2
# validators==0.28.3
# watchdog==4.0.1
# web3==3.16.3
# zipp==3.19.2

# removing eth-typing brings web3 up to 5.23.0
# dependencies = [
#     "numpy",
#     "streamlit",
#     "web3"
# ]
# === PIP FREEZE (pip-24.0) ===
# aiohttp==3.9.5
# aiosignal==1.3.1
# altair==5.3.0
# async-timeout==4.0.3
# attrs==23.2.0
# bitarray==2.9.2
# blinker==1.8.2
# cachetools==5.3.3
# certifi==2024.6.2
# charset-normalizer==3.3.2
# ckzg==1.0.2
# click==8.1.7
# cytoolz==0.12.3
# eth-account==0.11.2
# eth-hash==0.7.0
# eth-keyfile==0.8.1
# eth-keys==0.5.1
# eth-rlp==1.0.1
# eth-typing==4.3.1
# eth-utils==4.1.1
# eth_abi==5.1.0
# frozenlist==1.4.1
# gitdb==4.0.11
# GitPython==3.1.43
# hexbytes==0.3.1
# idna==3.7
# Jinja2==3.1.4
# jsonschema==4.22.0
# jsonschema-specifications==2023.12.1
# lru-dict==1.2.0
# markdown-it-py==3.0.0
# MarkupSafe==2.1.5
# mdurl==0.1.2
# multidict==6.0.5
# numpy==1.26.4
# packaging==24.1
# pandas==2.2.2
# parsimonious==0.10.0
# pillow==10.3.0
# project==1
# protobuf==4.25.3
# pyarrow==16.1.0
# pycryptodome==3.20.0
# pydeck==0.9.1
# Pygments==2.18.0
# python-dateutil==2.9.0.post0
# pytz==2024.1
# pyunormalize==15.1.0
# referencing==0.35.1
# regex==2024.5.15
# requests==2.32.3
# rich==13.7.1
# rlp==4.0.1
# rpds-py==0.18.1
# six==1.16.0
# smmap==5.0.1
# streamlit==1.35.0
# tenacity==8.4.1
# toml==0.10.2
# toolz==0.12.1
# tornado==6.4.1
# typing_extensions==4.12.2
# tzdata==2024.1
# urllib3==2.2.2
# watchdog==4.0.1
# web3==6.19.0
# websockets==12.0
# yarl==1.9.4
# === PIP FREEZE (pip-24.0) ===
# aiohttp==3.9.5
# aiosignal==1.3.1
# altair==4.2.2
# async-timeout==4.0.3
# attrs==23.2.0
# base58==2.1.1
# bitarray==2.9.2
# blinker==1.8.2
# cachetools==5.3.3
# certifi==2024.6.2
# charset-normalizer==3.3.2
# click==8.1.7
# cytoolz==0.12.3
# entrypoints==0.4
# eth-abi==2.2.0
# eth-account==0.5.9
# eth-hash==0.7.0
# eth-keyfile==0.5.1
# eth-keys==0.3.4
# eth-rlp==0.2.1
# eth-typing==2.3.0
# eth-utils==1.9.5
# frozenlist==1.4.1
# gitdb==4.0.11
# gitpython==3.1.43
# hexbytes==0.3.1
# idna==3.7
# importlib-metadata==7.1.0
# ipfshttpclient==0.7.0
# jinja2==3.1.4
# jsonschema==3.2.0
# lru-dict==1.3.0
# markdown-it-py==3.0.0
# markupsafe==2.1.5
# mdurl==0.1.2
# multiaddr==0.0.9
# multidict==6.0.5
# netaddr==1.3.0
# numpy==2.0.0
# packaging==24.1
# pandas==2.2.2
# parsimonious==0.8.1
# pillow==10.3.0
# -e file:///code/testrepo
# protobuf==3.20.3
# pyarrow==16.1.0
# pycryptodome==3.20.0
# pydeck==0.9.1
# pygments==2.18.0
# pympler==1.0.1
# pyrsistent==0.20.0
# python-dateutil==2.9.0.post0
# pytz==2024.1
# requests==2.32.3
# rich==13.7.1
# rlp==2.0.1
# setuptools==70.0.0
# six==1.16.0
# smmap==5.0.1
# streamlit==1.22.0
# tenacity==8.4.1
# toml==0.10.2
# toolz==0.12.1
# tornado==6.4.1
# typing-extensions==4.12.2
# tzdata==2024.1
# tzlocal==5.2
# urllib3==2.2.2
# validators==0.28.3
# varint==1.0.2
# watchdog==4.0.1
# web3==5.23.0
# websockets==9.1
# yarl==1.9.4
# zipp==3.19.2

# removing either numpy or streamlit brings web3 up to 6.19.0
# dependencies = [
#     "numpy",
#     "web3"
# ]
# === PIP FREEZE (pip-24.0) ===
# aiohttp==3.9.5
# aiosignal==1.3.1
# async-timeout==4.0.3
# attrs==23.2.0
# bitarray==2.9.2
# certifi==2024.6.2
# charset-normalizer==3.3.2
# ckzg==1.0.2
# cytoolz==0.12.3
# eth-account==0.11.2
# eth-hash==0.7.0
# eth-keyfile==0.8.1
# eth-keys==0.5.1
# eth-rlp==1.0.1
# eth-typing==4.3.1
# eth-utils==4.1.1
# eth_abi==5.1.0
# frozenlist==1.4.1
# hexbytes==0.3.1
# idna==3.7
# jsonschema==4.22.0
# jsonschema-specifications==2023.12.1
# lru-dict==1.2.0
# multidict==6.0.5
# numpy==2.0.0
# parsimonious==0.10.0
# project==1
# protobuf==5.27.1
# pycryptodome==3.20.0
# pyunormalize==15.1.0
# referencing==0.35.1
# regex==2024.5.15
# requests==2.32.3
# rlp==4.0.1
# rpds-py==0.18.1
# toolz==0.12.1
# typing_extensions==4.12.2
# urllib3==2.2.2
# web3==6.19.0
# websockets==12.0
# yarl==1.9.4
# === UV PIP FREEZE (uv 0.2.12) ===
# aiohttp==3.9.5
# aiosignal==1.3.1
# async-timeout==4.0.3
# attrs==23.2.0
# bitarray==2.9.2
# certifi==2024.6.2
# charset-normalizer==3.3.2
# ckzg==1.0.2
# cytoolz==0.12.3
# eth-abi==5.1.0
# eth-account==0.11.2
# eth-hash==0.7.0
# eth-keyfile==0.8.1
# eth-keys==0.5.1
# eth-rlp==1.0.1
# eth-typing==4.3.1
# eth-utils==4.1.1
# frozenlist==1.4.1
# hexbytes==0.3.1
# idna==3.7
# jsonschema==4.22.0
# jsonschema-specifications==2023.12.1
# lru-dict==1.2.0
# multidict==6.0.5
# numpy==2.0.0
# parsimonious==0.10.0
# -e file:///code/testrepo
# protobuf==5.27.1
# pycryptodome==3.20.0
# pyunormalize==15.1.0
# referencing==0.35.1
# regex==2024.5.15
# requests==2.32.3
# rlp==4.0.1
# rpds-py==0.18.1
# toolz==0.12.1
# typing-extensions==4.12.2
# urllib3==2.2.2
# web3==6.19.0
# websockets==12.0
# yarl==1.9.4

# removing either numpy or streamlit brings web3 up to 6.19.0
# dependencies = [
#     "streamlit",
#     "web3"
# ]
# === PIP FREEZE (pip-24.0) ===
# aiohttp==3.9.5
# aiosignal==1.3.1
# altair==5.3.0
# async-timeout==4.0.3
# attrs==23.2.0
# bitarray==2.9.2
# blinker==1.8.2
# cachetools==5.3.3
# certifi==2024.6.2
# charset-normalizer==3.3.2
# ckzg==1.0.2
# click==8.1.7
# cytoolz==0.12.3
# eth-account==0.11.2
# eth-hash==0.7.0
# eth-keyfile==0.8.1
# eth-keys==0.5.1
# eth-rlp==1.0.1
# eth-typing==4.3.1
# eth-utils==4.1.1
# eth_abi==5.1.0
# frozenlist==1.4.1
# gitdb==4.0.11
# GitPython==3.1.43
# hexbytes==0.3.1
# idna==3.7
# Jinja2==3.1.4
# jsonschema==4.22.0
# jsonschema-specifications==2023.12.1
# lru-dict==1.2.0
# markdown-it-py==3.0.0
# MarkupSafe==2.1.5
# mdurl==0.1.2
# multidict==6.0.5
# numpy==1.26.4
# packaging==24.1
# pandas==2.2.2
# parsimonious==0.10.0
# pillow==10.3.0
# project==1
# protobuf==4.25.3
# pyarrow==16.1.0
# pycryptodome==3.20.0
# pydeck==0.9.1
# Pygments==2.18.0
# python-dateutil==2.9.0.post0
# pytz==2024.1
# pyunormalize==15.1.0
# referencing==0.35.1
# regex==2024.5.15
# requests==2.32.3
# rich==13.7.1
# rlp==4.0.1
# rpds-py==0.18.1
# six==1.16.0
# smmap==5.0.1
# streamlit==1.35.0
# tenacity==8.4.1
# toml==0.10.2
# toolz==0.12.1
# tornado==6.4.1
# typing_extensions==4.12.2
# tzdata==2024.1
# urllib3==2.2.2
# watchdog==4.0.1
# web3==6.19.0
# websockets==12.0
# yarl==1.9.4
# === UV PIP FREEZE (uv 0.2.12) ===
# aiohttp==3.9.5
# aiosignal==1.3.1
# altair==5.3.0
# async-timeout==4.0.3
# attrs==23.2.0
# bitarray==2.9.2
# blinker==1.8.2
# cachetools==5.3.3
# certifi==2024.6.2
# charset-normalizer==3.3.2
# ckzg==1.0.2
# click==8.1.7
# cytoolz==0.12.3
# eth-abi==5.1.0
# eth-account==0.11.2
# eth-hash==0.7.0
# eth-keyfile==0.8.1
# eth-keys==0.5.1
# eth-rlp==1.0.1
# eth-typing==4.3.1
# eth-utils==4.1.1
# frozenlist==1.4.1
# gitdb==4.0.11
# gitpython==3.1.43
# hexbytes==0.3.1
# idna==3.7
# jinja2==3.1.4
# jsonschema==4.22.0
# jsonschema-specifications==2023.12.1
# lru-dict==1.2.0
# markdown-it-py==3.0.0
# markupsafe==2.1.5
# mdurl==0.1.2
# multidict==6.0.5
# numpy==1.26.4
# packaging==24.1
# pandas==2.2.2
# parsimonious==0.10.0
# pillow==10.3.0
# -e file:///code/testrepo
# protobuf==4.25.3
# pyarrow==16.1.0
# pycryptodome==3.20.0
# pydeck==0.9.1
# pygments==2.18.0
# python-dateutil==2.9.0.post0
# pytz==2024.1
# pyunormalize==15.1.0
# referencing==0.35.1
# regex==2024.5.15
# requests==2.32.3
# rich==13.7.1
# rlp==4.0.1
# rpds-py==0.18.1
# six==1.16.0
# smmap==5.0.1
# streamlit==1.35.0
# tenacity==8.4.1
# toml==0.10.2
# toolz==0.12.1
# tornado==6.4.1
# typing-extensions==4.12.2
# tzdata==2024.1
# urllib3==2.2.2
# watchdog==4.0.1
# web3==6.19.0
# websockets==12.0
# yarl==1.9.4
zanieb commented 4 months ago

Hi! Can you add lower bounds to any of your dependencies? It's very hard to choose which package to backtrack on and any heuristic improves one case at the cost of others. See also #4333 and https://github.com/astral-sh/uv/issues/1398

wakamex commented 4 months ago

This is a contrived example to show that something is broken. Adding lower bounds seems like it would make it more contrived?

The repo this broke on has a much longer dependency list, which took me a while to sort through to narrow it down to these 3 as the cause. See our pyproject.toml here: https://github.com/delvtech/agent0/blob/main/pyproject.toml.

I want the latest compatible version of everything, as I do in our repo (don't think this is a rare use-case). Pip finds 6.19.0, while uv fails to find a recent compatible version and just keeps back-searching until it somehow resolves to a really old version.

I guess I could set the expected pip solution of 6.19.0 as the lower bound? Then it should just fail?

Just tried it, and guess what? It successfully installs the expected version. Seems like a bug somewhere. Why wouldn't uv just try the latest release of web3 first? Are there different levels of compatibility that it somehow dismisses 6.19.0 as less compatible than 3.16.3?

dependencies = [
    "eth-typing",
    "numpy",
    "streamlit",
    "web3>=6.19.0"
]
# === PIP FREEZE (pip-24.0) ===
# aiohttp==3.9.5
# aiosignal==1.3.1
# altair==5.3.0
# async-timeout==4.0.3
# attrs==23.2.0
# bitarray==2.9.2
# blinker==1.8.2
# cachetools==5.3.3
# certifi==2024.6.2
# charset-normalizer==3.3.2
# ckzg==1.0.2
# click==8.0.4
# cytoolz==0.12.3
# eth-abi==5.1.0
# eth-account==0.11.2
# eth-hash==0.7.0
# eth-keyfile==0.8.1
# eth-keys==0.5.1
# eth-rlp==1.0.1
# eth-typing==4.3.1
# eth-utils==4.1.1
# frozenlist==1.4.1
# gitdb==4.0.11
# gitpython==3.1.43
# hexbytes==0.3.1
# idna==3.7
# importlib-metadata==7.1.0
# jinja2==3.1.4
# jsonschema==4.22.0
# jsonschema-specifications==2023.12.1
# lru-dict==1.2.0
# markupsafe==2.1.5
# multidict==6.0.5
# numpy==2.0.0
# packaging==24.1
# pandas==2.2.2
# parsimonious==0.10.0
# pillow==10.3.0
# -e file:///code/testrepo
# protobuf==5.27.1
# pyarrow==16.1.0
# pycryptodome==3.20.0
# pydeck==0.9.1
# pympler==1.0.1
# python-dateutil==2.9.0.post0
# pytz==2024.1
# pyunormalize==15.1.0
# referencing==0.35.1
# regex==2024.5.15
# requests==2.32.3
# rlp==4.0.1
# rpds-py==0.18.1
# semver==3.0.2
# six==1.16.0
# smmap==5.0.1
# streamlit==1.9.0
# toml==0.10.2
# toolz==0.12.1
# tornado==6.4.1
# typing-extensions==4.12.2
# tzdata==2024.1
# tzlocal==5.2
# urllib3==2.2.2
# validators==0.28.3
# watchdog==4.0.1
# web3==6.19.0
# websockets==12.0
# yarl==1.9.4
# zipp==3.19.2
zanieb commented 4 months ago

Just tried it, and guess what? It successfully installs the expected version. Seems like a bug somewhere. Why wouldn't uv just try the latest release of web3 first?

That's usually the case because you've constrained the problem space of the resolver. If there isn't a lower bound on dependencies it is possible for the resolver to backtrack in an unexpected way due to transitive constraints added by other dependencies. The resolution we're producing is "valid" with the given constraints, just not expected or ideal. I'd highly recommend taking a look at the linked issues as there's a lot of detail about how the resolver can get in situations like this.

zanieb commented 4 months ago

To be clear, we want to improve the behavior of the resolver in cases like this but it's an open research problem what the best way to do so is.

wakamex commented 4 months ago

using -v 2>&1 | grep -E "Selecting: |Adding transitive dependency" tells an interesting story:

so it's the timeless problem of "should we have upper bounds" wherein we favor older versions that are sure to work versus ruling out compatibility with future versions that might work.

EDIT: pip choose to backtrack through numpy, which scores a hole in one in this case EDIT2: I wanted to see if pip does something smart like "choose to backtrack on numpy because it released a major version 2 days ago and is most likely to be the one causing problems" but instead it collects the latest version of the 3 deps, fails, then uses a sorted key to choose the next step. in this case, the requested_order is the tie-breaker, so it backtracks on numpy as the first item on the list.

{'not requires_python': True, 'not direct': False, 'not pinned': True, 'not backtrack_cause': True, 'inferred_depth': 1.0, 'requested_order': 0, 'not unfree': True, 'identifier': 'numpy'}
{'not requires_python': True, 'not direct': False, 'not pinned': True, 'not backtrack_cause': True, 'inferred_depth': 1.0, 'requested_order': 1, 'not unfree': True, 'identifier': 'streamlit'}
{'not requires_python': True, 'not direct': False, 'not pinned': True, 'not backtrack_cause': True, 'inferred_depth': 1.0, 'requested_order': 2, 'not unfree': True, 'identifier': 'web3'}
charliermarsh commented 4 months ago

If the goal is to have behavior that’s closer to pip, we may be able to close this as a duplicate of https://github.com/astral-sh/uv/issues/3149 which would give us more similar “requested order” behavior.

In general though, if you’re seeing a resolution that gives you packages with lower versions that o you’d like, I still think the right solution is to add a lower bound. pip and uv could change heuristics at any time which could lead to a different resolution. If a resolution isn’t matching what you want, it makes sense to encode that in your constraints.

wakamex commented 4 months ago

I definitely learned more about dependency resolution. Seems like this is just a difference in approach.

There's a fundamentally different approach to dependency resolution, if I understand it correctly:

Both are arbitrary, just in different ways. I think I'd prefer backtracking on "most recent version" across all chosen packages. That would consistently pick numpy in this example, irrespective of listed order. However that requires having access to release date, which I'm not sure is even in the metadata. As well, pip's approach of fetching all packages before backtracking makes more sense to me, so you could find the "most recent version" reliably, without impact from listed order. If uv had a slower "bleeding edge" resolution mode that did this, I'd probably choose that for my personal projects where my aim is to try out the latest version of everything and help teams with issues as they arise.

My team is going to add lower bounds, and possibly upper bounds, going forward. So in practice that solves our problem. Feel free to close.

notatallshaw commented 4 months ago
  • pip checks the latest version of all deps, then backtracks on listed order in case of a version mismatch (numpy in this case)

It's a little more complicated than that for pip. Pip does indeed first do a breadth first search of the direct dependencies, check for the latest of each, but after that it does a depth first search of the transitive dependencies.

For example, pip will have a very different behavior if instead of having these dependencies in a requirements.txt you put them as dependencies in a pyproject.toml and install that package, these will now be treated as transitive dependencies (as the only direct dependency is now the package) and pip will do a depth first search for them immediately.

So there are many situations where pip can end up also installing really old versions of a package, and the advise is the same from pip, constrain your dependencies by adding reasonable lower bounds.

wakamex commented 4 months ago

pip will have a very different behavior if instead of having these dependencies in a requirements.txt you put them as dependencies in a pyproject.toml and install that package, these will now be treated as transitive dependencies (as the only direct dependency is now the package) and pip will do a depth first search for them immediately.

Seems that's not the case as my tests above where all with pyproject.toml, unless something else is going on. I agree pip's approach is basically equally arbitrary, and not superior in general. They just seem to have a different way of parsing "requested order".

zanieb commented 4 months ago

Thanks for doing some investigation! We appreciate it.

notatallshaw commented 4 months ago

Seems that's not the case as my tests above where all with pyproject.toml, unless something else is going on. I agree pip's approach is basically equally arbitrary, and not superior in general. They just seem to have a different way of parsing "requested order".

Well there is a big different between direct and transitive dependencies for pip, you will get "nicer" behavior when they are direct dependencies, such as pulled from a requirements.txt. I'm looking at ways to improve this, so pip can understanding if there is only 1 candidate for a package,to treat its dependencies as direct dependencies when resolving.

But also you're right, the ordering of transitive requirements is different, also resolvelib (the resolution library for pip) has less situations where it has to exhaust all candidates of a particular requirement compared pubgrub-rs (the resolution library for uv).