Let me approach the topic from another viewpoint: is there a use case for which Poetry lock files are not good enough? If so, what would be missing? And would that use case be something we need to care about in the scope of this PEP?
Not sure if itâs ânot good enoughâ, but Poetry lock files do not support a linear installation of anything. Since their lock file records dependency information in order to be a single lock file for all platforms with a single list of packages, you still have to perform a resolution to figure out what to install (either at the package, version, or file level). I think this might be why some switch off of Poetry as their resolution algorithm takes longer than some people want to wait in order to support that âuniversalâ lock file.
Could you clarify what âlinear installationâ means? If Poetry didnât record all that information, then wouldnât the lock file be specific to a particular environment (OS, arch, libc version) and set of extras? Is that what we want?
I can add to Brettâs (rather helpful) list of use cases.
Environments that continue to work over time. In the past, installing the scipy stack (numpy, pandas, scipy, jupyter, etc.) involved creating environments with many dependencies. Iâve observed such large environments fail to reproduce (usually from a dependency resolution conflict) even under the following pre-cautions:
pinned pip requirements.txt file
pinned conda environment.yml file
pinned, conda spec-files
same machine w/o updates
Utimately, Iâve had moderate success with hashed, pip-tools-style requirements.txt files in combination with the latter techniques. Iâd prefer to see a more standardized solution that would help this type of âreproductionâ dilemma, i.e. guarantee successful re-creation of a pre-existing environment.
Yep. Iâd be fine with having a format which allows me to turn the knob in one direction or the other, with sufficient UX to give me the feedback I need to know if the package manager isnât able to do what I want.
E.g. If I want near perfect reproducibility [2] then Iâll specify wheels and the package manager should refuse to install sdists, or warn if it canât resolve everything through wheels, or just fall back to âclose enoughâ reproducibility if it has to resolve via sdists.
An installer can look at a list of packages and just install what it sees without any thought of what it should be installing.
Depends on what you expect from a lock file. If youâre okay with both lock file generation and consumption to require logic to calculate what needs to be installed, then what Poetry does is fine (from what I can tell, but Iâm not expert at its abilities). But if you want installation to be simpler and more reproducible then you probably donât want decisions made at install time in terms of a decision tree of what should get installed and instead have a âdumbâ installer that just installs a list of wheels. In that instance then the lock file would be specific to the wheel tags and markers used when the lock file was generated, but Iâm personally fine with that as long as you can have multiple instances of that to cover each platform you care about.
As Paul has said, simply locking down what youâre after is a massive part of this entire endeavour.
If you had this many forms of pinned requirements, what exactly went wrong then? This information would be very helpful in determining how to design the spec.
This came up last time - from the consumer side, a key requirement should be that itâs possible to write a tool that reads a lockfile and reports what would be installed, without that tool needing to implement a resolver.
Iâve said it before but if this invariant is clarified / enforced (even if only via a standard and not directly in PyPI), I believe that it unlocks a huge amount of value and simplification towards a potential cross-platform lockfile.
Relevant bits of zen:
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Yes, I agree actually. My language got awkward and Iâm going to blame my previous commentary about âreproducibilityâ. Every time âreproducibilityâ is brought up (via the nix crowd or otherwise), nerd-sniping occurs about how far into the turing tarpit do we go, or byte-for-byte reproducibility isnât practical / possible etc.
I was mostly responding to that and trying to include that in my MUST/MUST NOT proposal.
I think it was a mistake in hind-sight. I think true âreproducibilityâ (in a byte-for-byte sense) is not what this PEP should be targeting as a goal or benefit. Having a lockfile will improve the probability of being able to reproduce an environment, but it should not be guaranteed at all.
If I feel motivated again, I might try spike out a simpler set of MUST / MUST NOT for my lockfile metaphor where itâs a way to specify a virtual wheelhouse (or PyPI / index allowlist) that can be used to install build and runtime dependencies into an environment, with only loose gaurantees that it will succeed.
Possibly, but the benefit of not allowing it to be dynamic (or at least not varying across platforms), is that lockfiles can be generated on one platform and installed on other platforms. This is the not-technically-correct-but-very-practical simplifying assumption that poetry presently makes with its lockfile.
This addresses the universal pain felt around the world by engineers using windows or macos and having to commit lockfiles that run on linux CI or linux production servers. And here is where I expect folks to start yelling âdocker, dockerâ at me. We donât all want to (or need to) use docker for something like this.
Cool. So my follow up question is whatâs wrong with that approach? Iâm trying to pinpoint what problem weâre attempting to solve hereâŠ
Apologies if Iâm completely missing the point here. This isnât a use case I personally have any need for, so Iâm somewhat blind when it comes to understanding it.
I agree with this. I think the PEP currently has some of this in the âmotivationâ section but itâs not completely clear (at least to me) why the different motivations outlined there were synthesized into the eventual proposal in this particular way.
In my own experience, when I hear people wanting lockfiles, it is mainly as a tool for deployment and perhaps a bit for reproducibility of a dev environment. People want to set up something like a web app and then say âI know everything works with these versions of these packages, so I want to lock that so I can install exactly these versions again later and thus know my app will workâ. And maybe sometimes they want to do something like âIâm developing this, I want to lock my dev environment so I can send it to a coworker working on related stuff, to be sure weâre both working in compatible environments.â (This is in some sense still deployment, just deployment of in-progress rather than finished work.)
From that perspective, it seems to me that simple locking based only on declared package versions would go a long way towards meeting peopleâs lockfile needs. If you lock your environment and reinstalling the same version of the same Package X later leads to different behavior, then itâs either a bug in Package X or itâs a result of some Python-external difference thatâs out of scope.
Cross-platform locking and strict security concerns (like ensuring byte-for-byte identity) are definitely relevant, but it seems like they open up huge new worlds of complexity.
In the end a lot of what people want from lockfiles (again, just as I see it) has to do with the behavior of code â they want some kind of guarantee to the effect of âI can get this code to run correctly againâ. That conceptual definition may be difficult to achieve, but the current best proxy we have for âthe code works the sameâ is version numbers.[1]
I personally hope we can standardize on a default value of Pythonâs first public release (I have that Unix timestamp written down somewhere).
Nothingâs wrong with it just like everything that functions without a standard isnât âwrongâ. But the fact everyone is re-implementing the same concept feels wrong.
Iâll also toss in that thereâs a security-in-depth issue thatâs missing with this approach. If the only protection mechanism you have is âonly use our package index and donât accidentally hit another index like PyPIâ then youâre one configuration mistake away from getting a wheel you didnât expect or want. But if you at least make things like hash validation an opt-out thing and something baked into a standard then you have another layer at installation-time to make sure you are only installing what you intend to install.
Going back to PEP 665, take 2 -- A file format to list Python dependencies for reproducibility of an application - #177 by brettcannon and my motivations for why I made PEP 665 the way I did, the security point also extends to cloud hosting scenarios where you are doing installs on other machines that might be automating installations. In those scenarios you also want to watch out for configuration mishaps, especially when you might not directly control how the installation tool is invoked (e.g. if they are using pip on your behalf and they do not let you specify e.g., --require-hashes). If a tool was following a lock file standard that said, âyou must validate hashes (when present)â then you donât have to be so concerned about how the tool is executed to get the features you want turned on (and yes, you could argue the cloud host should provide the right knobs, but we all know vendors donât necessarily give you what you want).
A few more things that I discovered while working on repairwheel:
Ordering of files in the archive. The wheel spec suggests placing .dist-info at the end, but doesnât seem to say anything about writing files in any sort of sorted order. I found it convenient to place all non-dist-info files at the front sorted lexicographically, then all dist-info files except for RECORD, and finally RECORD at the very end.
Ordering of files in RECORD. Should probably match the order of files in the archive described above
Newlines in meta files. Use \n, even when building wheels on Windows.
Setting the ZipInfo.create_system value which differs by default on Windows vs. unix.