I tried to get this to work but I kept getting errors:
The Poetry configuration is invalid:
- Additional properties are not allowed ('tool' was unexpected)
- Additional properties are not allowed ('build-system' was unexpected)
But everything worked fine under Poetry 1.8.3, so I’m not sure what I did wrong. If you can get it working with the example pyproject.toml
I have in my gist mentioned below I will happily include it.
I created a GitHub gist containing lock files from PDM, Poetry, and uv. I used the following as the requirements:
dependencies = ["httpx;platform_python_implementation=='CPython'", "requests"]
The reason I did this was to see how the lock files looked when a top-level dependency had a marker on it (and I chose a marker that I didn’t expect a package to use for its own dependencies), that dependency had unique dependencies of its own (i.e. its dependencies should only be installed if the marker for the top-level dependency passed), and the top-level dependencies shared some dependencies.
NOTE: some of the inline TOML examples may require scrolling to see all the content!
Simple example
Let’s look at a small example: idna
which both httpx
and requests
require and has no dependencies of its own.
PDM
[[package]]
name = "idna"
version = "3.8"
requires_python = ">=3.6"
summary = "Internationalized Domain Names in Applications (IDNA)"
groups = ["default"]
files = [
{file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"},
{file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"},
]
Poetry
[[package]]
name = "idna"
version = "3.8"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.6"
files = [
{file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"},
{file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"},
]
uv
[[package]]
name = "idna"
version = "3.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e8/ac/e349c5e6d4543326c6883ee9491e3921e0d07b55fdf3cce184b40d63e72a/idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603", size = 189467 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/7e/d71db821f177828df9dea8c42ac46473366f191be53080e552e628aad991/idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac", size = 66894 },
]
If you ignore things that are the same but just using different key names, there a few key differences:
- Different approaches to recording what group a dependency belongs in
- PDM has
groups
- Poetry has
optional
- uv doesn’t have it embedded in the package data because they use a graph traversal to determine what to install
- uv leaves out the Python requirement while PDM and Poetry embed it (uv covers it with a global
requires-python
)
- uv separates out the sdist and wheels explicitly while PDM and Poetry leave it up to file name processing
- uv records the file size
- uv records the source of the package data
- PDM and Poetry record the summary from the package
- PDM and Poetry record the filename while uv records the URL (I don’t know what uv does if the URL doesn’t end in the expected filename which is guaranteed by the HTML Simple index API but not the JSON API)
A dependency has a marker requirement
anyio
not only is only installed if httpx
is installed, but its dependency on typing-extensions
is only if Python <= 3.11.
PDM
[[package]]
name = "anyio"
version = "4.4.0"
requires_python = ">=3.8"
summary = "High level compatibility layer for multiple asynchronous event loop implementations"
groups = ["default"]
marker = "platform_python_implementation == \"CPython\""
dependencies = [
"exceptiongroup>=1.0.2; python_version < \"3.11\"",
"idna>=2.8",
"sniffio>=1.1",
"typing-extensions>=4.1; python_version < \"3.11\"",
]
files = [
{file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"},
{file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"},
]
Poetry
[[package]]
name = "anyio"
version = "4.4.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false
python-versions = ">=3.8"
files = [
{file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"},
{file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"},
]
[package.dependencies]
exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""}
idna = ">=2.8"
sniffio = ">=1.1"
typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""}
[package.extras]
doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
trio = ["trio (>=0.23)"]
uv
[[package]]
name = "anyio"
version = "4.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "idna" },
{ name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e6/e3/c4c8d473d6780ef1853d630d581f70d655b4f8d7553c6997958c283039a2/anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94", size = 163930 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/a2/10639a79341f6c019dedc95bd48a4928eed9f1d1197f4c04f546fc7ae0ff/anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7", size = 86780 },
]
- PDM propagates the marker requirement for
httpx
as you can scan its lock file from start to finish linearly, while Poetry and uv don’t work that way
- uv and Poetry separate out version requirements from markers on a per-requirement basis while PDM keeps the requirements as full strings
Knowing the top-level dependencies
How do you know what was specified?
PDM
[metadata]
groups = ["default"]
strategy = ["inherit_metadata"]
lock_version = "4.5.0"
content_hash = "sha256:ad8d737d864c796dd999f825b17b9226e856eca6f3e7b8c2c654917eec423d19"
[[metadata.targets]]
requires_python = ">=3.8"
Poetry
Based on default
and doing a full resolve.
uv
[[package]]
name = "lock-example"
version = "2024"
source = { virtual = "." }
dependencies = [
{ name = "httpx", marker = "platform_python_implementation == 'CPython'" },
{ name = "requests" },
]
[package.metadata]
requires-dist = [
{ name = "httpx", marker = "platform_python_implementation == 'CPython'" },
{ name = "requests" },
]
- PDM has the concept of groups, but otherwise you scan the lock file from top to bottom and evaluating each file independently, so the top-level dependencies are not explicitly listed
- uv makes the package you are installing from its own package and thus acts as its own top-level dependency list
Overall file metadata
What details about the file itself are recorded?
PDM
# This file is @generated by PDM.
# It is not intended for manual editing.
[metadata]
groups = ["default"]
strategy = ["inherit_metadata"]
lock_version = "4.5.0"
content_hash = "sha256:ad8d737d864c796dd999f825b17b9226e856eca6f3e7b8c2c654917eec423d19"
[[metadata.targets]]
requires_python = ">=3.8"
Poetry
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
# ... content
[metadata]
lock-version = "2.0"
python-versions = ">=3.8"
content-hash = "d120ac5b1079e801d817067b736686bfa7d27e81331a17922554282c3c847dab"
uv
version = 1
requires-python = ">=3.8"
- PDM and Poetry record the content hash of
pyproject.toml
The questions I have after looking at this are:
- @frostming any reason why you didn’t explicitly record the top-level dependencies?
- @charliermarsh
- any reason you didn’t record the filename separately for each file from the URL?
- why did you go with using the project’s own name to record top-level details instead of a dedicated section at the top of the lock file?
- What would you do if there’s ever a wheel 2 format? Keep
wheels
and choose based on filename, separate key, or not really thought about it yet?
- @radoering any reason you don’t record the overall required Python version?