1# -*- coding: utf-8 -*- 2 3"""wheel tests 4""" 5 6from distutils.sysconfig import get_config_var 7from distutils.util import get_platform 8import contextlib 9import glob 10import inspect 11import os 12import shutil 13import subprocess 14import sys 15import zipfile 16 17import pytest 18from jaraco import path 19 20from pkg_resources import Distribution, PathMetadata, PY_MAJOR 21from setuptools.extern.packaging.utils import canonicalize_name 22from setuptools.extern.packaging.tags import parse_tag 23from setuptools.wheel import Wheel 24 25from .contexts import tempdir 26from .textwrap import DALS 27 28 29WHEEL_INFO_TESTS = ( 30 ('invalid.whl', ValueError), 31 ('simplewheel-2.0-1-py2.py3-none-any.whl', { 32 'project_name': 'simplewheel', 33 'version': '2.0', 34 'build': '1', 35 'py_version': 'py2.py3', 36 'abi': 'none', 37 'platform': 'any', 38 }), 39 ('simple.dist-0.1-py2.py3-none-any.whl', { 40 'project_name': 'simple.dist', 41 'version': '0.1', 42 'build': None, 43 'py_version': 'py2.py3', 44 'abi': 'none', 45 'platform': 'any', 46 }), 47 ('example_pkg_a-1-py3-none-any.whl', { 48 'project_name': 'example_pkg_a', 49 'version': '1', 50 'build': None, 51 'py_version': 'py3', 52 'abi': 'none', 53 'platform': 'any', 54 }), 55 ('PyQt5-5.9-5.9.1-cp35.cp36.cp37-abi3-manylinux1_x86_64.whl', { 56 'project_name': 'PyQt5', 57 'version': '5.9', 58 'build': '5.9.1', 59 'py_version': 'cp35.cp36.cp37', 60 'abi': 'abi3', 61 'platform': 'manylinux1_x86_64', 62 }), 63) 64 65 66@pytest.mark.parametrize( 67 ('filename', 'info'), WHEEL_INFO_TESTS, 68 ids=[t[0] for t in WHEEL_INFO_TESTS] 69) 70def test_wheel_info(filename, info): 71 if inspect.isclass(info): 72 with pytest.raises(info): 73 Wheel(filename) 74 return 75 w = Wheel(filename) 76 assert {k: getattr(w, k) for k in info.keys()} == info 77 78 79@contextlib.contextmanager 80def build_wheel(extra_file_defs=None, **kwargs): 81 file_defs = { 82 'setup.py': (DALS( 83 ''' 84 # -*- coding: utf-8 -*- 85 from setuptools import setup 86 import setuptools 87 setup(**%r) 88 ''' 89 ) % kwargs).encode('utf-8'), 90 } 91 if extra_file_defs: 92 file_defs.update(extra_file_defs) 93 with tempdir() as source_dir: 94 path.build(file_defs, source_dir) 95 subprocess.check_call((sys.executable, 'setup.py', 96 '-q', 'bdist_wheel'), cwd=source_dir) 97 yield glob.glob(os.path.join(source_dir, 'dist', '*.whl'))[0] 98 99 100def tree_set(root): 101 contents = set() 102 for dirpath, dirnames, filenames in os.walk(root): 103 for filename in filenames: 104 contents.add(os.path.join(os.path.relpath(dirpath, root), 105 filename)) 106 return contents 107 108 109def flatten_tree(tree): 110 """Flatten nested dicts and lists into a full list of paths""" 111 output = set() 112 for node, contents in tree.items(): 113 if isinstance(contents, dict): 114 contents = flatten_tree(contents) 115 116 for elem in contents: 117 if isinstance(elem, dict): 118 output |= {os.path.join(node, val) 119 for val in flatten_tree(elem)} 120 else: 121 output.add(os.path.join(node, elem)) 122 return output 123 124 125def format_install_tree(tree): 126 return { 127 x.format( 128 py_version=PY_MAJOR, 129 platform=get_platform(), 130 shlib_ext=get_config_var('EXT_SUFFIX') or get_config_var('SO')) 131 for x in tree} 132 133 134def _check_wheel_install(filename, install_dir, install_tree_includes, 135 project_name, version, requires_txt): 136 w = Wheel(filename) 137 egg_path = os.path.join(install_dir, w.egg_name()) 138 w.install_as_egg(egg_path) 139 if install_tree_includes is not None: 140 install_tree = format_install_tree(install_tree_includes) 141 exp = tree_set(install_dir) 142 assert install_tree.issubset(exp), (install_tree - exp) 143 144 metadata = PathMetadata(egg_path, os.path.join(egg_path, 'EGG-INFO')) 145 dist = Distribution.from_filename(egg_path, metadata=metadata) 146 assert dist.project_name == project_name 147 assert dist.version == version 148 if requires_txt is None: 149 assert not dist.has_metadata('requires.txt') 150 else: 151 # Order must match to ensure reproducibility. 152 assert requires_txt == dist.get_metadata('requires.txt').lstrip() 153 154 155class Record: 156 157 def __init__(self, id, **kwargs): 158 self._id = id 159 self._fields = kwargs 160 161 def __repr__(self): 162 return '%s(**%r)' % (self._id, self._fields) 163 164 165WHEEL_INSTALL_TESTS = ( 166 167 dict( 168 id='basic', 169 file_defs={ 170 'foo': { 171 '__init__.py': '' 172 } 173 }, 174 setup_kwargs=dict( 175 packages=['foo'], 176 ), 177 install_tree=flatten_tree({ 178 'foo-1.0-py{py_version}.egg': { 179 'EGG-INFO': [ 180 'PKG-INFO', 181 'RECORD', 182 'WHEEL', 183 'top_level.txt' 184 ], 185 'foo': ['__init__.py'] 186 } 187 }), 188 ), 189 190 dict( 191 id='utf-8', 192 setup_kwargs=dict( 193 description='Description accentuée', 194 ) 195 ), 196 197 dict( 198 id='data', 199 file_defs={ 200 'data.txt': DALS( 201 ''' 202 Some data... 203 ''' 204 ), 205 }, 206 setup_kwargs=dict( 207 data_files=[('data_dir', ['data.txt'])], 208 ), 209 install_tree=flatten_tree({ 210 'foo-1.0-py{py_version}.egg': { 211 'EGG-INFO': [ 212 'PKG-INFO', 213 'RECORD', 214 'WHEEL', 215 'top_level.txt' 216 ], 217 'data_dir': [ 218 'data.txt' 219 ] 220 } 221 }), 222 ), 223 224 dict( 225 id='extension', 226 file_defs={ 227 'extension.c': DALS( 228 ''' 229 #include "Python.h" 230 231 #if PY_MAJOR_VERSION >= 3 232 233 static struct PyModuleDef moduledef = { 234 PyModuleDef_HEAD_INIT, 235 "extension", 236 NULL, 237 0, 238 NULL, 239 NULL, 240 NULL, 241 NULL, 242 NULL 243 }; 244 245 #define INITERROR return NULL 246 247 PyMODINIT_FUNC PyInit_extension(void) 248 249 #else 250 251 #define INITERROR return 252 253 void initextension(void) 254 255 #endif 256 { 257 #if PY_MAJOR_VERSION >= 3 258 PyObject *module = PyModule_Create(&moduledef); 259 #else 260 PyObject *module = Py_InitModule("extension", NULL); 261 #endif 262 if (module == NULL) 263 INITERROR; 264 #if PY_MAJOR_VERSION >= 3 265 return module; 266 #endif 267 } 268 ''' 269 ), 270 }, 271 setup_kwargs=dict( 272 ext_modules=[ 273 Record('setuptools.Extension', 274 name='extension', 275 sources=['extension.c']) 276 ], 277 ), 278 install_tree=flatten_tree({ 279 'foo-1.0-py{py_version}-{platform}.egg': [ 280 'extension{shlib_ext}', 281 {'EGG-INFO': [ 282 'PKG-INFO', 283 'RECORD', 284 'WHEEL', 285 'top_level.txt', 286 ]}, 287 ] 288 }), 289 ), 290 291 dict( 292 id='header', 293 file_defs={ 294 'header.h': DALS( 295 ''' 296 ''' 297 ), 298 }, 299 setup_kwargs=dict( 300 headers=['header.h'], 301 ), 302 install_tree=flatten_tree({ 303 'foo-1.0-py{py_version}.egg': [ 304 'header.h', 305 {'EGG-INFO': [ 306 'PKG-INFO', 307 'RECORD', 308 'WHEEL', 309 'top_level.txt', 310 ]}, 311 ] 312 }), 313 ), 314 315 dict( 316 id='script', 317 file_defs={ 318 'script.py': DALS( 319 ''' 320 #/usr/bin/python 321 print('hello world!') 322 ''' 323 ), 324 'script.sh': DALS( 325 ''' 326 #/bin/sh 327 echo 'hello world!' 328 ''' 329 ), 330 }, 331 setup_kwargs=dict( 332 scripts=['script.py', 'script.sh'], 333 ), 334 install_tree=flatten_tree({ 335 'foo-1.0-py{py_version}.egg': { 336 'EGG-INFO': [ 337 'PKG-INFO', 338 'RECORD', 339 'WHEEL', 340 'top_level.txt', 341 {'scripts': [ 342 'script.py', 343 'script.sh' 344 ]} 345 346 ] 347 } 348 }) 349 ), 350 351 dict( 352 id='requires1', 353 install_requires='foobar==2.0', 354 install_tree=flatten_tree({ 355 'foo-1.0-py{py_version}.egg': { 356 'EGG-INFO': [ 357 'PKG-INFO', 358 'RECORD', 359 'WHEEL', 360 'requires.txt', 361 'top_level.txt', 362 ] 363 } 364 }), 365 requires_txt=DALS( 366 ''' 367 foobar==2.0 368 ''' 369 ), 370 ), 371 372 dict( 373 id='requires2', 374 install_requires=''' 375 bar 376 foo<=2.0; %r in sys_platform 377 ''' % sys.platform, 378 requires_txt=DALS( 379 ''' 380 bar 381 foo<=2.0 382 ''' 383 ), 384 ), 385 386 dict( 387 id='requires3', 388 install_requires=''' 389 bar; %r != sys_platform 390 ''' % sys.platform, 391 ), 392 393 dict( 394 id='requires4', 395 install_requires=''' 396 foo 397 ''', 398 extras_require={ 399 'extra': 'foobar>3', 400 }, 401 requires_txt=DALS( 402 ''' 403 foo 404 405 [extra] 406 foobar>3 407 ''' 408 ), 409 ), 410 411 dict( 412 id='requires5', 413 extras_require={ 414 'extra': 'foobar; %r != sys_platform' % sys.platform, 415 }, 416 requires_txt=DALS( 417 ''' 418 [extra] 419 ''' 420 ), 421 ), 422 423 dict( 424 id='requires_ensure_order', 425 install_requires=''' 426 foo 427 bar 428 baz 429 qux 430 ''', 431 extras_require={ 432 'extra': ''' 433 foobar>3 434 barbaz>4 435 bazqux>5 436 quxzap>6 437 ''', 438 }, 439 requires_txt=DALS( 440 ''' 441 foo 442 bar 443 baz 444 qux 445 446 [extra] 447 foobar>3 448 barbaz>4 449 bazqux>5 450 quxzap>6 451 ''' 452 ), 453 ), 454 455 dict( 456 id='namespace_package', 457 file_defs={ 458 'foo': { 459 'bar': { 460 '__init__.py': '' 461 }, 462 }, 463 }, 464 setup_kwargs=dict( 465 namespace_packages=['foo'], 466 packages=['foo.bar'], 467 ), 468 install_tree=flatten_tree({ 469 'foo-1.0-py{py_version}.egg': [ 470 'foo-1.0-py{py_version}-nspkg.pth', 471 {'EGG-INFO': [ 472 'PKG-INFO', 473 'RECORD', 474 'WHEEL', 475 'namespace_packages.txt', 476 'top_level.txt', 477 ]}, 478 {'foo': [ 479 '__init__.py', 480 {'bar': ['__init__.py']}, 481 ]}, 482 ] 483 }), 484 ), 485 486 dict( 487 id='empty_namespace_package', 488 file_defs={ 489 'foobar': { 490 '__init__.py': 491 "__import__('pkg_resources').declare_namespace(__name__)", 492 }, 493 }, 494 setup_kwargs=dict( 495 namespace_packages=['foobar'], 496 packages=['foobar'], 497 ), 498 install_tree=flatten_tree({ 499 'foo-1.0-py{py_version}.egg': [ 500 'foo-1.0-py{py_version}-nspkg.pth', 501 {'EGG-INFO': [ 502 'PKG-INFO', 503 'RECORD', 504 'WHEEL', 505 'namespace_packages.txt', 506 'top_level.txt', 507 ]}, 508 {'foobar': [ 509 '__init__.py', 510 ]}, 511 ] 512 }), 513 ), 514 515 dict( 516 id='data_in_package', 517 file_defs={ 518 'foo': { 519 '__init__.py': '', 520 'data_dir': { 521 'data.txt': DALS( 522 ''' 523 Some data... 524 ''' 525 ), 526 } 527 } 528 }, 529 setup_kwargs=dict( 530 packages=['foo'], 531 data_files=[('foo/data_dir', ['foo/data_dir/data.txt'])], 532 ), 533 install_tree=flatten_tree({ 534 'foo-1.0-py{py_version}.egg': { 535 'EGG-INFO': [ 536 'PKG-INFO', 537 'RECORD', 538 'WHEEL', 539 'top_level.txt', 540 ], 541 'foo': [ 542 '__init__.py', 543 {'data_dir': [ 544 'data.txt', 545 ]} 546 ] 547 } 548 }), 549 ), 550 551) 552 553 554@pytest.mark.parametrize( 555 'params', WHEEL_INSTALL_TESTS, 556 ids=list(params['id'] for params in WHEEL_INSTALL_TESTS), 557) 558def test_wheel_install(params): 559 project_name = params.get('name', 'foo') 560 version = params.get('version', '1.0') 561 install_requires = params.get('install_requires', []) 562 extras_require = params.get('extras_require', {}) 563 requires_txt = params.get('requires_txt', None) 564 install_tree = params.get('install_tree') 565 file_defs = params.get('file_defs', {}) 566 setup_kwargs = params.get('setup_kwargs', {}) 567 with build_wheel( 568 name=project_name, 569 version=version, 570 install_requires=install_requires, 571 extras_require=extras_require, 572 extra_file_defs=file_defs, 573 **setup_kwargs 574 ) as filename, tempdir() as install_dir: 575 _check_wheel_install(filename, install_dir, 576 install_tree, project_name, 577 version, requires_txt) 578 579 580def test_wheel_install_pep_503(): 581 project_name = 'Foo_Bar' # PEP 503 canonicalized name is "foo-bar" 582 version = '1.0' 583 with build_wheel( 584 name=project_name, 585 version=version, 586 ) as filename, tempdir() as install_dir: 587 new_filename = filename.replace(project_name, 588 canonicalize_name(project_name)) 589 shutil.move(filename, new_filename) 590 _check_wheel_install(new_filename, install_dir, None, 591 canonicalize_name(project_name), 592 version, None) 593 594 595def test_wheel_no_dist_dir(): 596 project_name = 'nodistinfo' 597 version = '1.0' 598 wheel_name = '{0}-{1}-py2.py3-none-any.whl'.format(project_name, version) 599 with tempdir() as source_dir: 600 wheel_path = os.path.join(source_dir, wheel_name) 601 # create an empty zip file 602 zipfile.ZipFile(wheel_path, 'w').close() 603 with tempdir() as install_dir: 604 with pytest.raises(ValueError): 605 _check_wheel_install(wheel_path, install_dir, None, 606 project_name, 607 version, None) 608 609 610def test_wheel_is_compatible(monkeypatch): 611 def sys_tags(): 612 for t in parse_tag('cp36-cp36m-manylinux1_x86_64'): 613 yield t 614 monkeypatch.setattr('setuptools.wheel.sys_tags', sys_tags) 615 assert Wheel( 616 'onnxruntime-0.1.2-cp36-cp36m-manylinux1_x86_64.whl').is_compatible() 617