From 4516501d3d761b1206290a02945e6458254a7f6e Mon Sep 17 00:00:00 2001
From: Marek Chrastina <marek.chrastina@vsb.cz>
Date: Thu, 29 Aug 2019 10:47:46 +0200
Subject: [PATCH] Case vanished module; all deps are taken from metadata;
 hotfix extras

---
 pipdeps/pipdeps.py | 157 ++++++++++++++++++++++++++++++++++-----------
 1 file changed, 118 insertions(+), 39 deletions(-)

diff --git a/pipdeps/pipdeps.py b/pipdeps/pipdeps.py
index 073a216..d5571de 100644
--- a/pipdeps/pipdeps.py
+++ b/pipdeps/pipdeps.py
@@ -13,6 +13,7 @@ import urllib2
 import tarfile
 import tempfile
 import zipfile
+import wheel.metadata
 
 import tabulate
 import packaging.specifiers
@@ -143,7 +144,11 @@ def find_available_vers(package_name, pyver):
     Return descending list of available strict version
     """
     versions = []
-    data = get_json("https://pypi.python.org/pypi/%s/json" % (package_name,))
+    try:
+        data = get_json("https://pypi.python.org/pypi/%s/json" % (package_name,))
+    except urllib2.HTTPError, err:
+        print "%s %s" % (err, err.url)
+        raise urllib2.HTTPError(err.url, err.code, None, err.hdrs, err.fp)
     releases = data["releases"].keys()
     for release in releases:
         requires_python = []
@@ -158,6 +163,9 @@ def get_newer_vers(available_version, required_version, installed_version=None):
     """
     Return list of newer versions which conforms pipdeptree dependencies, otherwise return none.
     """
+    if required_version is None:
+        result = [aver for aver in list(available_version)]
+        return sorted(result, key=distutils.version.StrictVersion, reverse=True)
     if [rver for rver in required_version if re.search(r'(^==.*|^\d.*)', rver) is not None]:
         return None
     result = []
@@ -182,60 +190,128 @@ def get_newer_vers(available_version, required_version, installed_version=None):
         return sorted(result, key=distutils.version.StrictVersion, reverse=True)
     return None
 
-def parse_requires_txt(package, version):
+def write_metadata(tmp_file):
+    """
+    Write package metadata
+    """
+    try:
+        tar_file = tarfile.open(tmp_file.name, 'r')
+        for member in tar_file.getmembers():
+            if 'requires.txt' in member.name:
+                with open('/tmp/requires.txt', 'w') as tmpf:
+                    tmpf.write(tar_file.extractfile(member).read())
+            if 'PKG-INFO' in member.name:
+                with open('/tmp/PKG-INFO', 'w') as tmpf:
+                    tmpf.write(tar_file.extractfile(member).read())
+    except tarfile.ReadError:
+        zip_file = zipfile.ZipFile(tmp_file.name, 'r')
+        for member in zip_file.namelist():
+            if 'requires.txt' in member:
+                with open('/tmp/requires.txt', 'w') as tmpf:
+                    tmpf.write(zip_file.read(member))
+            if 'PKG-INFO' in member:
+                with open('/tmp/PKG-INFO', 'w') as tmpf:
+                    tmpf.write(zip_file.read(member))
+
+def get_metadata(package, version):
     """
