• 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 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