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

Use === to install lockfile versions #3250

Closed
ryanhiebert opened this issue Nov 18, 2018 · 14 comments
Closed

Use === to install lockfile versions #3250

ryanhiebert opened this issue Nov 18, 2018 · 14 comments

Comments

@ryanhiebert
Copy link

ryanhiebert commented Nov 18, 2018

Issue description

We have a private package registry that we use to ship temporary versions of code that we need to put in production before new releases have been merged upstream. It is used in combination with the normal PyPI index. I've started using local version identifiers for these custom releases, so that I can be sure that they won't conflict with upstream releases. Nobody but us should see these releases.

Unfortunately, just the act of uploading a new release with a local version identifier is breaking my build, because, it appears, it is using the new uploaded version, even though the lock file has been locked to the PyPI version (without the local version identifier). This gives me no opportunity to test such a local release version, which rather defeats the purpose of having a lockfile.

Expected result

I expect that once I've locked a set of packages for a set of package indexes, that it will only use the exact version as locked in the lockfile, and not automatically use local versions.

Actual result

It uses the local version (with the +aspiredu version string appended), and then subsequently fails, appropriately, because the version it's trying to install doesn't have the same hash as the one that it was locked with.

$ pipenv install
Installing dependencies from Pipfile.lock (316fb6)…
An error occurred while installing django-cleanup==3.0.1 --hash=sha256:53316b04b9c91725fce9eac252cdc762a988b5a1640aeee8ab43048ccf9d98bd! Will try again.
  🐍   ▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉ 1/1 — 00:00:02
Installing initially failed dependencies…
[pipenv.exceptions.InstallError]:   File "/home/ryan/.local/lib/python3.6/site-packages/pipenv/core.py", line 1872, in do_install
[pipenv.exceptions.InstallError]:       keep_outdated=keep_outdated
[pipenv.exceptions.InstallError]:   File "/home/ryan/.local/lib/python3.6/site-packages/pipenv/core.py", line 1232, in do_init
[pipenv.exceptions.InstallError]:       pypi_mirror=pypi_mirror,
[pipenv.exceptions.InstallError]:   File "/home/ryan/.local/lib/python3.6/site-packages/pipenv/core.py", line 841, in do_install_dependencies
[pipenv.exceptions.InstallError]:       retry_list, procs, failed_deps_queue, requirements_dir, **install_kwargs
[pipenv.exceptions.InstallError]:   File "/home/ryan/.local/lib/python3.6/site-packages/pipenv/core.py", line 748, in batch_install
[pipenv.exceptions.InstallError]:       _cleanup_procs(procs, not blocking, failed_deps_queue, retry=retry)
[pipenv.exceptions.InstallError]:   File "/home/ryan/.local/lib/python3.6/site-packages/pipenv/core.py", line 676, in _cleanup_procs
[pipenv.exceptions.InstallError]:       raise exceptions.InstallError(c.dep.name, extra=err_lines)
[pipenv.exceptions.InstallError]: ['Looking in indexes: https://pypi.org/simple, https://pypi.fury.io/OHXIw-9Umgf3jlbpG5ULSp97w456Q/aspiredu/', 'Collecting django-cleanup==3.0.1 (from -r /tmp/pipenv-y59vqrv4-requirements/pipenv-9xa6khw7-requirement.txt (line 1))', '  Using cached https://pypi.fury.io/OHXIw-9Umgf3jlbpG5ULSp97w456Q/aspiredu/-/ver_CNb66/django_cleanup-3.0.1+aspiredu1-py2.py3-none-any.whl']
[pipenv.exceptions.InstallError]: ['THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS FILE. If you have updated the package versions, please update the hashes. Otherwise, examine the package contents carefully; someone may have tampered with them.', '    django-cleanup==3.0.1 from https://pypi.fury.io/OHXIw-9Umgf3jlbpG5ULSp97w456Q/aspiredu/-/ver_CNb66/django_cleanup-3.0.1+aspiredu1-py2.py3-none-any.whl#md5=e8f73301a21bb37eab6dfea9855d8ec8 (from -r /tmp/pipenv-y59vqrv4-requirements/pipenv-9xa6khw7-requirement.txt (line 1)):', '        Expected sha256 53316b04b9c91725fce9eac252cdc762a988b5a1640aeee8ab43048ccf9d98bd', '             Got        f858211dab4255b103aef153107756916a352458ec9c9c4d7307dd2661c04eca']
ERROR: ERROR: Package installation failed...
  ☤  ▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉ 0/1 —

