1"""Tests for distutils.dist.""" 2import os 3import io 4import sys 5import unittest 6import warnings 7import textwrap 8 9from unittest import mock 10 11from distutils.dist import Distribution, fix_help_options 12from distutils.cmd import Command 13 14from test.support import ( 15 captured_stdout, captured_stderr, run_unittest 16) 17from .py38compat import TESTFN 18from distutils.tests import support 19from distutils import log 20 21 22class test_dist(Command): 23 """Sample distutils extension command.""" 24 25 user_options = [ 26 ("sample-option=", "S", "help text"), 27 ] 28 29 def initialize_options(self): 30 self.sample_option = None 31 32 33class TestDistribution(Distribution): 34 """Distribution subclasses that avoids the default search for 35 configuration files. 36 37 The ._config_files attribute must be set before 38 .parse_config_files() is called. 39 """ 40 41 def find_config_files(self): 42 return self._config_files 43 44 45class DistributionTestCase(support.LoggingSilencer, 46 support.TempdirManager, 47 support.EnvironGuard, 48 unittest.TestCase): 49 50 def setUp(self): 51 super(DistributionTestCase, self).setUp() 52 self.argv = sys.argv, sys.argv[:] 53 del sys.argv[1:] 54 55 def tearDown(self): 56 sys.argv = self.argv[0] 57 sys.argv[:] = self.argv[1] 58 super(DistributionTestCase, self).tearDown() 59 60 def create_distribution(self, configfiles=()): 61 d = TestDistribution() 62 d._config_files = configfiles 63 d.parse_config_files() 64 d.parse_command_line() 65 return d 66 67 def test_command_packages_unspecified(self): 68 sys.argv.append("build") 69 d = self.create_distribution() 70 self.assertEqual(d.get_command_packages(), ["distutils.command"]) 71 72 def test_command_packages_cmdline(self): 73 from distutils.tests.test_dist import test_dist 74 sys.argv.extend(["--command-packages", 75 "foo.bar,distutils.tests", 76 "test_dist", 77 "-Ssometext", 78 ]) 79 d = self.create_distribution() 80 # let's actually try to load our test command: 81 self.assertEqual(d.get_command_packages(), 82 ["distutils.command", "foo.bar", "distutils.tests"]) 83 cmd = d.get_command_obj("test_dist") 84 self.assertIsInstance(cmd, test_dist) 85 self.assertEqual(cmd.sample_option, "sometext") 86 87 @unittest.skipIf( 88 'distutils' not in Distribution.parse_config_files.__module__, 89 'Cannot test when virtualenv has monkey-patched Distribution.', 90 ) 91 def test_venv_install_options(self): 92 sys.argv.append("install") 93 self.addCleanup(os.unlink, TESTFN) 94 95 fakepath = '/somedir' 96 97 with open(TESTFN, "w") as f: 98 print(("[install]\n" 99 "install-base = {0}\n" 100 "install-platbase = {0}\n" 101 "install-lib = {0}\n" 102 "install-platlib = {0}\n" 103 "install-purelib = {0}\n" 104 "install-headers = {0}\n" 105 "install-scripts = {0}\n" 106 "install-data = {0}\n" 107 "prefix = {0}\n" 108 "exec-prefix = {0}\n" 109 "home = {0}\n" 110 "user = {0}\n" 111 "root = {0}").format(fakepath), file=f) 112 113 # Base case: Not in a Virtual Environment 114 with mock.patch.multiple(sys, prefix='/a', base_prefix='/a') as values: 115 d = self.create_distribution([TESTFN]) 116 117 option_tuple = (TESTFN, fakepath) 118 119 result_dict = { 120 'install_base': option_tuple, 121 'install_platbase': option_tuple, 122 'install_lib': option_tuple, 123 'install_platlib': option_tuple, 124 'install_purelib': option_tuple, 125 'install_headers': option_tuple, 126 'install_scripts': option_tuple, 127 'install_data': option_tuple, 128 'prefix': option_tuple, 129 'exec_prefix': option_tuple, 130 'home': option_tuple, 131 'user': option_tuple, 132 'root': option_tuple, 133 } 134 135 self.assertEqual( 136 sorted(d.command_options.get('install').keys()), 137 sorted(result_dict.keys())) 138 139 for (key, value) in d.command_options.get('install').items(): 140 self.assertEqual(value, result_dict[key]) 141 142 # Test case: In a Virtual Environment 143 with mock.patch.multiple(sys, prefix='/a', base_prefix='/b') as values: 144 d = self.create_distribution([TESTFN]) 145 146 for key in result_dict.keys(): 147 self.assertNotIn(key, d.command_options.get('install', {})) 148 149 def test_command_packages_configfile(self): 150 sys.argv.append("build") 151 self.addCleanup(os.unlink, TESTFN) 152 f = open(TESTFN, "w") 153 try: 154 print("[global]", file=f) 155 print("command_packages = foo.bar, splat", file=f) 156 finally: 157 f.close() 158 159 d = self.create_distribution([TESTFN]) 160 self.assertEqual(d.get_command_packages(), 161 ["distutils.command", "foo.bar", "splat"]) 162 163 # ensure command line overrides config: 164 sys.argv[1:] = ["--command-packages", "spork", "build"] 165 d = self.create_distribution([TESTFN]) 166 self.assertEqual(d.get_command_packages(), 167 ["distutils.command", "spork"]) 168 169 # Setting --command-packages to '' should cause the default to 170 # be used even if a config file specified something else: 171 sys.argv[1:] = ["--command-packages", "", "build"] 172 d = self.create_distribution([TESTFN]) 173 self.assertEqual(d.get_command_packages(), ["distutils.command"]) 174 175 def test_empty_options(self): 176 # an empty options dictionary should not stay in the 177 # list of attributes 178 179 # catching warnings 180 warns = [] 181 182 def _warn(msg): 183 warns.append(msg) 184 185 self.addCleanup(setattr, warnings, 'warn', warnings.warn) 186 warnings.warn = _warn 187 dist = Distribution(attrs={'author': 'xxx', 'name': 'xxx', 188 'version': 'xxx', 'url': 'xxxx', 189 'options': {}}) 190 191 self.assertEqual(len(warns), 0) 192 self.assertNotIn('options', dir(dist)) 193 194 def test_finalize_options(self): 195 attrs = {'keywords': 'one,two', 196 'platforms': 'one,two'} 197 198 dist = Distribution(attrs=attrs) 199 dist.finalize_options() 200 201 # finalize_option splits platforms and keywords 202 self.assertEqual(dist.metadata.platforms, ['one', 'two']) 203 self.assertEqual(dist.metadata.keywords, ['one', 'two']) 204 205 attrs = {'keywords': 'foo bar', 206 'platforms': 'foo bar'} 207 dist = Distribution(attrs=attrs) 208 dist.finalize_options() 209 self.assertEqual(dist.metadata.platforms, ['foo bar']) 210 self.assertEqual(dist.metadata.keywords, ['foo bar']) 211 212 def test_get_command_packages(self): 213 dist = Distribution() 214 self.assertEqual(dist.command_packages, None) 215 cmds = dist.get_command_packages() 216 self.assertEqual(cmds, ['distutils.command']) 217 self.assertEqual(dist.command_packages, 218 ['distutils.command']) 219 220 dist.command_packages = 'one,two' 221 cmds = dist.get_command_packages() 222 self.assertEqual(cmds, ['distutils.command', 'one', 'two']) 223 224 def test_announce(self): 225 # make sure the level is known 226 dist = Distribution() 227 args = ('ok',) 228 kwargs = {'level': 'ok2'} 229 self.assertRaises(ValueError, dist.announce, args, kwargs) 230 231 232 def test_find_config_files_disable(self): 233 # Ticket #1180: Allow user to disable their home config file. 234 temp_home = self.mkdtemp() 235 if os.name == 'posix': 236 user_filename = os.path.join(temp_home, ".pydistutils.cfg") 237 else: 238 user_filename = os.path.join(temp_home, "pydistutils.cfg") 239 240 with open(user_filename, 'w') as f: 241 f.write('[distutils]\n') 242 243 def _expander(path): 244 return temp_home 245 246 old_expander = os.path.expanduser 247 os.path.expanduser = _expander 248 try: 249 d = Distribution() 250 all_files = d.find_config_files() 251 252 d = Distribution(attrs={'script_args': ['--no-user-cfg']}) 253 files = d.find_config_files() 254 finally: 255 os.path.expanduser = old_expander 256 257 # make sure --no-user-cfg disables the user cfg file 258 self.assertEqual(len(all_files)-1, len(files)) 259 260class MetadataTestCase(support.TempdirManager, support.EnvironGuard, 261 unittest.TestCase): 262 263 def setUp(self): 264 super(MetadataTestCase, self).setUp() 265 self.argv = sys.argv, sys.argv[:] 266 267 def tearDown(self): 268 sys.argv = self.argv[0] 269 sys.argv[:] = self.argv[1] 270 super(MetadataTestCase, self).tearDown() 271 272 def format_metadata(self, dist): 273 sio = io.StringIO() 274 dist.metadata.write_pkg_file(sio) 275 return sio.getvalue() 276 277 def test_simple_metadata(self): 278 attrs = {"name": "package", 279 "version": "1.0"} 280 dist = Distribution(attrs) 281 meta = self.format_metadata(dist) 282 self.assertIn("Metadata-Version: 1.0", meta) 283 self.assertNotIn("provides:", meta.lower()) 284 self.assertNotIn("requires:", meta.lower()) 285 self.assertNotIn("obsoletes:", meta.lower()) 286 287 def test_provides(self): 288 attrs = {"name": "package", 289 "version": "1.0", 290 "provides": ["package", "package.sub"]} 291 dist = Distribution(attrs) 292 self.assertEqual(dist.metadata.get_provides(), 293 ["package", "package.sub"]) 294 self.assertEqual(dist.get_provides(), 295 ["package", "package.sub"]) 296 meta = self.format_metadata(dist) 297 self.assertIn("Metadata-Version: 1.1", meta) 298 self.assertNotIn("requires:", meta.lower()) 299 self.assertNotIn("obsoletes:", meta.lower()) 300 301 def test_provides_illegal(self): 302 self.assertRaises(ValueError, Distribution, 303 {"name": "package", 304 "version": "1.0", 305 "provides": ["my.pkg (splat)"]}) 306 307 def test_requires(self): 308 attrs = {"name": "package", 309 "version": "1.0", 310 "requires": ["other", "another (==1.0)"]} 311 dist = Distribution(attrs) 312 self.assertEqual(dist.metadata.get_requires(), 313 ["other", "another (==1.0)"]) 314 self.assertEqual(dist.get_requires(), 315 ["other", "another (==1.0)"]) 316 meta = self.format_metadata(dist) 317 self.assertIn("Metadata-Version: 1.1", meta) 318 self.assertNotIn("provides:", meta.lower()) 319 self.assertIn("Requires: other", meta) 320 self.assertIn("Requires: another (==1.0)", meta) 321 self.assertNotIn("obsoletes:", meta.lower()) 322 323 def test_requires_illegal(self): 324 self.assertRaises(ValueError, Distribution, 325 {"name": "package", 326 "version": "1.0", 327 "requires": ["my.pkg (splat)"]}) 328 329 def test_requires_to_list(self): 330 attrs = {"name": "package", 331 "requires": iter(["other"])} 332 dist = Distribution(attrs) 333 self.assertIsInstance(dist.metadata.requires, list) 334 335 336 def test_obsoletes(self): 337 attrs = {"name": "package", 338 "version": "1.0", 339 "obsoletes": ["other", "another (<1.0)"]} 340 dist = Distribution(attrs) 341 self.assertEqual(dist.metadata.get_obsoletes(), 342 ["other", "another (<1.0)"]) 343 self.assertEqual(dist.get_obsoletes(), 344 ["other", "another (<1.0)"]) 345 meta = self.format_metadata(dist) 346 self.assertIn("Metadata-Version: 1.1", meta) 347 self.assertNotIn("provides:", meta.lower()) 348 self.assertNotIn("requires:", meta.lower()) 349 self.assertIn("Obsoletes: other", meta) 350 self.assertIn("Obsoletes: another (<1.0)", meta) 351 352 def test_obsoletes_illegal(self): 353 self.assertRaises(ValueError, Distribution, 354 {"name": "package", 355 "version": "1.0", 356 "obsoletes": ["my.pkg (splat)"]}) 357 358 def test_obsoletes_to_list(self): 359 attrs = {"name": "package", 360 "obsoletes": iter(["other"])} 361 dist = Distribution(attrs) 362 self.assertIsInstance(dist.metadata.obsoletes, list) 363 364 def test_classifier(self): 365 attrs = {'name': 'Boa', 'version': '3.0', 366 'classifiers': ['Programming Language :: Python :: 3']} 367 dist = Distribution(attrs) 368 self.assertEqual(dist.get_classifiers(), 369 ['Programming Language :: Python :: 3']) 370 meta = self.format_metadata(dist) 371 self.assertIn('Metadata-Version: 1.1', meta) 372 373 def test_classifier_invalid_type(self): 374 attrs = {'name': 'Boa', 'version': '3.0', 375 'classifiers': ('Programming Language :: Python :: 3',)} 376 with captured_stderr() as error: 377 d = Distribution(attrs) 378 # should have warning about passing a non-list 379 self.assertIn('should be a list', error.getvalue()) 380 # should be converted to a list 381 self.assertIsInstance(d.metadata.classifiers, list) 382 self.assertEqual(d.metadata.classifiers, 383 list(attrs['classifiers'])) 384 385 def test_keywords(self): 386 attrs = {'name': 'Monty', 'version': '1.0', 387 'keywords': ['spam', 'eggs', 'life of brian']} 388 dist = Distribution(attrs) 389 self.assertEqual(dist.get_keywords(), 390 ['spam', 'eggs', 'life of brian']) 391 392 def test_keywords_invalid_type(self): 393 attrs = {'name': 'Monty', 'version': '1.0', 394 'keywords': ('spam', 'eggs', 'life of brian')} 395 with captured_stderr() as error: 396 d = Distribution(attrs) 397 # should have warning about passing a non-list 398 self.assertIn('should be a list', error.getvalue()) 399 # should be converted to a list 400 self.assertIsInstance(d.metadata.keywords, list) 401 self.assertEqual(d.metadata.keywords, list(attrs['keywords'])) 402 403 def test_platforms(self): 404 attrs = {'name': 'Monty', 'version': '1.0', 405 'platforms': ['GNU/Linux', 'Some Evil Platform']} 406 dist = Distribution(attrs) 407 self.assertEqual(dist.get_platforms(), 408 ['GNU/Linux', 'Some Evil Platform']) 409 410 def test_platforms_invalid_types(self): 411 attrs = {'name': 'Monty', 'version': '1.0', 412 'platforms': ('GNU/Linux', 'Some Evil Platform')} 413 with captured_stderr() as error: 414 d = Distribution(attrs) 415 # should have warning about passing a non-list 416 self.assertIn('should be a list', error.getvalue()) 417 # should be converted to a list 418 self.assertIsInstance(d.metadata.platforms, list) 419 self.assertEqual(d.metadata.platforms, list(attrs['platforms'])) 420 421 def test_download_url(self): 422 attrs = {'name': 'Boa', 'version': '3.0', 423 'download_url': 'http://example.org/boa'} 424 dist = Distribution(attrs) 425 meta = self.format_metadata(dist) 426 self.assertIn('Metadata-Version: 1.1', meta) 427 428 def test_long_description(self): 429 long_desc = textwrap.dedent("""\ 430 example:: 431 We start here 432 and continue here 433 and end here.""") 434 attrs = {"name": "package", 435 "version": "1.0", 436 "long_description": long_desc} 437 438 dist = Distribution(attrs) 439 meta = self.format_metadata(dist) 440 meta = meta.replace('\n' + 8 * ' ', '\n') 441 self.assertIn(long_desc, meta) 442 443 def test_custom_pydistutils(self): 444 # fixes #2166 445 # make sure pydistutils.cfg is found 446 if os.name == 'posix': 447 user_filename = ".pydistutils.cfg" 448 else: 449 user_filename = "pydistutils.cfg" 450 451 temp_dir = self.mkdtemp() 452 user_filename = os.path.join(temp_dir, user_filename) 453 f = open(user_filename, 'w') 454 try: 455 f.write('.') 456 finally: 457 f.close() 458 459 try: 460 dist = Distribution() 461 462 # linux-style 463 if sys.platform in ('linux', 'darwin'): 464 os.environ['HOME'] = temp_dir 465 files = dist.find_config_files() 466 self.assertIn(user_filename, files) 467 468 # win32-style 469 if sys.platform == 'win32': 470 # home drive should be found 471 os.environ['USERPROFILE'] = temp_dir 472 files = dist.find_config_files() 473 self.assertIn(user_filename, files, 474 '%r not found in %r' % (user_filename, files)) 475 finally: 476 os.remove(user_filename) 477 478 def test_fix_help_options(self): 479 help_tuples = [('a', 'b', 'c', 'd'), (1, 2, 3, 4)] 480 fancy_options = fix_help_options(help_tuples) 481 self.assertEqual(fancy_options[0], ('a', 'b', 'c')) 482 self.assertEqual(fancy_options[1], (1, 2, 3)) 483 484 def test_show_help(self): 485 # smoke test, just makes sure some help is displayed 486 self.addCleanup(log.set_threshold, log._global_log.threshold) 487 dist = Distribution() 488 sys.argv = [] 489 dist.help = 1 490 dist.script_name = 'setup.py' 491 with captured_stdout() as s: 492 dist.parse_command_line() 493 494 output = [line for line in s.getvalue().split('\n') 495 if line.strip() != ''] 496 self.assertTrue(output) 497 498 499 def test_read_metadata(self): 500 attrs = {"name": "package", 501 "version": "1.0", 502 "long_description": "desc", 503 "description": "xxx", 504 "download_url": "http://example.com", 505 "keywords": ['one', 'two'], 506 "requires": ['foo']} 507 508 dist = Distribution(attrs) 509 metadata = dist.metadata 510 511 # write it then reloads it 512 PKG_INFO = io.StringIO() 513 metadata.write_pkg_file(PKG_INFO) 514 PKG_INFO.seek(0) 515 metadata.read_pkg_file(PKG_INFO) 516 517 self.assertEqual(metadata.name, "package") 518 self.assertEqual(metadata.version, "1.0") 519 self.assertEqual(metadata.description, "xxx") 520 self.assertEqual(metadata.download_url, 'http://example.com') 521 self.assertEqual(metadata.keywords, ['one', 'two']) 522 self.assertEqual(metadata.platforms, ['UNKNOWN']) 523 self.assertEqual(metadata.obsoletes, None) 524 self.assertEqual(metadata.requires, ['foo']) 525 526def test_suite(): 527 suite = unittest.TestSuite() 528 suite.addTest(unittest.TestLoader().loadTestsFromTestCase(DistributionTestCase)) 529 suite.addTest(unittest.TestLoader().loadTestsFromTestCase(MetadataTestCase)) 530 return suite 531 532if __name__ == "__main__": 533 run_unittest(test_suite()) 534