Skip to content

Commit e627d9c

Browse files
authored
Fix keyerror in keep_outdated when using VCS dependencies (#3768)
Fix keyerror in keep_outdated when using VCS dependencies
2 parents f8bace8 + 9f1fb72 commit e627d9c

File tree

6 files changed

+98
-27
lines changed

6 files changed

+98
-27
lines changed

news/3768.bugfix.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed a ``KeyError`` which could occur when pinning outdated VCS dependencies via ``pipenv lock --keep-outdated``.

pipenv/resolver.py

+2-9
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,8 @@ def get_cleaned_dict(self, keep_outdated=False):
237237
if entry_hashes != locked_hashes and not self.is_updated:
238238
self.entry_dict["hashes"] = list(entry_hashes | locked_hashes)
239239
self.entry_dict["name"] = self.name
240-
self.entry_dict["version"] = self.strip_version(self.entry_dict["version"])
240+
if "version" in self.entry_dict:
241+
self.entry_dict["version"] = self.strip_version(self.entry_dict["version"])
241242
_, self.entry_dict = self.get_markers_from_dict(self.entry_dict)
242243
return self.entry_dict
243244

@@ -779,14 +780,6 @@ def main():
779780
warnings.simplefilter("ignore", category=ResourceWarning)
780781
replace_with_text_stream("stdout")
781782
replace_with_text_stream("stderr")
782-
# from pipenv.vendor import colorama
783-
# if os.name == "nt" and (
784-
# all(getattr(stream, method, None) for stream in [sys.stdout, sys.stderr] for method in ["write", "isatty"]) and
785-
# all(stream.isatty() for stream in [sys.stdout, sys.stderr])
786-
# ):
787-
# colorama.init(wrap=False)
788-
# elif os.name != "nt":
789-
# colorama.init()
790783
os.environ["PIP_DISABLE_PIP_VERSION_CHECK"] = str("1")
791784
os.environ["PYTHONIOENCODING"] = str("utf-8")
792785
os.environ["PYTHONUNBUFFERED"] = str("1")

pipenv/utils.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -985,8 +985,8 @@ def resolve(cmd, sp):
985985
_out = decode_output("{0}\n".format(_out))
986986
out += _out
987987
sp.text = to_native_string("{0}".format(_out[:100]))
988-
# if environments.is_verbose():
989-
# sp.hide_and_write(_out.rstrip())
988+
if environments.is_verbose():
989+
sp.hide_and_write(_out.rstrip())
990990
_out = to_native_string("")
991991
if not result and not _out:
992992
break
@@ -2019,7 +2019,7 @@ def find_python(finder, line=None):
20192019
)
20202020
if line and os.path.isabs(line):
20212021
if os.name == "nt":
2022-
line = posixpath.join(*line.split(os.path.sep))
2022+
line = make_posix(line)
20232023
return line
20242024
if not finder:
20252025
from pipenv.vendor.pythonfinder import Finder

pipenv/vendor/requirementslib/models/requirements.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -814,22 +814,22 @@ def vcsrepo(self):
814814
@cached_property
815815
def metadata(self):
816816
# type: () -> Dict[Any, Any]
817-
if self.is_local and is_installable_dir(self.path):
817+
if self.is_local and self.path and is_installable_dir(self.path):
818818
return get_metadata(self.path)
819819
return {}
820820

821821
@cached_property
822822
def parsed_setup_cfg(self):
823823
# type: () -> Dict[Any, Any]
824-
if self.is_local and is_installable_dir(self.path):
824+
if self.is_local and self.path and is_installable_dir(self.path):
825825
if self.setup_cfg:
826826
return parse_setup_cfg(self.setup_cfg)
827827
return {}
828828

829829
@cached_property
830830
def parsed_setup_py(self):
831831
# type: () -> Dict[Any, Any]
832-
if self.is_local and is_installable_dir(self.path):
832+
if self.is_local and self.path and is_installable_dir(self.path):
833833
if self.setup_py:
834834
return ast_parse_setup_py(self.setup_py)
835835
return {}

