• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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