Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 97
Fix .files and inferred packages_distributions for .egg-info packages#437
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fed3a41578322a61b0f298026db222d9ea5a107ce5b391f77110f00deeb2ed1a2dc88af62bf9561eca319b165a933eb7b4fa9cca470ff9915dbe83c4e7f79f812db6f387f3bd88184323d7ee19b8a8b5dFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,9 @@ | ||
| v6.3.0 | ||
| ====== | ||
| * #115: Support ``installed-files.txt`` for ``Distribution.files`` | ||
| when present. | ||
| v6.2.1 | ||
| ====== | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -12,6 +12,7 @@ | ||
| import functools | ||
| import itertools | ||
| import posixpath | ||
| import contextlib | ||
| import collections | ||
| import inspect | ||
| @@ -461,8 +462,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. | ||
| """ | ||
| @@ -475,9 +476,19 @@ def make_file(name, hash=None, size_str=None): | ||
| @pass_none | ||
| def make_files(lines): | ||
| return list(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()) | ||
| @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): | ||
| """ | ||
| @@ -486,10 +497,43 @@ 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): | ||
| """ | ||
| SOURCES.txt might contain literal commas, so wrap each line | ||
| in quotes. | ||
| 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. | ||
| 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) | ||
| 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) | ||
jaraco marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| def _read_files_egginfo_sources(self): | ||
| """ | ||
| 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 | ||
| 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()) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -86,8 +86,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] | ||
| ] | ||
jaraco marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| class DistInfoPkg(OnSysPath, SiteDir): | ||
| @@ -214,6 +216,97 @@ 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): | ||
jaraco marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| files: FilesDef = { | ||
| "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 | ||
| 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 | ||
| # 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 EggInfoPkgSourcesFallback(OnSysPath, SiteDir): | ||
| files: FilesDef = { | ||
| "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": """ | ||
| sources_fallback.py | ||
| setup.py | ||
| sources_fallback_pkg.egg-info/PKG-INFO | ||
| sources_fallback_pkg.egg-info/SOURCES.txt | ||
| """, | ||
| # missing installed-files.txt (i.e. not installed by pip) and | ||
| # missing top_level.txt (to trigger fallback to SOURCES.txt) | ||
| }, | ||
| "sources_fallback.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": """ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -171,11 +171,21 @@ 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.EggInfoPkgPipInstalledNoToplevel, | ||
| fixtures.EggInfoPkgPipInstalledNoModules, | ||
| fixtures.EggInfoPkgSourcesFallback, | ||
jaraco marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| 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'] == 'egg_with_module-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) | ||
| def test_invalid_usage(self): | ||
| @@ -362,3 +372,40 @@ def test_packages_distributions_all_module_types(self): | ||
| assert distributions[f'in_package_{i}'] == ['all_distributions'] | ||
| assert not any(name.endswith('.dist-info') for name in distributions) | ||
| class PackagesDistributionsEggTest( | ||
| fixtures.EggInfoPkg, | ||
| fixtures.EggInfoPkgPipInstalledNoToplevel, | ||
| 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', | ||
| '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'} | ||
| # 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'} | ||
| # 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('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) | ||
| assert import_names_from_package('sources_fallback-pkg') == {'sources_fallback'} | ||
Uh oh!
There was an error while loading. Please reload this page.