pipenv/vendor/requirementslib/models/setup_info.py

+88-11
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,19 @@ def parse_special_directives(setup_entry, package_dir=None):
160160
sys.path.insert(0, package_dir)
161161
if "." in resource:
162162
resource, _, attribute = resource.rpartition(".")
163+
package, _, path = resource.partition(".")
164+
base_path = os.path.join(package_dir, package)
165+
if path:
166+
path = os.path.join(base_path, os.path.join(*path.split(".")))
167+
else:
168+
path = base_path
169+
if not os.path.exists(path) and os.path.exists("{0}.py".format(path)):
170+
path = "{0}.py".format(path)
171+
elif os.path.isdir(path):
172+
path = os.path.join(path, "__init__.py")
173+
rv = ast_parse_attribute_from_file(path, attribute)
174+
if rv:
175+
return str(rv)
163176
module = importlib.import_module(resource)
164177
rv = getattr(module, attribute)
165178
if not isinstance(rv, six.string_types):
@@ -203,10 +216,10 @@ def setuptools_parse_setup_cfg(path):
203216

204217
def get_package_dir_from_setupcfg(parser, base_dir=None):
205218
# type: (configparser.ConfigParser, STRING_TYPE) -> Text
206-
if not base_dir:
207-
package_dir = os.getcwd()
208-
else:
219+
if base_dir is not None:
209220
package_dir = base_dir
221+
else:
222+
package_dir = os.getcwd()
210223
if parser.has_option("options", "packages.find"):
211224
pkg_dir = parser.get("options", "packages.find")
212225
if isinstance(package_dir, Mapping):
@@ -217,6 +230,15 @@ def get_package_dir_from_setupcfg(parser, base_dir=None):
217230
_, pkg_dir = pkg_dir.split("find:")
218231
pkg_dir = pkg_dir.strip()
219232
package_dir = os.path.join(package_dir, pkg_dir)
233+
elif os.path.exists(os.path.join(package_dir, "setup.py")):
234+
setup_py = ast_parse_setup_py(os.path.join(package_dir, "setup.py"))
235+
if "package_dir" in setup_py:
236+
package_lookup = setup_py["package_dir"]
237+
if not isinstance(package_lookup, Mapping):
238+
return package_lookup
239+
return package_lookup.get(
240+
next(iter(list(package_lookup.keys()))), package_dir
241+
)
220242
return package_dir
221243

222244

@@ -638,7 +660,7 @@ def match_assignment_name(self, match):
638660

