Repository: pypa/pip

Issue:
"pip show" incorrect result for "Required-by"
**Environment**

* pip version: 19.2.3
* Python version: 3.7.4
* OS: macOS Mojave 10.14.6

<!-- Feel free to add more information about your environment here -->

**Description**
<!-- A clear and concise description of what the bug is. -->
Running `pip show <package>` does not display all dependent packages under "Required-by:", even when `<package>` shows up in the "Requires:" field of a dependent package.

**Expected behavior**
Running `pip show <package>` should show all packages dependent on `<package>` under the "Required-by:" field of the output.

**How to Reproduce**
```shell
virtualenv pip_show_bug
source pip_show_bug/bin/activate
pip install black
pip show click
pip show black
```
`click` is clearly a dependency of `black`, but `black` does not show up under "Required-by:" when you run `pip show click`. All of `black`'s other dependencies (i.e. `appdirs`, `attrs`, and `toml`) behave as expected with `pip show`, and running `pip show black` *does* show `click` under "Required:"

**Output**
```shell
bash-5.0$ virtualenv pip_show_broken
Using base prefix '/usr/local/Cellar/python/3.7.4/Frameworks/Python.framework/Versions/3.7'
New python executable in /Users/kevin/pip_show_broken/bin/python3.7
Also creating executable in /Users/kevin/pip_show_broken/bin/python
Installing setuptools, pip, wheel...
done.
bash-5.0$ source pip_show_broken/bin/activate
(pip_show_broken) bash-5.0$ pip install black
Collecting black
  Using cached https://files.pythonhosted.org/packages/30/62/cf549544a5fe990bbaeca21e9c419501b2de7a701ab0afb377bc81676600/black-19.3b0-py36-none-any.whl
Collecting appdirs (from black)
  Using cached https://files.pythonhosted.org/packages/56/eb/810e700ed1349edde4cbdc1b2a21e28cdf115f9faf263f6bbf8447c1abf3/appdirs-1.4.3-py2.py3-none-any.whl
Collecting attrs>=18.1.0 (from black)
  Using cached https://files.pythonhosted.org/packages/23/96/d828354fa2dbdf216eaa7b7de0db692f12c234f7ef888cc14980ef40d1d2/attrs-19.1.0-py2.py3-none-any.whl
Collecting toml>=0.9.4 (from black)
  Using cached https://files.pythonhosted.org/packages/a2/12/ced7105d2de62fa7c8fb5fce92cc4ce66b57c95fb875e9318dba7f8c5db0/toml-0.10.0-py2.py3-none-any.whl
Collecting click>=6.5 (from black)
  Using cached https://files.pythonhosted.org/packages/fa/37/45185cb5abbc30d7257104c434fe0b07e5a195a6847506c074527aa599ec/Click-7.0-py2.py3-none-any.whl
Installing collected packages: appdirs, attrs, toml, click, black
Successfully installed appdirs-1.4.3 attrs-19.1.0 black-19.3b0 click-7.0 toml-0.10.0
(pip_show_broken) bash-5.0$ pip show click
Name: Click
Version: 7.0
Summary: Composable command line interface toolkit
Home-page: https://palletsprojects.com/p/click/
Author: Armin Ronacher
Author-email: armin.ronacher@active-4.com
License: BSD
Location: /Users/kevin/pip_show_broken/lib/python3.7/site-packages
Requires: 
Required-by: 
(pip_show_broken) bash-5.0$ pip show black
Name: black
Version: 19.3b0
Summary: The uncompromising code formatter.
Home-page: https://github.com/ambv/black
Author: Łukasz Langa
Author-email: lukasz@langa.pl
License: MIT
Location: /Users/kevin/pip_show_broken/lib/python3.7/site-packages
Requires: appdirs, click, toml, attrs
Required-by: 
```



Discussion:
Thanks for the report, I can reproduce the issue.

The fix should be quite simple:
https://github.com/pypa/pip/blob/3b2d1384001c16c4946ec133dc3da0973543a9fc/src/pip/_internal/commands/show.py#L140-L144

We are comparing the project names without normalisation and `Click` would require it since it uses a capital C.

```
>>> [pkg for pkg in pkg_resources.working_set if pkg.project_name == 'Click'][0].project_name
'Click'
>>> [pkg for pkg in pkg_resources.working_set if pkg.project_name == 'black'][0].requires()
[Requirement.parse('click>=6.5'), Requirement.parse('attrs>=18.1.0'), Requirement.parse('appdirs'), Requirement.parse('toml>=0.9.4')]
```

For the contributors interested in fixing it, `canonicalize_name` would be the function to use.

