Skip to content

Commit

Permalink
Add tests for egg-info package with no installed modules
Browse files Browse the repository at this point in the history
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#115 issue for more details.

[1]: qiskit is found at https://pypi.org/project/qiskit/0.41.1/#files
  • Loading branch information
jherland committed Mar 11, 2023
1 parent fed3a41 commit 578322a
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 10 deletions.
30 changes: 30 additions & 0 deletions tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": """
Expand Down
30 changes: 22 additions & 8 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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')
Expand Down
35 changes: 33 additions & 2 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
"""
Expand All @@ -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()

0 comments on commit 578322a

Please sign in to comment.