639661
def ast_unparse(item, initial_mapping=False, analyzer=None, recurse=True): # noqa:C901
640662
# type: (Any, bool, Optional[Analyzer], bool) -> Union[List[Any], Dict[Any, Any], Tuple[Any, ...], STRING_TYPE]
641-
unparse = partial(ast_unparse, initial_mapping=initial_mapping, analyzer=analyzer)
663+
unparse = partial(ast_unparse, initial_mapping=initial_mapping, analyzer=analyzer, recurse=recurse)
642664
if isinstance(item, ast.Dict):
643665
unparsed = dict(zip(unparse(item.keys), unparse(item.values)))
644666
elif isinstance(item, ast.List):
@@ -665,13 +687,35 @@ def ast_unparse(item, initial_mapping=False, analyzer=None, recurse=True): # no
665687
unparsed = item
666688
elif six.PY3 and isinstance(item, ast.NameConstant):
667689
unparsed = item.value
690+
elif isinstance(item, ast.Attribute):
691+
attr_name = getattr(item, "value", None)
692+
attr_attr = getattr(item, "attr", None)
693+
name = None
694+
if initial_mapping:
695+
unparsed = item
696+
elif attr_name and not recurse:
697+
name = attr_name
698+
else:
699+
name = unparse(attr_name) if attr_name is not None else attr_attr
700+
if name and attr_attr:
701+
if not initial_mapping and isinstance(name, six.string_types):
702+
unparsed = ".".join([item for item in (name, attr_attr) if item])
703+
else:
704+
unparsed = item
705+
elif attr_attr and not name and not initial_mapping:
706+
unparsed = attr_attr
707+
else:
708+
unparsed = name if not unparsed else unparsed
668709
elif isinstance(item, ast.Call):
669710
unparsed = {}
670711
if isinstance(item.func, ast.Name):
671-
name = unparse(item.func)
672-
unparsed[name] = {}
712+
func_name = unparse(item.func)
713+
elif isinstance(item.func, ast.Attribute):
714+
func_name = unparse(item.func)
715+
if func_name:
716+
unparsed[func_name] = {}
673717
for keyword in item.keywords:
674-
unparsed[name].update(unparse(keyword))
718+
unparsed[func_name].update(unparse(keyword))
675719
elif isinstance(item, ast.keyword):
676720
unparsed = {unparse(item.arg): unparse(item.value)}
677721
elif isinstance(item, ast.Assign):
@@ -681,7 +725,7 @@ def ast_unparse(item, initial_mapping=False, analyzer=None, recurse=True): # no
681725
# XXX: Original reference
682726
if not initial_mapping:
683727
target = unparse(next(iter(item.targets)), recurse=False)
684-
val = unparse(item.value)
728+
val = unparse(item.value, recurse=False)
685729
if isinstance(target, (tuple, set, list)):
686730
unparsed = dict(zip(target, val))
687731
else:
@@ -704,15 +748,48 @@ def ast_unparse(item, initial_mapping=False, analyzer=None, recurse=True): # no
704748
return unparsed
705749

706750

707-
def ast_parse_setup_py(path):
708-
# type: (S) -> Dict[Any, Any]
751+
def ast_parse_attribute_from_file(path, attribute):
752+
# type: (S) -> Any
753+
analyzer = ast_parse_file(path)
754+
target_value = None
755+
for k, v in analyzer.assignments.items():
756+
name = ""
757+
if isinstance(k, ast.Name):
758+
name = k.id
759+
elif isinstance(k, ast.Attribute):
760+
fn = ast_unparse(k)
761+
if isinstance(fn, six.string_types):
762+
_, _, name = fn.rpartition(".")
763+
if name == attribute:
764+
target_value = ast_unparse(v, analyzer=analyzer)
765+
break
766+
if isinstance(target_value, Mapping) and attribute in target_value:
767+
return target_value[attribute]
768+
return target_value
769+
770+
771+
def ast_parse_file(path):
772+
# type: (S) -> Analyzer
709773
with open(path, "r") as fh:
710774
tree = ast.parse(fh.read())
711775
ast_analyzer = Analyzer()
712776
ast_analyzer.visit(tree)
777+
return ast_analyzer
778+
779+
780+
def ast_parse_setup_py(path):
781+
# type: (S) -> Dict[Any, Any]
782+
ast_analyzer = ast_parse_file(path)
713783
setup = {} # type: Dict[Any, Any]
714784
for k, v in ast_analyzer.function_map.items():
715-
if isinstance(k, ast.Name) and k.id == "setup":
785+
fn_name = ""
786+
if isinstance(k, ast.Name):
787+
fn_name = k.id
788+
elif isinstance(k, ast.Attribute):
789+
fn = ast_unparse(k)
790+
if isinstance(fn, six.string_types):
791+
_, _, fn_name = fn.rpartition(".")
792+
if fn_name == "setup":
716793
setup = v
717794
cleaned_setup = ast_unparse(setup, analyzer=ast_analyzer)
718795
return cleaned_setup

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
required = [
2525
"pip>=18.0",
2626
"certifi",
27-
"setuptools>=41.0.0",
27+
"setuptools>=36.2.1",
2828
"virtualenv-clone>=0.2.5",
2929
"virtualenv",
3030
'enum34; python_version<"3"',

0 commit comments

Comments
 (0)