From fed3a41fa037ccf0f601b89a899ce6f22ff266af Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Fri, 10 Mar 2023 14:26:46 +0100 Subject: [PATCH 01/20] tests/fixtures: Fix FilesDef type to include bytes values The build_files() helper which handles these FilesDef nested dicts already has code to handle values of type 'bytes' (they get written verbatim instead of first being processed by DALS()). Changing the FilesDef type to reflect this prevents Mypy from failing when encountering legitimate bytes values in these dicts. --- tests/fixtures.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 6d9a9d2b..080add19 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -83,8 +83,10 @@ def setUp(self): # Except for python/mypy#731, prefer to define -# FilesDef = Dict[str, Union['FilesDef', str]] -FilesDef = Dict[str, Union[Dict[str, Union[Dict[str, str], str]], str]] +# FilesDef = Dict[str, Union['FilesDef', str, bytes]] +FilesDef = Dict[ + str, Union[Dict[str, Union[Dict[str, Union[str, bytes]], str, bytes]], str, bytes] +] class DistInfoPkg(OnSysPath, SiteDir): From 578322a37d6b26acf8cede13e327dce105f3577d Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Fri, 10 Mar 2023 14:45:19 +0100 Subject: [PATCH 02/20] Add tests for egg-info package with no installed modules This corresponds to the qiskit[1] meta-package which: - does not contain any (runtime) Python code itself, but serves as a mechanism to install its transitive dependencies (which populate the qiskit package namespace). - is distributed as a source archive. - includes a top_level.txt which is empty (contains a single newline), arguably correct given that it does not directly install any importable packages/modules. - when installed as an egg, provides a SOURCES.txt which is incorrect from a runtime POV: it references 3 .py files, a setup.py and two files under test/, none of which are actually installed. - when installed (as an egg) by pip, provides an installed-files.txt file which is _more_ accurate than SOURCES.txt, since it reflects the files that are actually available after installation. importlib_metadata reports incorrect .files for this package, because we end up using SOURCES.txt. It is better to use installed-files.txt when it is available. Furthermore, as a result of this, packages_distributions() also incorrectly reports that this packages provides imports names that do not actually exist ("setup" and "test", in qiskit's case). This commit adds EggInfoPkgPipInstalledNoModules, a test project that mimics the egg installation of qiskit, and adds it to existing test cases, as well as adding a new test cases specifically for verifying packages_distributions() with egg-info packages. The following tests fail in this commit, but will be fixed in the next commit: - PackagesDistributionsTest.test_packages_distributions_on_eggs - APITests.test_files_egg_info See the python/importlib_metadata#115 issue for more details. [1]: qiskit is found at https://pypi.org/project/qiskit/0.41.1/#files --- tests/fixtures.py | 30 ++++++++++++++++++++++++++++++ tests/test_api.py | 30 ++++++++++++++++++++++-------- tests/test_main.py | 35 +++++++++++++++++++++++++++++++++-- 3 files changed, 85 insertions(+), 10 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 080add19..aa6ffac9 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -213,6 +213,36 @@ def setUp(self): build_files(EggInfoPkg.files, prefix=self.site_dir) +class EggInfoPkgPipInstalledNoModules(OnSysPath, SiteDir): + files: FilesDef = { + "empty_egg_pkg.egg-info": { + "PKG-INFO": "Name: empty_egg-pkg", + # SOURCES.txt is made from the source archive, and contains files + # (setup.py) that are not present after installation. + "SOURCES.txt": """ + setup.py + empty_egg_pkg.egg-info/PKG-INFO + empty_egg_pkg.egg-info/SOURCES.txt + empty_egg_pkg.egg-info/top_level.txt + """, + # installed-files.txt is written by pip, and is a strictly more + # accurate source than SOURCES.txt as to the installed contents of + # the package. + "installed-files.txt": """ + PKG-INFO + SOURCES.txt + top_level.txt + """, + # top_level.txt correctly reflects that no modules are installed + "top_level.txt": b"\n", + }, + } + + def setUp(self): + super().setUp() + build_files(EggInfoPkgPipInstalledNoModules.files, prefix=self.site_dir) + + class EggInfoFile(OnSysPath, SiteDir): files: FilesDef = { "egginfo_file.egg-info": """ diff --git a/tests/test_api.py b/tests/test_api.py index 504d0553..3bf4a41f 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -27,12 +27,12 @@ def suppress_known_deprecation(): class APITests( fixtures.EggInfoPkg, + fixtures.EggInfoPkgPipInstalledNoModules, fixtures.DistInfoPkg, fixtures.DistInfoPkgWithDot, fixtures.EggInfoFile, unittest.TestCase, ): - version_pattern = r'\d+\.\d+(\.\d)?' def test_retrieves_version_of_self(self): @@ -63,15 +63,28 @@ def test_prefix_not_matched(self): distribution(prefix) def test_for_top_level(self): - self.assertEqual( - distribution('egginfo-pkg').read_text('top_level.txt').strip(), 'mod' - ) + tests = [ + ('egginfo-pkg', 'mod'), + ('empty_egg-pkg', ''), + ] + for pkg_name, expect_content in tests: + with self.subTest(pkg_name): + self.assertEqual( + distribution(pkg_name).read_text('top_level.txt').strip(), + expect_content, + ) def test_read_text(self): - top_level = [ - path for path in files('egginfo-pkg') if path.name == 'top_level.txt' - ][0] - self.assertEqual(top_level.read_text(), 'mod\n') + tests = [ + ('egginfo-pkg', 'mod\n'), + ('empty_egg-pkg', '\n'), + ] + for pkg_name, expect_content in tests: + with self.subTest(pkg_name): + top_level = [ + path for path in files(pkg_name) if path.name == 'top_level.txt' + ][0] + self.assertEqual(top_level.read_text(), expect_content) def test_entry_points(self): eps = entry_points() @@ -171,6 +184,7 @@ def test_files_dist_info(self): def test_files_egg_info(self): self._test_files(files('egginfo-pkg')) + self._test_files(files('empty_egg-pkg')) def test_version_egg_info_file(self): self.assertEqual(version('egginfo-file'), '0.1') diff --git a/tests/test_main.py b/tests/test_main.py index f0f84983..4d28fa26 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -170,11 +170,17 @@ def test_metadata_loads_egg_info(self): assert meta['Description'] == 'pôrˈtend' -class DiscoveryTests(fixtures.EggInfoPkg, fixtures.DistInfoPkg, unittest.TestCase): +class DiscoveryTests( + fixtures.EggInfoPkg, + fixtures.EggInfoPkgPipInstalledNoModules, + fixtures.DistInfoPkg, + unittest.TestCase, +): def test_package_discovery(self): dists = list(distributions()) assert all(isinstance(dist, Distribution) for dist in dists) assert any(dist.metadata['Name'] == 'egginfo-pkg' for dist in dists) + assert any(dist.metadata['Name'] == 'empty_egg-pkg' for dist in dists) assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists) def test_invalid_usage(self): @@ -304,7 +310,11 @@ def test_packages_distributions_example2(self): class PackagesDistributionsTest( - fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase + fixtures.EggInfoPkg, + fixtures.EggInfoPkgPipInstalledNoModules, + fixtures.OnSysPath, + fixtures.SiteDir, + unittest.TestCase, ): def test_packages_distributions_neither_toplevel_nor_files(self): """ @@ -322,3 +332,24 @@ def test_packages_distributions_neither_toplevel_nor_files(self): prefix=self.site_dir, ) packages_distributions() + + def test_packages_distributions_on_eggs(self): + """ + Test old-style egg packages with a variation of 'top_level.txt', + 'SOURCES.txt', and 'installed-files.txt', available. + """ + distributions = packages_distributions() + + def import_names_from_package(package_name): + return { + import_name + for import_name, package_names in distributions.items() + if package_name in package_names + } + + # egginfo-pkg declares one import ('mod') via top_level.txt + assert import_names_from_package('egginfo-pkg') == {'mod'} + + # empty_egg-pkg should not be associated with any import names + # (top_level.txt is empty, and installed-files.txt has no .py files) + assert import_names_from_package('empty_egg-pkg') == set() From 61b0f297960d678d260f31319e7d53584d901e36 Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Fri, 10 Mar 2023 14:45:19 +0100 Subject: [PATCH 03/20] Distribution.files: Prefer *.egg-info/installed-files.txt to SOURCES.txt When listing the files in a *.egg-info distribution, prefer using *.egg-info/installed-files.txt instead of *.egg-info/SOURCES.txt. installed-files.txt is written by pip[1] when installing a package, whereas the SOURCES.txt is written by setuptools when creating a source archive[2]. installed-files.txt is only present when the package has been installed by pip, so we cannot depend on it always being available. However, when it _is_ available, it is an accurate record of what files are installed. SOURCES.txt, on the other hand, is always avaiable, but is not always accurate: Since it is generated from the source archive, it will often include files (like 'setup.py') that are no longer available after the package has been installed. Fixes #115 for the cases where a installed-files.txt file is available. [1]: https://pip.pypa.io/en/stable/news/#v0-3 [2]: https://setuptools.pypa.io/en/latest/deprecated/python_eggs.html#sources-txt-source-files-manifest --- importlib_metadata/__init__.py | 37 ++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 9a36a8e6..3b0d8247 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -460,8 +460,8 @@ def files(self): :return: List of PackagePath for this distribution or None Result is `None` if the metadata file that enumerates files - (i.e. RECORD for dist-info or SOURCES.txt for egg-info) is - missing. + (i.e. RECORD for dist-info, or installed-files.txt or + SOURCES.txt for egg-info) is missing. Result may be empty if the metadata exists but is empty. """ @@ -476,7 +476,11 @@ def make_file(name, hash=None, size_str=None): def make_files(lines): return list(starmap(make_file, csv.reader(lines))) - return make_files(self._read_files_distinfo() or self._read_files_egginfo()) + return make_files( + self._read_files_distinfo() + or self._read_files_egginfo_installed() + or self._read_files_egginfo_sources() + ) def _read_files_distinfo(self): """ @@ -485,10 +489,35 @@ def _read_files_distinfo(self): text = self.read_text('RECORD') return text and text.splitlines() - def _read_files_egginfo(self): + def _read_files_egginfo_installed(self): + """ + installed-files.txt might contain literal commas, so wrap + each line in quotes. Also, the entries in installed-files.txt + are relative to the .egg-info/ subdir (not relative to the + parent site-packages directory that make_file() expects). + + This file is written when the package is installed by pip, + but it might not be written for other installation methods. + Hence, even if we can assume that this file is accurate + when it exists, we cannot assume that it always exists. + """ + text = self.read_text('installed-files.txt') + # We need to prepend the .egg-info/ subdir to the lines in this file. + # But this subdir is only available in the PathDistribution's self._path + # which is not easily accessible from this base class... + subdir = getattr(self, '_path', None) + return text and subdir and [f'"{subdir}/{line}"' for line in text.splitlines()] + + def _read_files_egginfo_sources(self): """ SOURCES.txt might contain literal commas, so wrap each line in quotes. + + Note that SOURCES.txt is not a reliable source for what + files are installed by a package. This file is generated + for a source archive, and the files that are present + there (e.g. setup.py) may not correctly reflect the files + that are present after the package has been installed. """ text = self.read_text('SOURCES.txt') return text and map('"{}"'.format, text.splitlines()) From 8026db2b63274af61d32007c3d7e8fc8b57bdd26 Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Fri, 10 Mar 2023 14:45:19 +0100 Subject: [PATCH 04/20] Add tests for egg-info package with .files from inaccurate SOURCES.txt As established in previous commits, the SOURCES.txt file is not always an accurate source of files that are present after a package has been installed. One situation where this inaccuracy is problematic is when top_level.txt is also missing, and packages_distributions() is forced to infer the provided import names based on Distribution.files. In this situation we end up with incorrect mappings between import packages and distribution packages, including import packages that clearly do not exist at all. For example, a SOURCES.txt that lists setup.py (which is used _when_ installing, but is not available after installation), will see that setup.py returned from .files, which then will cause packages_distributions() to claim a mapping from the non-existent 'setup' import name to this distribution. This commit adds EggInfoPkgSourcesFallback which demostrates such a scenario, and adds this new class to a couple of relevant tests. A couple of these tests are currently failing, to demonstrate the issue at hand. These test failures will be fixed in the next commit. See the python/importlib_metadata#115 issue for more details. --- tests/fixtures.py | 26 ++++++++++++++++++++++++++ tests/test_api.py | 2 ++ tests/test_main.py | 7 +++++++ 3 files changed, 35 insertions(+) diff --git a/tests/fixtures.py b/tests/fixtures.py index aa6ffac9..bbd9854b 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -243,6 +243,32 @@ def setUp(self): build_files(EggInfoPkgPipInstalledNoModules.files, prefix=self.site_dir) +class EggInfoPkgSourcesFallback(OnSysPath, SiteDir): + files: FilesDef = { + "starved_egg_pkg.egg-info": { + "PKG-INFO": "Name: starved_egg-pkg", + # SOURCES.txt is made from the source archive, and contains files + # (setup.py) that are not present after installation. + "SOURCES.txt": """ + starved_egg_pkg.py + setup.py + starved_egg_pkg.egg-info/PKG-INFO + starved_egg_pkg.egg-info/SOURCES.txt + """, + # missing installed-files.txt (i.e. not installed by pip) + # missing top_level.txt + }, + "starved_egg_pkg.py": """ + def main(): + print("hello world") + """, + } + + def setUp(self): + super().setUp() + build_files(EggInfoPkgSourcesFallback.files, prefix=self.site_dir) + + class EggInfoFile(OnSysPath, SiteDir): files: FilesDef = { "egginfo_file.egg-info": """ diff --git a/tests/test_api.py b/tests/test_api.py index 3bf4a41f..1f0f79ab 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -28,6 +28,7 @@ def suppress_known_deprecation(): class APITests( fixtures.EggInfoPkg, fixtures.EggInfoPkgPipInstalledNoModules, + fixtures.EggInfoPkgSourcesFallback, fixtures.DistInfoPkg, fixtures.DistInfoPkgWithDot, fixtures.EggInfoFile, @@ -185,6 +186,7 @@ def test_files_dist_info(self): def test_files_egg_info(self): self._test_files(files('egginfo-pkg')) self._test_files(files('empty_egg-pkg')) + self._test_files(files('starved_egg-pkg')) def test_version_egg_info_file(self): self.assertEqual(version('egginfo-file'), '0.1') diff --git a/tests/test_main.py b/tests/test_main.py index 4d28fa26..e08a7609 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -173,6 +173,7 @@ def test_metadata_loads_egg_info(self): class DiscoveryTests( fixtures.EggInfoPkg, fixtures.EggInfoPkgPipInstalledNoModules, + fixtures.EggInfoPkgSourcesFallback, fixtures.DistInfoPkg, unittest.TestCase, ): @@ -181,6 +182,7 @@ def test_package_discovery(self): assert all(isinstance(dist, Distribution) for dist in dists) assert any(dist.metadata['Name'] == 'egginfo-pkg' for dist in dists) assert any(dist.metadata['Name'] == 'empty_egg-pkg' for dist in dists) + assert any(dist.metadata['Name'] == 'starved_egg-pkg' for dist in dists) assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists) def test_invalid_usage(self): @@ -312,6 +314,7 @@ def test_packages_distributions_example2(self): class PackagesDistributionsTest( fixtures.EggInfoPkg, fixtures.EggInfoPkgPipInstalledNoModules, + fixtures.EggInfoPkgSourcesFallback, fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase, @@ -353,3 +356,7 @@ def import_names_from_package(package_name): # empty_egg-pkg should not be associated with any import names # (top_level.txt is empty, and installed-files.txt has no .py files) assert import_names_from_package('empty_egg-pkg') == set() + + # starved_egg-pkg has one import ('starved_egg_pkg') inferred + # from SOURCES.txt (top_level.txt is missing) + assert import_names_from_package('starved_egg-pkg') == {'starved_egg_pkg'} From 22d9ea5e307412c0e46c6cb90db54ec082d1d536 Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Fri, 10 Mar 2023 14:45:19 +0100 Subject: [PATCH 05/20] Distribution.files: Only return files that actually exist Add an extra filter on the paths returned from Distribution.files, to prevent paths that don't exist on the filesystem from being returned. This attempts to solve the issue of .files returning incorrect information based on the inaccuracies of SOURCES.txt. As the code currently is organized, it is more complicated to write this such that it only applies to the information read from SOURCES.txt specifically, hence we apply it to _all_ of .files instead. This fixes #115, also in the case where there is no installed-files.txt file available. [1]: https://pip.pypa.io/en/stable/news/#v0-3 [2]: https://setuptools.pypa.io/en/latest/deprecated/python_eggs.html#sources-txt-source-files-manifest --- importlib_metadata/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 3b0d8247..773b8fc1 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -474,7 +474,12 @@ def make_file(name, hash=None, size_str=None): @pass_none def make_files(lines): - return list(starmap(make_file, csv.reader(lines))) + return list( + filter( + lambda package_path: package_path.locate().exists(), + list(starmap(make_file, csv.reader(lines))), + ) + ) return make_files( self._read_files_distinfo() From b391f77d078b461da972226dbf6f0649e78eb5db Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Sun, 19 Mar 2023 02:06:58 +0100 Subject: [PATCH 06/20] squash! Add tests for egg-info package with no installed modules Move test_packages_distributions_on_eggs() method into a new class, PackagesDistributionsEggTest, to prevent applying unnecessary fixtures to existing tests. --- tests/test_main.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index d796808c..4c4b7b76 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -312,12 +312,7 @@ def test_packages_distributions_example2(self): class PackagesDistributionsTest( - fixtures.EggInfoPkg, - fixtures.EggInfoPkgPipInstalledNoModules, - fixtures.EggInfoPkgSourcesFallback, - fixtures.OnSysPath, - fixtures.SiteDir, - unittest.TestCase, + fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase ): def test_packages_distributions_neither_toplevel_nor_files(self): """ @@ -367,6 +362,13 @@ def test_packages_distributions_all_module_types(self): assert distributions[f'{i}-in-namespace'] == ['all_distributions'] assert distributions[f'{i}-in-package'] == ['all_distributions'] + +class PackagesDistributionsEggTest( + fixtures.EggInfoPkg, + fixtures.EggInfoPkgPipInstalledNoModules, + fixtures.EggInfoPkgSourcesFallback, + unittest.TestCase, +): def test_packages_distributions_on_eggs(self): """ Test old-style egg packages with a variation of 'top_level.txt', From 110f00d24304fdf446a640513f6bf7aef18ba9c3 Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Sun, 19 Mar 2023 02:49:43 +0100 Subject: [PATCH 07/20] Add test case demonstrating inferring module names from installed-files.txt --- tests/fixtures.py | 35 +++++++++++++++++++++++++++++++++++ tests/test_api.py | 2 ++ tests/test_main.py | 7 +++++++ 3 files changed, 44 insertions(+) diff --git a/tests/fixtures.py b/tests/fixtures.py index bbd9854b..0423c29d 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -213,6 +213,41 @@ def setUp(self): build_files(EggInfoPkg.files, prefix=self.site_dir) +class EggInfoPkgPipInstalledNoToplevel(OnSysPath, SiteDir): + files: FilesDef = { + "egg_with_module_pkg.egg-info": { + "PKG-INFO": "Name: egg_with_module-pkg", + # SOURCES.txt is made from the source archive, and contains files + # (setup.py) that are not present after installation. + "SOURCES.txt": """ + egg_with_module.py + setup.py + egg_with_module_pkg.egg-info/PKG-INFO + egg_with_module_pkg.egg-info/SOURCES.txt + egg_with_module_pkg.egg-info/top_level.txt + """, + # installed-files.txt is written by pip, and is a strictly more + # accurate source than SOURCES.txt as to the installed contents of + # the package. + "installed-files.txt": """ + ../egg_with_module.py + PKG-INFO + SOURCES.txt + top_level.txt + """, + # missing top_level.txt (to trigger fallback to installed-files.txt) + }, + "egg_with_module.py": """ + def main(): + print("hello world") + """ + } + + def setUp(self): + super().setUp() + build_files(EggInfoPkgPipInstalledNoToplevel.files, prefix=self.site_dir) + + class EggInfoPkgPipInstalledNoModules(OnSysPath, SiteDir): files: FilesDef = { "empty_egg_pkg.egg-info": { diff --git a/tests/test_api.py b/tests/test_api.py index 1f0f79ab..984c7707 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -27,6 +27,7 @@ def suppress_known_deprecation(): class APITests( fixtures.EggInfoPkg, + fixtures.EggInfoPkgPipInstalledNoToplevel, fixtures.EggInfoPkgPipInstalledNoModules, fixtures.EggInfoPkgSourcesFallback, fixtures.DistInfoPkg, @@ -185,6 +186,7 @@ def test_files_dist_info(self): def test_files_egg_info(self): self._test_files(files('egginfo-pkg')) + self._test_files(files('egg_with_module-pkg')) self._test_files(files('empty_egg-pkg')) self._test_files(files('starved_egg-pkg')) diff --git a/tests/test_main.py b/tests/test_main.py index 4c4b7b76..c67df8be 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -172,6 +172,7 @@ def test_metadata_loads_egg_info(self): class DiscoveryTests( fixtures.EggInfoPkg, + fixtures.EggInfoPkgPipInstalledNoToplevel, fixtures.EggInfoPkgPipInstalledNoModules, fixtures.EggInfoPkgSourcesFallback, fixtures.DistInfoPkg, @@ -181,6 +182,7 @@ def test_package_discovery(self): dists = list(distributions()) assert all(isinstance(dist, Distribution) for dist in dists) assert any(dist.metadata['Name'] == 'egginfo-pkg' for dist in dists) + assert any(dist.metadata['Name'] == 'egg_with_module-pkg' for dist in dists) assert any(dist.metadata['Name'] == 'empty_egg-pkg' for dist in dists) assert any(dist.metadata['Name'] == 'starved_egg-pkg' for dist in dists) assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists) @@ -365,6 +367,7 @@ def test_packages_distributions_all_module_types(self): class PackagesDistributionsEggTest( fixtures.EggInfoPkg, + fixtures.EggInfoPkgPipInstalledNoToplevel, fixtures.EggInfoPkgPipInstalledNoModules, fixtures.EggInfoPkgSourcesFallback, unittest.TestCase, @@ -386,6 +389,10 @@ def import_names_from_package(package_name): # egginfo-pkg declares one import ('mod') via top_level.txt assert import_names_from_package('egginfo-pkg') == {'mod'} + # egg_with_module-pkg has one import ('egg_with_module') inferred from + # installed-files.txt (top_level.txt is missing) + assert import_names_from_package('egg_with_module-pkg') == {'egg_with_module'} + # empty_egg-pkg should not be associated with any import names # (top_level.txt is empty, and installed-files.txt has no .py files) assert import_names_from_package('empty_egg-pkg') == set() From eeb2ed1593e25e4be3c7da8db073260a4ae8ec1e Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Sun, 19 Mar 2023 02:50:04 +0100 Subject: [PATCH 08/20] Fix issues with inferring module names from installed-files.txt --- importlib_metadata/__init__.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index c60a3582..dd7839db 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -512,7 +512,16 @@ def _read_files_egginfo_installed(self): # But this subdir is only available in the PathDistribution's self._path # which is not easily accessible from this base class... subdir = getattr(self, '_path', None) - return text and subdir and [f'"{subdir}/{line}"' for line in text.splitlines()] + try: + if text and subdir: + ret = [ + str((subdir / line).resolve().relative_to(self.locate_file(''))) + for line in text.splitlines() + ] + return map('"{}"'.format, ret) + except Exception: + pass + return None def _read_files_egginfo_sources(self): """ From a2dc88a299d6cb83052d91928b12cde0a6e5a2de Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Sun, 19 Mar 2023 02:51:27 +0100 Subject: [PATCH 09/20] squash! Add tests for egg-info package with .files from inaccurate SOURCES.txt Rename starved_egg to sources_fallback. --- tests/fixtures.py | 16 ++++++++-------- tests/test_api.py | 2 +- tests/test_main.py | 8 ++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 0423c29d..36c756f0 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -280,20 +280,20 @@ def setUp(self): class EggInfoPkgSourcesFallback(OnSysPath, SiteDir): files: FilesDef = { - "starved_egg_pkg.egg-info": { - "PKG-INFO": "Name: starved_egg-pkg", + "sources_fallback_pkg.egg-info": { + "PKG-INFO": "Name: sources_fallback-pkg", # SOURCES.txt is made from the source archive, and contains files # (setup.py) that are not present after installation. "SOURCES.txt": """ - starved_egg_pkg.py + sources_fallback.py setup.py - starved_egg_pkg.egg-info/PKG-INFO - starved_egg_pkg.egg-info/SOURCES.txt + sources_fallback_pkg.egg-info/PKG-INFO + sources_fallback_pkg.egg-info/SOURCES.txt """, - # missing installed-files.txt (i.e. not installed by pip) - # missing top_level.txt + # missing installed-files.txt (i.e. not installed by pip) and + # missing top_level.txt (to trigger fallback to SOURCES.txt) }, - "starved_egg_pkg.py": """ + "sources_fallback.py": """ def main(): print("hello world") """, diff --git a/tests/test_api.py b/tests/test_api.py index 984c7707..0c56dda9 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -188,7 +188,7 @@ def test_files_egg_info(self): self._test_files(files('egginfo-pkg')) self._test_files(files('egg_with_module-pkg')) self._test_files(files('empty_egg-pkg')) - self._test_files(files('starved_egg-pkg')) + self._test_files(files('sources_fallback-pkg')) def test_version_egg_info_file(self): self.assertEqual(version('egginfo-file'), '0.1') diff --git a/tests/test_main.py b/tests/test_main.py index c67df8be..a0f80f02 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -184,7 +184,7 @@ def test_package_discovery(self): assert any(dist.metadata['Name'] == 'egginfo-pkg' for dist in dists) assert any(dist.metadata['Name'] == 'egg_with_module-pkg' for dist in dists) assert any(dist.metadata['Name'] == 'empty_egg-pkg' for dist in dists) - assert any(dist.metadata['Name'] == 'starved_egg-pkg' for dist in dists) + assert any(dist.metadata['Name'] == 'sources_fallback-pkg' for dist in dists) assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists) def test_invalid_usage(self): @@ -397,6 +397,6 @@ def import_names_from_package(package_name): # (top_level.txt is empty, and installed-files.txt has no .py files) assert import_names_from_package('empty_egg-pkg') == set() - # starved_egg-pkg has one import ('starved_egg_pkg') inferred - # from SOURCES.txt (top_level.txt is missing) - assert import_names_from_package('starved_egg-pkg') == {'starved_egg_pkg'} + # sources_fallback-pkg has one import ('sources_fallback') inferred from + # SOURCES.txt (top_level.txt and installed-files.txt is missing) + assert import_names_from_package('sources_fallback-pkg') == {'sources_fallback'} From f62bf95d921fd8aca553813533f110d9010c2412 Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Sun, 19 Mar 2023 02:59:23 +0100 Subject: [PATCH 10/20] squash! Add tests for egg-info package with no installed modules Rename empty_egg to egg_with_no_modules --- tests/fixtures.py | 10 +++++----- tests/test_api.py | 6 +++--- tests/test_main.py | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 36c756f0..6c589b40 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -250,15 +250,15 @@ def setUp(self): class EggInfoPkgPipInstalledNoModules(OnSysPath, SiteDir): files: FilesDef = { - "empty_egg_pkg.egg-info": { - "PKG-INFO": "Name: empty_egg-pkg", + "egg_with_no_modules_pkg.egg-info": { + "PKG-INFO": "Name: egg_with_no_modules-pkg", # SOURCES.txt is made from the source archive, and contains files # (setup.py) that are not present after installation. "SOURCES.txt": """ setup.py - empty_egg_pkg.egg-info/PKG-INFO - empty_egg_pkg.egg-info/SOURCES.txt - empty_egg_pkg.egg-info/top_level.txt + egg_with_no_modules_pkg.egg-info/PKG-INFO + egg_with_no_modules_pkg.egg-info/SOURCES.txt + egg_with_no_modules_pkg.egg-info/top_level.txt """, # installed-files.txt is written by pip, and is a strictly more # accurate source than SOURCES.txt as to the installed contents of diff --git a/tests/test_api.py b/tests/test_api.py index 0c56dda9..e18ceaad 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -67,7 +67,7 @@ def test_prefix_not_matched(self): def test_for_top_level(self): tests = [ ('egginfo-pkg', 'mod'), - ('empty_egg-pkg', ''), + ('egg_with_no_modules-pkg', ''), ] for pkg_name, expect_content in tests: with self.subTest(pkg_name): @@ -79,7 +79,7 @@ def test_for_top_level(self): def test_read_text(self): tests = [ ('egginfo-pkg', 'mod\n'), - ('empty_egg-pkg', '\n'), + ('egg_with_no_modules-pkg', '\n'), ] for pkg_name, expect_content in tests: with self.subTest(pkg_name): @@ -187,7 +187,7 @@ def test_files_dist_info(self): def test_files_egg_info(self): self._test_files(files('egginfo-pkg')) self._test_files(files('egg_with_module-pkg')) - self._test_files(files('empty_egg-pkg')) + self._test_files(files('egg_with_no_modules-pkg')) self._test_files(files('sources_fallback-pkg')) def test_version_egg_info_file(self): diff --git a/tests/test_main.py b/tests/test_main.py index a0f80f02..c7c39094 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -183,7 +183,7 @@ def test_package_discovery(self): assert all(isinstance(dist, Distribution) for dist in dists) assert any(dist.metadata['Name'] == 'egginfo-pkg' for dist in dists) assert any(dist.metadata['Name'] == 'egg_with_module-pkg' for dist in dists) - assert any(dist.metadata['Name'] == 'empty_egg-pkg' for dist in dists) + assert any(dist.metadata['Name'] == 'egg_with_no_modules-pkg' for dist in dists) assert any(dist.metadata['Name'] == 'sources_fallback-pkg' for dist in dists) assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists) @@ -393,9 +393,9 @@ def import_names_from_package(package_name): # installed-files.txt (top_level.txt is missing) assert import_names_from_package('egg_with_module-pkg') == {'egg_with_module'} - # empty_egg-pkg should not be associated with any import names + # egg_with_no_modules-pkg should not be associated with any import names # (top_level.txt is empty, and installed-files.txt has no .py files) - assert import_names_from_package('empty_egg-pkg') == set() + assert import_names_from_package('egg_with_no_modules-pkg') == set() # sources_fallback-pkg has one import ('sources_fallback') inferred from # SOURCES.txt (top_level.txt and installed-files.txt is missing) From 61eca31a7456ccc39e3fda8e098497a4f28482b4 Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Sun, 19 Mar 2023 03:03:19 +0100 Subject: [PATCH 11/20] squash! Distribution.files: Only return files that actually exist Remove unnecessary list() call. --- importlib_metadata/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index dd7839db..d83025da 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -478,7 +478,7 @@ def make_files(lines): return list( filter( lambda package_path: package_path.locate().exists(), - list(starmap(make_file, csv.reader(lines))), + starmap(make_file, csv.reader(lines)), ) ) From 9b165a91af49bb11b7cf06b5661801f603aaeba6 Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Sun, 19 Mar 2023 03:23:47 +0100 Subject: [PATCH 12/20] Refactor logic for skipping missing files out of magic_files() --- importlib_metadata/__init__.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index d83025da..2884bed0 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -475,17 +475,18 @@ def make_file(name, hash=None, size_str=None): @pass_none def make_files(lines): - return list( - filter( - lambda package_path: package_path.locate().exists(), - starmap(make_file, csv.reader(lines)), - ) - ) + return starmap(make_file, csv.reader(lines)) - return make_files( - self._read_files_distinfo() - or self._read_files_egginfo_installed() - or self._read_files_egginfo_sources() + @pass_none + def skip_missing_files(package_paths): + return list(filter(lambda path: path.locate().exists(), package_paths)) + + return skip_missing_files( + make_files( + self._read_files_distinfo() + or self._read_files_egginfo_installed() + or self._read_files_egginfo_sources() + ) ) def _read_files_distinfo(self): From 33eb7b4a0312017048d849cf1aef0321282d329d Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Sun, 19 Mar 2023 03:24:13 +0100 Subject: [PATCH 13/20] Rewrite docstrings to clarify the expected output format, and why we need quoting --- importlib_metadata/__init__.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 2884bed0..776896b3 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -498,10 +498,10 @@ def _read_files_distinfo(self): def _read_files_egginfo_installed(self): """ - installed-files.txt might contain literal commas, so wrap - each line in quotes. Also, the entries in installed-files.txt - are relative to the .egg-info/ subdir (not relative to the - parent site-packages directory that make_file() expects). + Read installed-files.txt and return lines in a similar + CSV-parsable format as RECORD: each file must be placed + relative to the site-packages directory, and must also be + quoted (since file names can contain literal commas). This file is written when the package is installed by pip, but it might not be written for other installation methods. @@ -526,8 +526,9 @@ def _read_files_egginfo_installed(self): def _read_files_egginfo_sources(self): """ - SOURCES.txt might contain literal commas, so wrap each line - in quotes. + Read SOURCES.txt and return lines in a similar CSV-parsable + format as RECORD: each file name must be quoted (since it + might contain literal commas). Note that SOURCES.txt is not a reliable source for what files are installed by a package. This file is generated From fa9cca4ebb6c5aaf43197b04c4e28bf62bdb11ce Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Sun, 19 Mar 2023 04:09:18 +0100 Subject: [PATCH 14/20] test_packages_distributions_all_module_types() must create existing files for all the entries in RECORD --- tests/fixtures.py | 1 + tests/test_main.py | 21 ++++++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 6c589b40..7a96dca6 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -351,6 +351,7 @@ def build_files(file_defs, prefix=pathlib.Path()): full_name.mkdir() build_files(contents, prefix=full_name) else: + full_name.parent.mkdir(parents=True, exist_ok=True) if isinstance(contents, bytes): with full_name.open('wb') as f: f.write(contents) diff --git a/tests/test_main.py b/tests/test_main.py index c7c39094..8225e2d1 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -3,6 +3,7 @@ import unittest import importlib import importlib_metadata +import itertools import pyfakefs.fake_filesystem_unittest as ffs from . import fixtures @@ -338,6 +339,17 @@ def test_packages_distributions_all_module_types(self): Test top-level modules detected on a package without 'top-level.txt'. """ suffixes = importlib.machinery.all_suffixes() + filenames = list( + itertools.chain.from_iterable( + [ + f'{i}-top-level{suffix}', + f'{i}-in-namespace/mod{suffix}', + f'{i}-in-package/__init__.py', + f'{i}-in-package/mod{suffix}', + ] + for i, suffix in enumerate(suffixes) + ) + ) fixtures.build_files( { 'all_distributions-1.0.0.dist-info': { @@ -345,17 +357,12 @@ def test_packages_distributions_all_module_types(self): Name: all_distributions Version: 1.0.0 """, - 'RECORD': ''.join( - f'{i}-top-level{suffix},,\n' - f'{i}-in-namespace/mod{suffix},,\n' - f'{i}-in-package/__init__.py,,\n' - f'{i}-in-package/mod{suffix},,\n' - for i, suffix in enumerate(suffixes) - ), + 'RECORD': ''.join(f'{fname},,\n' for fname in filenames), }, }, prefix=self.site_dir, ) + fixtures.build_files({fname: "" for fname in filenames}, prefix=self.site_dir) distributions = packages_distributions() From 70ff991377f8d0848865c8c210edd2c7c41c119c Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Sun, 19 Mar 2023 19:04:12 +0100 Subject: [PATCH 15/20] test_packages_distributions_all_module_types: Create valid import names The import names that were created by these tests were not valid Python identifiers. Fix that, and furthermore: add another check to verify that _all_ import names returned from packages_distributions() are always valid Python identifiers. Ideally we should check that all keys returned from packages_distributions() are valid import names (i.e. can be imported), but this is at least a step in the right direction. --- tests/test_main.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index 8225e2d1..883b4487 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -342,10 +342,10 @@ def test_packages_distributions_all_module_types(self): filenames = list( itertools.chain.from_iterable( [ - f'{i}-top-level{suffix}', - f'{i}-in-namespace/mod{suffix}', - f'{i}-in-package/__init__.py', - f'{i}-in-package/mod{suffix}', + f'top_level_{i}{suffix}', + f'in_namespace_{i}/mod{suffix}', + f'in_package_{i}/__init__.py', + f'in_package_{i}/mod{suffix}', ] for i, suffix in enumerate(suffixes) ) @@ -367,9 +367,14 @@ def test_packages_distributions_all_module_types(self): distributions = packages_distributions() for i in range(len(suffixes)): - assert distributions[f'{i}-top-level'] == ['all_distributions'] - assert distributions[f'{i}-in-namespace'] == ['all_distributions'] - assert distributions[f'{i}-in-package'] == ['all_distributions'] + assert distributions[f'top_level_{i}'] == ['all_distributions'] + assert distributions[f'in_namespace_{i}'] == ['all_distributions'] + assert distributions[f'in_package_{i}'] == ['all_distributions'] + + # All keys returned from packages_distributions() should be valid import + # names, which means that they must _at least_ be valid identifiers: + for import_name in distributions.keys(): + assert import_name.isidentifier(), import_name class PackagesDistributionsEggTest( From 5dbe83cdb0565e13f5a57629a6a9a334e07ebe93 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 9 Apr 2023 15:49:38 -0400 Subject: [PATCH 16/20] Revert "test_packages_distributions_all_module_types: Create valid import names" This reverts commit 70ff991377f8d0848865c8c210edd2c7c41c119c. This behavior was adopted in 5e8260c8e545d7f21c779fb8b57004bc280ae330 and subsequently adapted as part of #443. --- tests/test_main.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index 883b4487..8225e2d1 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -342,10 +342,10 @@ def test_packages_distributions_all_module_types(self): filenames = list( itertools.chain.from_iterable( [ - f'top_level_{i}{suffix}', - f'in_namespace_{i}/mod{suffix}', - f'in_package_{i}/__init__.py', - f'in_package_{i}/mod{suffix}', + f'{i}-top-level{suffix}', + f'{i}-in-namespace/mod{suffix}', + f'{i}-in-package/__init__.py', + f'{i}-in-package/mod{suffix}', ] for i, suffix in enumerate(suffixes) ) @@ -367,14 +367,9 @@ def test_packages_distributions_all_module_types(self): distributions = packages_distributions() for i in range(len(suffixes)): - assert distributions[f'top_level_{i}'] == ['all_distributions'] - assert distributions[f'in_namespace_{i}'] == ['all_distributions'] - assert distributions[f'in_package_{i}'] == ['all_distributions'] - - # All keys returned from packages_distributions() should be valid import - # names, which means that they must _at least_ be valid identifiers: - for import_name in distributions.keys(): - assert import_name.isidentifier(), import_name + assert distributions[f'{i}-top-level'] == ['all_distributions'] + assert distributions[f'{i}-in-namespace'] == ['all_distributions'] + assert distributions[f'{i}-in-package'] == ['all_distributions'] class PackagesDistributionsEggTest( From 4e7f79f5998cb08c2d84ca250c83a2820dd32e8a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 9 Apr 2023 18:17:40 -0400 Subject: [PATCH 17/20] Revert "test_packages_distributions_all_module_types() must create existing files for all the entries in RECORD" This reverts commit fa9cca4ebb6c5aaf43197b04c4e28bf62bdb11ce. --- tests/fixtures.py | 1 - tests/test_main.py | 21 +++++++-------------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 7a96dca6..6c589b40 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -351,7 +351,6 @@ def build_files(file_defs, prefix=pathlib.Path()): full_name.mkdir() build_files(contents, prefix=full_name) else: - full_name.parent.mkdir(parents=True, exist_ok=True) if isinstance(contents, bytes): with full_name.open('wb') as f: f.write(contents) diff --git a/tests/test_main.py b/tests/test_main.py index 8225e2d1..c7c39094 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -3,7 +3,6 @@ import unittest import importlib import importlib_metadata -import itertools import pyfakefs.fake_filesystem_unittest as ffs from . import fixtures @@ -339,17 +338,6 @@ def test_packages_distributions_all_module_types(self): Test top-level modules detected on a package without 'top-level.txt'. """ suffixes = importlib.machinery.all_suffixes() - filenames = list( - itertools.chain.from_iterable( - [ - f'{i}-top-level{suffix}', - f'{i}-in-namespace/mod{suffix}', - f'{i}-in-package/__init__.py', - f'{i}-in-package/mod{suffix}', - ] - for i, suffix in enumerate(suffixes) - ) - ) fixtures.build_files( { 'all_distributions-1.0.0.dist-info': { @@ -357,12 +345,17 @@ def test_packages_distributions_all_module_types(self): Name: all_distributions Version: 1.0.0 """, - 'RECORD': ''.join(f'{fname},,\n' for fname in filenames), + 'RECORD': ''.join( + f'{i}-top-level{suffix},,\n' + f'{i}-in-namespace/mod{suffix},,\n' + f'{i}-in-package/__init__.py,,\n' + f'{i}-in-package/mod{suffix},,\n' + for i, suffix in enumerate(suffixes) + ), }, }, prefix=self.site_dir, ) - fixtures.build_files({fname: "" for fname in filenames}, prefix=self.site_dir) distributions = packages_distributions() From 8818432f9c71f77bbb79fcf56a9aa53e4b9a6bd5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 9 Apr 2023 21:31:31 -0400 Subject: [PATCH 18/20] =?UTF-8?q?=E2=9A=AB=20Fade=20to=20black.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/fixtures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 7db4c30a..6e72c6ab 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -243,7 +243,7 @@ class EggInfoPkgPipInstalledNoToplevel(OnSysPath, SiteDir): "egg_with_module.py": """ def main(): print("hello world") - """ + """, } def setUp(self): From 3d7ee19cd58ceb67f91a6766e9f6035a918b95f2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 9 Apr 2023 22:13:59 -0400 Subject: [PATCH 19/20] Refactor to avoid missed coverage --- importlib_metadata/__init__.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 1008a2c6..a298d7a1 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -12,6 +12,7 @@ import functools import itertools import posixpath +import contextlib import collections import inspect @@ -513,16 +514,14 @@ def _read_files_egginfo_installed(self): # But this subdir is only available in the PathDistribution's self._path # which is not easily accessible from this base class... subdir = getattr(self, '_path', None) - try: - if text and subdir: - ret = [ - str((subdir / line).resolve().relative_to(self.locate_file(''))) - for line in text.splitlines() - ] - return map('"{}"'.format, ret) - except Exception: - pass - return None + if not text or not subdir: + return + with contextlib.suppress(Exception): + ret = [ + str((subdir / line).resolve().relative_to(self.locate_file(''))) + for line in text.splitlines() + ] + return map('"{}"'.format, ret) def _read_files_egginfo_sources(self): """ From b8a8b5d35e6ca31a4a7cdfa5a3ee61562e8e7456 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 9 Apr 2023 22:16:03 -0400 Subject: [PATCH 20/20] Update changelog. --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 1bc5222e..6ec9d1f3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +v6.3.0 +====== + +* #115: Support ``installed-files.txt`` for ``Distribution.files`` + when present. + v6.2.1 ======