aboutsummaryrefslogtreecommitdiff
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