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

Full implementation of --keep-outdated #3304

Merged
merged 43 commits into from
Mar 30, 2019
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
566e1c1
Fix accidentally-shortened installation timeouts
techalchemy Nov 21, 2018
052c5b8
Create sample implementation for keep outdated functionality
techalchemy Nov 21, 2018
fb99f71
Add PEEP to outline behavior
techalchemy Nov 21, 2018
d3bcd83
Merge branch 'master' into feature/keep-outdated-peep
techalchemy Nov 25, 2018
5b8b10d
Fully functional `--keep-outdated` implementation
techalchemy Nov 26, 2018
97962d9
PEEP update and resolver fix
techalchemy Nov 26, 2018
76a5e29
Merge branch 'master' into feature/keep-outdated-peep
techalchemy Nov 26, 2018
de24a5f
Merge branch 'master' into feature/keep-outdated-peep
techalchemy Nov 27, 2018
de20227
Merge branch 'master' into feature/keep-outdated-peep
techalchemy Dec 4, 2018
83f5791
Merge branch 'feature/keep-outdated-peep' of github.com:pypa/pipenv i…
techalchemy Dec 4, 2018
df712cc
Bugfixes for set resolution
techalchemy Dec 5, 2018
3feeaa7
Merge branch 'master' into feature/keep-outdated-peep
ncoghlan Jan 21, 2019
552d127
Merge with updates
techalchemy Feb 17, 2019
698dfb8
Port over changes that were overwritten
techalchemy Feb 18, 2019
a33ad9d
Update utils to work with patch
techalchemy Feb 18, 2019
1fcafb8
Re-integrate changes from `keep_outdated`
techalchemy Feb 18, 2019
9c0bac1
add updated requirementslib
techalchemy Feb 18, 2019
7798cbe
Update lockfile
techalchemy Feb 18, 2019
fae1dba
Update setup.py to keep pytest pinned below 4.0
techalchemy Feb 18, 2019
adaf442
Fix missing import of `ConnectionError`
techalchemy Feb 18, 2019
fbdf933
Fix test script
techalchemy Feb 18, 2019
86ca58c
Merge branch 'bugfix/3148' into feature/keep-outdated-peep
techalchemy Feb 20, 2019
6604712
Create requirements inside spinner for better UX
techalchemy Feb 25, 2019
f69aaf3
Merge branch 'bugfix/3148' into feature/keep-outdated-peep
techalchemy Feb 25, 2019
5383db1
Update lockfile
techalchemy Feb 26, 2019
1207e3d
Update keep outdated implementation with new pipenv code
techalchemy Feb 26, 2019
3e96bfe
Fix pip version check code
techalchemy Feb 26, 2019
b9f7852
Update lockfile
techalchemy Feb 26, 2019
ecb3352
Merge branch 'master' into feature/keep-outdated-peep
techalchemy Mar 7, 2019
93b65fe
Remove old constraint line from locking function
techalchemy Mar 7, 2019
7413f2f
Update lockfile and switch to the correct python version for tests
techalchemy Mar 8, 2019
6552e8d
Add tests for `--keep-outdated`
techalchemy Mar 10, 2019
0cc3e25
Rename peep to `004`
techalchemy Mar 10, 2019
0382e0c
Merge branch 'master' into feature/keep-outdated-peep
techalchemy Mar 10, 2019
3131e2f
Rename peep to 005 to make room for kenneth's peep
techalchemy Mar 10, 2019
93019f3
Merge branch 'master' into feature/keep-outdated-peep
techalchemy Mar 11, 2019
83b1f12
Merge branch 'master' into feature/keep-outdated-peep
techalchemy Mar 11, 2019
6382f51
Merge branch 'master' into feature/keep-outdated-peep
techalchemy Mar 18, 2019
e16608c
sort pytest markers by length
techalchemy Mar 18, 2019
0b5826e
Fix typo in PEEP
techalchemy Mar 18, 2019
c7b2a52
Merge branch 'master' into feature/keep-outdated-peep
techalchemy Mar 25, 2019
ea9129a
Merge branch 'master' into feature/keep-outdated-peep
techalchemy Mar 25, 2019
1b84c6b
Merge branch 'master' into feature/keep-outdated-peep
techalchemy Mar 29, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions peeps/PEEP-005.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# PEEP-005: Do Not Remove Entries from the Lockfile When Using `--keep-outdated`

**PROPOSED**

This PEEP describes a change that would retain entries in the Lockfile even if they were not returned during resolution when the user passes the `--keep-outdated` flag.


