Repository: pypa/setuptools

Issue:
Inconsistent handling of trailing slashes in pkg_resources.ZipProvider.resource_listdir()
Given a ZipProvider, if the underlying ZipImporter `prefix` is empty, then `resource_listdir('')` and `resource_listdir('subdir/')` fail, while `resource_listdir('/')` and `resource_listdir('subdir')` succeed.

On the other hand, if the underlying ZipImporter `prefix` is not empty, then `resource_listdir('/')` fails but `resource_listdir('')` succeeds.

Testcase demonstrating bug:
https://github.com/duggelz/setuptools/commit/0d37f59682905bd69a2674c045017a67cdf9822d

Proposed fix:
https://github.com/duggelz/setuptools/commit/ccd83dbd3bae9e35fe8dbdbdddfdf0ac46965b62

If this seems reasonable, let me know and I will open a pull request.  I have tried to match the style, structure, and naming, let me know if you would like anything modified.

Note that resource_names with '.' or '..', e.g. 'subdir/.' or 'subdir/../mod.py' are not handled by either the original code or my proposed fix, as this seems beyond the desired scope of the API.


Discussion:

Resolving patch:
diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py
--- a/pkg_resources/__init__.py
+++ b/pkg_resources/__init__.py
@@ -1693,6 +1693,9 @@ def __init__(self, module):
     def _zipinfo_name(self, fspath):
         # Convert a virtual filename (full path to file) into a zipfile subpath
         # usable with the zipimport directory cache for our target archive
+        fspath = fspath.rstrip(os.sep)
+        if fspath == self.loader.archive:
+            return ''
         if fspath.startswith(self.zip_pre):
             return fspath[len(self.zip_pre):]
         raise AssertionError(
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -89,7 +89,7 @@ def pypi_link(pkg_filename):
 
 setup_params = dict(
     name="setuptools",
-    version="38.2.4",
+    version="38.2.5",
     description="Easily download, build, install, upgrade, and uninstall "
         "Python packages",
     author="Python Packaging Authority",


Test patch:
diff --git a/pkg_resources/tests/test_pkg_resources.py b/pkg_resources/tests/test_pkg_resources.py
--- a/pkg_resources/tests/test_pkg_resources.py
+++ b/pkg_resources/tests/test_pkg_resources.py
@@ -62,10 +62,21 @@ def setup_class(cls):
         zip_info.filename = 'data.dat'
         zip_info.date_time = cls.ref_time.timetuple()
         zip_egg.writestr(zip_info, 'hello, world!')
+        zip_info = zipfile.ZipInfo()
+        zip_info.filename = 'subdir/mod2.py'
+        zip_info.date_time = cls.ref_time.timetuple()
+        zip_egg.writestr(zip_info, 'x = 6\n')
+        zip_info = zipfile.ZipInfo()
+        zip_info.filename = 'subdir/data2.dat'
+        zip_info.date_time = cls.ref_time.timetuple()
+        zip_egg.writestr(zip_info, 'goodbye, world!')
         zip_egg.close()
         egg.close()
 
         sys.path.append(egg.name)
+        subdir = os.path.join(egg.name, 'subdir')
+        sys.path.append(subdir)
+        cls.finalizers.append(EggRemover(subdir))
         cls.finalizers.append(EggRemover(egg.name))
 
     @classmethod
@@ -73,6 +84,30 @@ def teardown_class(cls):
         for finalizer in cls.finalizers:
             finalizer()
 
+    def test_resource_listdir(self):
+        import mod
+        zp = pkg_resources.ZipProvider(mod)
+
+        expected_root = ['data.dat', 'mod.py', 'subdir']
+        assert sorted(zp.resource_listdir('')) == expected_root
+        assert sorted(zp.resource_listdir('/')) == expected_root
+
+        expected_subdir = ['data2.dat', 'mod2.py']
+        assert sorted(zp.resource_listdir('subdir')) == expected_subdir
+        assert sorted(zp.resource_listdir('subdir/')) == expected_subdir
+
+        assert zp.resource_listdir('nonexistent') == []
+        assert zp.resource_listdir('nonexistent/') == []
+
+        import mod2
+        zp2 = pkg_resources.ZipProvider(mod2)
+
+        assert sorted(zp2.resource_listdir('')) == expected_subdir
+        assert sorted(zp2.resource_listdir('/')) == expected_subdir
+
+        assert zp2.resource_listdir('subdir') == []
+        assert zp2.resource_listdir('subdir/') == []
+
     def test_resource_filename_rewrites_on_change(self):
         """
         If a previous call to get_resource_filename has saved the file, but