codekingpro/portable-devtools
114k
1"""wheel tests"""2 3from __future__ import annotations4 5import contextlib6import glob7import inspect8import os9import pathlib10import stat11import subprocess12import sys13import sysconfig14import zipfile15from typing import Any16 17import pytest18from jaraco import path19from packaging.tags import parse_tag20 21from setuptools._importlib import metadata22from setuptools.wheel import Wheel23 24from .contexts import tempdir25from .textwrap import DALS26 27from distutils.sysconfig import get_config_var28from distutils.util import get_platform29 30WHEEL_INFO_TESTS = (31 ('invalid.whl', ValueError),32 (33 'simplewheel-2.0-1-py2.py3-none-any.whl',34 {35 'project_name': 'simplewheel',36 'version': '2.0',37 'build': '1',38 'py_version': 'py2.py3',39 'abi': 'none',40 'platform': 'any',41 },42 ),43 (44 'simple.dist-0.1-py2.py3-none-any.whl',45 {46 'project_name': 'simple.dist',47 'version': '0.1',48 'build': None,49 'py_version': 'py2.py3',50 'abi': 'none',51 'platform': 'any',52 },53 ),54 (55 'example_pkg_a-1-py3-none-any.whl',56 {57 'project_name': 'example_pkg_a',58 'version': '1',59 'build': None,60 'py_version': 'py3',61 'abi': 'none',62 'platform': 'any',63 },64 ),65 (66 'PyQt5-5.9-5.9.1-cp35.cp36.cp37-abi3-manylinux1_x86_64.whl',67 {68 'project_name': 'PyQt5',69 'version': '5.9',70 'build': '5.9.1',71 'py_version': 'cp35.cp36.cp37',72 'abi': 'abi3',73 'platform': 'manylinux1_x86_64',74 },75 ),76)77 78 79@pytest.mark.parametrize(80 ('filename', 'info'), WHEEL_INFO_TESTS, ids=[t[0] for t in WHEEL_INFO_TESTS]81)82def test_wheel_info(filename, info):83 if inspect.isclass(info):84 with pytest.raises(info):85 Wheel(filename)86 return87 w = Wheel(filename)88 assert {k: getattr(w, k) for k in info.keys()} == info89 90 91@contextlib.contextmanager92def build_wheel(extra_file_defs=None, **kwargs):93 file_defs = {94 'setup.py': (95 DALS(96 """97 # -*- coding: utf-8 -*-98 from setuptools import setup99 import setuptools100 setup(**%r)101 """102 )103 % kwargs104 ).encode('utf-8'),105 }106 if extra_file_defs:107 file_defs.update(extra_file_defs)108 with tempdir() as source_dir:109 path.build(file_defs, source_dir)110 subprocess.check_call(111 (sys.executable, 'setup.py', '-q', 'bdist_wheel'), cwd=source_dir112 )113 yield glob.glob(os.path.join(source_dir, 'dist', '*.whl'))[0]114 115 116def tree_set(root):117 return {118 os.path.join(os.path.relpath(dirpath, root), filename)119 for dirpath, dirnames, filenames in os.walk(root)120 for filename in filenames121 }122 123 124def flatten_tree(tree):125 """Flatten nested dicts and lists into a full list of paths"""126 output = set()127 for node, contents in tree.items():128 if isinstance(contents, dict):129 contents = flatten_tree(contents)130 131 for elem in contents:132 if isinstance(elem, dict):133 output |= {os.path.join(node, val) for val in flatten_tree(elem)}134 else:135 output.add(os.path.join(node, elem))136 return output137 138 139def format_install_tree(tree):140 return {141 x.format(142 py_version=sysconfig.get_python_version(),143 platform=get_platform(),144 shlib_ext=get_config_var('EXT_SUFFIX') or get_config_var('SO'),145 )146 for x in tree147 }148 149 150def _check_wheel_install(151 filename, install_dir, install_tree_includes, project_name, version, requires_txt152):153 w = Wheel(filename)154 egg_path = os.path.join(install_dir, w.egg_name())155 w.install_as_egg(egg_path)156 if install_tree_includes is not None:157 install_tree = format_install_tree(install_tree_includes)158 exp = tree_set(install_dir)159 assert install_tree.issubset(exp), install_tree - exp160 161 (dist,) = metadata.Distribution.discover(path=[egg_path])162 163 # pyright is nitpicky; fine to assume dist.metadata.__getitem__ will fail or return None164 # (https://github.com/pypa/setuptools/pull/5006#issuecomment-2894774288)165 assert dist.metadata['Name'] == project_name # pyright: ignore # noqa: PGH003166 assert dist.metadata['Version'] == version # pyright: ignore # noqa: PGH003167 assert dist.read_text('requires.txt') == requires_txt168 169 170class Record:171 def __init__(self, id, **kwargs) -> None:172 self._id = id173 self._fields = kwargs174 175 def __repr__(self) -> str:176 return f'{self._id}(**{self._fields!r})'177 178 179# Using Any to avoid possible type union issues later in test180# making a TypedDict is not worth in a test and anonymous/inline TypedDict are experimental181# https://github.com/python/mypy/issues/9884182WHEEL_INSTALL_TESTS: tuple[dict[str, Any], ...] = (183 dict(184 id='basic',185 file_defs={'foo': {'__init__.py': ''}},186 setup_kwargs=dict(187 packages=['foo'],188 ),189 install_tree=flatten_tree({190 'foo-1.0-py{py_version}.egg': {191 'EGG-INFO': ['PKG-INFO', 'RECORD', 'WHEEL', 'top_level.txt'],192 'foo': ['__init__.py'],193 }194 }),195 ),196 dict(197 id='utf-8',198 setup_kwargs=dict(199 description='Description accentuée',200 ),201 ),202 dict(203 id='data',204 file_defs={205 'data.txt': DALS(206 """207 Some data...208 """209 ),210 },211 setup_kwargs=dict(212 data_files=[('data_dir', ['data.txt'])],213 ),214 install_tree=flatten_tree({215 'foo-1.0-py{py_version}.egg': {216 'EGG-INFO': ['PKG-INFO', 'RECORD', 'WHEEL', 'top_level.txt'],217 'data_dir': ['data.txt'],218 }219 }),220 ),221 dict(222 id='extension',223 file_defs={224 'extension.c': DALS(225 """226 #include "Python.h"227 228 #if PY_MAJOR_VERSION >= 3229 230 static struct PyModuleDef moduledef = {231 PyModuleDef_HEAD_INIT,232 "extension",233 NULL,234 0,235 NULL,236 NULL,237 NULL,238 NULL,239 NULL240 };241 242 #define INITERROR return NULL243 244 PyMODINIT_FUNC PyInit_extension(void)245 246 #else247 248 #define INITERROR return249 250 void initextension(void)251 252 #endif253 {254 #if PY_MAJOR_VERSION >= 3255 PyObject *module = PyModule_Create(&moduledef);256 #else257 PyObject *module = Py_InitModule("extension", NULL);258 #endif259 if (module == NULL)260 INITERROR;261 #if PY_MAJOR_VERSION >= 3262 return module;263 #endif264 }265 """266 ),267 },268 setup_kwargs=dict(269 ext_modules=[270 Record(271 'setuptools.Extension', name='extension', sources=['extension.c']272 )273 ],274 ),275 install_tree=flatten_tree({276 'foo-1.0-py{py_version}-{platform}.egg': [277 'extension{shlib_ext}',278 {279 'EGG-INFO': [280 'PKG-INFO',281 'RECORD',282 'WHEEL',283 'top_level.txt',284 ]285 },286 ]287 }),288 ),289 dict(290 id='header',291 file_defs={292 'header.h': DALS(293 """294 """295 ),296 },297 setup_kwargs=dict(298 headers=['header.h'],299 ),300 install_tree=flatten_tree({301 'foo-1.0-py{py_version}.egg': [302 'header.h',303 {304 'EGG-INFO': [305 'PKG-INFO',306 'RECORD',307 'WHEEL',308 'top_level.txt',309 ]310 },311 ]312 }),313 ),314 dict(315 id='script',316 file_defs={317 'script.py': DALS(318 """319 #/usr/bin/python320 print('hello world!')321 """322 ),323 'script.sh': DALS(324 """325 #/bin/sh326 echo 'hello world!'327 """328 ),329 },330 setup_kwargs=dict(331 scripts=['script.py', 'script.sh'],332 ),333 install_tree=flatten_tree({334 'foo-1.0-py{py_version}.egg': {335 'EGG-INFO': [336 'PKG-INFO',337 'RECORD',338 'WHEEL',339 'top_level.txt',340 {'scripts': ['script.py', 'script.sh']},341 ]342 }343 }),344 ),345 dict(346 id='requires1',347 install_requires='foobar==2.0',348 install_tree=flatten_tree({349 'foo-1.0-py{py_version}.egg': {350 'EGG-INFO': [351 'PKG-INFO',352 'RECORD',353 'WHEEL',354 'requires.txt',355 'top_level.txt',356 ]357 }358 }),359 requires_txt=DALS(360 """361 foobar==2.0362 """363 ),364 ),365 dict(366 id='requires2',367 install_requires=f"""368 bar369 foo<=2.0; {sys.platform!r} in sys_platform370 """,371 requires_txt=DALS(372 """373 bar374 foo<=2.0375 """376 ),377 ),378 dict(379 id='requires3',380 install_requires=f"""381 bar; {sys.platform!r} != sys_platform382 """,383 ),384 dict(385 id='requires4',386 install_requires="""387 foo388 """,389 extras_require={390 'extra': 'foobar>3',391 },392 requires_txt=DALS(393 """394 foo395 396 [extra]397 foobar>3398 """399 ),400 ),401 dict(402 id='requires5',403 extras_require={404 'extra': f'foobar; {sys.platform!r} != sys_platform',405 },406 requires_txt='\n'407 + DALS(408 """409 [extra]410 """411 ),412 ),413 dict(414 id='requires_ensure_order',415 install_requires="""416 foo417 bar418 baz419 qux420 """,421 extras_require={422 'extra': """423 foobar>3424 barbaz>4425 bazqux>5426 quxzap>6427 """,428 },429 requires_txt=DALS(430 """431 foo432 bar433 baz434 qux435 436 [extra]437 foobar>3438 barbaz>4439 bazqux>5440 quxzap>6441 """442 ),443 ),444 dict(445 id='namespace_package',446 file_defs={447 'foo': {448 'bar': {'__init__.py': ''},449 },450 },451 setup_kwargs=dict(452 namespace_packages=['foo'],453 packages=['foo.bar'],454 ),455 install_tree=flatten_tree({456 'foo-1.0-py{py_version}.egg': [457 'foo-1.0-py{py_version}-nspkg.pth',458 {459 'EGG-INFO': [460 'PKG-INFO',461 'RECORD',462 'WHEEL',463 'namespace_packages.txt',464 'top_level.txt',465 ]466 },467 {468 'foo': [469 '__init__.py',470 {'bar': ['__init__.py']},471 ]472 },473 ]474 }),475 ),476 dict(477 id='empty_namespace_package',478 file_defs={479 'foobar': {480 '__init__.py': (481 "__import__('pkg_resources').declare_namespace(__name__)"482 )483 },484 },485 setup_kwargs=dict(486 namespace_packages=['foobar'],487 packages=['foobar'],488 ),489 install_tree=flatten_tree({490 'foo-1.0-py{py_version}.egg': [491 'foo-1.0-py{py_version}-nspkg.pth',492 {493 'EGG-INFO': [494 'PKG-INFO',495 'RECORD',496 'WHEEL',497 'namespace_packages.txt',498 'top_level.txt',499 ]500 },501 {502 'foobar': [503 '__init__.py',504 ]505 },506 ]507 }),508 ),509 dict(510 id='data_in_package',511 file_defs={512 'foo': {513 '__init__.py': '',514 'data_dir': {515 'data.txt': DALS(516 """517 Some data...518 """519 ),520 },521 }522 },523 setup_kwargs=dict(524 packages=['foo'],525 data_files=[('foo/data_dir', ['foo/data_dir/data.txt'])],526 ),527 install_tree=flatten_tree({528 'foo-1.0-py{py_version}.egg': {529 'EGG-INFO': [530 'PKG-INFO',531 'RECORD',532 'WHEEL',533 'top_level.txt',534 ],535 'foo': [536 '__init__.py',537 {538 'data_dir': [539 'data.txt',540 ]541 },542 ],543 }544 }),545 ),546)547 548 549@pytest.mark.parametrize(550 'params',551 WHEEL_INSTALL_TESTS,552 ids=[params['id'] for params in WHEEL_INSTALL_TESTS],553)554def test_wheel_install(params):555 project_name = params.get('name', 'foo')556 version = params.get('version', '1.0')557 install_requires = params.get('install_requires', [])558 extras_require = params.get('extras_require', {})559 requires_txt = params.get('requires_txt', None)560 install_tree = params.get('install_tree')561 file_defs = params.get('file_defs', {})562 setup_kwargs = params.get('setup_kwargs', {})563 with (564 build_wheel(565 name=project_name,566 version=version,567 install_requires=install_requires,568 extras_require=extras_require,569 extra_file_defs=file_defs,570 **setup_kwargs,571 ) as filename,572 tempdir() as install_dir,573 ):574 _check_wheel_install(575 filename, install_dir, install_tree, project_name, version, requires_txt576 )577 578 579def test_wheel_no_dist_dir():580 project_name = 'nodistinfo'581 version = '1.0'582 wheel_name = f'{project_name}-{version}-py2.py3-none-any.whl'583 with tempdir() as source_dir:584 wheel_path = os.path.join(source_dir, wheel_name)585 # create an empty zip file586 zipfile.ZipFile(wheel_path, 'w').close()587 with tempdir() as install_dir:588 with pytest.raises(ValueError):589 _check_wheel_install(590 wheel_path, install_dir, None, project_name, version, None591 )592 593 594def test_wheel_is_compatible(monkeypatch):595 def sys_tags():596 return {597 (t.interpreter, t.abi, t.platform)598 for t in parse_tag('cp36-cp36m-manylinux1_x86_64')599 }600 601 monkeypatch.setattr('setuptools.wheel._get_supported_tags', sys_tags)602 assert Wheel('onnxruntime-0.1.2-cp36-cp36m-manylinux1_x86_64.whl').is_compatible()603 604 605def test_wheel_mode():606 @contextlib.contextmanager607 def build_wheel(extra_file_defs=None, **kwargs):608 file_defs = {609 'setup.py': (610 DALS(611 """612 # -*- coding: utf-8 -*-613 from setuptools import setup614 import setuptools615 setup(**%r)616 """617 )618 % kwargs619 ).encode('utf-8'),620 }621 if extra_file_defs:622 file_defs.update(extra_file_defs)623 with tempdir() as source_dir:624 path.build(file_defs, source_dir)625 runsh = pathlib.Path(source_dir) / "script.sh"626 os.chmod(runsh, 0o777)627 subprocess.check_call(628 (sys.executable, 'setup.py', '-q', 'bdist_wheel'), cwd=source_dir629 )630 yield glob.glob(os.path.join(source_dir, 'dist', '*.whl'))[0]631 632 params = dict(633 id='script',634 file_defs={635 'script.py': DALS(636 """637 #/usr/bin/python638 print('hello world!')639 """640 ),641 'script.sh': DALS(642 """643 #/bin/sh644 echo 'hello world!'645 """646 ),647 },648 setup_kwargs=dict(649 scripts=['script.py', 'script.sh'],650 ),651 install_tree=flatten_tree({652 'foo-1.0-py{py_version}.egg': {653 'EGG-INFO': [654 'PKG-INFO',655 'RECORD',656 'WHEEL',657 'top_level.txt',658 {'scripts': ['script.py', 'script.sh']},659 ]660 }661 }),662 )663 664 project_name = params.get('name', 'foo')665 version = params.get('version', '1.0')666 install_tree = params.get('install_tree')667 file_defs = params.get('file_defs', {})668 setup_kwargs = params.get('setup_kwargs', {})669 670 with (671 build_wheel(672 name=project_name,673 version=version,674 install_requires=[],675 extras_require={},676 extra_file_defs=file_defs,677 **setup_kwargs,678 ) as filename,679 tempdir() as install_dir,680 ):681 _check_wheel_install(682 filename, install_dir, install_tree, project_name, version, None683 )684 w = Wheel(filename)685 base = pathlib.Path(install_dir) / w.egg_name()686 script_sh = base / "EGG-INFO" / "scripts" / "script.sh"687 assert script_sh.exists()688 if sys.platform != 'win32':689 # Editable file mode has no effect on Windows690 assert oct(stat.S_IMODE(script_sh.stat().st_mode)) == "0o777"691 