The `--keep-outdated` flag is currently provided by Pipenv for the purpose of holding back outdated dependencies (i.e. dependencies that are not newly introduced). This proposal attempts to identify the reasoning behind the flag and identifies a need for a project-wide scoping. Finally, this proposal outlines the expected behavior of `--keep-outdated` under the specified circumstances, as well as the required changes to achieve full implementation.

## Retaining Outdated Dependencies

The purpose of retaining outdated dependencies is to allow the user to introduce a new package to their environment with a minimal impact on their existing environment. In an effort to achieve this, `keep_outdated` was proposed as both a flag and a Pipfile setting [in this issue](https://github.com/pypa/pipenv/issues/1255#issuecomment-354585775), originally described as follows:

> pipenv lock --keep-outdated to request a minimal update that only adjusts the lock file to account for Pipfile changes (additions, removals, and changes to version constraints)... and pipenv install --keep-outdated needed to request only the minimal changes required to satisfy the installation request

However, the current implementation always fully re-locks, rather than only locking the new dependencies. As a result, dependencies in the `Pipfile.lock` with markers for a python version different from that of the running interpreter will be removed, even if they have nothing to do with the current changeset. For instance, say you have the following dependency in your `Pipfile.lock`:

```json
{
"default": {
"backports.weakref": {
"hashes": [...],
"version": "==1.5",
"markers": "python_version<='3.4'"
}
}
}
```

If this lockfile were to be re-generated with Python 3, even with `--keep-outdated`, this entry would be removed. This makes it very difficult to maintain lockfiles which are compatible across major python versions, yet all that would be required to correct this would be a tweak to the implementation of `keep-outdated`. I believe this was the goal to begin with, but I feel this behavior should be documented and clarified before moving forward.

## Desired Behavior

1. The only changes that should occur in `Pipfile.lock` when `--keep-outdated` is passed should be changes resulting from new packages added or pin changes in the project `Pipfile`;
2. Existing packages in the project `Pipfile.lock` should remain in place, even if they are not returned during resolution;
3. New dependencies should be written to the lockfile;
4. Conflicts should be resolved as outlined below.

## Conflict Resolution

If a conflict should occur due to the presence in the `Pipfile.lock` of a dependency of a new package, the following steps should be undertaken before alerting the user:

1. Determine whether the previously locked version of the dependency meets the constraints required of the new package; if so, pin that version;
2. If the previously locked version is not present in the `Pipfile` and is not a dependency of any other dependencies (i.e. has no presence in `pipenv graph`, etc), update the lockfile with the new version;
3. If there is a new or existing dependency which has a conflict with existing entries in the lockfile, perform an intermediate resolution step by checking:
a. If the new dependency can be satisfied by existing installs;
b. Whether conflicts can be upgraded without affecting locked dependencies;
c. If locked dependencies must be upgraded, whether those dependencies ultimately have any dependencies in the `Pipfile`;
d. If a traversal up the graph lands in the `Pipfile`, create _abstract dependencies_ from the `Pipfile` entries and determine whether they will still be satisfied by the new version;
e. If a new pin is required, ensure that any subdependencies of the newly pinned dependencies are therefore also re-pinned (simply prefer the updated lockfile instead of the cached version);
4. Raise an Exception alerting the user that they either need to do a full lock or manually pin a version.

## Necessary Changes

In order to make these changes, we will need to modify the dependency resolution process. Overall, locking will require the following implementaiton changes:
Copy link
Member

Choose a reason for hiding this comment

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

Typo in "implementaiton"


1. The ability to restore any entries that would otherwise be removed when the `--keep-outdated` flag is passed. The process already provides a caching mechanism, so we simply need to restore missing cache keys;
2. Conflict resolution steps:
a. Check an abstract dependency/candidate against a lockfile entry;
b. Requirements mapping for each dependency in the environment to determine if a lockfile entry is a descendent of any other entries;


Author: Dan Ryan <[email protected]>
9 changes: 8 additions & 1 deletion pipenv/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1065,7 +1065,8 @@ def do_lock(
allow_global=system,
pypi_mirror=pypi_mirror,
pipfile=packages,
lockfile=lockfile
lockfile=lockfile,
keep_outdated=keep_outdated
)

# Support for --keep-outdated…
Expand All @@ -1082,6 +1083,12 @@ def do_lock(
lockfile[section_name][canonical_name] = cached_lockfile[
section_name
][canonical_name].copy()
for key in ["default", "develop"]:
packages = set(cached_lockfile[key].keys())
new_lockfile = set(lockfile[key].keys())
missing = packages - new_lockfile
for missing_pkg in missing:
lockfile[key][missing_pkg] = cached_lockfile[key][missing_pkg].copy()
# Overwrite any develop packages with default packages.
lockfile["develop"].update(overwrite_dev(lockfile.get("default", {}), lockfile["develop"]))
if write:
Expand Down
93 changes: 72 additions & 21 deletions pipenv/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,33 +351,84 @@ def get_outdated_packages(self, pre=False):
if pkg.latest_version._version > pkg.parsed_version._version
]

def get_package_requirements(self):
@classmethod
def _get_requirements_for_package(cls, node, key_tree, parent=None, chain=None):
if chain is None:
chain = [node.project_name]

d = node.as_dict()
if parent:
d['required_version'] = node.version_spec if node.version_spec else 'Any'
else:
d['required_version'] = d['installed_version']

get_children = lambda n: key_tree.get(n.key, [])

d['dependencies'] = [
cls._get_requirements_for_package(c, key_tree, parent=node,
chain=chain+[c.project_name])
for c in get_children(node)
if c.project_name not in chain
]

return d

def get_package_requirements(self, pkg=None):
from .vendor.pipdeptree import flatten, sorted_tree, build_dist_index, construct_tree
dist_index = build_dist_index(self.get_installed_packages())
packages = self.get_installed_packages()
if pkg:
packages = [p for p in packages if p.key == pkg]
dist_index = build_dist_index(packages)
tree = sorted_tree(construct_tree(dist_index))
branch_keys = set(r.key for r in flatten(tree.values()))
nodes = [p for p in tree.keys() if p.key not in branch_keys]
if pkg is not None:
nodes = [p for p in tree.keys() if p.key == pkg]
else:
nodes = [p for p in tree.keys() if p.key not in branch_keys]
key_tree = dict((k.key, v) for k, v in tree.items())
get_children = lambda n: key_tree.get(n.key, [])

def aux(node, parent=None, chain=None):
if chain is None:
chain = [node.project_name]

d = node.as_dict()
if parent:
d['required_version'] = node.version_spec if node.version_spec else 'Any'
else:
d['required_version'] = d['installed_version']

d['dependencies'] = [
aux(c, parent=node, chain=chain+[c.project_name])
for c in get_children(node)
if c.project_name not in chain
]
return [self._get_requirements_for_package(p, key_tree) for p in nodes]

return d
return [aux(p) for p in nodes]
@classmethod
def reverse_dependency(cls, node):
new_node = {
"package_name": node["package_name"],
"installed_version": node["installed_version"],
"required_version": node["required_version"]
}
for dependency in node.get("dependencies", []):
for dep in cls.reverse_dependency(dependency):
new_dep = dep.copy()
new_dep["parent"] = (node["package_name"], node["installed_version"])
yield new_dep
yield new_node

def reverse_dependencies(self):
from vistir.misc import unnest
rdeps = {}
for req in self.get_package_requirements():
for d in self.reverse_dependency(req):
name = d["package_name"]
pkg = {
name: {
"installed": d["installed_version"],
"required": d["required_version"]
}
}
if d.get("parent"):
pkg[name]["parents"] = list(d["parent"])
if rdeps.get(name):
if rdeps[name].get("parents"):
rdeps[name]["parents"].append(pkg[name]["parents"])
else:
rdeps[name].update(pkg[name])
else:
rdeps.update(pkg)
for k in list(rdeps.keys()):
entry = rdeps[k]
if entry.get("parents"):
rdeps[k]["parents"] = set([p for p in unnest(entry["parents"])])
jxltom marked this conversation as resolved.
Show resolved Hide resolved
return rdeps

def get_working_set(self):
"""Retrieve the working set of installed packages for the environment.
Expand Down
9 changes: 9 additions & 0 deletions pipenv/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,15 @@ def __init__(self, path, **kwargs):
super(PipenvException, self).__init__(message=fix_utf8(message))


class DependencyConflict(PipenvException):
def __init__(self, message):
extra = [fix_utf8("{0} {1}".format(
crayons.red("ERROR:", bold=True),
crayons.white("A dependency conflict was detected and could not be resolved.", bold=True),
)),]
super(DependencyConflict, self).__init__(fix_utf8(message), extra=extra)


class ResolutionFailure(PipenvException):
def __init__(self, message, no_version_found=False):
extra = (
Expand Down
Loading