• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import inspect
2import re
3import textwrap
4import functools
5
6import pytest
7
8import pkg_resources
9
10from .test_resources import Metadata
11
12
13def strip_comments(s):
14    return '\n'.join(
15        l for l in s.split('\n')
16        if l.strip() and not l.strip().startswith('#')
17    )
18
19
20def parse_distributions(s):
21    '''
22    Parse a series of distribution specs of the form:
23    {project_name}-{version}
24       [optional, indented requirements specification]
25
26    Example:
27
28        foo-0.2
29        bar-1.0
30          foo>=3.0
31          [feature]
32          baz
33
34    yield 2 distributions:
35        - project_name=foo, version=0.2
36        - project_name=bar, version=1.0,
37          requires=['foo>=3.0', 'baz; extra=="feature"']
38    '''
39    s = s.strip()
40    for spec in re.split('\n(?=[^\s])', s):
41        if not spec:
42            continue
43        fields = spec.split('\n', 1)
44        assert 1 <= len(fields) <= 2
45        name, version = fields.pop(0).split('-')
46        if fields:
47            requires = textwrap.dedent(fields.pop(0))
48            metadata = Metadata(('requires.txt', requires))
49        else:
50            metadata = None
51        dist = pkg_resources.Distribution(project_name=name,
52                                          version=version,
53                                          metadata=metadata)
54        yield dist
55
56
57class FakeInstaller(object):
58
59    def __init__(self, installable_dists):
60        self._installable_dists = installable_dists
61
62    def __call__(self, req):
63        return next(iter(filter(lambda dist: dist in req,
64                                self._installable_dists)), None)
65
66
67def parametrize_test_working_set_resolve(*test_list):
68    idlist = []
69    argvalues = []
70    for test in test_list:
71        (
72            name,
73            installed_dists,
74            installable_dists,
75            requirements,
76            expected1, expected2
77        ) = [
78            strip_comments(s.lstrip()) for s in
79            textwrap.dedent(test).lstrip().split('\n\n', 5)
80        ]
81        installed_dists = list(parse_distributions(installed_dists))
82        installable_dists = list(parse_distributions(installable_dists))
83        requirements = list(pkg_resources.parse_requirements(requirements))
84        for id_, replace_conflicting, expected in (
85            (name, False, expected1),
86            (name + '_replace_conflicting', True, expected2),
87        ):
88            idlist.append(id_)
89            expected = strip_comments(expected.strip())
90            if re.match('\w+$', expected):
91                expected = getattr(pkg_resources, expected)
92                assert issubclass(expected, Exception)
93            else:
94                expected = list(parse_distributions(expected))
95            argvalues.append(pytest.param(installed_dists, installable_dists,
96                                          requirements, replace_conflicting,
97                                          expected))
98    return pytest.mark.parametrize('installed_dists,installable_dists,'
99                                   'requirements,replace_conflicting,'
100                                   'resolved_dists_or_exception',
101                                   argvalues, ids=idlist)
102
103
104@parametrize_test_working_set_resolve(
105    '''
106    # id
107    noop
108
109    # installed
110
111    # installable
112
113    # wanted
114
115    # resolved
116
117    # resolved [replace conflicting]
118    ''',
119
120    '''
121    # id
122    already_installed
123
124    # installed
125    foo-3.0
126
127    # installable
128
129    # wanted
130    foo>=2.1,!=3.1,<4
131
132    # resolved
133    foo-3.0
134
135    # resolved [replace conflicting]
136    foo-3.0
137    ''',
138
139    '''
140    # id
141    installable_not_installed
142
143    # installed
144
145    # installable
146    foo-3.0
147    foo-4.0
148
149    # wanted
150    foo>=2.1,!=3.1,<4
151
152    # resolved
153    foo-3.0
154
155    # resolved [replace conflicting]
156    foo-3.0
157    ''',
158
159    '''
160    # id
161    not_installable
162
163    # installed
164
165    # installable
166
167    # wanted
168    foo>=2.1,!=3.1,<4
169
170    # resolved
171    DistributionNotFound
172
173    # resolved [replace conflicting]
174    DistributionNotFound
175    ''',
176
177    '''
178    # id
179    no_matching_version
180
181    # installed
182
183    # installable
184    foo-3.1
185
186    # wanted
187    foo>=2.1,!=3.1,<4
188
189    # resolved
190    DistributionNotFound
191
192    # resolved [replace conflicting]
193    DistributionNotFound
194    ''',
195
196    '''
197    # id
198    installable_with_installed_conflict
199
200    # installed
201    foo-3.1
202
203    # installable
204    foo-3.5
205
206    # wanted
207    foo>=2.1,!=3.1,<4
208
209    # resolved
210    VersionConflict
211
212    # resolved [replace conflicting]
213    foo-3.5
214    ''',
215
216    '''
217    # id
218    not_installable_with_installed_conflict
219
220    # installed
221    foo-3.1
222
223    # installable
224
225    # wanted
226    foo>=2.1,!=3.1,<4
227
228    # resolved
229    VersionConflict
230
231    # resolved [replace conflicting]
232    DistributionNotFound
233    ''',
234
235    '''
236    # id
237    installed_with_installed_require
238
239    # installed
240    foo-3.9
241    baz-0.1
242        foo>=2.1,!=3.1,<4
243
244    # installable
245
246    # wanted
247    baz
248
249    # resolved
250    foo-3.9
251    baz-0.1
252
253    # resolved [replace conflicting]
254    foo-3.9
255    baz-0.1
256    ''',
257
258    '''
259    # id
260    installed_with_conflicting_installed_require
261
262    # installed
263    foo-5
264    baz-0.1
265        foo>=2.1,!=3.1,<4
266
267    # installable
268
269    # wanted
270    baz
271
272    # resolved
273    VersionConflict
274
275    # resolved [replace conflicting]
276    DistributionNotFound
277    ''',
278
279    '''
280    # id
281    installed_with_installable_conflicting_require
282
283    # installed
284    foo-5
285    baz-0.1
286        foo>=2.1,!=3.1,<4
287
288    # installable
289    foo-2.9
290
291    # wanted
292    baz
293
294    # resolved
295    VersionConflict
296
297    # resolved [replace conflicting]
298    baz-0.1
299    foo-2.9
300    ''',
301
302    '''
303    # id
304    installed_with_installable_require
305
306    # installed
307    baz-0.1
308        foo>=2.1,!=3.1,<4
309
310    # installable
311    foo-3.9
312
313    # wanted
314    baz
315
316    # resolved
317    foo-3.9
318    baz-0.1
319
320    # resolved [replace conflicting]
321    foo-3.9
322    baz-0.1
323    ''',
324
325    '''
326    # id
327    installable_with_installed_require
328
329    # installed
330    foo-3.9
331
332    # installable
333    baz-0.1
334        foo>=2.1,!=3.1,<4
335
336    # wanted
337    baz
338
339    # resolved
340    foo-3.9
341    baz-0.1
342
343    # resolved [replace conflicting]
344    foo-3.9
345    baz-0.1
346    ''',
347
348    '''
349    # id
350    installable_with_installable_require
351
352    # installed
353
354    # installable
355    foo-3.9
356    baz-0.1
357        foo>=2.1,!=3.1,<4
358
359    # wanted
360    baz
361
362    # resolved
363    foo-3.9
364    baz-0.1
365
366    # resolved [replace conflicting]
367    foo-3.9
368    baz-0.1
369    ''',
370
371    '''
372    # id
373    installable_with_conflicting_installable_require
374
375    # installed
376    foo-5
377
378    # installable
379    foo-2.9
380    baz-0.1
381        foo>=2.1,!=3.1,<4
382
383    # wanted
384    baz
385
386    # resolved
387    VersionConflict
388
389    # resolved [replace conflicting]
390    baz-0.1
391    foo-2.9
392    ''',
393
394    '''
395    # id
396    conflicting_installables
397
398    # installed
399
400    # installable
401    foo-2.9
402    foo-5.0
403
404    # wanted
405    foo>=2.1,!=3.1,<4
406    foo>=4
407
408    # resolved
409    VersionConflict
410
411    # resolved [replace conflicting]
412    VersionConflict
413    ''',
414
415    '''
416    # id
417    installables_with_conflicting_requires
418
419    # installed
420
421    # installable
422    foo-2.9
423        dep==1.0
424    baz-5.0
425        dep==2.0
426    dep-1.0
427    dep-2.0
428
429    # wanted
430    foo
431    baz
432
433    # resolved
434    VersionConflict
435
436    # resolved [replace conflicting]
437    VersionConflict
438    ''',
439
440    '''
441    # id
442    installables_with_conflicting_nested_requires
443
444    # installed
445
446    # installable
447    foo-2.9
448        dep1
449    dep1-1.0
450        subdep<1.0
451    baz-5.0
452        dep2
453    dep2-1.0
454        subdep>1.0
455    subdep-0.9
456    subdep-1.1
457
458    # wanted
459    foo
460    baz
461
462    # resolved
463    VersionConflict
464
465    # resolved [replace conflicting]
466    VersionConflict
467    ''',
468)
469def test_working_set_resolve(installed_dists, installable_dists, requirements,
470                             replace_conflicting, resolved_dists_or_exception):
471    ws = pkg_resources.WorkingSet([])
472    list(map(ws.add, installed_dists))
473    resolve_call = functools.partial(
474        ws.resolve,
475        requirements, installer=FakeInstaller(installable_dists),
476        replace_conflicting=replace_conflicting,
477    )
478    if inspect.isclass(resolved_dists_or_exception):
479        with pytest.raises(resolved_dists_or_exception):
480            resolve_call()
481    else:
482        assert sorted(resolve_call()) == sorted(resolved_dists_or_exception)
483