Resolving patch:
diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py
--- a/src/pip/_internal/commands/show.py
+++ b/src/pip/_internal/commands/show.py
@@ -67,12 +67,22 @@ def search_packages_info(query):
     if missing:
         logger.warning('Package(s) not found: %s', ', '.join(missing))
 
+    def get_requiring_packages(package_name):
+        canonical_name = canonicalize_name(package_name)
+        return [
+            pkg.project_name for pkg in pkg_resources.working_set
+            if canonical_name in
+               [canonicalize_name(required.name) for required in
+                pkg.requires()]
+        ]
+
     for dist in [installed[pkg] for pkg in query_names if pkg in installed]:
         package = {
             'name': dist.project_name,
             'version': dist.version,
             'location': dist.location,
             'requires': [dep.project_name for dep in dist.requires()],
+            'required_by': get_requiring_packages(dist.project_name)
         }
         file_list = None
         metadata = None
@@ -137,13 +147,7 @@ def print_results(distributions, list_files=False, verbose=False):
         if i > 0:
             write_output("---")
 
-        name = dist.get('name', '')
-        required_by = [
-            pkg.project_name for pkg in pkg_resources.working_set
-            if name in [required.name for required in pkg.requires()]
-        ]
-
-        write_output("Name: %s", name)
+        write_output("Name: %s", dist.get('name', ''))
         write_output("Version: %s", dist.get('version', ''))
         write_output("Summary: %s", dist.get('summary', ''))
         write_output("Home-page: %s", dist.get('home-page', ''))
@@ -152,7 +156,7 @@ def print_results(distributions, list_files=False, verbose=False):
         write_output("License: %s", dist.get('license', ''))
         write_output("Location: %s", dist.get('location', ''))
         write_output("Requires: %s", ', '.join(dist.get('requires', [])))
-        write_output("Required-by: %s", ', '.join(required_by))
+        write_output("Required-by: %s", ', '.join(dist.get('required_by', [])))
 
         if verbose:
             write_output("Metadata-Version: %s",


Test patch:
diff --git a/tests/data/src/requires_capitalized/setup.py b/tests/data/src/requires_capitalized/setup.py
new file mode 100644
--- /dev/null
+++ b/tests/data/src/requires_capitalized/setup.py
@@ -0,0 +1,6 @@
+from setuptools import setup
+
+setup(name='Requires_Capitalized',
+      version='0.1',
+      install_requires=['simple==1.0']
+      )
diff --git a/tests/data/src/requires_requires_capitalized/setup.py b/tests/data/src/requires_requires_capitalized/setup.py
new file mode 100644
--- /dev/null
+++ b/tests/data/src/requires_requires_capitalized/setup.py
@@ -0,0 +1,6 @@
+from setuptools import setup
+
+setup(name='requires_requires_capitalized',
+      version='1.0',
+      install_requires=['requires_Capitalized==0.1']
+      )
diff --git a/tests/functional/test_show.py b/tests/functional/test_show.py
--- a/tests/functional/test_show.py
+++ b/tests/functional/test_show.py
@@ -205,7 +205,7 @@ def test_package_name_is_canonicalized(script, data):
     assert underscore_upper_show_result.stdout == dash_show_result.stdout
 
 
-def test_show_required_by_packages(script, data):
+def test_show_required_by_packages_basic(script, data):
     """
     Test that installed packages that depend on this package are shown
     """
@@ -219,3 +219,43 @@ def test_show_required_by_packages(script, data):
 
     assert 'Name: simple' in lines
     assert 'Required-by: requires-simple' in lines
+
+
+def test_show_required_by_packages_capitalized(script, data):
+    """
+    Test that the installed packages which depend on a package are shown
+    where the package has a capital letter
+    """
+    editable_path = os.path.join(data.src, 'requires_capitalized')
+    script.pip(
+        'install', '--no-index', '-f', data.find_links, editable_path
+    )
+
+    result = script.pip('show', 'simple')
+    lines = result.stdout.splitlines()
+
+    assert 'Name: simple' in lines
+    assert 'Required-by: Requires-Capitalized' in lines
+
+
+def test_show_required_by_packages_requiring_capitalized(script, data):
+    """
+    Test that the installed packages which depend on a package are shown
+    where the package has a name with a mix of
+    lower and upper case letters
+    """
+    required_package_path = os.path.join(data.src, 'requires_capitalized')
+    script.pip(
+        'install', '--no-index', '-f', data.find_links, required_package_path
+    )
+    editable_path = os.path.join(data.src, 'requires_requires_capitalized')
+    script.pip(
+        'install', '--no-index', '-f', data.find_links, editable_path
+    )
+
+    result = script.pip('show', 'Requires_Capitalized')
+    lines = result.stdout.splitlines()
+    print(lines)
+
+    assert 'Name: Requires-Capitalized' in lines
+    assert 'Required-by: requires-requires-capitalized' in lines