test_wheel.py 15 KB

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