-    Return content of requires.txt until first [ appears
+    Return package metadata
     """
-    content = None
-    release_data = get_json("https://pypi.python.org/pypi/%s/%s/json" % (package, version,))
-    for item in release_data['releases'][version]:
+    metadata = None
+    for item in get_json("https://pypi.python.org/pypi/%s/%s/json" % (package, version,)) \
+        ['releases'][version]:
         if item['packagetype'] == 'sdist':
             tmp_file = file_download(item['url'])
+            write_metadata(tmp_file)
+            if os.path.isfile('/tmp/requires.txt') and os.path.isfile('/tmp/PKG-INFO'):
+                metadata = [
+                    line.decode('utf-8') \
+                    for line in wheel.metadata.pkginfo_to_metadata('/tmp', '/tmp/PKG-INFO') \
+                    .as_string().splitlines()]
+            try:
+                os.unlink('/tmp/requires.txt')
+            except OSError:
+                pass
             try:
-                tar_file = tarfile.open(tmp_file.name, 'r')
-                for member in tar_file.getmembers():
-                    if 'requires.txt' in member.name:
-                        content = tar_file.extractfile(member)
-            except tarfile.ReadError:
-                zip_file = zipfile.ZipFile(tmp_file.name, 'r')
-                for member in zip_file.namelist():
-                    if 'requires.txt' in member:
-                        content = zip_file.read(member)
-    if content is not None:
-        par = []
-        for line in content:
-            if '[' in line:
+                os.unlink('/tmp/PKG-INFO')
+            except OSError:
+                pass
+            os.unlink(tmp_file.name)
+            if metadata:
                 break
-            else:
-                par.append(line.strip())
-        content = "\n".join(par)
-    os.unlink(tmp_file.name)
-    return content
+        elif item['packagetype'] == 'bdist_wheel':
+            tmp_file = file_download(item['url'])
+            zip_file = zipfile.ZipFile(tmp_file.name, 'r')
+            for member in zip_file.namelist():
+                if 'METADATA' in member:
+                    metadata = [line.decode('utf-8') for line in zip_file.read(member).splitlines()]
+            os.unlink(tmp_file.name)
+            break
+    return metadata
+
+def parse_metadata(metadata, pyver):
+    """
+    Return dependencies parsed from metadata
+    """
+    for line in metadata:
+        if 'Metadata-Version' in line.decode('utf-8'):
+            metadata_version = line.replace('Metadata-Version:', '').strip()
+            break
+    if packaging.version.Version(metadata_version) >= packaging.version.Version('2.0'):
+        out = []
+        for dep in [
+                line.replace('Requires-Dist:', '').strip() \
+                for line in metadata if re.search(r'^Requires-Dist:', line)]:
+            if ';' in dep:
+                dep = dep.split(';')
+                if 'python_version' in dep[1]:
+                    if packaging.specifiers.SpecifierSet(
+                            dep[1].replace('python_version', '').replace('"', '').strip()) \
+                        .contains(packaging.version.Version(pyver)):
+                        dep = dep[0]
+                    else:
+                        continue
+                else:
+                    continue
+            dep = dep.split()
+            try:
+                pkg = re.search(r'(.*)(\[.*\])', dep[0]).group(1)
+            except AttributeError:
+                pkg = dep[0]
+            try:
+                pkg = re.search(r'(^[\w\.\-]*)(.*)', dep[0]).group(1)
+                dep.append(re.search(r'(^[\w\.\-]*)(.*)', dep[0]).group(2))
+            except AttributeError:
+                pkg = dep[0]
+            try:
+                ver = dep[1].replace('(', '').replace(')', '').replace(';', '')
+            except IndexError:
+                ver = None
+            out.append((pkg, ver))
+    return out
 
 def find_new_dependencies(package, version, package_list, pyver):
     """
     Return package dependencies parsed from pypi json
     """
-    content = parse_requires_txt(package, version)
-    if content is not None:
-        for line in content.split("\n"):
-            try:
-                pkg = re.search(r'^([a-zA-Z0-9_.-]+)', line).group(0)
-                dep = line.replace(pkg, '').strip()
-                if not dep:
-                    dep = None
-                if pkg in package_list:
-                    yield (pkg, dep)
-                else:
+    content = parse_metadata(get_metadata(package, version), pyver)
+    for pkg, ver in content:
+        try:
+            if pkg in package_list:
+                yield (pkg, ver)
+            else:
+                try:
                     for child in find_new_dependencies(
                             pkg,
-                            get_newer_vers(find_available_vers(pkg, pyver), dep, None)[0],
+                            get_newer_vers(find_available_vers(pkg, pyver), ver, None)[0],
                             package_list,
                             pyver
                         ):
                         yield child
-            except AttributeError:
-                pass
+                except TypeError:
+                    pass
+        except AttributeError:
+            pass
 
 def depless_vers(res):
     """
@@ -269,7 +345,10 @@ def collect_packages(package_list, jsonpipdeptree, pyver=None):
             )):
             if 'Any' not in dep:
                 required_version.append(dep)
-        available_version = find_available_vers(package, pyver)
+        try:
+            available_version = find_available_vers(package, pyver)
+        except urllib2.HTTPError:
+            available_version = [installed_version]
         newer_version = get_newer_vers(available_version, required_version, installed_version)
         rev = {'installed_version': installed_version,
                'required_version': required_version,
-- 
GitLab