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