Repository: django-extensions/django-extensions

Issue:
Pip 20.1 breaks pipchecker
When upgrading to pip==20.1, the following exception appears:
```
Traceback (most recent call last):
  File "./manage.py", line 22, in <module>
    execute_from_command_line(sys.argv)
  File "./lib/python3.8/site-packages/django/core/management/__init__.py", line 381, in execute_from_command_line
    utility.execute()
  File "./lib/python3.8/site-packages/django/core/management/__init__.py", line 375, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "./lib/python3.8/site-packages/django/core/management/base.py", line 323, in run_from_argv
    self.execute(*args, **cmd_options)
  File "./lib/python3.8/site-packages/django/core/management/base.py", line 364, in execute
    output = self.handle(*args, **options)
  File "./lib/python3.8/site-packages/django_extensions/management/utils.py", line 62, in inner
    ret = func(self, *args, **kwargs)
  File "./lib/python3.8/site-packages/django_extensions/management/commands/pipchecker.py", line 92, in handle
    name = req.name if req.name else req.link.filename
AttributeError: 'ParsedRequirement' object has no attribute 'name'
```

It's working fine when downgrading to pip 20.0.1.


django-extensions==2.2.9


Discussion:
I tried to be helpful here but it appears the internals of pip changed quite a bit, so I won't be able to help much, except by saying the commit that changes everything is:
https://github.com/pypa/pip/commit/90e4eb3eedc7c550c2dd036b7a535cdec50a93d5#diff-851cc85103a872db421c81afadd4fee5

They changed the signature of the function and it now returns a different class. (see L176)
I modified this code before. Let me solve this problem. :)

Resolving patch:
diff --git a/django_extensions/management/commands/pipchecker.py b/django_extensions/management/commands/pipchecker.py
--- a/django_extensions/management/commands/pipchecker.py
+++ b/django_extensions/management/commands/pipchecker.py
@@ -6,6 +6,10 @@
 
 import pip
 from django.core.management.base import BaseCommand, CommandError
+from pip._internal.req import InstallRequirement
+
+if LooseVersion(pip.__version__) >= LooseVersion('19.0'):
+    from pip._internal.req.constructors import install_req_from_line  # noqa
 
 try:
     try:
@@ -26,16 +30,10 @@
 from django_extensions.management.color import color_style
 from django_extensions.management.utils import signalcommand
 
-try:
-    from urllib.parse import urlparse
-    from urllib.error import HTTPError
-    from urllib.request import Request, urlopen
-    from xmlrpc.client import ServerProxy
-except ImportError:
-    # Python 2
-    from urlparse import urlparse  # type: ignore
-    from urllib2 import HTTPError, Request, urlopen  # type: ignore
-    from xmlrpclib import ServerProxy  # type: ignore
+from urllib.parse import urlparse
+from urllib.error import HTTPError
+from urllib.request import Request, urlopen
+from xmlrpc.client import ServerProxy
 
 try:
     import requests
@@ -89,7 +87,10 @@ def handle(self, *args, **options):
         with PipSession() as session:
             for filename in req_files:
                 for req in parse_requirements(filename, session=session):
+                    if not isinstance(req, InstallRequirement):
+                        req = install_req_from_line(req.requirement)
                     name = req.name if req.name else req.link.filename
+
                     # url attribute changed to link in pip version 6.1.0 and above
                     if LooseVersion(pip.__version__) > LooseVersion('6.0.8'):
                         self.reqs[name] = {


Test patch:
diff --git a/tests/management/commands/test_pipchecker.py b/tests/management/commands/test_pipchecker.py
--- a/tests/management/commands/test_pipchecker.py
+++ b/tests/management/commands/test_pipchecker.py
@@ -5,6 +5,7 @@
 import sys
 
 import pip
+import pkg_resources
 import pytest
 from django.core.management import call_command
 from django.test import TestCase
@@ -44,11 +45,7 @@ def test_pipchecker_with_outdated_requirement(self):
         f.close()
 
         subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', requirements_path])
-        if sys.version_info.major == 3:
-            pip._vendor.pkg_resources = importlib.reload(pip._vendor.pkg_resources)
-        else:
-            # Python 2.7
-            pip._vendor.pkg_resources = reload(pip._vendor.pkg_resources)  # noqa
+        pip._vendor.pkg_resources = importlib.reload(pip._vendor.pkg_resources)
         call_command('pipchecker', '-r', requirements_path, stdout=out)
 
         value = out.getvalue()
@@ -68,11 +65,7 @@ def test_pipchecker_with_up_to_date_requirement(self):
         f.close()
 
         subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', requirements_path])
-        if sys.version_info.major == 3:
-            pip._vendor.pkg_resources = importlib.reload(pip._vendor.pkg_resources)
-        else:
-            # Python 2.7
-            pip._vendor.pkg_resources = reload(pip._vendor.pkg_resources)  # noqa
+        pip._vendor.pkg_resources = importlib.reload(pip._vendor.pkg_resources)
         call_command('pipchecker', '-r', requirements_path, stdout=out)
 
         value = out.getvalue()
@@ -91,11 +84,7 @@ def test_pipchecker_with_github_url_requirement(self):
         f.close()
 
         subprocess.call([sys.executable, '-m', 'pip', 'install', 'django-json-widget'])
-        if sys.version_info.major == 3:
-            pip._vendor.pkg_resources = importlib.reload(pip._vendor.pkg_resources)
-        else:
-            # Python 2.7
-            pip._vendor.pkg_resources = reload(pip._vendor.pkg_resources)  # noqa
+        pip._vendor.pkg_resources = importlib.reload(pip._vendor.pkg_resources)
         call_command('pipchecker', '-r', requirements_path, stdout=out)
 
         value = out.getvalue()
@@ -104,3 +93,25 @@ def test_pipchecker_with_github_url_requirement(self):
         os.remove(requirements_path)
 
         self.assertTrue(value.endswith('repo is not frozen\n'))
+
+    def test_pipchecker_with_outdated_requirement_on_pip20_1(self):
+        subprocess.call([sys.executable, '-m', 'pip', 'install', '-U', 'pip==20.1'])
+        importlib.reload(pip)
+
+        requirements_path = './requirements.txt'
+        out = StringIO()
+
+        f = open(requirements_path, 'wt')
+        f.write('djangorestframework==3.0.0')
+        f.close()
+
+        subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', requirements_path])
+        importlib.reload(pkg_resources)
+        call_command('pipchecker', '-r', requirements_path, stdout=out)
+
+        value = out.getvalue()
+
+        subprocess.call([sys.executable, '-m', 'pip', 'uninstall', '--yes', '-r', requirements_path])
+        os.remove(requirements_path)
+
+        self.assertTrue(value.endswith('available\n'))