codekingpro/portable-devtools
114k
1from __future__ import annotations2 3import ast4import glob5import os6import re7import stat8import sys9import time10from pathlib import Path11from unittest import mock12 13import pytest14from jaraco import path15 16from setuptools import errors17from setuptools.command.egg_info import egg_info, manifest_maker, write_entries18from setuptools.dist import Distribution19 20from . import contexts, environment21from .textwrap import DALS22 23 24class Environment(str):25 pass26 27 28@pytest.fixture29def env():30 with contexts.tempdir(prefix='setuptools-test.') as env_dir:31 env = Environment(env_dir)32 os.chmod(env_dir, stat.S_IRWXU)33 subs = 'home', 'lib', 'scripts', 'data', 'egg-base'34 env.paths = dict((dirname, os.path.join(env_dir, dirname)) for dirname in subs)35 list(map(os.mkdir, env.paths.values()))36 path.build({37 env.paths['home']: {38 '.pydistutils.cfg': DALS(39 """40 [egg_info]41 egg-base = {egg-base}42 """.format(**env.paths)43 )44 }45 })46 yield env47 48 49class TestEggInfo:50 setup_script = DALS(51 """52 from setuptools import setup53 54 setup(55 name='foo',56 py_modules=['hello'],57 entry_points={'console_scripts': ['hi = hello.run']},58 zip_safe=False,59 )60 """61 )62 63 def _create_project(self):64 path.build({65 'setup.py': self.setup_script,66 'hello.py': DALS(67 """68 def run():69 print('hello')70 """71 ),72 })73 74 @staticmethod75 def _extract_mv_version(pkg_info_lines: list[str]) -> tuple[int, int]:76 version_str = pkg_info_lines[0].split(' ')[1]77 major, minor = map(int, version_str.split('.')[:2])78 return major, minor79 80 def test_egg_info_save_version_info_setup_empty(self, tmpdir_cwd, env):81 """82 When the egg_info section is empty or not present, running83 save_version_info should add the settings to the setup.cfg84 in a deterministic order.85 """86 setup_cfg = os.path.join(env.paths['home'], 'setup.cfg')87 dist = Distribution()88 ei = egg_info(dist)89 ei.initialize_options()90 ei.save_version_info(setup_cfg)91 92 with open(setup_cfg, 'r', encoding="utf-8") as f:93 content = f.read()94 95 assert '[egg_info]' in content96 assert 'tag_build =' in content97 assert 'tag_date = 0' in content98 99 expected_order = (100 'tag_build',101 'tag_date',102 )103 104 self._validate_content_order(content, expected_order)105 106 @staticmethod107 def _validate_content_order(content, expected):108 """109 Assert that the strings in expected appear in content110 in order.111 """112 pattern = '.*'.join(expected)113 flags = re.MULTILINE | re.DOTALL114 assert re.search(pattern, content, flags)115 116 def test_egg_info_save_version_info_setup_defaults(self, tmpdir_cwd, env):117 """118 When running save_version_info on an existing setup.cfg119 with the 'default' values present from a previous run,120 the file should remain unchanged.121 """122 setup_cfg = os.path.join(env.paths['home'], 'setup.cfg')123 path.build({124 setup_cfg: DALS(125 """126 [egg_info]127 tag_build =128 tag_date = 0129 """130 ),131 })132 dist = Distribution()133 ei = egg_info(dist)134 ei.initialize_options()135 ei.save_version_info(setup_cfg)136 137 with open(setup_cfg, 'r', encoding="utf-8") as f:138 content = f.read()139 140 assert '[egg_info]' in content141 assert 'tag_build =' in content142 assert 'tag_date = 0' in content143 144 expected_order = (145 'tag_build',146 'tag_date',147 )148 149 self._validate_content_order(content, expected_order)150 151 def test_expected_files_produced(self, tmpdir_cwd, env):152 self._create_project()153 154 self._run_egg_info_command(tmpdir_cwd, env)155 actual = os.listdir('foo.egg-info')156 157 expected = [158 'PKG-INFO',159 'SOURCES.txt',160 'dependency_links.txt',161 'entry_points.txt',162 'not-zip-safe',163 'top_level.txt',164 ]165 assert sorted(actual) == expected166 167 def test_handling_utime_error(self, tmpdir_cwd, env):168 dist = Distribution()169 ei = egg_info(dist)170 utime_patch = mock.patch('os.utime', side_effect=OSError("TEST"))171 mkpath_patch = mock.patch(172 'setuptools.command.egg_info.egg_info.mkpath', return_val=None173 )174 175 with utime_patch, mkpath_patch:176 import distutils.errors177 178 msg = r"Cannot update time stamp of directory 'None'"179 with pytest.raises(distutils.errors.DistutilsFileError, match=msg):180 ei.run()181 182 def test_license_is_a_string(self, tmpdir_cwd, env):183 setup_config = DALS(184 """185 [metadata]186 name=foo187 version=0.0.1188 license=file:MIT189 """190 )191 192 setup_script = DALS(193 """194 from setuptools import setup195 196 setup()197 """198 )199 200 path.build({201 'setup.py': setup_script,202 'setup.cfg': setup_config,203 })204 205 # This command should fail with a ValueError, but because it's206 # currently configured to use a subprocess, the actual traceback207 # object is lost and we need to parse it from stderr208 with pytest.raises(AssertionError) as exc:209 self._run_egg_info_command(tmpdir_cwd, env)210 211 # The only argument to the assertion error should be a traceback212 # containing a ValueError213 assert 'ValueError' in exc.value.args[0]214 215 def test_rebuilt(self, tmpdir_cwd, env):216 """Ensure timestamps are updated when the command is re-run."""217 self._create_project()218 219 self._run_egg_info_command(tmpdir_cwd, env)220 timestamp_a = os.path.getmtime('foo.egg-info')221 222 # arbitrary sleep just to handle *really* fast systems223 time.sleep(0.001)224 225 self._run_egg_info_command(tmpdir_cwd, env)226 timestamp_b = os.path.getmtime('foo.egg-info')227 228 assert timestamp_a != timestamp_b229 230 def test_manifest_template_is_read(self, tmpdir_cwd, env):231 self._create_project()232 path.build({233 'MANIFEST.in': DALS(234 """235 recursive-include docs *.rst236 """237 ),238 'docs': {239 'usage.rst': "Run 'hi'",240 },241 })242 self._run_egg_info_command(tmpdir_cwd, env)243 egg_info_dir = os.path.join('.', 'foo.egg-info')244 sources_txt = os.path.join(egg_info_dir, 'SOURCES.txt')245 with open(sources_txt, encoding="utf-8") as f:246 assert 'docs/usage.rst' in f.read().split('\n')247 248 def _setup_script_with_requires(self, requires, use_setup_cfg=False):249 setup_script = DALS(250 """251 from setuptools import setup252 253 setup(name='foo', zip_safe=False, %s)254 """255 ) % ('' if use_setup_cfg else requires)256 setup_config = requires if use_setup_cfg else ''257 path.build({258 'setup.py': setup_script,259 'setup.cfg': setup_config,260 })261 262 mismatch_marker = f"python_version<'{sys.version_info[0]}'"263 # Alternate equivalent syntax.264 mismatch_marker_alternate = f'python_version < "{sys.version_info[0]}"'265 invalid_marker = "<=>++"266 267 class RequiresTestHelper:268 @staticmethod269 def parametrize(*test_list, **format_dict):270 idlist = []271 argvalues = []272 for test in test_list:273 test_params = test.lstrip().split('\n\n', 3)274 name_kwargs = test_params.pop(0).split('\n')275 if len(name_kwargs) > 1:276 val = name_kwargs[1].strip()277 install_cmd_kwargs = ast.literal_eval(val)278 else:279 install_cmd_kwargs = {}280 name = name_kwargs[0].strip()281 setup_py_requires, setup_cfg_requires, expected_requires = [282 DALS(a).format(**format_dict) for a in test_params283 ]284 for id_, requires, use_cfg in (285 (name, setup_py_requires, False),286 (name + '_in_setup_cfg', setup_cfg_requires, True),287 ):288 idlist.append(id_)289 marks = ()290 if requires.startswith('@xfail\n'):291 requires = requires[7:]292 marks = pytest.mark.xfail293 argvalues.append(294 pytest.param(295 requires,296 use_cfg,297 expected_requires,298 install_cmd_kwargs,299 marks=marks,300 )301 )302 return pytest.mark.parametrize(303 (304 "requires",305 "use_setup_cfg",306 "expected_requires",307 "install_cmd_kwargs",308 ),309 argvalues,310 ids=idlist,311 )312 313 @RequiresTestHelper.parametrize(314 # Format of a test:315 #316 # id317 # install_cmd_kwargs [optional]318 #319 # requires block (when used in setup.py)320 #321 # requires block (when used in setup.cfg)322 #323 # expected contents of requires.txt324 """325 install_requires_deterministic326 327 install_requires=["wheel>=0.5", "pytest"]328 329 [options]330 install_requires =331 wheel>=0.5332 pytest333 334 wheel>=0.5335 pytest336 """,337 """338 install_requires_ordered339 340 install_requires=["pytest>=3.0.2,!=10.9999"]341 342 [options]343 install_requires =344 pytest>=3.0.2,!=10.9999345 346 pytest!=10.9999,>=3.0.2347 """,348 """349 install_requires_with_marker350 351 install_requires=["barbazquux;{mismatch_marker}"],352 353 [options]354 install_requires =355 barbazquux; {mismatch_marker}356 357 [:{mismatch_marker_alternate}]358 barbazquux359 """,360 """361 install_requires_with_extra362 {'cmd': ['egg_info']}363 364 install_requires=["barbazquux [test]"],365 366 [options]367 install_requires =368 barbazquux [test]369 370 barbazquux[test]371 """,372 """373 install_requires_with_extra_and_marker374 375 install_requires=["barbazquux [test]; {mismatch_marker}"],376 377 [options]378 install_requires =379 barbazquux [test]; {mismatch_marker}380 381 [:{mismatch_marker_alternate}]382 barbazquux[test]383 """,384 """385 setup_requires_with_markers386 387 setup_requires=["barbazquux;{mismatch_marker}"],388 389 [options]390 setup_requires =391 barbazquux; {mismatch_marker}392 393 """,394 """395 extras_require_with_extra396 {'cmd': ['egg_info']}397 398 extras_require={{"extra": ["barbazquux [test]"]}},399 400 [options.extras_require]401 extra = barbazquux [test]402 403 [extra]404 barbazquux[test]405 """,406 """407 extras_require_with_extra_and_marker_in_req408 409 extras_require={{"extra": ["barbazquux [test]; {mismatch_marker}"]}},410 411 [options.extras_require]412 extra =413 barbazquux [test]; {mismatch_marker}414 415 [extra]416 417 [extra:{mismatch_marker_alternate}]418 barbazquux[test]419 """,420 # FIXME: ConfigParser does not allow : in key names!421 """422 extras_require_with_marker423 424 extras_require={{":{mismatch_marker}": ["barbazquux"]}},425 426 @xfail427 [options.extras_require]428 :{mismatch_marker} = barbazquux429 430 [:{mismatch_marker}]431 barbazquux432 """,433 """434 extras_require_with_marker_in_req435 436 extras_require={{"extra": ["barbazquux; {mismatch_marker}"]}},437 438 [options.extras_require]439 extra =440 barbazquux; {mismatch_marker}441 442 [extra]443 444 [extra:{mismatch_marker_alternate}]445 barbazquux446 """,447 """448 extras_require_with_empty_section449 450 extras_require={{"empty": []}},451 452 [options.extras_require]453 empty =454 455 [empty]456 """,457 # Format arguments.458 invalid_marker=invalid_marker,459 mismatch_marker=mismatch_marker,460 mismatch_marker_alternate=mismatch_marker_alternate,461 )462 def test_requires(463 self,464 tmpdir_cwd,465 env,466 requires,467 use_setup_cfg,468 expected_requires,469 install_cmd_kwargs,470 ):471 self._setup_script_with_requires(requires, use_setup_cfg)472 self._run_egg_info_command(tmpdir_cwd, env, **install_cmd_kwargs)473 egg_info_dir = os.path.join('.', 'foo.egg-info')474 requires_txt = os.path.join(egg_info_dir, 'requires.txt')475 if os.path.exists(requires_txt):476 with open(requires_txt, encoding="utf-8") as fp:477 install_requires = fp.read()478 else:479 install_requires = ''480 assert install_requires.lstrip() == expected_requires481 assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []482 483 def test_install_requires_unordered_disallowed(self, tmpdir_cwd, env):484 """485 Packages that pass unordered install_requires sequences486 should be rejected as they produce non-deterministic487 builds. See #458.488 """489 req = 'install_requires={"fake-factory==0.5.2", "pytz"}'490 self._setup_script_with_requires(req)491 with pytest.raises(AssertionError):492 self._run_egg_info_command(tmpdir_cwd, env)493 494 def test_extras_require_with_invalid_marker(self, tmpdir_cwd, env):495 tmpl = 'extras_require={{":{marker}": ["barbazquux"]}},'496 req = tmpl.format(marker=self.invalid_marker)497 self._setup_script_with_requires(req)498 with pytest.raises(AssertionError):499 self._run_egg_info_command(tmpdir_cwd, env)500 assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []501 502 def test_extras_require_with_invalid_marker_in_req(self, tmpdir_cwd, env):503 tmpl = 'extras_require={{"extra": ["barbazquux; {marker}"]}},'504 req = tmpl.format(marker=self.invalid_marker)505 self._setup_script_with_requires(req)506 with pytest.raises(AssertionError):507 self._run_egg_info_command(tmpdir_cwd, env)508 assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []509 510 def test_provides_extra(self, tmpdir_cwd, env):511 self._setup_script_with_requires('extras_require={"foobar": ["barbazquux"]},')512 environ = os.environ.copy().update(513 HOME=env.paths['home'],514 )515 environment.run_setup_py(516 cmd=['egg_info'],517 pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),518 data_stream=1,519 env=environ,520 )521 egg_info_dir = os.path.join('.', 'foo.egg-info')522 with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:523 pkg_info_lines = fp.read().split('\n')524 assert 'Provides-Extra: foobar' in pkg_info_lines525 assert 'Metadata-Version: 2.4' in pkg_info_lines526 527 def test_doesnt_provides_extra(self, tmpdir_cwd, env):528 self._setup_script_with_requires(529 """install_requires=["spam ; python_version<'3.6'"]"""530 )531 environ = os.environ.copy().update(532 HOME=env.paths['home'],533 )534 environment.run_setup_py(535 cmd=['egg_info'],536 pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),537 data_stream=1,538 env=environ,539 )540 egg_info_dir = os.path.join('.', 'foo.egg-info')541 with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:542 pkg_info_text = fp.read()543 assert 'Provides-Extra:' not in pkg_info_text544 545 @pytest.mark.parametrize(546 ('files', 'license_in_sources'),547 [548 (549 {550 'setup.cfg': DALS(551 """552 [metadata]553 license_file = LICENSE554 """555 ),556 'LICENSE': "Test license",557 },558 True,559 ), # with license560 (561 {562 'setup.cfg': DALS(563 """564 [metadata]565 license_file = INVALID_LICENSE566 """567 ),568 'LICENSE': "Test license",569 },570 False,571 ), # with an invalid license572 (573 {574 'setup.cfg': DALS(575 """576 """577 ),578 'LICENSE': "Test license",579 },580 True,581 ), # no license_file attribute, LICENSE auto-included582 (583 {584 'setup.cfg': DALS(585 """586 [metadata]587 license_file = LICENSE588 """589 ),590 'MANIFEST.in': "exclude LICENSE",591 'LICENSE': "Test license",592 },593 True,594 ), # manifest is overwritten by license_file595 pytest.param(596 {597 'setup.cfg': DALS(598 """599 [metadata]600 license_file = LICEN[CS]E*601 """602 ),603 'LICENSE': "Test license",604 },605 True,606 id="glob_pattern",607 ),608 ],609 )610 def test_setup_cfg_license_file(self, tmpdir_cwd, env, files, license_in_sources):611 self._create_project()612 path.build(files)613 614 environment.run_setup_py(615 cmd=['egg_info'],616 pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),617 )618 egg_info_dir = os.path.join('.', 'foo.egg-info')619 620 sources_text = Path(egg_info_dir, "SOURCES.txt").read_text(encoding="utf-8")621 622 if license_in_sources:623 assert 'LICENSE' in sources_text624 else:625 assert 'LICENSE' not in sources_text626 # for invalid license test627 assert 'INVALID_LICENSE' not in sources_text628 629 @pytest.mark.parametrize(630 ('files', 'incl_licenses', 'excl_licenses'),631 [632 (633 {634 'setup.cfg': DALS(635 """636 [metadata]637 license_files =638 LICENSE-ABC639 LICENSE-XYZ640 """641 ),642 'LICENSE-ABC': "ABC license",643 'LICENSE-XYZ': "XYZ license",644 },645 ['LICENSE-ABC', 'LICENSE-XYZ'],646 [],647 ), # with licenses648 (649 {650 'setup.cfg': DALS(651 """652 [metadata]653 license_files = LICENSE-ABC, LICENSE-XYZ654 """655 ),656 'LICENSE-ABC': "ABC license",657 'LICENSE-XYZ': "XYZ license",658 },659 ['LICENSE-ABC', 'LICENSE-XYZ'],660 [],661 ), # with commas662 (663 {664 'setup.cfg': DALS(665 """666 [metadata]667 license_files =668 LICENSE-ABC669 """670 ),671 'LICENSE-ABC': "ABC license",672 'LICENSE-XYZ': "XYZ license",673 },674 ['LICENSE-ABC'],675 ['LICENSE-XYZ'],676 ), # with one license677 (678 {679 'setup.cfg': DALS(680 """681 [metadata]682 license_files =683 """684 ),685 'LICENSE-ABC': "ABC license",686 'LICENSE-XYZ': "XYZ license",687 },688 [],689 ['LICENSE-ABC', 'LICENSE-XYZ'],690 ), # empty691 (692 {693 'setup.cfg': DALS(694 """695 [metadata]696 license_files = LICENSE-XYZ697 """698 ),699 'LICENSE-ABC': "ABC license",700 'LICENSE-XYZ': "XYZ license",701 },702 ['LICENSE-XYZ'],703 ['LICENSE-ABC'],704 ), # on same line705 (706 {707 'setup.cfg': DALS(708 """709 [metadata]710 license_files =711 LICENSE-ABC712 INVALID_LICENSE713 """714 ),715 'LICENSE-ABC': "Test license",716 },717 ['LICENSE-ABC'],718 ['INVALID_LICENSE'],719 ), # with an invalid license720 (721 {722 'setup.cfg': DALS(723 """724 """725 ),726 'LICENSE': "Test license",727 },728 ['LICENSE'],729 [],730 ), # no license_files attribute, LICENSE auto-included731 (732 {733 'setup.cfg': DALS(734 """735 [metadata]736 license_files = LICENSE737 """738 ),739 'MANIFEST.in': "exclude LICENSE",740 'LICENSE': "Test license",741 },742 ['LICENSE'],743 [],744 ), # manifest is overwritten by license_files745 (746 {747 'setup.cfg': DALS(748 """749 [metadata]750 license_files =751 LICENSE-ABC752 LICENSE-XYZ753 """754 ),755 'MANIFEST.in': "exclude LICENSE-XYZ",756 'LICENSE-ABC': "ABC license",757 'LICENSE-XYZ': "XYZ license",758 # manifest is overwritten by license_files759 },760 ['LICENSE-ABC', 'LICENSE-XYZ'],761 [],762 ),763 pytest.param(764 {765 'setup.cfg': "",766 'LICENSE-ABC': "ABC license",767 'COPYING-ABC': "ABC copying",768 'NOTICE-ABC': "ABC notice",769 'AUTHORS-ABC': "ABC authors",770 'LICENCE-XYZ': "XYZ license",771 'LICENSE': "License",772 'INVALID-LICENSE': "Invalid license",773 },774 [775 'LICENSE-ABC',776 'COPYING-ABC',777 'NOTICE-ABC',778 'AUTHORS-ABC',779 'LICENCE-XYZ',780 'LICENSE',781 ],782 ['INVALID-LICENSE'],783 # ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*')784 id="default_glob_patterns",785 ),786 pytest.param(787 {788 'setup.cfg': DALS(789 """790 [metadata]791 license_files =792 LICENSE*793 """794 ),795 'LICENSE-ABC': "ABC license",796 'NOTICE-XYZ': "XYZ notice",797 },798 ['LICENSE-ABC'],799 ['NOTICE-XYZ'],800 id="no_default_glob_patterns",801 ),802 pytest.param(803 {804 'setup.cfg': DALS(805 """806 [metadata]807 license_files =808 LICENSE-ABC809 LICENSE*810 """811 ),812 'LICENSE-ABC': "ABC license",813 },814 ['LICENSE-ABC'],815 [],816 id="files_only_added_once",817 ),818 pytest.param(819 {820 'setup.cfg': DALS(821 """822 [metadata]823 license_files = **/LICENSE824 """825 ),826 'LICENSE': "ABC license",827 'LICENSE-OTHER': "Don't include",828 'vendor': {'LICENSE': "Vendor license"},829 },830 ['LICENSE', 'vendor/LICENSE'],831 ['LICENSE-OTHER'],832 id="recursive_glob",833 ),834 ],835 )836 def test_setup_cfg_license_files(837 self, tmpdir_cwd, env, files, incl_licenses, excl_licenses838 ):839 self._create_project()840 path.build(files)841 842 environment.run_setup_py(843 cmd=['egg_info'],844 pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),845 )846 egg_info_dir = os.path.join('.', 'foo.egg-info')847 848 sources_text = Path(egg_info_dir, "SOURCES.txt").read_text(encoding="utf-8")849 sources_lines = [line.strip() for line in sources_text.splitlines()]850 851 for lf in incl_licenses:852 assert sources_lines.count(lf) == 1853 854 for lf in excl_licenses:855 assert sources_lines.count(lf) == 0856 857 @pytest.mark.parametrize(858 ('files', 'incl_licenses', 'excl_licenses'),859 [860 (861 {862 'setup.cfg': DALS(863 """864 [metadata]865 license_file =866 license_files =867 """868 ),869 'LICENSE-ABC': "ABC license",870 'LICENSE-XYZ': "XYZ license",871 },872 [],873 ['LICENSE-ABC', 'LICENSE-XYZ'],874 ), # both empty875 (876 {877 'setup.cfg': DALS(878 """879 [metadata]880 license_file =881 LICENSE-ABC882 LICENSE-XYZ883 """884 ),885 'LICENSE-ABC': "ABC license",886 'LICENSE-XYZ': "XYZ license",887 # license_file is still singular888 },889 [],890 ['LICENSE-ABC', 'LICENSE-XYZ'],891 ),892 (893 {894 'setup.cfg': DALS(895 """896 [metadata]897 license_file = LICENSE-ABC898 license_files =899 LICENSE-XYZ900 LICENSE-PQR901 """902 ),903 'LICENSE-ABC': "ABC license",904 'LICENSE-PQR': "PQR license",905 'LICENSE-XYZ': "XYZ license",906 },907 ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'],908 [],909 ), # combined910 (911 {912 'setup.cfg': DALS(913 """914 [metadata]915 license_file = LICENSE-ABC916 license_files =917 LICENSE-ABC918 LICENSE-XYZ919 LICENSE-PQR920 """921 ),922 'LICENSE-ABC': "ABC license",923 'LICENSE-PQR': "PQR license",924 'LICENSE-XYZ': "XYZ license",925 # duplicate license926 },927 ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'],928 [],929 ),930 (931 {932 'setup.cfg': DALS(933 """934 [metadata]935 license_file = LICENSE-ABC936 license_files =937 LICENSE-XYZ938 """939 ),940 'LICENSE-ABC': "ABC license",941 'LICENSE-PQR': "PQR license",942 'LICENSE-XYZ': "XYZ license",943 # combined subset944 },945 ['LICENSE-ABC', 'LICENSE-XYZ'],946 ['LICENSE-PQR'],947 ),948 (949 {950 'setup.cfg': DALS(951 """952 [metadata]953 license_file = LICENSE-ABC954 license_files =955 LICENSE-XYZ956 LICENSE-PQR957 """958 ),959 'LICENSE-PQR': "Test license",960 # with invalid licenses961 },962 ['LICENSE-PQR'],963 ['LICENSE-ABC', 'LICENSE-XYZ'],964 ),965 (966 {967 'setup.cfg': DALS(968 """969 [metadata]970 license_file = LICENSE-ABC971 license_files =972 LICENSE-PQR973 LICENSE-XYZ974 """975 ),976 'MANIFEST.in': "exclude LICENSE-ABC\nexclude LICENSE-PQR",977 'LICENSE-ABC': "ABC license",978 'LICENSE-PQR': "PQR license",979 'LICENSE-XYZ': "XYZ license",980 # manifest is overwritten981 },982 ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'],983 [],984 ),985 pytest.param(986 {987 'setup.cfg': DALS(988 """989 [metadata]990 license_file = LICENSE*991 """992 ),993 'LICENSE-ABC': "ABC license",994 'NOTICE-XYZ': "XYZ notice",995 },996 ['LICENSE-ABC'],997 ['NOTICE-XYZ'],998 id="no_default_glob_patterns",999 ),1000 pytest.param(1001 {1002 'setup.cfg': DALS(1003 """1004 [metadata]1005 license_file = LICENSE*1006 license_files =1007 NOTICE*1008 """1009 ),1010 'LICENSE-ABC': "ABC license",1011 'NOTICE-ABC': "ABC notice",1012 'AUTHORS-ABC': "ABC authors",1013 },1014 ['LICENSE-ABC', 'NOTICE-ABC'],1015 ['AUTHORS-ABC'],1016 id="combined_glob_patterrns",1017 ),1018 ],1019 )1020 def test_setup_cfg_license_file_license_files(1021 self, tmpdir_cwd, env, files, incl_licenses, excl_licenses1022 ):1023 self._create_project()1024 path.build(files)1025 1026 environment.run_setup_py(1027 cmd=['egg_info'],1028 pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),1029 )1030 egg_info_dir = os.path.join('.', 'foo.egg-info')1031 1032 sources_text = Path(egg_info_dir, "SOURCES.txt").read_text(encoding="utf-8")1033 sources_lines = [line.strip() for line in sources_text.splitlines()]1034 1035 for lf in incl_licenses:1036 assert sources_lines.count(lf) == 11037 1038 for lf in excl_licenses:1039 assert sources_lines.count(lf) == 01040 1041 def test_license_file_attr_pkg_info(self, tmpdir_cwd, env):1042 """All matched license files should have a corresponding License-File."""1043 self._create_project()1044 path.build({1045 "setup.cfg": DALS(1046 """1047 [metadata]1048 license_files =1049 NOTICE*1050 LICENSE*1051 **/LICENSE1052 """1053 ),1054 "LICENSE-ABC": "ABC license",1055 "LICENSE-XYZ": "XYZ license",1056 "NOTICE": "included",1057 "IGNORE": "not include",1058 "vendor": {'LICENSE': "Vendor license"},1059 })1060 1061 environment.run_setup_py(1062 cmd=['egg_info'],1063 pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),1064 )1065 egg_info_dir = os.path.join('.', 'foo.egg-info')1066 with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:1067 pkg_info_lines = fp.read().split('\n')1068 license_file_lines = [1069 line for line in pkg_info_lines if line.startswith('License-File:')1070 ]1071 1072 # Only 'NOTICE', LICENSE-ABC', and 'LICENSE-XYZ' should have been matched1073 # Also assert that order from license_files is keeped1074 assert len(license_file_lines) == 41075 assert "License-File: NOTICE" == license_file_lines[0]1076 assert "License-File: LICENSE-ABC" in license_file_lines[1:]1077 assert "License-File: LICENSE-XYZ" in license_file_lines[1:]1078 assert "License-File: vendor/LICENSE" in license_file_lines[3]1079 1080 def test_metadata_version(self, tmpdir_cwd, env):1081 """Make sure latest metadata version is used by default."""1082 self._setup_script_with_requires("")1083 environment.run_setup_py(1084 cmd=['egg_info'],1085 pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),1086 data_stream=1,1087 )1088 egg_info_dir = os.path.join('.', 'foo.egg-info')1089 with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:1090 pkg_info_lines = fp.read().split('\n')1091 # Update metadata version if changed1092 assert self._extract_mv_version(pkg_info_lines) == (2, 4)1093 1094 def test_long_description_content_type(self, tmpdir_cwd, env):1095 # Test that specifying a `long_description_content_type` keyword arg to1096 # the `setup` function results in writing a `Description-Content-Type`1097 # line to the `PKG-INFO` file in the `<distribution>.egg-info`1098 # directory.1099 # `Description-Content-Type` is described at1100 # https://github.com/pypa/python-packaging-user-guide/pull/2581101 1102 self._setup_script_with_requires(1103 """long_description_content_type='text/markdown',"""1104 )1105 environ = os.environ.copy().update(1106 HOME=env.paths['home'],1107 )1108 environment.run_setup_py(1109 cmd=['egg_info'],1110 pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),1111 data_stream=1,1112 env=environ,1113 )1114 egg_info_dir = os.path.join('.', 'foo.egg-info')1115 with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:1116 pkg_info_lines = fp.read().split('\n')1117 expected_line = 'Description-Content-Type: text/markdown'1118 assert expected_line in pkg_info_lines1119 assert 'Metadata-Version: 2.4' in pkg_info_lines1120 1121 def test_long_description(self, tmpdir_cwd, env):1122 # Test that specifying `long_description` and `long_description_content_type`1123 # keyword args to the `setup` function results in writing1124 # the description in the message payload of the `PKG-INFO` file1125 # in the `<distribution>.egg-info` directory.1126 self._setup_script_with_requires(1127 "long_description='This is a long description\\nover multiple lines',"1128 "long_description_content_type='text/markdown',"1129 )1130 environment.run_setup_py(1131 cmd=['egg_info'],1132 pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),1133 data_stream=1,1134 )1135 egg_info_dir = os.path.join('.', 'foo.egg-info')1136 with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:1137 pkg_info_lines = fp.read().split('\n')1138 assert 'Metadata-Version: 2.4' in pkg_info_lines1139 assert '' == pkg_info_lines[-1] # last line should be empty1140 long_desc_lines = pkg_info_lines[pkg_info_lines.index('') :]1141 assert 'This is a long description' in long_desc_lines1142 assert 'over multiple lines' in long_desc_lines1143 1144 def test_project_urls(self, tmpdir_cwd, env):1145 # Test that specifying a `project_urls` dict to the `setup`1146 # function results in writing multiple `Project-URL` lines to1147 # the `PKG-INFO` file in the `<distribution>.egg-info`1148 # directory.1149 # `Project-URL` is described at https://packaging.python.org1150 # /specifications/core-metadata/#project-url-multiple-use1151 1152 self._setup_script_with_requires(1153 """project_urls={1154 'Link One': 'https://example.com/one/',1155 'Link Two': 'https://example.com/two/',1156 },"""1157 )1158 environ = os.environ.copy().update(1159 HOME=env.paths['home'],1160 )1161 environment.run_setup_py(1162 cmd=['egg_info'],1163 pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),1164 data_stream=1,1165 env=environ,1166 )1167 egg_info_dir = os.path.join('.', 'foo.egg-info')1168 with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:1169 pkg_info_lines = fp.read().split('\n')1170 expected_line = 'Project-URL: Link One, https://example.com/one/'1171 assert expected_line in pkg_info_lines1172 expected_line = 'Project-URL: Link Two, https://example.com/two/'1173 assert expected_line in pkg_info_lines1174 assert self._extract_mv_version(pkg_info_lines) >= (1, 2)1175 1176 def test_license(self, tmpdir_cwd, env):1177 """Test single line license."""1178 self._setup_script_with_requires("license='MIT',")1179 environment.run_setup_py(1180 cmd=['egg_info'],1181 pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),1182 data_stream=1,1183 )1184 egg_info_dir = os.path.join('.', 'foo.egg-info')1185 with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:1186 pkg_info_lines = fp.read().split('\n')1187 assert 'License: MIT' in pkg_info_lines1188 1189 def test_license_escape(self, tmpdir_cwd, env):1190 """Test license is escaped correctly if longer than one line."""1191 self._setup_script_with_requires(1192 "license='This is a long license text \\nover multiple lines',"1193 )1194 environment.run_setup_py(1195 cmd=['egg_info'],1196 pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),1197 data_stream=1,1198 )1199 egg_info_dir = os.path.join('.', 'foo.egg-info')1200 with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: