1import sys 2import ast 3import os 4import glob 5import re 6import stat 7import time 8from typing import List, Tuple 9 10import pytest 11from jaraco import path 12 13from setuptools.command.egg_info import ( 14 egg_info, manifest_maker, EggInfoDeprecationWarning, get_pkg_info_revision, 15) 16from setuptools.dist import Distribution 17 18from . import environment 19from .textwrap import DALS 20from . import contexts 21 22 23class Environment(str): 24 pass 25 26 27class TestEggInfo: 28 29 setup_script = DALS(""" 30 from setuptools import setup 31 32 setup( 33 name='foo', 34 py_modules=['hello'], 35 entry_points={'console_scripts': ['hi = hello.run']}, 36 zip_safe=False, 37 ) 38 """) 39 40 def _create_project(self): 41 path.build({ 42 'setup.py': self.setup_script, 43 'hello.py': DALS(""" 44 def run(): 45 print('hello') 46 """) 47 }) 48 49 @staticmethod 50 def _extract_mv_version(pkg_info_lines: List[str]) -> Tuple[int, int]: 51 version_str = pkg_info_lines[0].split(' ')[1] 52 return tuple(map(int, version_str.split('.')[:2])) 53 54 @pytest.fixture 55 def env(self): 56 with contexts.tempdir(prefix='setuptools-test.') as env_dir: 57 env = Environment(env_dir) 58 os.chmod(env_dir, stat.S_IRWXU) 59 subs = 'home', 'lib', 'scripts', 'data', 'egg-base' 60 env.paths = dict( 61 (dirname, os.path.join(env_dir, dirname)) 62 for dirname in subs 63 ) 64 list(map(os.mkdir, env.paths.values())) 65 path.build({ 66 env.paths['home']: { 67 '.pydistutils.cfg': DALS(""" 68 [egg_info] 69 egg-base = %(egg-base)s 70 """ % env.paths) 71 } 72 }) 73 yield env 74 75 def test_egg_info_save_version_info_setup_empty(self, tmpdir_cwd, env): 76 """ 77 When the egg_info section is empty or not present, running 78 save_version_info should add the settings to the setup.cfg 79 in a deterministic order. 80 """ 81 setup_cfg = os.path.join(env.paths['home'], 'setup.cfg') 82 dist = Distribution() 83 ei = egg_info(dist) 84 ei.initialize_options() 85 ei.save_version_info(setup_cfg) 86 87 with open(setup_cfg, 'r') as f: 88 content = f.read() 89 90 assert '[egg_info]' in content 91 assert 'tag_build =' in content 92 assert 'tag_date = 0' in content 93 94 expected_order = 'tag_build', 'tag_date', 95 96 self._validate_content_order(content, expected_order) 97 98 @staticmethod 99 def _validate_content_order(content, expected): 100 """ 101 Assert that the strings in expected appear in content 102 in order. 103 """ 104 pattern = '.*'.join(expected) 105 flags = re.MULTILINE | re.DOTALL 106 assert re.search(pattern, content, flags) 107 108 def test_egg_info_save_version_info_setup_defaults(self, tmpdir_cwd, env): 109 """ 110 When running save_version_info on an existing setup.cfg 111 with the 'default' values present from a previous run, 112 the file should remain unchanged. 113 """ 114 setup_cfg = os.path.join(env.paths['home'], 'setup.cfg') 115 path.build({ 116 setup_cfg: DALS(""" 117 [egg_info] 118 tag_build = 119 tag_date = 0 120 """), 121 }) 122 dist = Distribution() 123 ei = egg_info(dist) 124 ei.initialize_options() 125 ei.save_version_info(setup_cfg) 126 127 with open(setup_cfg, 'r') as f: 128 content = f.read() 129 130 assert '[egg_info]' in content 131 assert 'tag_build =' in content 132 assert 'tag_date = 0' in content 133 134 expected_order = 'tag_build', 'tag_date', 135 136 self._validate_content_order(content, expected_order) 137 138 def test_expected_files_produced(self, tmpdir_cwd, env): 139 self._create_project() 140 141 self._run_egg_info_command(tmpdir_cwd, env) 142 actual = os.listdir('foo.egg-info') 143 144 expected = [ 145 'PKG-INFO', 146 'SOURCES.txt', 147 'dependency_links.txt', 148 'entry_points.txt', 149 'not-zip-safe', 150 'top_level.txt', 151 ] 152 assert sorted(actual) == expected 153 154 def test_license_is_a_string(self, tmpdir_cwd, env): 155 setup_config = DALS(""" 156 [metadata] 157 name=foo 158 version=0.0.1 159 license=file:MIT 160 """) 161 162 setup_script = DALS(""" 163 from setuptools import setup 164 165 setup() 166 """) 167 168 path.build({ 169 'setup.py': setup_script, 170 'setup.cfg': setup_config, 171 }) 172 173 # This command should fail with a ValueError, but because it's 174 # currently configured to use a subprocess, the actual traceback 175 # object is lost and we need to parse it from stderr 176 with pytest.raises(AssertionError) as exc: 177 self._run_egg_info_command(tmpdir_cwd, env) 178 179 # Hopefully this is not too fragile: the only argument to the 180 # assertion error should be a traceback, ending with: 181 # ValueError: .... 182 # 183 # assert not 1 184 tb = exc.value.args[0].split('\n') 185 assert tb[-3].lstrip().startswith('ValueError') 186 187 def test_rebuilt(self, tmpdir_cwd, env): 188 """Ensure timestamps are updated when the command is re-run.""" 189 self._create_project() 190 191 self._run_egg_info_command(tmpdir_cwd, env) 192 timestamp_a = os.path.getmtime('foo.egg-info') 193 194 # arbitrary sleep just to handle *really* fast systems 195 time.sleep(.001) 196 197 self._run_egg_info_command(tmpdir_cwd, env) 198 timestamp_b = os.path.getmtime('foo.egg-info') 199 200 assert timestamp_a != timestamp_b 201 202 def test_manifest_template_is_read(self, tmpdir_cwd, env): 203 self._create_project() 204 path.build({ 205 'MANIFEST.in': DALS(""" 206 recursive-include docs *.rst 207 """), 208 'docs': { 209 'usage.rst': "Run 'hi'", 210 } 211 }) 212 self._run_egg_info_command(tmpdir_cwd, env) 213 egg_info_dir = os.path.join('.', 'foo.egg-info') 214 sources_txt = os.path.join(egg_info_dir, 'SOURCES.txt') 215 with open(sources_txt) as f: 216 assert 'docs/usage.rst' in f.read().split('\n') 217 218 def _setup_script_with_requires(self, requires, use_setup_cfg=False): 219 setup_script = DALS( 220 ''' 221 from setuptools import setup 222 223 setup(name='foo', zip_safe=False, %s) 224 ''' 225 ) % ('' if use_setup_cfg else requires) 226 setup_config = requires if use_setup_cfg else '' 227 path.build({ 228 'setup.py': setup_script, 229 'setup.cfg': setup_config, 230 }) 231 232 mismatch_marker = "python_version<'{this_ver}'".format( 233 this_ver=sys.version_info[0], 234 ) 235 # Alternate equivalent syntax. 236 mismatch_marker_alternate = 'python_version < "{this_ver}"'.format( 237 this_ver=sys.version_info[0], 238 ) 239 invalid_marker = "<=>++" 240 241 class RequiresTestHelper: 242 243 @staticmethod 244 def parametrize(*test_list, **format_dict): 245 idlist = [] 246 argvalues = [] 247 for test in test_list: 248 test_params = test.lstrip().split('\n\n', 3) 249 name_kwargs = test_params.pop(0).split('\n') 250 if len(name_kwargs) > 1: 251 val = name_kwargs[1].strip() 252 install_cmd_kwargs = ast.literal_eval(val) 253 else: 254 install_cmd_kwargs = {} 255 name = name_kwargs[0].strip() 256 setup_py_requires, setup_cfg_requires, expected_requires = ( 257 DALS(a).format(**format_dict) for a in test_params 258 ) 259 for id_, requires, use_cfg in ( 260 (name, setup_py_requires, False), 261 (name + '_in_setup_cfg', setup_cfg_requires, True), 262 ): 263 idlist.append(id_) 264 marks = () 265 if requires.startswith('@xfail\n'): 266 requires = requires[7:] 267 marks = pytest.mark.xfail 268 argvalues.append(pytest.param(requires, use_cfg, 269 expected_requires, 270 install_cmd_kwargs, 271 marks=marks)) 272 return pytest.mark.parametrize( 273 'requires,use_setup_cfg,' 274 'expected_requires,install_cmd_kwargs', 275 argvalues, ids=idlist, 276 ) 277 278 @RequiresTestHelper.parametrize( 279 # Format of a test: 280 # 281 # id 282 # install_cmd_kwargs [optional] 283 # 284 # requires block (when used in setup.py) 285 # 286 # requires block (when used in setup.cfg) 287 # 288 # expected contents of requires.txt 289 290 ''' 291 install_requires_deterministic 292 293 install_requires=["wheel>=0.5", "pytest"] 294 295 [options] 296 install_requires = 297 wheel>=0.5 298 pytest 299 300 wheel>=0.5 301 pytest 302 ''', 303 304 ''' 305 install_requires_ordered 306 307 install_requires=["pytest>=3.0.2,!=10.9999"] 308 309 [options] 310 install_requires = 311 pytest>=3.0.2,!=10.9999 312 313 pytest!=10.9999,>=3.0.2 314 ''', 315 316 ''' 317 install_requires_with_marker 318 319 install_requires=["barbazquux;{mismatch_marker}"], 320 321 [options] 322 install_requires = 323 barbazquux; {mismatch_marker} 324 325 [:{mismatch_marker_alternate}] 326 barbazquux 327 ''', 328 329 ''' 330 install_requires_with_extra 331 {'cmd': ['egg_info']} 332 333 install_requires=["barbazquux [test]"], 334 335 [options] 336 install_requires = 337 barbazquux [test] 338 339 barbazquux[test] 340 ''', 341 342 ''' 343 install_requires_with_extra_and_marker 344 345 install_requires=["barbazquux [test]; {mismatch_marker}"], 346 347 [options] 348 install_requires = 349 barbazquux [test]; {mismatch_marker} 350 351 [:{mismatch_marker_alternate}] 352 barbazquux[test] 353 ''', 354 355 ''' 356 setup_requires_with_markers 357 358 setup_requires=["barbazquux;{mismatch_marker}"], 359 360 [options] 361 setup_requires = 362 barbazquux; {mismatch_marker} 363 364 ''', 365 366 ''' 367 tests_require_with_markers 368 {'cmd': ['test'], 'output': "Ran 0 tests in"} 369 370 tests_require=["barbazquux;{mismatch_marker}"], 371 372 [options] 373 tests_require = 374 barbazquux; {mismatch_marker} 375 376 ''', 377 378 ''' 379 extras_require_with_extra 380 {'cmd': ['egg_info']} 381 382 extras_require={{"extra": ["barbazquux [test]"]}}, 383 384 [options.extras_require] 385 extra = barbazquux [test] 386 387 [extra] 388 barbazquux[test] 389 ''', 390 391 ''' 392 extras_require_with_extra_and_marker_in_req 393 394 extras_require={{"extra": ["barbazquux [test]; {mismatch_marker}"]}}, 395 396 [options.extras_require] 397 extra = 398 barbazquux [test]; {mismatch_marker} 399 400 [extra] 401 402 [extra:{mismatch_marker_alternate}] 403 barbazquux[test] 404 ''', 405 406 # FIXME: ConfigParser does not allow : in key names! 407 ''' 408 extras_require_with_marker 409 410 extras_require={{":{mismatch_marker}": ["barbazquux"]}}, 411 412 @xfail 413 [options.extras_require] 414 :{mismatch_marker} = barbazquux 415 416 [:{mismatch_marker}] 417 barbazquux 418 ''', 419 420 ''' 421 extras_require_with_marker_in_req 422 423 extras_require={{"extra": ["barbazquux; {mismatch_marker}"]}}, 424 425 [options.extras_require] 426 extra = 427 barbazquux; {mismatch_marker} 428 429 [extra] 430 431 [extra:{mismatch_marker_alternate}] 432 barbazquux 433 ''', 434 435 ''' 436 extras_require_with_empty_section 437 438 extras_require={{"empty": []}}, 439 440 [options.extras_require] 441 empty = 442 443 [empty] 444 ''', 445 # Format arguments. 446 invalid_marker=invalid_marker, 447 mismatch_marker=mismatch_marker, 448 mismatch_marker_alternate=mismatch_marker_alternate, 449 ) 450 def test_requires( 451 self, tmpdir_cwd, env, requires, use_setup_cfg, 452 expected_requires, install_cmd_kwargs): 453 self._setup_script_with_requires(requires, use_setup_cfg) 454 self._run_egg_info_command(tmpdir_cwd, env, **install_cmd_kwargs) 455 egg_info_dir = os.path.join('.', 'foo.egg-info') 456 requires_txt = os.path.join(egg_info_dir, 'requires.txt') 457 if os.path.exists(requires_txt): 458 with open(requires_txt) as fp: 459 install_requires = fp.read() 460 else: 461 install_requires = '' 462 assert install_requires.lstrip() == expected_requires 463 assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == [] 464 465 def test_install_requires_unordered_disallowed(self, tmpdir_cwd, env): 466 """ 467 Packages that pass unordered install_requires sequences 468 should be rejected as they produce non-deterministic 469 builds. See #458. 470 """ 471 req = 'install_requires={"fake-factory==0.5.2", "pytz"}' 472 self._setup_script_with_requires(req) 473 with pytest.raises(AssertionError): 474 self._run_egg_info_command(tmpdir_cwd, env) 475 476 def test_extras_require_with_invalid_marker(self, tmpdir_cwd, env): 477 tmpl = 'extras_require={{":{marker}": ["barbazquux"]}},' 478 req = tmpl.format(marker=self.invalid_marker) 479 self._setup_script_with_requires(req) 480 with pytest.raises(AssertionError): 481 self._run_egg_info_command(tmpdir_cwd, env) 482 assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == [] 483 484 def test_extras_require_with_invalid_marker_in_req(self, tmpdir_cwd, env): 485 tmpl = 'extras_require={{"extra": ["barbazquux; {marker}"]}},' 486 req = tmpl.format(marker=self.invalid_marker) 487 self._setup_script_with_requires(req) 488 with pytest.raises(AssertionError): 489 self._run_egg_info_command(tmpdir_cwd, env) 490 assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == [] 491 492 def test_provides_extra(self, tmpdir_cwd, env): 493 self._setup_script_with_requires( 494 'extras_require={"foobar": ["barbazquux"]},') 495 environ = os.environ.copy().update( 496 HOME=env.paths['home'], 497 ) 498 code, data = environment.run_setup_py( 499 cmd=['egg_info'], 500 pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), 501 data_stream=1, 502 env=environ, 503 ) 504 egg_info_dir = os.path.join('.', 'foo.egg-info') 505 with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: 506 pkg_info_lines = pkginfo_file.read().split('\n') 507 assert 'Provides-Extra: foobar' in pkg_info_lines 508 assert 'Metadata-Version: 2.1' in pkg_info_lines 509 510 def test_doesnt_provides_extra(self, tmpdir_cwd, env): 511 self._setup_script_with_requires( 512 '''install_requires=["spam ; python_version<'3.6'"]''') 513 environ = os.environ.copy().update( 514 HOME=env.paths['home'], 515 ) 516 environment.run_setup_py( 517 cmd=['egg_info'], 518 pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), 519 data_stream=1, 520 env=environ, 521 ) 522 egg_info_dir = os.path.join('.', 'foo.egg-info') 523 with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: 524 pkg_info_text = pkginfo_file.read() 525 assert 'Provides-Extra:' not in pkg_info_text 526 527 @pytest.mark.parametrize("files, license_in_sources", [ 528 ({ 529 'setup.cfg': DALS(""" 530 [metadata] 531 license_file = LICENSE 532 """), 533 'LICENSE': "Test license" 534 }, True), # with license 535 ({ 536 'setup.cfg': DALS(""" 537 [metadata] 538 license_file = INVALID_LICENSE 539 """), 540 'LICENSE': "Test license" 541 }, False), # with an invalid license 542 ({ 543 'setup.cfg': DALS(""" 544 """), 545 'LICENSE': "Test license" 546 }, True), # no license_file attribute, LICENSE auto-included 547 ({ 548 'setup.cfg': DALS(""" 549 [metadata] 550 license_file = LICENSE 551 """), 552 'MANIFEST.in': "exclude LICENSE", 553 'LICENSE': "Test license" 554 }, True), # manifest is overwritten by license_file 555 pytest.param({ 556 'setup.cfg': DALS(""" 557 [metadata] 558 license_file = LICEN[CS]E* 559 """), 560 'LICENSE': "Test license", 561 }, True, 562 id="glob_pattern"), 563 ]) 564 def test_setup_cfg_license_file( 565 self, tmpdir_cwd, env, files, license_in_sources): 566 self._create_project() 567 path.build(files) 568 569 environment.run_setup_py( 570 cmd=['egg_info'], 571 pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]) 572 ) 573 egg_info_dir = os.path.join('.', 'foo.egg-info') 574 575 with open(os.path.join(egg_info_dir, 'SOURCES.txt')) as sources_file: 576 sources_text = sources_file.read() 577 578 if license_in_sources: 579 assert 'LICENSE' in sources_text 580 else: 581 assert 'LICENSE' not in sources_text 582 # for invalid license test 583 assert 'INVALID_LICENSE' not in sources_text 584 585 @pytest.mark.parametrize("files, incl_licenses, excl_licenses", [ 586 ({ 587 'setup.cfg': DALS(""" 588 [metadata] 589 license_files = 590 LICENSE-ABC 591 LICENSE-XYZ 592 """), 593 'LICENSE-ABC': "ABC license", 594 'LICENSE-XYZ': "XYZ license" 595 }, ['LICENSE-ABC', 'LICENSE-XYZ'], []), # with licenses 596 ({ 597 'setup.cfg': DALS(""" 598 [metadata] 599 license_files = LICENSE-ABC, LICENSE-XYZ 600 """), 601 'LICENSE-ABC': "ABC license", 602 'LICENSE-XYZ': "XYZ license" 603 }, ['LICENSE-ABC', 'LICENSE-XYZ'], []), # with commas 604 ({ 605 'setup.cfg': DALS(""" 606 [metadata] 607 license_files = 608 LICENSE-ABC 609 """), 610 'LICENSE-ABC': "ABC license", 611 'LICENSE-XYZ': "XYZ license" 612 }, ['LICENSE-ABC'], ['LICENSE-XYZ']), # with one license 613 ({ 614 'setup.cfg': DALS(""" 615 [metadata] 616 license_files = 617 """), 618 'LICENSE-ABC': "ABC license", 619 'LICENSE-XYZ': "XYZ license" 620 }, [], ['LICENSE-ABC', 'LICENSE-XYZ']), # empty 621 ({ 622 'setup.cfg': DALS(""" 623 [metadata] 624 license_files = LICENSE-XYZ 625 """), 626 'LICENSE-ABC': "ABC license", 627 'LICENSE-XYZ': "XYZ license" 628 }, ['LICENSE-XYZ'], ['LICENSE-ABC']), # on same line 629 ({ 630 'setup.cfg': DALS(""" 631 [metadata] 632 license_files = 633 LICENSE-ABC 634 INVALID_LICENSE 635 """), 636 'LICENSE-ABC': "Test license" 637 }, ['LICENSE-ABC'], ['INVALID_LICENSE']), # with an invalid license 638 ({ 639 'setup.cfg': DALS(""" 640 """), 641 'LICENSE': "Test license" 642 }, ['LICENSE'], []), # no license_files attribute, LICENSE auto-included 643 ({ 644 'setup.cfg': DALS(""" 645 [metadata] 646 license_files = LICENSE 647 """), 648 'MANIFEST.in': "exclude LICENSE", 649 'LICENSE': "Test license" 650 }, ['LICENSE'], []), # manifest is overwritten by license_files 651 ({ 652 'setup.cfg': DALS(""" 653 [metadata] 654 license_files = 655 LICENSE-ABC 656 LICENSE-XYZ 657 """), 658 'MANIFEST.in': "exclude LICENSE-XYZ", 659 'LICENSE-ABC': "ABC license", 660 'LICENSE-XYZ': "XYZ license" 661 # manifest is overwritten by license_files 662 }, ['LICENSE-ABC', 'LICENSE-XYZ'], []), 663 pytest.param({ 664 'setup.cfg': "", 665 'LICENSE-ABC': "ABC license", 666 'COPYING-ABC': "ABC copying", 667 'NOTICE-ABC': "ABC notice", 668 'AUTHORS-ABC': "ABC authors", 669 'LICENCE-XYZ': "XYZ license", 670 'LICENSE': "License", 671 'INVALID-LICENSE': "Invalid license", 672 }, [ 673 'LICENSE-ABC', 674 'COPYING-ABC', 675 'NOTICE-ABC', 676 'AUTHORS-ABC', 677 'LICENCE-XYZ', 678 'LICENSE', 679 ], ['INVALID-LICENSE'], 680 # ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*') 681 id="default_glob_patterns"), 682 pytest.param({ 683 'setup.cfg': DALS(""" 684 [metadata] 685 license_files = 686 LICENSE* 687 """), 688 'LICENSE-ABC': "ABC license", 689 'NOTICE-XYZ': "XYZ notice", 690 }, ['LICENSE-ABC'], ['NOTICE-XYZ'], 691 id="no_default_glob_patterns"), 692 pytest.param({ 693 'setup.cfg': DALS(""" 694 [metadata] 695 license_files = 696 LICENSE-ABC 697 LICENSE* 698 """), 699 'LICENSE-ABC': "ABC license", 700 }, ['LICENSE-ABC'], [], 701 id="files_only_added_once", 702 ), 703 ]) 704 def test_setup_cfg_license_files( 705 self, tmpdir_cwd, env, files, incl_licenses, excl_licenses): 706 self._create_project() 707 path.build(files) 708 709 environment.run_setup_py( 710 cmd=['egg_info'], 711 pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]) 712 ) 713 egg_info_dir = os.path.join('.', 'foo.egg-info') 714 715 with open(os.path.join(egg_info_dir, 'SOURCES.txt')) as sources_file: 716 sources_lines = list(line.strip() for line in sources_file) 717 718 for lf in incl_licenses: 719 assert sources_lines.count(lf) == 1 720 721 for lf in excl_licenses: 722 assert sources_lines.count(lf) == 0 723 724 @pytest.mark.parametrize("files, incl_licenses, excl_licenses", [ 725 ({ 726 'setup.cfg': DALS(""" 727 [metadata] 728 license_file = 729 license_files = 730 """), 731 'LICENSE-ABC': "ABC license", 732 'LICENSE-XYZ': "XYZ license" 733 }, [], ['LICENSE-ABC', 'LICENSE-XYZ']), # both empty 734 ({ 735 'setup.cfg': DALS(""" 736 [metadata] 737 license_file = 738 LICENSE-ABC 739 LICENSE-XYZ 740 """), 741 'LICENSE-ABC': "ABC license", 742 'LICENSE-XYZ': "XYZ license" 743 # license_file is still singular 744 }, [], ['LICENSE-ABC', 'LICENSE-XYZ']), 745 ({ 746 'setup.cfg': DALS(""" 747 [metadata] 748 license_file = LICENSE-ABC 749 license_files = 750 LICENSE-XYZ 751 LICENSE-PQR 752 """), 753 'LICENSE-ABC': "ABC license", 754 'LICENSE-PQR': "PQR license", 755 'LICENSE-XYZ': "XYZ license" 756 }, ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'], []), # combined 757 ({ 758 'setup.cfg': DALS(""" 759 [metadata] 760 license_file = LICENSE-ABC 761 license_files = 762 LICENSE-ABC 763 LICENSE-XYZ 764 LICENSE-PQR 765 """), 766 'LICENSE-ABC': "ABC license", 767 'LICENSE-PQR': "PQR license", 768 'LICENSE-XYZ': "XYZ license" 769 # duplicate license 770 }, ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'], []), 771 ({ 772 'setup.cfg': DALS(""" 773 [metadata] 774 license_file = LICENSE-ABC 775 license_files = 776 LICENSE-XYZ 777 """), 778 'LICENSE-ABC': "ABC license", 779 'LICENSE-PQR': "PQR license", 780 'LICENSE-XYZ': "XYZ license" 781 # combined subset 782 }, ['LICENSE-ABC', 'LICENSE-XYZ'], ['LICENSE-PQR']), 783 ({ 784 'setup.cfg': DALS(""" 785 [metadata] 786 license_file = LICENSE-ABC 787 license_files = 788 LICENSE-XYZ 789 LICENSE-PQR 790 """), 791 'LICENSE-PQR': "Test license" 792 # with invalid licenses 793 }, ['LICENSE-PQR'], ['LICENSE-ABC', 'LICENSE-XYZ']), 794 ({ 795 'setup.cfg': DALS(""" 796 [metadata] 797 license_file = LICENSE-ABC 798 license_files = 799 LICENSE-PQR 800 LICENSE-XYZ 801 """), 802 'MANIFEST.in': "exclude LICENSE-ABC\nexclude LICENSE-PQR", 803 'LICENSE-ABC': "ABC license", 804 'LICENSE-PQR': "PQR license", 805 'LICENSE-XYZ': "XYZ license" 806 # manifest is overwritten 807 }, ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'], []), 808 pytest.param({ 809 'setup.cfg': DALS(""" 810 [metadata] 811 license_file = LICENSE* 812 """), 813 'LICENSE-ABC': "ABC license", 814 'NOTICE-XYZ': "XYZ notice", 815 }, ['LICENSE-ABC'], ['NOTICE-XYZ'], 816 id="no_default_glob_patterns"), 817 pytest.param({ 818 'setup.cfg': DALS(""" 819 [metadata] 820 license_file = LICENSE* 821 license_files = 822 NOTICE* 823 """), 824 'LICENSE-ABC': "ABC license", 825 'NOTICE-ABC': "ABC notice", 826 'AUTHORS-ABC': "ABC authors", 827 }, ['LICENSE-ABC', 'NOTICE-ABC'], ['AUTHORS-ABC'], 828 id="combined_glob_patterrns"), 829 ]) 830 def test_setup_cfg_license_file_license_files( 831 self, tmpdir_cwd, env, files, incl_licenses, excl_licenses): 832 self._create_project() 833 path.build(files) 834 835 environment.run_setup_py( 836 cmd=['egg_info'], 837 pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]) 838 ) 839 egg_info_dir = os.path.join('.', 'foo.egg-info') 840 841 with open(os.path.join(egg_info_dir, 'SOURCES.txt')) as sources_file: 842 sources_lines = list(line.strip() for line in sources_file) 843 844 for lf in incl_licenses: 845 assert sources_lines.count(lf) == 1 846 847 for lf in excl_licenses: 848 assert sources_lines.count(lf) == 0 849 850 def test_license_file_attr_pkg_info(self, tmpdir_cwd, env): 851 """All matched license files should have a corresponding License-File.""" 852 self._create_project() 853 path.build({ 854 "setup.cfg": DALS(""" 855 [metadata] 856 license_files = 857 NOTICE* 858 LICENSE* 859 """), 860 "LICENSE-ABC": "ABC license", 861 "LICENSE-XYZ": "XYZ license", 862 "NOTICE": "included", 863 "IGNORE": "not include", 864 }) 865 866 environment.run_setup_py( 867 cmd=['egg_info'], 868 pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]) 869 ) 870 egg_info_dir = os.path.join('.', 'foo.egg-info') 871 with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: 872 pkg_info_lines = pkginfo_file.read().split('\n') 873 license_file_lines = [ 874 line for line in pkg_info_lines if line.startswith('License-File:')] 875 876 # Only 'NOTICE', LICENSE-ABC', and 'LICENSE-XYZ' should have been matched 877 # Also assert that order from license_files is keeped 878 assert "License-File: NOTICE" == license_file_lines[0] 879 assert "License-File: LICENSE-ABC" in license_file_lines[1:] 880 assert "License-File: LICENSE-XYZ" in license_file_lines[1:] 881 882 def test_metadata_version(self, tmpdir_cwd, env): 883 """Make sure latest metadata version is used by default.""" 884 self._setup_script_with_requires("") 885 code, data = environment.run_setup_py( 886 cmd=['egg_info'], 887 pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), 888 data_stream=1, 889 ) 890 egg_info_dir = os.path.join('.', 'foo.egg-info') 891 with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: 892 pkg_info_lines = pkginfo_file.read().split('\n') 893 # Update metadata version if changed 894 assert self._extract_mv_version(pkg_info_lines) == (2, 1) 895 896 def test_long_description_content_type(self, tmpdir_cwd, env): 897 # Test that specifying a `long_description_content_type` keyword arg to 898 # the `setup` function results in writing a `Description-Content-Type` 899 # line to the `PKG-INFO` file in the `<distribution>.egg-info` 900 # directory. 901 # `Description-Content-Type` is described at 902 # https://github.com/pypa/python-packaging-user-guide/pull/258 903 904 self._setup_script_with_requires( 905 """long_description_content_type='text/markdown',""") 906 environ = os.environ.copy().update( 907 HOME=env.paths['home'], 908 ) 909 code, data = environment.run_setup_py( 910 cmd=['egg_info'], 911 pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), 912 data_stream=1, 913 env=environ, 914 ) 915 egg_info_dir = os.path.join('.', 'foo.egg-info') 916 with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: 917 pkg_info_lines = pkginfo_file.read().split('\n') 918 expected_line = 'Description-Content-Type: text/markdown' 919 assert expected_line in pkg_info_lines 920 assert 'Metadata-Version: 2.1' in pkg_info_lines 921 922 def test_long_description(self, tmpdir_cwd, env): 923 # Test that specifying `long_description` and `long_description_content_type` 924 # keyword args to the `setup` function results in writing 925 # the description in the message payload of the `PKG-INFO` file 926 # in the `<distribution>.egg-info` directory. 927 self._setup_script_with_requires( 928 "long_description='This is a long description\\nover multiple lines'," 929 "long_description_content_type='text/markdown'," 930 ) 931 code, data = environment.run_setup_py( 932 cmd=['egg_info'], 933 pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), 934 data_stream=1, 935 ) 936 egg_info_dir = os.path.join('.', 'foo.egg-info') 937 with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: 938 pkg_info_lines = pkginfo_file.read().split('\n') 939 assert 'Metadata-Version: 2.1' in pkg_info_lines 940 assert '' == pkg_info_lines[-1] # last line should be empty 941 long_desc_lines = pkg_info_lines[pkg_info_lines.index(''):] 942 assert 'This is a long description' in long_desc_lines 943 assert 'over multiple lines' in long_desc_lines 944 945 def test_project_urls(self, tmpdir_cwd, env): 946 # Test that specifying a `project_urls` dict to the `setup` 947 # function results in writing multiple `Project-URL` lines to 948 # the `PKG-INFO` file in the `<distribution>.egg-info` 949 # directory. 950 # `Project-URL` is described at https://packaging.python.org 951 # /specifications/core-metadata/#project-url-multiple-use 952 953 self._setup_script_with_requires( 954 """project_urls={ 955 'Link One': 'https://example.com/one/', 956 'Link Two': 'https://example.com/two/', 957 },""") 958 environ = os.environ.copy().update( 959 HOME=env.paths['home'], 960 ) 961 code, data = environment.run_setup_py( 962 cmd=['egg_info'], 963 pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), 964 data_stream=1, 965 env=environ, 966 ) 967 egg_info_dir = os.path.join('.', 'foo.egg-info') 968 with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: 969 pkg_info_lines = pkginfo_file.read().split('\n') 970 expected_line = 'Project-URL: Link One, https://example.com/one/' 971 assert expected_line in pkg_info_lines 972 expected_line = 'Project-URL: Link Two, https://example.com/two/' 973 assert expected_line in pkg_info_lines 974 assert self._extract_mv_version(pkg_info_lines) >= (1, 2) 975 976 def test_license(self, tmpdir_cwd, env): 977 """Test single line license.""" 978 self._setup_script_with_requires( 979 "license='MIT'," 980 ) 981 code, data = environment.run_setup_py( 982 cmd=['egg_info'], 983 pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), 984 data_stream=1, 985 ) 986 egg_info_dir = os.path.join('.', 'foo.egg-info') 987 with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: 988 pkg_info_lines = pkginfo_file.read().split('\n') 989 assert 'License: MIT' in pkg_info_lines 990 991 def test_license_escape(self, tmpdir_cwd, env): 992 """Test license is escaped correctly if longer than one line.""" 993 self._setup_script_with_requires( 994 "license='This is a long license text \\nover multiple lines'," 995 ) 996 code, data = environment.run_setup_py( 997 cmd=['egg_info'], 998 pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), 999 data_stream=1, 1000 ) 1001 egg_info_dir = os.path.join('.', 'foo.egg-info') 1002 with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: 1003 pkg_info_lines = pkginfo_file.read().split('\n') 1004 1005 assert 'License: This is a long license text ' in pkg_info_lines 1006 assert ' over multiple lines' in pkg_info_lines 1007 assert 'text \n over multiple' in '\n'.join(pkg_info_lines) 1008 1009 def test_python_requires_egg_info(self, tmpdir_cwd, env): 1010 self._setup_script_with_requires( 1011 """python_requires='>=2.7.12',""") 1012 environ = os.environ.copy().update( 1013 HOME=env.paths['home'], 1014 ) 1015 code, data = environment.run_setup_py( 1016 cmd=['egg_info'], 1017 pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), 1018 data_stream=1, 1019 env=environ, 1020 ) 1021 egg_info_dir = os.path.join('.', 'foo.egg-info') 1022 with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: 1023 pkg_info_lines = pkginfo_file.read().split('\n') 1024 assert 'Requires-Python: >=2.7.12' in pkg_info_lines 1025 assert self._extract_mv_version(pkg_info_lines) >= (1, 2) 1026 1027 def test_manifest_maker_warning_suppression(self): 1028 fixtures = [ 1029 "standard file not found: should have one of foo.py, bar.py", 1030 "standard file 'setup.py' not found" 1031 ] 1032 1033 for msg in fixtures: 1034 assert manifest_maker._should_suppress_warning(msg) 1035 1036 def test_egg_info_includes_setup_py(self, tmpdir_cwd): 1037 self._create_project() 1038 dist = Distribution({"name": "foo", "version": "0.0.1"}) 1039 dist.script_name = "non_setup.py" 1040 egg_info_instance = egg_info(dist) 1041 egg_info_instance.finalize_options() 1042 egg_info_instance.run() 1043 1044 assert 'setup.py' in egg_info_instance.filelist.files 1045 1046 with open(egg_info_instance.egg_info + "/SOURCES.txt") as f: 1047 sources = f.read().split('\n') 1048 assert 'setup.py' in sources 1049 1050 def _run_egg_info_command(self, tmpdir_cwd, env, cmd=None, output=None): 1051 environ = os.environ.copy().update( 1052 HOME=env.paths['home'], 1053 ) 1054 if cmd is None: 1055 cmd = [ 1056 'egg_info', 1057 ] 1058 code, data = environment.run_setup_py( 1059 cmd=cmd, 1060 pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), 1061 data_stream=1, 1062 env=environ, 1063 ) 1064 assert not code, data 1065 1066 if output: 1067 assert output in data 1068 1069 def test_egg_info_tag_only_once(self, tmpdir_cwd, env): 1070 self._create_project() 1071 path.build({ 1072 'setup.cfg': DALS(""" 1073 [egg_info] 1074 tag_build = dev 1075 tag_date = 0 1076 tag_svn_revision = 0 1077 """), 1078 }) 1079 self._run_egg_info_command(tmpdir_cwd, env) 1080 egg_info_dir = os.path.join('.', 'foo.egg-info') 1081 with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: 1082 pkg_info_lines = pkginfo_file.read().split('\n') 1083 assert 'Version: 0.0.0.dev0' in pkg_info_lines 1084 1085 def test_get_pkg_info_revision_deprecated(self): 1086 pytest.warns(EggInfoDeprecationWarning, get_pkg_info_revision) 1087