about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/setuptools/tests/test_egg_info.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/setuptools/tests/test_egg_info.py')
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_egg_info.py1305
1 files changed, 1305 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_egg_info.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_egg_info.py
new file mode 100644
index 00000000..528e2c13
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_egg_info.py
@@ -0,0 +1,1305 @@
+from __future__ import annotations
+
+import ast
+import glob
+import os
+import re
+import stat
+import sys
+import time
+from pathlib import Path
+from unittest import mock
+
+import pytest
+from jaraco import path
+
+from setuptools import errors
+from setuptools.command.egg_info import egg_info, manifest_maker, write_entries
+from setuptools.dist import Distribution
+
+from . import contexts, environment
+from .textwrap import DALS
+
+
+class Environment(str):
+    pass
+
+
+@pytest.fixture
+def env():
+    with contexts.tempdir(prefix='setuptools-test.') as env_dir:
+        env = Environment(env_dir)
+        os.chmod(env_dir, stat.S_IRWXU)
+        subs = 'home', 'lib', 'scripts', 'data', 'egg-base'
+        env.paths = dict((dirname, os.path.join(env_dir, dirname)) for dirname in subs)
+        list(map(os.mkdir, env.paths.values()))
+        path.build({
+            env.paths['home']: {
+                '.pydistutils.cfg': DALS(
+                    """
+                [egg_info]
+                egg-base = {egg-base}
+                """.format(**env.paths)
+                )
+            }
+        })
+        yield env
+
+
+class TestEggInfo:
+    setup_script = DALS(
+        """
+        from setuptools import setup
+
+        setup(
+            name='foo',
+            py_modules=['hello'],
+            entry_points={'console_scripts': ['hi = hello.run']},
+            zip_safe=False,
+        )
+        """
+    )
+
+    def _create_project(self):
+        path.build({
+            'setup.py': self.setup_script,
+            'hello.py': DALS(
+                """
+                def run():
+                    print('hello')
+                """
+            ),
+        })
+
+    @staticmethod
+    def _extract_mv_version(pkg_info_lines: list[str]) -> tuple[int, int]:
+        version_str = pkg_info_lines[0].split(' ')[1]
+        major, minor = map(int, version_str.split('.')[:2])
+        return major, minor
+
+    def test_egg_info_save_version_info_setup_empty(self, tmpdir_cwd, env):
+        """
+        When the egg_info section is empty or not present, running
+        save_version_info should add the settings to the setup.cfg
+        in a deterministic order.
+        """
+        setup_cfg = os.path.join(env.paths['home'], 'setup.cfg')
+        dist = Distribution()
+        ei = egg_info(dist)
+        ei.initialize_options()
+        ei.save_version_info(setup_cfg)
+
+        with open(setup_cfg, 'r', encoding="utf-8") as f:
+            content = f.read()
+
+        assert '[egg_info]' in content
+        assert 'tag_build =' in content
+        assert 'tag_date = 0' in content
+
+        expected_order = (
+            'tag_build',
+            'tag_date',
+        )
+
+        self._validate_content_order(content, expected_order)
+
+    @staticmethod
+    def _validate_content_order(content, expected):
+        """
+        Assert that the strings in expected appear in content
+        in order.
+        """
+        pattern = '.*'.join(expected)
+        flags = re.MULTILINE | re.DOTALL
+        assert re.search(pattern, content, flags)
+
+    def test_egg_info_save_version_info_setup_defaults(self, tmpdir_cwd, env):
+        """
+        When running save_version_info on an existing setup.cfg
+        with the 'default' values present from a previous run,
+        the file should remain unchanged.
+        """
+        setup_cfg = os.path.join(env.paths['home'], 'setup.cfg')
+        path.build({
+            setup_cfg: DALS(
+                """
+            [egg_info]
+            tag_build =
+            tag_date = 0
+            """
+            ),
+        })
+        dist = Distribution()
+        ei = egg_info(dist)
+        ei.initialize_options()
+        ei.save_version_info(setup_cfg)
+
+        with open(setup_cfg, 'r', encoding="utf-8") as f:
+            content = f.read()
+
+        assert '[egg_info]' in content
+        assert 'tag_build =' in content
+        assert 'tag_date = 0' in content
+
+        expected_order = (
+            'tag_build',
+            'tag_date',
+        )
+
+        self._validate_content_order(content, expected_order)
+
+    def test_expected_files_produced(self, tmpdir_cwd, env):
+        self._create_project()
+
+        self._run_egg_info_command(tmpdir_cwd, env)
+        actual = os.listdir('foo.egg-info')
+
+        expected = [
+            'PKG-INFO',
+            'SOURCES.txt',
+            'dependency_links.txt',
+            'entry_points.txt',
+            'not-zip-safe',
+            'top_level.txt',
+        ]
+        assert sorted(actual) == expected
+
+    def test_handling_utime_error(self, tmpdir_cwd, env):
+        dist = Distribution()
+        ei = egg_info(dist)
+        utime_patch = mock.patch('os.utime', side_effect=OSError("TEST"))
+        mkpath_patch = mock.patch(
+            'setuptools.command.egg_info.egg_info.mkpath', return_val=None
+        )
+
+        with utime_patch, mkpath_patch:
+            import distutils.errors
+
+            msg = r"Cannot update time stamp of directory 'None'"
+            with pytest.raises(distutils.errors.DistutilsFileError, match=msg):
+                ei.run()
+
+    def test_license_is_a_string(self, tmpdir_cwd, env):
+        setup_config = DALS(
+            """
+            [metadata]
+            name=foo
+            version=0.0.1
+            license=file:MIT
+            """
+        )
+
+        setup_script = DALS(
+            """
+            from setuptools import setup
+
+            setup()
+            """
+        )
+
+        path.build({
+            'setup.py': setup_script,
+            'setup.cfg': setup_config,
+        })
+
+        # This command should fail with a ValueError, but because it's
+        # currently configured to use a subprocess, the actual traceback
+        # object is lost and we need to parse it from stderr
+        with pytest.raises(AssertionError) as exc:
+            self._run_egg_info_command(tmpdir_cwd, env)
+
+        # The only argument to the assertion error should be a traceback
+        # containing a ValueError
+        assert 'ValueError' in exc.value.args[0]
+
+    def test_rebuilt(self, tmpdir_cwd, env):
+        """Ensure timestamps are updated when the command is re-run."""
+        self._create_project()
+
+        self._run_egg_info_command(tmpdir_cwd, env)
+        timestamp_a = os.path.getmtime('foo.egg-info')
+
+        # arbitrary sleep just to handle *really* fast systems
+        time.sleep(0.001)
+
+        self._run_egg_info_command(tmpdir_cwd, env)
+        timestamp_b = os.path.getmtime('foo.egg-info')
+
+        assert timestamp_a != timestamp_b
+
+    def test_manifest_template_is_read(self, tmpdir_cwd, env):
+        self._create_project()
+        path.build({
+            'MANIFEST.in': DALS(
+                """
+                recursive-include docs *.rst
+            """
+            ),
+            'docs': {
+                'usage.rst': "Run 'hi'",
+            },
+        })
+        self._run_egg_info_command(tmpdir_cwd, env)
+        egg_info_dir = os.path.join('.', 'foo.egg-info')
+        sources_txt = os.path.join(egg_info_dir, 'SOURCES.txt')
+        with open(sources_txt, encoding="utf-8") as f:
+            assert 'docs/usage.rst' in f.read().split('\n')
+
+    def _setup_script_with_requires(self, requires, use_setup_cfg=False):
+        setup_script = DALS(
+            """
+            from setuptools import setup
+
+            setup(name='foo', zip_safe=False, %s)
+            """
+        ) % ('' if use_setup_cfg else requires)
+        setup_config = requires if use_setup_cfg else ''
+        path.build({
+            'setup.py': setup_script,
+            'setup.cfg': setup_config,
+        })
+
+    mismatch_marker = f"python_version<'{sys.version_info[0]}'"
+    # Alternate equivalent syntax.
+    mismatch_marker_alternate = f'python_version < "{sys.version_info[0]}"'
+    invalid_marker = "<=>++"
+
+    class RequiresTestHelper:
+        @staticmethod
+        def parametrize(*test_list, **format_dict):
+            idlist = []
+            argvalues = []
+            for test in test_list:
+                test_params = test.lstrip().split('\n\n', 3)
+                name_kwargs = test_params.pop(0).split('\n')
+                if len(name_kwargs) > 1:
+                    val = name_kwargs[1].strip()
+                    install_cmd_kwargs = ast.literal_eval(val)
+                else:
+                    install_cmd_kwargs = {}
+                name = name_kwargs[0].strip()
+                setup_py_requires, setup_cfg_requires, expected_requires = [
+                    DALS(a).format(**format_dict) for a in test_params
+                ]
+                for id_, requires, use_cfg in (
+                    (name, setup_py_requires, False),
+                    (name + '_in_setup_cfg', setup_cfg_requires, True),
+                ):
+                    idlist.append(id_)
+                    marks = ()
+                    if requires.startswith('@xfail\n'):
+                        requires = requires[7:]
+                        marks = pytest.mark.xfail
+                    argvalues.append(
+                        pytest.param(
+                            requires,
+                            use_cfg,
+                            expected_requires,
+                            install_cmd_kwargs,
+                            marks=marks,
+                        )
+                    )
+            return pytest.mark.parametrize(
+                (
+                    "requires",
+                    "use_setup_cfg",
+                    "expected_requires",
+                    "install_cmd_kwargs",
+                ),
+                argvalues,
+                ids=idlist,
+            )
+
+    @RequiresTestHelper.parametrize(
+        # Format of a test:
+        #
+        # id
+        # install_cmd_kwargs [optional]
+        #
+        # requires block (when used in setup.py)
+        #
+        # requires block (when used in setup.cfg)
+        #
+        # expected contents of requires.txt
+        """
+        install_requires_deterministic
+
+        install_requires=["wheel>=0.5", "pytest"]
+
+        [options]
+        install_requires =
+            wheel>=0.5
+            pytest
+
+        wheel>=0.5
+        pytest
+        """,
+        """
+        install_requires_ordered
+
+        install_requires=["pytest>=3.0.2,!=10.9999"]
+
+        [options]
+        install_requires =
+            pytest>=3.0.2,!=10.9999
+
+        pytest!=10.9999,>=3.0.2
+        """,
+        """
+        install_requires_with_marker
+
+        install_requires=["barbazquux;{mismatch_marker}"],
+
+        [options]
+        install_requires =
+            barbazquux; {mismatch_marker}
+
+        [:{mismatch_marker_alternate}]
+        barbazquux
+        """,
+        """
+        install_requires_with_extra
+        {'cmd': ['egg_info']}
+
+        install_requires=["barbazquux [test]"],
+
+        [options]
+        install_requires =
+            barbazquux [test]
+
+        barbazquux[test]
+        """,
+        """
+        install_requires_with_extra_and_marker
+
+        install_requires=["barbazquux [test]; {mismatch_marker}"],
+
+        [options]
+        install_requires =
+            barbazquux [test]; {mismatch_marker}
+
+        [:{mismatch_marker_alternate}]
+        barbazquux[test]
+        """,
+        """
+        setup_requires_with_markers
+
+        setup_requires=["barbazquux;{mismatch_marker}"],
+
+        [options]
+        setup_requires =
+            barbazquux; {mismatch_marker}
+
+        """,
+        """
+        extras_require_with_extra
+        {'cmd': ['egg_info']}
+
+        extras_require={{"extra": ["barbazquux [test]"]}},
+
+        [options.extras_require]
+        extra = barbazquux [test]
+
+        [extra]
+        barbazquux[test]
+        """,
+        """
+        extras_require_with_extra_and_marker_in_req
+
+        extras_require={{"extra": ["barbazquux [test]; {mismatch_marker}"]}},
+
+        [options.extras_require]
+        extra =
+            barbazquux [test]; {mismatch_marker}
+
+        [extra]
+
+        [extra:{mismatch_marker_alternate}]
+        barbazquux[test]
+        """,
+        # FIXME: ConfigParser does not allow : in key names!
+        """
+        extras_require_with_marker
+
+        extras_require={{":{mismatch_marker}": ["barbazquux"]}},
+
+        @xfail
+        [options.extras_require]
+        :{mismatch_marker} = barbazquux
+
+        [:{mismatch_marker}]
+        barbazquux
+        """,
+        """
+        extras_require_with_marker_in_req
+
+        extras_require={{"extra": ["barbazquux; {mismatch_marker}"]}},
+
+        [options.extras_require]
+        extra =
+            barbazquux; {mismatch_marker}
+
+        [extra]
+
+        [extra:{mismatch_marker_alternate}]
+        barbazquux
+        """,
+        """
+        extras_require_with_empty_section
+
+        extras_require={{"empty": []}},
+
+        [options.extras_require]
+        empty =
+
+        [empty]
+        """,
+        # Format arguments.
+        invalid_marker=invalid_marker,
+        mismatch_marker=mismatch_marker,
+        mismatch_marker_alternate=mismatch_marker_alternate,
+    )
+    def test_requires(
+        self,
+        tmpdir_cwd,
+        env,
+        requires,
+        use_setup_cfg,
+        expected_requires,
+        install_cmd_kwargs,
+    ):
+        self._setup_script_with_requires(requires, use_setup_cfg)
+        self._run_egg_info_command(tmpdir_cwd, env, **install_cmd_kwargs)
+        egg_info_dir = os.path.join('.', 'foo.egg-info')
+        requires_txt = os.path.join(egg_info_dir, 'requires.txt')
+        if os.path.exists(requires_txt):
+            with open(requires_txt, encoding="utf-8") as fp:
+                install_requires = fp.read()
+        else:
+            install_requires = ''
+        assert install_requires.lstrip() == expected_requires
+        assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
+
+    def test_install_requires_unordered_disallowed(self, tmpdir_cwd, env):
+        """
+        Packages that pass unordered install_requires sequences
+        should be rejected as they produce non-deterministic
+        builds. See #458.
+        """
+        req = 'install_requires={"fake-factory==0.5.2", "pytz"}'
+        self._setup_script_with_requires(req)
+        with pytest.raises(AssertionError):
+            self._run_egg_info_command(tmpdir_cwd, env)
+
+    def test_extras_require_with_invalid_marker(self, tmpdir_cwd, env):
+        tmpl = 'extras_require={{":{marker}": ["barbazquux"]}},'
+        req = tmpl.format(marker=self.invalid_marker)
+        self._setup_script_with_requires(req)
+        with pytest.raises(AssertionError):
+            self._run_egg_info_command(tmpdir_cwd, env)
+        assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
+
+    def test_extras_require_with_invalid_marker_in_req(self, tmpdir_cwd, env):
+        tmpl = 'extras_require={{"extra": ["barbazquux; {marker}"]}},'
+        req = tmpl.format(marker=self.invalid_marker)
+        self._setup_script_with_requires(req)
+        with pytest.raises(AssertionError):
+            self._run_egg_info_command(tmpdir_cwd, env)
+        assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
+
+    def test_provides_extra(self, tmpdir_cwd, env):
+        self._setup_script_with_requires('extras_require={"foobar": ["barbazquux"]},')
+        environ = os.environ.copy().update(
+            HOME=env.paths['home'],
+        )
+        environment.run_setup_py(
+            cmd=['egg_info'],
+            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+            data_stream=1,
+            env=environ,
+        )
+        egg_info_dir = os.path.join('.', 'foo.egg-info')
+        with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
+            pkg_info_lines = fp.read().split('\n')
+        assert 'Provides-Extra: foobar' in pkg_info_lines
+        assert 'Metadata-Version: 2.4' in pkg_info_lines
+
+    def test_doesnt_provides_extra(self, tmpdir_cwd, env):
+        self._setup_script_with_requires(
+            """install_requires=["spam ; python_version<'3.6'"]"""
+        )
+        environ = os.environ.copy().update(
+            HOME=env.paths['home'],
+        )
+        environment.run_setup_py(
+            cmd=['egg_info'],
+            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+            data_stream=1,
+            env=environ,
+        )
+        egg_info_dir = os.path.join('.', 'foo.egg-info')
+        with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
+            pkg_info_text = fp.read()
+        assert 'Provides-Extra:' not in pkg_info_text
+
+    @pytest.mark.parametrize(
+        ('files', 'license_in_sources'),
+        [
+            (
+                {
+                    'setup.cfg': DALS(
+                        """
+                              [metadata]
+                              license_file = LICENSE
+                              """
+                    ),
+                    'LICENSE': "Test license",
+                },
+                True,
+            ),  # with license
+            (
+                {
+                    'setup.cfg': DALS(
+                        """
+                              [metadata]
+                              license_file = INVALID_LICENSE
+                              """
+                    ),
+                    'LICENSE': "Test license",
+                },
+                False,
+            ),  # with an invalid license
+            (
+                {
+                    'setup.cfg': DALS(
+                        """
+                              """
+                    ),
+                    'LICENSE': "Test license",
+                },
+                True,
+            ),  # no license_file attribute, LICENSE auto-included
+            (
+                {
+                    'setup.cfg': DALS(
+                        """
+                              [metadata]
+                              license_file = LICENSE
+                              """
+                    ),
+                    'MANIFEST.in': "exclude LICENSE",
+                    'LICENSE': "Test license",
+                },
+                True,
+            ),  # manifest is overwritten by license_file
+            pytest.param(
+                {
+                    'setup.cfg': DALS(
+                        """
+                              [metadata]
+                              license_file = LICEN[CS]E*
+                              """
+                    ),
+                    'LICENSE': "Test license",
+                },
+                True,
+                id="glob_pattern",
+            ),
+        ],
+    )
+    def test_setup_cfg_license_file(self, tmpdir_cwd, env, files, license_in_sources):
+        self._create_project()
+        path.build(files)
+
+        environment.run_setup_py(
+            cmd=['egg_info'],
+            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+        )
+        egg_info_dir = os.path.join('.', 'foo.egg-info')
+
+        sources_text = Path(egg_info_dir, "SOURCES.txt").read_text(encoding="utf-8")
+
+        if license_in_sources:
+            assert 'LICENSE' in sources_text
+        else:
+            assert 'LICENSE' not in sources_text
+            # for invalid license test
+            assert 'INVALID_LICENSE' not in sources_text
+
+    @pytest.mark.parametrize(
+        ('files', 'incl_licenses', 'excl_licenses'),
+        [
+            (
+                {
+                    'setup.cfg': DALS(
+                        """
+                              [metadata]
+                              license_files =
+                                  LICENSE-ABC
+                                  LICENSE-XYZ
+                              """
+                    ),
+                    'LICENSE-ABC': "ABC license",
+                    'LICENSE-XYZ': "XYZ license",
+                },
+                ['LICENSE-ABC', 'LICENSE-XYZ'],
+                [],
+            ),  # with licenses
+            (
+                {
+                    'setup.cfg': DALS(
+                        """
+                              [metadata]
+                              license_files = LICENSE-ABC, LICENSE-XYZ
+                              """
+                    ),
+                    'LICENSE-ABC': "ABC license",
+                    'LICENSE-XYZ': "XYZ license",
+                },
+                ['LICENSE-ABC', 'LICENSE-XYZ'],
+                [],
+            ),  # with commas
+            (
+                {
+                    'setup.cfg': DALS(
+                        """
+                              [metadata]
+                              license_files =
+                                  LICENSE-ABC
+                              """
+                    ),
+                    'LICENSE-ABC': "ABC license",
+                    'LICENSE-XYZ': "XYZ license",
+                },
+                ['LICENSE-ABC'],
+                ['LICENSE-XYZ'],
+            ),  # with one license
+            (
+                {
+                    'setup.cfg': DALS(
+                        """
+                              [metadata]
+                              license_files =
+                              """
+                    ),
+                    'LICENSE-ABC': "ABC license",
+                    'LICENSE-XYZ': "XYZ license",
+                },
+                [],
+                ['LICENSE-ABC', 'LICENSE-XYZ'],
+            ),  # empty
+            (
+                {
+                    'setup.cfg': DALS(
+                        """
+                              [metadata]
+                              license_files = LICENSE-XYZ
+                              """
+                    ),
+                    'LICENSE-ABC': "ABC license",
+                    'LICENSE-XYZ': "XYZ license",
+                },
+                ['LICENSE-XYZ'],
+                ['LICENSE-ABC'],
+            ),  # on same line
+            (
+                {
+                    'setup.cfg': DALS(
+                        """
+                              [metadata]
+                              license_files =
+                                  LICENSE-ABC
+                                  INVALID_LICENSE
+                              """
+                    ),
+                    'LICENSE-ABC': "Test license",
+                },
+                ['LICENSE-ABC'],
+                ['INVALID_LICENSE'],
+            ),  # with an invalid license
+            (
+                {
+                    'setup.cfg': DALS(
+                        """
+                              """
+                    ),
+                    'LICENSE': "Test license",
+                },
+                ['LICENSE'],
+                [],
+            ),  # no license_files attribute, LICENSE auto-included
+            (
+                {
+                    'setup.cfg': DALS(
+                        """
+                              [metadata]
+                              license_files = LICENSE
+                              """
+                    ),
+                    'MANIFEST.in': "exclude LICENSE",
+                    'LICENSE': "Test license",
+                },
+                ['LICENSE'],
+                [],
+            ),  # manifest is overwritten by license_files
+            (
+                {
+                    'setup.cfg': DALS(
+                        """
+                              [metadata]
+                              license_files =
+                                  LICENSE-ABC
+                                  LICENSE-XYZ
+                              """
+                    ),
+                    'MANIFEST.in': "exclude LICENSE-XYZ",
+                    'LICENSE-ABC': "ABC license",
+                    'LICENSE-XYZ': "XYZ license",
+                    # manifest is overwritten by license_files
+                },
+                ['LICENSE-ABC', 'LICENSE-XYZ'],
+                [],
+            ),
+            pytest.param(
+                {
+                    'setup.cfg': "",
+                    'LICENSE-ABC': "ABC license",
+                    'COPYING-ABC': "ABC copying",
+                    'NOTICE-ABC': "ABC notice",
+                    'AUTHORS-ABC': "ABC authors",
+                    'LICENCE-XYZ': "XYZ license",
+                    'LICENSE': "License",
+                    'INVALID-LICENSE': "Invalid license",
+                },
+                [
+                    'LICENSE-ABC',
+                    'COPYING-ABC',
+                    'NOTICE-ABC',
+                    'AUTHORS-ABC',
+                    'LICENCE-XYZ',
+                    'LICENSE',
+                ],
+                ['INVALID-LICENSE'],
+                # ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*')
+                id="default_glob_patterns",
+            ),
+            pytest.param(
+                {
+                    'setup.cfg': DALS(
+                        """
+                              [metadata]
+                              license_files =
+                                  LICENSE*
+                              """
+                    ),
+                    'LICENSE-ABC': "ABC license",
+                    'NOTICE-XYZ': "XYZ notice",
+                },
+                ['LICENSE-ABC'],
+                ['NOTICE-XYZ'],
+                id="no_default_glob_patterns",
+            ),
+            pytest.param(
+                {
+                    'setup.cfg': DALS(
+                        """
+                              [metadata]
+                              license_files =
+                                  LICENSE-ABC
+                                  LICENSE*
+                              """
+                    ),
+                    'LICENSE-ABC': "ABC license",
+                },
+                ['LICENSE-ABC'],
+                [],
+                id="files_only_added_once",
+            ),
+            pytest.param(
+                {
+                    'setup.cfg': DALS(
+                        """
+                              [metadata]
+                              license_files = **/LICENSE
+                              """
+                    ),
+                    'LICENSE': "ABC license",
+                    'LICENSE-OTHER': "Don't include",
+                    'vendor': {'LICENSE': "Vendor license"},
+                },
+                ['LICENSE', 'vendor/LICENSE'],
+                ['LICENSE-OTHER'],
+                id="recursive_glob",
+            ),
+        ],
+    )
+    def test_setup_cfg_license_files(
+        self, tmpdir_cwd, env, files, incl_licenses, excl_licenses
+    ):
+        self._create_project()
+        path.build(files)
+
+        environment.run_setup_py(
+            cmd=['egg_info'],
+            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+        )
+        egg_info_dir = os.path.join('.', 'foo.egg-info')
+
+        sources_text = Path(egg_info_dir, "SOURCES.txt").read_text(encoding="utf-8")
+        sources_lines = [line.strip() for line in sources_text.splitlines()]
+
+        for lf in incl_licenses:
+            assert sources_lines.count(lf) == 1
+
+        for lf in excl_licenses:
+            assert sources_lines.count(lf) == 0
+
+    @pytest.mark.parametrize(
+        ('files', 'incl_licenses', 'excl_licenses'),
+        [
+            (
+                {
+                    'setup.cfg': DALS(
+                        """
+                              [metadata]
+                              license_file =
+                              license_files =
+                              """
+                    ),
+                    'LICENSE-ABC': "ABC license",
+                    'LICENSE-XYZ': "XYZ license",
+                },
+                [],
+                ['LICENSE-ABC', 'LICENSE-XYZ'],
+            ),  # both empty
+            (
+                {
+                    'setup.cfg': DALS(
+                        """
+                              [metadata]
+                              license_file =
+                                  LICENSE-ABC
+                                  LICENSE-XYZ
+                              """
+                    ),
+                    'LICENSE-ABC': "ABC license",
+                    'LICENSE-XYZ': "XYZ license",
+                    # license_file is still singular
+                },
+                [],
+                ['LICENSE-ABC', 'LICENSE-XYZ'],
+            ),
+            (
+                {
+                    'setup.cfg': DALS(
+                        """
+                              [metadata]
+                              license_file = LICENSE-ABC
+                              license_files =
+                                  LICENSE-XYZ
+                                  LICENSE-PQR
+                              """
+                    ),
+                    'LICENSE-ABC': "ABC license",
+                    'LICENSE-PQR': "PQR license",
+                    'LICENSE-XYZ': "XYZ license",
+                },
+                ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'],
+                [],
+            ),  # combined
+            (
+                {
+                    'setup.cfg': DALS(
+                        """
+                              [metadata]
+                              license_file = LICENSE-ABC
+                              license_files =
+                                  LICENSE-ABC
+                                  LICENSE-XYZ
+                                  LICENSE-PQR
+                              """
+                    ),
+                    'LICENSE-ABC': "ABC license",
+                    'LICENSE-PQR': "PQR license",
+                    'LICENSE-XYZ': "XYZ license",
+                    # duplicate license
+                },
+                ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'],
+                [],
+            ),
+            (
+                {
+                    'setup.cfg': DALS(
+                        """
+                              [metadata]
+                              license_file = LICENSE-ABC
+                              license_files =
+                                  LICENSE-XYZ
+                              """
+                    ),
+                    'LICENSE-ABC': "ABC license",
+                    'LICENSE-PQR': "PQR license",
+                    'LICENSE-XYZ': "XYZ license",
+                    # combined subset
+                },
+                ['LICENSE-ABC', 'LICENSE-XYZ'],
+                ['LICENSE-PQR'],
+            ),
+            (
+                {
+                    'setup.cfg': DALS(
+                        """
+                              [metadata]
+                              license_file = LICENSE-ABC
+                              license_files =
+                                  LICENSE-XYZ
+                                  LICENSE-PQR
+                              """
+                    ),
+                    'LICENSE-PQR': "Test license",
+                    # with invalid licenses
+                },
+                ['LICENSE-PQR'],
+                ['LICENSE-ABC', 'LICENSE-XYZ'],
+            ),
+            (
+                {
+                    'setup.cfg': DALS(
+                        """
+                              [metadata]
+                              license_file = LICENSE-ABC
+                              license_files =
+                                LICENSE-PQR
+                                LICENSE-XYZ
+                              """
+                    ),
+                    'MANIFEST.in': "exclude LICENSE-ABC\nexclude LICENSE-PQR",
+                    'LICENSE-ABC': "ABC license",
+                    'LICENSE-PQR': "PQR license",
+                    'LICENSE-XYZ': "XYZ license",
+                    # manifest is overwritten
+                },
+                ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'],
+                [],
+            ),
+            pytest.param(
+                {
+                    'setup.cfg': DALS(
+                        """
+                              [metadata]
+                              license_file = LICENSE*
+                              """
+                    ),
+                    'LICENSE-ABC': "ABC license",
+                    'NOTICE-XYZ': "XYZ notice",
+                },
+                ['LICENSE-ABC'],
+                ['NOTICE-XYZ'],
+                id="no_default_glob_patterns",
+            ),
+            pytest.param(
+                {
+                    'setup.cfg': DALS(
+                        """
+                              [metadata]
+                              license_file = LICENSE*
+                              license_files =
+                                NOTICE*
+                              """
+                    ),
+                    'LICENSE-ABC': "ABC license",
+                    'NOTICE-ABC': "ABC notice",
+                    'AUTHORS-ABC': "ABC authors",
+                },
+                ['LICENSE-ABC', 'NOTICE-ABC'],
+                ['AUTHORS-ABC'],
+                id="combined_glob_patterrns",
+            ),
+        ],
+    )
+    def test_setup_cfg_license_file_license_files(
+        self, tmpdir_cwd, env, files, incl_licenses, excl_licenses
+    ):
+        self._create_project()
+        path.build(files)
+
+        environment.run_setup_py(
+            cmd=['egg_info'],
+            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+        )
+        egg_info_dir = os.path.join('.', 'foo.egg-info')
+
+        sources_text = Path(egg_info_dir, "SOURCES.txt").read_text(encoding="utf-8")
+        sources_lines = [line.strip() for line in sources_text.splitlines()]
+
+        for lf in incl_licenses:
+            assert sources_lines.count(lf) == 1
+
+        for lf in excl_licenses:
+            assert sources_lines.count(lf) == 0
+
+    def test_license_file_attr_pkg_info(self, tmpdir_cwd, env):
+        """All matched license files should have a corresponding License-File."""
+        self._create_project()
+        path.build({
+            "setup.cfg": DALS(
+                """
+                              [metadata]
+                              license_files =
+                                  NOTICE*
+                                  LICENSE*
+                                  **/LICENSE
+                              """
+            ),
+            "LICENSE-ABC": "ABC license",
+            "LICENSE-XYZ": "XYZ license",
+            "NOTICE": "included",
+            "IGNORE": "not include",
+            "vendor": {'LICENSE': "Vendor license"},
+        })
+
+        environment.run_setup_py(
+            cmd=['egg_info'],
+            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+        )
+        egg_info_dir = os.path.join('.', 'foo.egg-info')
+        with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
+            pkg_info_lines = fp.read().split('\n')
+        license_file_lines = [
+            line for line in pkg_info_lines if line.startswith('License-File:')
+        ]
+
+        # Only 'NOTICE', LICENSE-ABC', and 'LICENSE-XYZ' should have been matched
+        # Also assert that order from license_files is keeped
+        assert len(license_file_lines) == 4
+        assert "License-File: NOTICE" == license_file_lines[0]
+        assert "License-File: LICENSE-ABC" in license_file_lines[1:]
+        assert "License-File: LICENSE-XYZ" in license_file_lines[1:]
+        assert "License-File: vendor/LICENSE" in license_file_lines[3]
+
+    def test_metadata_version(self, tmpdir_cwd, env):
+        """Make sure latest metadata version is used by default."""
+        self._setup_script_with_requires("")
+        environment.run_setup_py(
+            cmd=['egg_info'],
+            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+            data_stream=1,
+        )
+        egg_info_dir = os.path.join('.', 'foo.egg-info')
+        with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
+            pkg_info_lines = fp.read().split('\n')
+        # Update metadata version if changed
+        assert self._extract_mv_version(pkg_info_lines) == (2, 4)
+
+    def test_long_description_content_type(self, tmpdir_cwd, env):
+        # Test that specifying a `long_description_content_type` keyword arg to
+        # the `setup` function results in writing a `Description-Content-Type`
+        # line to the `PKG-INFO` file in the `<distribution>.egg-info`
+        # directory.
+        # `Description-Content-Type` is described at
+        # https://github.com/pypa/python-packaging-user-guide/pull/258
+
+        self._setup_script_with_requires(
+            """long_description_content_type='text/markdown',"""
+        )
+        environ = os.environ.copy().update(
+            HOME=env.paths['home'],
+        )
+        environment.run_setup_py(
+            cmd=['egg_info'],
+            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+            data_stream=1,
+            env=environ,
+        )
+        egg_info_dir = os.path.join('.', 'foo.egg-info')
+        with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
+            pkg_info_lines = fp.read().split('\n')
+        expected_line = 'Description-Content-Type: text/markdown'
+        assert expected_line in pkg_info_lines
+        assert 'Metadata-Version: 2.4' in pkg_info_lines
+
+    def test_long_description(self, tmpdir_cwd, env):
+        # Test that specifying `long_description` and `long_description_content_type`
+        # keyword args to the `setup` function results in writing
+        # the description in the message payload of the `PKG-INFO` file
+        # in the `<distribution>.egg-info` directory.
+        self._setup_script_with_requires(
+            "long_description='This is a long description\\nover multiple lines',"
+            "long_description_content_type='text/markdown',"
+        )
+        environment.run_setup_py(
+            cmd=['egg_info'],
+            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+            data_stream=1,
+        )
+        egg_info_dir = os.path.join('.', 'foo.egg-info')
+        with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
+            pkg_info_lines = fp.read().split('\n')
+        assert 'Metadata-Version: 2.4' in pkg_info_lines
+        assert '' == pkg_info_lines[-1]  # last line should be empty
+        long_desc_lines = pkg_info_lines[pkg_info_lines.index('') :]
+        assert 'This is a long description' in long_desc_lines
+        assert 'over multiple lines' in long_desc_lines
+
+    def test_project_urls(self, tmpdir_cwd, env):
+        # Test that specifying a `project_urls` dict to the `setup`
+        # function results in writing multiple `Project-URL` lines to
+        # the `PKG-INFO` file in the `<distribution>.egg-info`
+        # directory.
+        # `Project-URL` is described at https://packaging.python.org
+        #     /specifications/core-metadata/#project-url-multiple-use
+
+        self._setup_script_with_requires(
+            """project_urls={
+                'Link One': 'https://example.com/one/',
+                'Link Two': 'https://example.com/two/',
+                },"""
+        )
+        environ = os.environ.copy().update(
+            HOME=env.paths['home'],
+        )
+        environment.run_setup_py(
+            cmd=['egg_info'],
+            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+            data_stream=1,
+            env=environ,
+        )
+        egg_info_dir = os.path.join('.', 'foo.egg-info')
+        with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
+            pkg_info_lines = fp.read().split('\n')
+        expected_line = 'Project-URL: Link One, https://example.com/one/'
+        assert expected_line in pkg_info_lines
+        expected_line = 'Project-URL: Link Two, https://example.com/two/'
+        assert expected_line in pkg_info_lines
+        assert self._extract_mv_version(pkg_info_lines) >= (1, 2)
+
+    def test_license(self, tmpdir_cwd, env):
+        """Test single line license."""
+        self._setup_script_with_requires("license='MIT',")
+        environment.run_setup_py(
+            cmd=['egg_info'],
+            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+            data_stream=1,
+        )
+        egg_info_dir = os.path.join('.', 'foo.egg-info')
+        with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
+            pkg_info_lines = fp.read().split('\n')
+        assert 'License: MIT' in pkg_info_lines
+
+    def test_license_escape(self, tmpdir_cwd, env):
+        """Test license is escaped correctly if longer than one line."""
+        self._setup_script_with_requires(
+            "license='This is a long license text \\nover multiple lines',"
+        )
+        environment.run_setup_py(
+            cmd=['egg_info'],
+            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+            data_stream=1,
+        )
+        egg_info_dir = os.path.join('.', 'foo.egg-info')
+        with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
+            pkg_info_lines = fp.read().split('\n')
+
+        assert 'License: This is a long license text ' in pkg_info_lines
+        assert '        over multiple lines' in pkg_info_lines
+        assert 'text \n        over multiple' in '\n'.join(pkg_info_lines)
+
+    def test_python_requires_egg_info(self, tmpdir_cwd, env):
+        self._setup_script_with_requires("""python_requires='>=2.7.12',""")
+        environ = os.environ.copy().update(
+            HOME=env.paths['home'],
+        )
+        environment.run_setup_py(
+            cmd=['egg_info'],
+            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+            data_stream=1,
+            env=environ,
+        )
+        egg_info_dir = os.path.join('.', 'foo.egg-info')
+        with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
+            pkg_info_lines = fp.read().split('\n')
+        assert 'Requires-Python: >=2.7.12' in pkg_info_lines
+        assert self._extract_mv_version(pkg_info_lines) >= (1, 2)
+
+    def test_manifest_maker_warning_suppression(self):
+        fixtures = [
+            "standard file not found: should have one of foo.py, bar.py",
+            "standard file 'setup.py' not found",
+        ]
+
+        for msg in fixtures:
+            assert manifest_maker._should_suppress_warning(msg)
+
+    def test_egg_info_includes_setup_py(self, tmpdir_cwd):
+        self._create_project()
+        dist = Distribution({"name": "foo", "version": "0.0.1"})
+        dist.script_name = "non_setup.py"
+        egg_info_instance = egg_info(dist)
+        egg_info_instance.finalize_options()
+        egg_info_instance.run()
+
+        assert 'setup.py' in egg_info_instance.filelist.files
+
+        with open(egg_info_instance.egg_info + "/SOURCES.txt", encoding="utf-8") as f:
+            sources = f.read().split('\n')
+            assert 'setup.py' in sources
+
+    def _run_egg_info_command(self, tmpdir_cwd, env, cmd=None, output=None):
+        environ = os.environ.copy().update(
+            HOME=env.paths['home'],
+        )
+        if cmd is None:
+            cmd = [
+                'egg_info',
+            ]
+        code, data = environment.run_setup_py(
+            cmd=cmd,
+            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+            data_stream=1,
+            env=environ,
+        )
+        assert not code, data
+
+        if output:
+            assert output in data
+
+    def test_egg_info_tag_only_once(self, tmpdir_cwd, env):
+        self._create_project()
+        path.build({
+            'setup.cfg': DALS(
+                """
+                              [egg_info]
+                              tag_build = dev
+                              tag_date = 0
+                              tag_svn_revision = 0
+                              """
+            ),
+        })
+        self._run_egg_info_command(tmpdir_cwd, env)
+        egg_info_dir = os.path.join('.', 'foo.egg-info')
+        with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
+            pkg_info_lines = fp.read().split('\n')
+        assert 'Version: 0.0.0.dev0' in pkg_info_lines
+
+
+class TestWriteEntries:
+    def test_invalid_entry_point(self, tmpdir_cwd, env):
+        dist = Distribution({"name": "foo", "version": "0.0.1"})
+        dist.entry_points = {"foo": "foo = invalid-identifier:foo"}
+        cmd = dist.get_command_obj("egg_info")
+        expected_msg = r"Problems to parse .*invalid-identifier.*"
+        with pytest.raises(errors.OptionError, match=expected_msg) as ex:
+            write_entries(cmd, "entry_points", "entry_points.txt")
+            assert "ensure entry-point follows the spec" in ex.value.args[0]
+
+    def test_valid_entry_point(self, tmpdir_cwd, env):
+        dist = Distribution({"name": "foo", "version": "0.0.1"})
+        dist.entry_points = {
+            "abc": "foo = bar:baz",
+            "def": ["faa = bor:boz"],
+        }
+        cmd = dist.get_command_obj("egg_info")
+        write_entries(cmd, "entry_points", "entry_points.txt")
+        content = Path("entry_points.txt").read_text(encoding="utf-8")
+        assert "[abc]\nfoo = bar:baz\n" in content
+        assert "[def]\nfaa = bor:boz\n" in content