Steps to replicate

In order to independently replicate, you'll need to have access to an index that you can upload to. First create the pipfile and lock it before you upload a local version. If you don't have access to a private repository, you can use this repository, though you'll have to trust that I did things the way I'm saying.

https://github.com/ryanhiebert/pipenv-local

You need to have both indexes added to the file before you lock, so that you'll have the custom index in place when you go to install, and it sees that there's a new local version in the custom index.

[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[[source]]
name = "aspiredu"
verify_ssl = true
url = "https://pypi.fury.io/OHXIw-9Umgf3jlbpG5ULSp97w456Q/aspiredu/"

[dev-packages]

[packages]
django-cleanup = "*"

[requires]
python_version = "3.7"
{
    "_meta": {
        "hash": {
            "sha256": "a1162dba4d383f6f7924058142a87bc04355d6e68529b0c51d14743a32316fb6"
        },
        "pipfile-spec": 6,
        "requires": {
            "python_version": "3.7"
        },
        "sources": [
            {
                "name": "pypi",
                "url": "https://pypi.org/simple",
                "verify_ssl": true
            },
            {
                "name": "aspiredu",
                "url": "https://pypi.fury.io/OHXIw-9Umgf3jlbpG5ULSp97w456Q/aspiredu/",
                "verify_ssl": true
            }
        ]
    },
    "default": {
        "django-cleanup": {
            "hashes": [
                "sha256:53316b04b9c91725fce9eac252cdc762a988b5a1640aeee8ab43048ccf9d98bd"
            ],
            "index": "pypi",
            "version": "==3.0.1"
        }
    },
    "develop": {}
}

Then upload a new version to the custom index, with the same version but with a local segment added (3.0.1+aspiredu1 in my example). Attempt to install from the lockfile again using pipenv install, unchanged, and it will pull in from the custom index, instead of the locked PyPI version, causing the hash to mismatch and the installation to fail.


$ pipenv --support

Pipenv version: '2018.11.14'

Pipenv location: '/home/ryan/.local/lib/python3.6/site-packages/pipenv'

Python location: '/home/ryan/.pyenv/versions/3.6.6/bin/python3.6'

Python installations found:

  • 3.7.1: /home/ryan/.pyenv/versions/3.7.1/bin/python3
  • 3.7.1: /home/ryan/.pyenv/versions/3.7.1/bin/python3.7m
  • 3.7.0: /home/ryan/.pyenv/versions/3.7.0/bin/python3
  • 3.7.0: /home/ryan/.pyenv/versions/3.7.0/bin/python3.7m
  • 3.6.7: /home/ryan/.pyenv/versions/3.6.7/bin/python3
  • 3.6.7: /home/ryan/.pyenv/versions/3.6.7/bin/python3.6m
  • 3.6.6: /home/ryan/.pyenv/versions/3.6.6/bin/python3
  • 3.6.6: /home/ryan/.pyenv/versions/3.6.6/bin/python3.6m
  • 3.6.6: /usr/bin/python3
  • 3.6.6: /usr/bin/python3.6m
  • 2.7.15rc1: /usr/bin/python2.7

PEP 508 Information:

{'implementation_name': 'cpython',
 'implementation_version': '3.6.6',
 'os_name': 'posix',
 'platform_machine': 'x86_64',
 'platform_python_implementation': 'CPython',
 'platform_release': '4.15.0-38-generic',
 'platform_system': 'Linux',
 'platform_version': '#41-Ubuntu SMP Wed Oct 10 10:59:38 UTC 2018',
 'python_full_version': '3.6.6',
 'python_version': '3.6',
 'sys_platform': 'linux'}

System environment variables:

  • CLUTTER_IM_MODULE
  • LS_COLORS
  • LESSCLOSE
  • XDG_MENU_PREFIX
  • LANG
  • GDM_LANG
  • DISPLAY
  • QT_STYLE_OVERRIDE
  • COLORTERM
  • PYENV_VIRTUALENV_INIT
  • XDG_VTNR
  • SSH_AUTH_SOCK
  • MANDATORY_PATH
  • XDG_SESSION_ID
  • XDG_GREETER_DATA_DIR
  • USER
  • DESKTOP_SESSION
  • QT4_IM_MODULE
  • TEXTDOMAINDIR
  • GNOME_TERMINAL_SCREEN
  • DEFAULTS_PATH
  • QT_QPA_PLATFORMTHEME
  • PWD
  • HOME
  • TEXTDOMAIN
  • SSH_AGENT_PID
  • QT_ACCESSIBILITY
  • XDG_SESSION_TYPE
  • XDG_DATA_DIRS
  • XDG_SESSION_DESKTOP
  • GTK_MODULES
  • TERM
  • SHELL
  • VTE_VERSION
  • XDG_SEAT_PATH
  • QT_IM_MODULE
  • XMODIFIERS
  • IM_CONFIG_PHASE
  • XDG_CURRENT_DESKTOP
  • GPG_AGENT_INFO
  • GNOME_TERMINAL_SERVICE
  • XDG_SEAT
  • SHLVL
  • PYENV_SHELL
  • LANGUAGE
  • GDMSESSION
  • GNOME_DESKTOP_SESSION_ID
  • LOGNAME
  • DBUS_SESSION_BUS_ADDRESS
  • XDG_RUNTIME_DIR
  • XAUTHORITY
  • XDG_SESSION_PATH
  • XDG_CONFIG_DIRS
  • PATH
  • SESSION_MANAGER
  • LESSOPEN
  • GTK_IM_MODULE
  • OLDPWD
  • _
  • PIP_DISABLE_PIP_VERSION_CHECK
  • PYTHONDONTWRITEBYTECODE
  • PIP_SHIMS_BASE_MODULE
  • PIP_PYTHON_PATH
  • PYTHONFINDER_IGNORE_UNSUPPORTED

Pipenv–specific environment variables:

Debug–specific environment variables:

  • PATH: /home/ryan/bin:/home/ryan/.local/bin:/home/ryan/.pyenv/plugins/pyenv-virtualenv/shims:/home/ryan/.pyenv/shims:/home/ryan/.pyenv/bin:/home/ryan/.local/bin:/home/ryan/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
  • SHELL: /bin/bash
  • LANG: en_US.UTF-8
  • PWD: /home/ryan/Code/aspiredu/pipenv-local

Contents of Pipfile ('/home/ryan/Code/aspiredu/pipenv-local/Pipfile'):

[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[[source]]
name = "aspiredu"
verify_ssl = true
url = "https://pypi.fury.io/OHXIw-9Umgf3jlbpG5ULSp97w456Q/aspiredu/"

[dev-packages]

[packages]
django-cleanup = "*"

[requires]
python_version = "3.7"

Contents of Pipfile.lock ('/home/ryan/Code/aspiredu/pipenv-local/Pipfile.lock'):

{
    "_meta": {
        "hash": {
            "sha256": "a1162dba4d383f6f7924058142a87bc04355d6e68529b0c51d14743a32316fb6"
        },
        "pipfile-spec": 6,
        "requires": {
            "python_version": "3.7"
        },
        "sources": [
            {
                "name": "pypi",
                "url": "https://pypi.org/simple",
                "verify_ssl": true
            },
            {
                "name": "aspiredu",
                "url": "https://pypi.fury.io/OHXIw-9Umgf3jlbpG5ULSp97w456Q/aspiredu/",
                "verify_ssl": true
            }
        ]
    },
    "default": {
        "django-cleanup": {
            "hashes": [
                "sha256:53316b04b9c91725fce9eac252cdc762a988b5a1640aeee8ab43048ccf9d98bd"
            ],
            "index": "pypi",
            "version": "==3.0.1"
        }
    },
    "develop": {}
}
@duplicate-issues
Copy link

Hey @ryanhiebert,

We did a quick check and this issue looks very darn similar to

This could be a coincidence, but if any of these issues solves your problem then I did a good job 😄

If not, the maintainers will get to this issue shortly.

Cheers,
Your Friendly Neighborhood ProBot

@frostming
Copy link
Contributor

frostming commented Dec 4, 2018

@ryanhiebert Please specify the source in the Pipfile:

django-cleanup = {version = "*", index="aspiredu"}

and relock

@ryanhiebert
Copy link
Author

#3248 is about it writing the incorrect index to the wrong, and it does work to manually specify the index in the Pipfile, though it is unsatisfying that I'd need to specify the index just in order to get it to lock to the correct index.

This issue is about the fact that uploading a new and local version to a custom index that's already in the Pipfile will cause previously generated lockfiles to make builds that are broken. This happens because even though the lockfile specifies an exact version on PyPI, and includes checksums from the PyPI index, the version with the local part (+aspiredu1) on a custom index is taken as if it were the same as the one in the lockfile from PyPI without that local part of the version. Until the checksum is checked, at which point it fails.

If it is the position of the maintainers of Pipenv that the solution is to specify the indexes for the package in the Pipfile, that would mean that if I wish to have two indexes set up, I must always, and explicitly, specify which index each package in the Pipfile should come to, even if the index is PyPI, because I might, one day, wish to upload a local version to the custom index, and I don't want it to break my build.

I find this solution unsatisfying, because by needing to explicitly indicate which index the package should come from in the Pipfile itself, I am no longer able to use pipenv update to get newer versions of the package from PyPI that would supersede my local version.

I hope that clarifies the purpose of this issue.

@frostming
Copy link
Contributor

frostming commented Dec 5, 2018

@ryanhiebert I setup a private pypi as you described and do some testing as follows:

  1. $ pipenv lock -> Lock version 3.0.1 and hash from PyPI
  2. Upload version 3.0.1+abc to private PyPI
  3. $ pipenv install -> Hash mismatch
  4. $ pipenv update -> Lock version 3.0.1+abc and hash from private PyPI, install success.
  5. Return to step 2, but version string is 3.0.1_abc
  6. $ pipenv install -> Installation success

So the only failed case is 1, 2, 3. The key point here is PyPI recognizes X.Y,Z+blah as a compatible version while X.Y.Z_blah is not, I am not familiar with PEPs but it must be defined somewhere. BTW the index string in lockfile is not respected at all. However, relocking will always bring you to the right point and should be a safe solution.

Conclusion

I understand your requirement here and I recommend two workarounds:

  • Name your private package as X.Y.Z_blah or X.Y.Z-blah
  • Relock before you install, or just do pipenv update

Thanks.

@ryanhiebert
Copy link
Author

Thank you for the workaround, it will likely be a way to avoid this issue until a better solution is found, though it is not an ideal solution in the end, because as far as I can tell, both by reading the text of the PEP and the regular expression, it doesn't appear that those names are valid version identifiers from PEP 440.

Re-locking or updating before install is not reasonable. I'm using Pipenv in a CI environment, and I've got to be able to rebuild the dependencies from the lockfile, or else I simply can't trust Pipenv in my builds.


It seems to me that perhaps using arbitrary equality matching would be most appropriate for lockfiles. It completely avoids this issue, and the special case of local version identifiers is explicitly mentioned in the PEP. Unfortunately, it appears that the authors of the PEP may not have been considering lockfile use-cases when they were writing it, because they have some very strong recommendations to prefer not using arbitrary equality.

https://www.python.org/dev/peps/pep-0440/#arbitrary-equality
https://www.python.org/dev/peps/pep-0440/#adding-arbitrary-equality

@dstufft or @ncoghlan : As the authors of that PEP, do you think that Pipenv would be better served by using arbitrary equality when installing package versions from the lockfile, to avoid issues arising from local version identifiers being compatible with identifiers with the same public version?

@ncoghlan
Copy link
Member

ncoghlan commented Dec 6, 2018

Yes, using === in the lock files seems like a reasonable idea to me.

PEP 440 specifically calls that out as a possible use case:

This operator may also be used to explicitly require an unpatched version of a project such as ===1.0 which would not match for a version 1.0+downstream1.

The admonition in the paragraph immediately after that one could be softened to also allow for the hash-checked lockfile with pre-normalised version identifiers use case.

@ryanhiebert
Copy link
Author

Thank you, @ncoghlan , that's what I suspected that paragraph was intending, but the wording of the admonition to avoid using prompted me to want to check on that. Thank you for confirming that.

@ryanhiebert ryanhiebert changed the title Uploading a local version to a custom index Use === to install lockfile versions Dec 6, 2018
@techalchemy
Copy link
Member

Yeah this is spot on actually, but it does make me wonder whether we should then avoid normalizing names we write to the lockfile? For example, if we asked pypi directly for this kind of version number, would we get a response? I’m pretty sure we would. Not 100%, but it’s just a matter of checking.

I feel like we should avoid normalizing this, so the question is just, where is the normalization occurring? I’m also pretty sure packaging has a normalization function for versions. We should likely prefer that

@ryanhiebert
Copy link
Author

ryanhiebert commented Dec 6, 2018

I just spent 20 minutes trying to find where the version is written out from the lockfile, and I just got lost, I wasn't able to figure out how to make it write the version verbatim using === when installing a non-vcs or file dependency from the lockfile.

@ncoghlan
Copy link
Member

ncoghlan commented Dec 6, 2018

@techalchemy From the PyPI side, the server doesn't know why you're asking for a particular version string, so it always performs the normalisation when looking for a project version: https://github.com/pypa/warehouse/blob/1fbb4ac752e68b5840b9e09b68e44a165569bfa6/warehouse/packaging/models.py#L135

This means that while the text on this front is currently ambiguous in PEP 440 itself, I think it would make sense to propose a clarification that says that for PEP 440 compliant strings, normalisation is still taken into account, and it's only non-compliant strings that are handled as being completely opaque.

The only parts that won't happen are the parts which are part of the definition of the == version matching operator (zero padding, ignoring local version segments, wildcard prefix matching).

@techalchemy
Copy link
Member

So to summarize the basic proposal is to, for example, switch to using packaging.version.parse() to see if we can parse a version, if we get a Version back we can pin with === in the lockfile, but if we get a LegacyVersion back we should still pin with ==. Does that seem right @ncoghlan ?

@ncoghlan
Copy link
Member

ncoghlan commented Dec 8, 2018

@techalchemy That sounds like a pretty safe option to me, but there's one corner case you're going to need to consider closely: the zero-padding case, where you can specify a version like 1.0 and == version matching will match it with 1.0.0 (and vice-versa).

I think it should be fine, since generating the lock file necessarily gets the version to pin from the found artifact (not from the original request in Pipfile), but it's the one aspect of the desired behaviour which is technically part of the definition of version matching, rather than part of version normalisation.

@techalchemy
Copy link
Member

version matching, rather than part of version normalisation.

This is the kind of distinction that makes me really happy to have access to you, there is a 99% chance I would gloss over it without it specifically being pointed out. I suspect I'll at least ignore it for now, but at least I'll do it consciously :)

Thanks!

@matteius
Copy link
Member

matteius commented Mar 4, 2023

This has been solved with my work on restricted package indexes last year, so I don't think we need to support a ===. Just assign the appropriate index name to the requirement and it will pull from that only unless you add install_search_all_sources = true to the pipenv section of the Pipfile, then this would search all index sources during install.

@matteius matteius closed this as completed Mar 4, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants