Aluode/PerceptionLabPortable
0
1"""Tests for distutils.dist."""2 3import email4import email.generator5import email.policy6import functools7import io8import os9import sys10import textwrap11import unittest.mock as mock12import warnings13from distutils.cmd import Command14from distutils.dist import Distribution, fix_help_options15from distutils.tests import support16from typing import ClassVar17 18import jaraco.path19import pytest20 21pydistutils_cfg = '.' * (os.name == 'posix') + 'pydistutils.cfg'22 23 24class test_dist(Command):25 """Sample distutils extension command."""26 27 user_options: ClassVar[list[tuple[str, str, str]]] = [28 ("sample-option=", "S", "help text"),29 ]30 31 def initialize_options(self):32 self.sample_option = None33 34 35class TestDistribution(Distribution):36 """Distribution subclasses that avoids the default search for37 configuration files.38 39 The ._config_files attribute must be set before40 .parse_config_files() is called.41 """42 43 def find_config_files(self):44 return self._config_files45 46 47@pytest.fixture48def clear_argv():49 del sys.argv[1:]50 51 52@support.combine_markers53@pytest.mark.usefixtures('save_env')54@pytest.mark.usefixtures('save_argv')55class TestDistributionBehavior(support.TempdirManager):56 def create_distribution(self, configfiles=()):57 d = TestDistribution()58 d._config_files = configfiles59 d.parse_config_files()60 d.parse_command_line()61 return d62 63 def test_command_packages_unspecified(self, clear_argv):64 sys.argv.append("build")65 d = self.create_distribution()66 assert d.get_command_packages() == ["distutils.command"]67 68 def test_command_packages_cmdline(self, clear_argv):69 from distutils.tests.test_dist import test_dist70 71 sys.argv.extend([72 "--command-packages",73 "foo.bar,distutils.tests",74 "test_dist",75 "-Ssometext",76 ])77 d = self.create_distribution()78 # let's actually try to load our test command:79 assert d.get_command_packages() == [80 "distutils.command",81 "foo.bar",82 "distutils.tests",83 ]84 cmd = d.get_command_obj("test_dist")85 assert isinstance(cmd, test_dist)86 assert cmd.sample_option == "sometext"87 88 @pytest.mark.skipif(89 'distutils' not in Distribution.parse_config_files.__module__,90 reason='Cannot test when virtualenv has monkey-patched Distribution',91 )92 def test_venv_install_options(self, tmp_path, clear_argv):93 sys.argv.append("install")94 file = str(tmp_path / 'file')95 96 fakepath = '/somedir'97 98 jaraco.path.build({99 file: f"""100 [install]101 install-base = {fakepath}102 install-platbase = {fakepath}103 install-lib = {fakepath}104 install-platlib = {fakepath}105 install-purelib = {fakepath}106 install-headers = {fakepath}107 install-scripts = {fakepath}108 install-data = {fakepath}109 prefix = {fakepath}110 exec-prefix = {fakepath}111 home = {fakepath}112 user = {fakepath}113 root = {fakepath}114 """,115 })116 117 # Base case: Not in a Virtual Environment118 with mock.patch.multiple(sys, prefix='/a', base_prefix='/a'):119 d = self.create_distribution([file])120 121 option_tuple = (file, fakepath)122 123 result_dict = {124 'install_base': option_tuple,125 'install_platbase': option_tuple,126 'install_lib': option_tuple,127 'install_platlib': option_tuple,128 'install_purelib': option_tuple,129 'install_headers': option_tuple,130 'install_scripts': option_tuple,131 'install_data': option_tuple,132 'prefix': option_tuple,133 'exec_prefix': option_tuple,134 'home': option_tuple,135 'user': option_tuple,136 'root': option_tuple,137 }138 139 assert sorted(d.command_options.get('install').keys()) == sorted(140 result_dict.keys()141 )142 143 for key, value in d.command_options.get('install').items():144 assert value == result_dict[key]145 146 # Test case: In a Virtual Environment147 with mock.patch.multiple(sys, prefix='/a', base_prefix='/b'):148 d = self.create_distribution([file])149 150 for key in result_dict.keys():151 assert key not in d.command_options.get('install', {})152 153 def test_command_packages_configfile(self, tmp_path, clear_argv):154 sys.argv.append("build")155 file = str(tmp_path / "file")156 jaraco.path.build({157 file: """158 [global]159 command_packages = foo.bar, splat160 """,161 })162 163 d = self.create_distribution([file])164 assert d.get_command_packages() == ["distutils.command", "foo.bar", "splat"]165 166 # ensure command line overrides config:167 sys.argv[1:] = ["--command-packages", "spork", "build"]168 d = self.create_distribution([file])169 assert d.get_command_packages() == ["distutils.command", "spork"]170 171 # Setting --command-packages to '' should cause the default to172 # be used even if a config file specified something else:173 sys.argv[1:] = ["--command-packages", "", "build"]174 d = self.create_distribution([file])175 assert d.get_command_packages() == ["distutils.command"]176 177 def test_empty_options(self, request):178 # an empty options dictionary should not stay in the179 # list of attributes180 181 # catching warnings182 warns = []183 184 def _warn(msg):185 warns.append(msg)186 187 request.addfinalizer(188 functools.partial(setattr, warnings, 'warn', warnings.warn)189 )190 warnings.warn = _warn191 dist = Distribution(192 attrs={193 'author': 'xxx',194 'name': 'xxx',195 'version': 'xxx',196 'url': 'xxxx',197 'options': {},198 }199 )200 201 assert len(warns) == 0202 assert 'options' not in dir(dist)203 204 def test_finalize_options(self):205 attrs = {'keywords': 'one,two', 'platforms': 'one,two'}206 207 dist = Distribution(attrs=attrs)208 dist.finalize_options()209 210 # finalize_option splits platforms and keywords211 assert dist.metadata.platforms == ['one', 'two']212 assert dist.metadata.keywords == ['one', 'two']213 214 attrs = {'keywords': 'foo bar', 'platforms': 'foo bar'}215 dist = Distribution(attrs=attrs)216 dist.finalize_options()217 assert dist.metadata.platforms == ['foo bar']218 assert dist.metadata.keywords == ['foo bar']219 220 def test_get_command_packages(self):221 dist = Distribution()222 assert dist.command_packages is None223 cmds = dist.get_command_packages()224 assert cmds == ['distutils.command']225 assert dist.command_packages == ['distutils.command']226 227 dist.command_packages = 'one,two'228 cmds = dist.get_command_packages()229 assert cmds == ['distutils.command', 'one', 'two']230 231 def test_announce(self):232 # make sure the level is known233 dist = Distribution()234 with pytest.raises(TypeError):235 dist.announce('ok', level='ok2')236 237 def test_find_config_files_disable(self, temp_home):238 # Ticket #1180: Allow user to disable their home config file.239 jaraco.path.build({pydistutils_cfg: '[distutils]\n'}, temp_home)240 241 d = Distribution()242 all_files = d.find_config_files()243 244 d = Distribution(attrs={'script_args': ['--no-user-cfg']})245 files = d.find_config_files()246 247 # make sure --no-user-cfg disables the user cfg file248 assert len(all_files) - 1 == len(files)249 250 def test_script_args_list_coercion(self):251 d = Distribution(attrs={'script_args': ('build', '--no-user-cfg')})252 253 # make sure script_args is a list even if it started as a different iterable254 assert d.script_args == ['build', '--no-user-cfg']255 256 @pytest.mark.skipif(257 'platform.system() == "Windows"',258 reason='Windows does not honor chmod 000',259 )260 def test_find_config_files_permission_error(self, fake_home):261 """262 Finding config files should not fail when directory is inaccessible.263 """264 fake_home.joinpath(pydistutils_cfg).write_text('', encoding='utf-8')265 fake_home.chmod(0o000)266 Distribution().find_config_files()267 268 269@pytest.mark.usefixtures('save_env')270@pytest.mark.usefixtures('save_argv')271class TestMetadata(support.TempdirManager):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", "version": "1.0"}279 dist = Distribution(attrs)280 meta = self.format_metadata(dist)281 assert "Metadata-Version: 1.0" in meta282 assert "provides:" not in meta.lower()283 assert "requires:" not in meta.lower()284 assert "obsoletes:" not in meta.lower()285 286 def test_provides(self):287 attrs = {288 "name": "package",289 "version": "1.0",290 "provides": ["package", "package.sub"],291 }292 dist = Distribution(attrs)293 assert dist.metadata.get_provides() == ["package", "package.sub"]294 assert dist.get_provides() == ["package", "package.sub"]295 meta = self.format_metadata(dist)296 assert "Metadata-Version: 1.1" in meta297 assert "requires:" not in meta.lower()298 assert "obsoletes:" not in meta.lower()299 300 def test_provides_illegal(self):301 with pytest.raises(ValueError):302 Distribution(303 {"name": "package", "version": "1.0", "provides": ["my.pkg (splat)"]},304 )305 306 def test_requires(self):307 attrs = {308 "name": "package",309 "version": "1.0",310 "requires": ["other", "another (==1.0)"],311 }312 dist = Distribution(attrs)313 assert dist.metadata.get_requires() == ["other", "another (==1.0)"]314 assert dist.get_requires() == ["other", "another (==1.0)"]315 meta = self.format_metadata(dist)316 assert "Metadata-Version: 1.1" in meta317 assert "provides:" not in meta.lower()318 assert "Requires: other" in meta319 assert "Requires: another (==1.0)" in meta320 assert "obsoletes:" not in meta.lower()321 322 def test_requires_illegal(self):323 with pytest.raises(ValueError):324 Distribution(325 {"name": "package", "version": "1.0", "requires": ["my.pkg (splat)"]},326 )327 328 def test_requires_to_list(self):329 attrs = {"name": "package", "requires": iter(["other"])}330 dist = Distribution(attrs)331 assert isinstance(dist.metadata.requires, list)332 333 def test_obsoletes(self):334 attrs = {335 "name": "package",336 "version": "1.0",337 "obsoletes": ["other", "another (<1.0)"],338 }339 dist = Distribution(attrs)340 assert dist.metadata.get_obsoletes() == ["other", "another (<1.0)"]341 assert dist.get_obsoletes() == ["other", "another (<1.0)"]342 meta = self.format_metadata(dist)343 assert "Metadata-Version: 1.1" in meta344 assert "provides:" not in meta.lower()345 assert "requires:" not in meta.lower()346 assert "Obsoletes: other" in meta347 assert "Obsoletes: another (<1.0)" in meta348 349 def test_obsoletes_illegal(self):350 with pytest.raises(ValueError):351 Distribution(352 {"name": "package", "version": "1.0", "obsoletes": ["my.pkg (splat)"]},353 )354 355 def test_obsoletes_to_list(self):356 attrs = {"name": "package", "obsoletes": iter(["other"])}357 dist = Distribution(attrs)358 assert isinstance(dist.metadata.obsoletes, list)359 360 def test_classifier(self):361 attrs = {362 'name': 'Boa',363 'version': '3.0',364 'classifiers': ['Programming Language :: Python :: 3'],365 }366 dist = Distribution(attrs)367 assert dist.get_classifiers() == ['Programming Language :: Python :: 3']368 meta = self.format_metadata(dist)369 assert 'Metadata-Version: 1.1' in meta370 371 def test_classifier_invalid_type(self, caplog):372 attrs = {373 'name': 'Boa',374 'version': '3.0',375 'classifiers': ('Programming Language :: Python :: 3',),376 }377 d = Distribution(attrs)378 # should have warning about passing a non-list379 assert 'should be a list' in caplog.messages[0]380 # should be converted to a list381 assert isinstance(d.metadata.classifiers, list)382 assert d.metadata.classifiers == list(attrs['classifiers'])383 384 def test_keywords(self):385 attrs = {386 'name': 'Monty',387 'version': '1.0',388 'keywords': ['spam', 'eggs', 'life of brian'],389 }390 dist = Distribution(attrs)391 assert dist.get_keywords() == ['spam', 'eggs', 'life of brian']392 393 def test_keywords_invalid_type(self, caplog):394 attrs = {395 'name': 'Monty',396 'version': '1.0',397 'keywords': ('spam', 'eggs', 'life of brian'),398 }399 d = Distribution(attrs)400 # should have warning about passing a non-list401 assert 'should be a list' in caplog.messages[0]402 # should be converted to a list403 assert isinstance(d.metadata.keywords, list)404 assert d.metadata.keywords == list(attrs['keywords'])405 406 def test_platforms(self):407 attrs = {408 'name': 'Monty',409 'version': '1.0',410 'platforms': ['GNU/Linux', 'Some Evil Platform'],411 }412 dist = Distribution(attrs)413 assert dist.get_platforms() == ['GNU/Linux', 'Some Evil Platform']414 415 def test_platforms_invalid_types(self, caplog):416 attrs = {417 'name': 'Monty',418 'version': '1.0',419 'platforms': ('GNU/Linux', 'Some Evil Platform'),420 }421 d = Distribution(attrs)422 # should have warning about passing a non-list423 assert 'should be a list' in caplog.messages[0]424 # should be converted to a list425 assert isinstance(d.metadata.platforms, list)426 assert d.metadata.platforms == list(attrs['platforms'])427 428 def test_download_url(self):429 attrs = {430 'name': 'Boa',431 'version': '3.0',432 'download_url': 'http://example.org/boa',433 }434 dist = Distribution(attrs)435 meta = self.format_metadata(dist)436 assert 'Metadata-Version: 1.1' in meta437 438 def test_long_description(self):439 long_desc = textwrap.dedent(440 """\441 example::442 We start here443 and continue here444 and end here."""445 )446 attrs = {"name": "package", "version": "1.0", "long_description": long_desc}447 448 dist = Distribution(attrs)449 meta = self.format_metadata(dist)450 meta = meta.replace('\n' + 8 * ' ', '\n')451 assert long_desc in meta452 453 def test_custom_pydistutils(self, temp_home):454 """455 pydistutils.cfg is found456 """457 jaraco.path.build({pydistutils_cfg: ''}, temp_home)458 config_path = temp_home / pydistutils_cfg459 460 assert str(config_path) in Distribution().find_config_files()461 462 def test_extra_pydistutils(self, monkeypatch, tmp_path):463 jaraco.path.build({'overrides.cfg': ''}, tmp_path)464 filename = tmp_path / 'overrides.cfg'465 monkeypatch.setenv('DIST_EXTRA_CONFIG', str(filename))466 assert str(filename) in Distribution().find_config_files()467 468 def test_fix_help_options(self):469 help_tuples = [('a', 'b', 'c', 'd'), (1, 2, 3, 4)]470 fancy_options = fix_help_options(help_tuples)471 assert fancy_options[0] == ('a', 'b', 'c')472 assert fancy_options[1] == (1, 2, 3)473 474 def test_show_help(self, request, capsys):475 # smoke test, just makes sure some help is displayed476 dist = Distribution()477 sys.argv = []478 dist.help = True479 dist.script_name = 'setup.py'480 dist.parse_command_line()481 482 output = [483 line for line in capsys.readouterr().out.split('\n') if line.strip() != ''484 ]485 assert output486 487 def test_read_metadata(self):488 attrs = {489 "name": "package",490 "version": "1.0",491 "long_description": "desc",492 "description": "xxx",493 "download_url": "http://example.com",494 "keywords": ['one', 'two'],495 "requires": ['foo'],496 }497 498 dist = Distribution(attrs)499 metadata = dist.metadata500 501 # write it then reloads it502 PKG_INFO = io.StringIO()503 metadata.write_pkg_file(PKG_INFO)504 PKG_INFO.seek(0)505 metadata.read_pkg_file(PKG_INFO)506 507 assert metadata.name == "package"508 assert metadata.version == "1.0"509 assert metadata.description == "xxx"510 assert metadata.download_url == 'http://example.com'511 assert metadata.keywords == ['one', 'two']512 assert metadata.platforms is None513 assert metadata.obsoletes is None514 assert metadata.requires == ['foo']515 516 def test_round_trip_through_email_generator(self):517 """518 In pypa/setuptools#4033, it was shown that once PKG-INFO is519 re-generated using ``email.generator.Generator``, some control520 characters might cause problems.521 """522 # Given a PKG-INFO file ...523 attrs = {524 "name": "package",525 "version": "1.0",526 "long_description": "hello\x0b\nworld\n",527 }528 dist = Distribution(attrs)529 metadata = dist.metadata530 531 with io.StringIO() as buffer:532 metadata.write_pkg_file(buffer)533 msg = buffer.getvalue()534 535 # ... when it is read and re-written using stdlib's email library,536 orig = email.message_from_string(msg)537 policy = email.policy.EmailPolicy(538 utf8=True,539 mangle_from_=False,540 max_line_length=0,541 )542 with io.StringIO() as buffer:543 email.generator.Generator(buffer, policy=policy).flatten(orig)544 545 buffer.seek(0)546 regen = email.message_from_file(buffer)547 548 # ... then it should be the same as the original549 # (except for the specific line break characters)550 orig_desc = set(orig["Description"].splitlines())551 regen_desc = set(regen["Description"].splitlines())552 assert regen_desc == orig_desc553 