1import os
2import sys
3import string
4import platform
5import itertools
6
7import pytest
8from pkg_resources.extern import packaging
9
10import pkg_resources
11from pkg_resources import (
12    parse_requirements, VersionConflict, parse_version,
13    Distribution, EntryPoint, Requirement, safe_version, safe_name,
14    WorkingSet)
15
16
17# from Python 3.6 docs.
18def pairwise(iterable):
19    "s -> (s0,s1), (s1,s2), (s2, s3), ..."
20    a, b = itertools.tee(iterable)
21    next(b, None)
22    return zip(a, b)
23
24
25class Metadata(pkg_resources.EmptyProvider):
26    """Mock object to return metadata as if from an on-disk distribution"""
27
28    def __init__(self, *pairs):
29        self.metadata = dict(pairs)
30
31    def has_metadata(self, name):
32        return name in self.metadata
33
34    def get_metadata(self, name):
35        return self.metadata[name]
36
37    def get_metadata_lines(self, name):
38        return pkg_resources.yield_lines(self.get_metadata(name))
39
40
41dist_from_fn = pkg_resources.Distribution.from_filename
42
43
44class TestDistro:
45    def testCollection(self):
46        # empty path should produce no distributions
47        ad = pkg_resources.Environment([], platform=None, python=None)
48        assert list(ad) == []
49        assert ad['FooPkg'] == []
50        ad.add(dist_from_fn("FooPkg-1.3_1.egg"))
51        ad.add(dist_from_fn("FooPkg-1.4-py2.4-win32.egg"))
52        ad.add(dist_from_fn("FooPkg-1.2-py2.4.egg"))
53
54        # Name is in there now
55        assert ad['FooPkg']
56        # But only 1 package
57        assert list(ad) == ['foopkg']
58
59        # Distributions sort by version
60        expected = ['1.4', '1.3-1', '1.2']
61        assert [dist.version for dist in ad['FooPkg']] == expected
62
63        # Removing a distribution leaves sequence alone
64        ad.remove(ad['FooPkg'][1])
65        assert [dist.version for dist in ad['FooPkg']] == ['1.4', '1.2']
66
67        # And inserting adds them in order
68        ad.add(dist_from_fn("FooPkg-1.9.egg"))
69        assert [dist.version for dist in ad['FooPkg']] == ['1.9', '1.4', '1.2']
70
71        ws = WorkingSet([])
72        foo12 = dist_from_fn("FooPkg-1.2-py2.4.egg")
73        foo14 = dist_from_fn("FooPkg-1.4-py2.4-win32.egg")
74        req, = parse_requirements("FooPkg>=1.3")
75
76        # Nominal case: no distros on path, should yield all applicable
77        assert ad.best_match(req, ws).version == '1.9'
78        # If a matching distro is already installed, should return only that
79        ws.add(foo14)
80        assert ad.best_match(req, ws).version == '1.4'
81
82        # If the first matching distro is unsuitable, it's a version conflict
83        ws = WorkingSet([])
84        ws.add(foo12)
85        ws.add(foo14)
86        with pytest.raises(VersionConflict):
87            ad.best_match(req, ws)
88
89        # If more than one match on the path, the first one takes precedence
90        ws = WorkingSet([])
91        ws.add(foo14)
92        ws.add(foo12)
93        ws.add(foo14)
94        assert ad.best_match(req, ws).version == '1.4'
95
96    def checkFooPkg(self, d):
97        assert d.project_name == "FooPkg"
98        assert d.key == "foopkg"
99        assert d.version == "1.3.post1"
100        assert d.py_version == "2.4"
101        assert d.platform == "win32"
102        assert d.parsed_version == parse_version("1.3-1")
103
104    def testDistroBasics(self):
105        d = Distribution(
106            "/some/path",
107            project_name="FooPkg",
108            version="1.3-1",
109            py_version="2.4",
110            platform="win32",
111        )
112        self.checkFooPkg(d)
113
114        d = Distribution("/some/path")
115        assert d.py_version == '{}.{}'.format(*sys.version_info)
116        assert d.platform is None
117
118    def testDistroParse(self):
119        d = dist_from_fn("FooPkg-1.3.post1-py2.4-win32.egg")
120        self.checkFooPkg(d)
121        d = dist_from_fn("FooPkg-1.3.post1-py2.4-win32.egg-info")
122        self.checkFooPkg(d)
123
124    def testDistroMetadata(self):
125        d = Distribution(
126            "/some/path", project_name="FooPkg",
127            py_version="2.4", platform="win32",
128            metadata=Metadata(
129                ('PKG-INFO', "Metadata-Version: 1.0\nVersion: 1.3-1\n")
130            ),
131        )
132        self.checkFooPkg(d)
133
134    def distRequires(self, txt):
135        return Distribution("/foo", metadata=Metadata(('depends.txt', txt)))
136
137    def checkRequires(self, dist, txt, extras=()):
138        assert list(dist.requires(extras)) == list(parse_requirements(txt))
139
140    def testDistroDependsSimple(self):
141        for v in "Twisted>=1.5", "Twisted>=1.5\nZConfig>=2.0":
142            self.checkRequires(self.distRequires(v), v)
143
144    needs_object_dir = pytest.mark.skipif(
145        not hasattr(object, '__dir__'),
146        reason='object.__dir__ necessary for self.__dir__ implementation',
147    )
148
149    def test_distribution_dir(self):
150        d = pkg_resources.Distribution()
151        dir(d)
152
153    @needs_object_dir
154    def test_distribution_dir_includes_provider_dir(self):
155        d = pkg_resources.Distribution()
156        before = d.__dir__()
157        assert 'test_attr' not in before
158        d._provider.test_attr = None
159        after = d.__dir__()
160        assert len(after) == len(before) + 1
161        assert 'test_attr' in after
162
163    @needs_object_dir
164    def test_distribution_dir_ignores_provider_dir_leading_underscore(self):
165        d = pkg_resources.Distribution()
166        before = d.__dir__()
167        assert '_test_attr' not in before
168        d._provider._test_attr = None
169        after = d.__dir__()
170        assert len(after) == len(before)
171        assert '_test_attr' not in after
172
173    def testResolve(self):
174        ad = pkg_resources.Environment([])
175        ws = WorkingSet([])
176        # Resolving no requirements -> nothing to install
177        assert list(ws.resolve([], ad)) == []
178        # Request something not in the collection -> DistributionNotFound
179        with pytest.raises(pkg_resources.DistributionNotFound):
180            ws.resolve(parse_requirements("Foo"), ad)
181
182        Foo = Distribution.from_filename(
183            "/foo_dir/Foo-1.2.egg",
184            metadata=Metadata(('depends.txt', "[bar]\nBaz>=2.0"))
185        )
186        ad.add(Foo)
187        ad.add(Distribution.from_filename("Foo-0.9.egg"))
188
189        # Request thing(s) that are available -> list to activate
190        for i in range(3):
191            targets = list(ws.resolve(parse_requirements("Foo"), ad))
192            assert targets == [Foo]
193            list(map(ws.add, targets))
194        with pytest.raises(VersionConflict):
195            ws.resolve(parse_requirements("Foo==0.9"), ad)
196        ws = WorkingSet([])  # reset
197
198        # Request an extra that causes an unresolved dependency for "Baz"
199        with pytest.raises(pkg_resources.DistributionNotFound):
200            ws.resolve(parse_requirements("Foo[bar]"), ad)
201        Baz = Distribution.from_filename(
202            "/foo_dir/Baz-2.1.egg", metadata=Metadata(('depends.txt', "Foo"))
203        )
204        ad.add(Baz)
205
206        # Activation list now includes resolved dependency
207        assert (
208            list(ws.resolve(parse_requirements("Foo[bar]"), ad))
209            == [Foo, Baz]
210        )
211        # Requests for conflicting versions produce VersionConflict
212        with pytest.raises(VersionConflict) as vc:
213            ws.resolve(parse_requirements("Foo==1.2\nFoo!=1.2"), ad)
214
215        msg = 'Foo 0.9 is installed but Foo==1.2 is required'
216        assert vc.value.report() == msg
217
218    def test_environment_marker_evaluation_negative(self):
219        """Environment markers are evaluated at resolution time."""
220        ad = pkg_resources.Environment([])
221        ws = WorkingSet([])
222        res = ws.resolve(parse_requirements("Foo;python_version<'2'"), ad)
223        assert list(res) == []
224
225    def test_environment_marker_evaluation_positive(self):
226        ad = pkg_resources.Environment([])
227        ws = WorkingSet([])
228        Foo = Distribution.from_filename("/foo_dir/Foo-1.2.dist-info")
229        ad.add(Foo)
230        res = ws.resolve(parse_requirements("Foo;python_version>='2'"), ad)
231        assert list(res) == [Foo]
232
233    def test_environment_marker_evaluation_called(self):
234        """
235        If one package foo requires bar without any extras,
236        markers should pass for bar without extras.
237        """
238        parent_req, = parse_requirements("foo")
239        req, = parse_requirements("bar;python_version>='2'")
240        req_extras = pkg_resources._ReqExtras({req: parent_req.extras})
241        assert req_extras.markers_pass(req)
242
243        parent_req, = parse_requirements("foo[]")
244        req, = parse_requirements("bar;python_version>='2'")
245        req_extras = pkg_resources._ReqExtras({req: parent_req.extras})
246        assert req_extras.markers_pass(req)
247
248    def test_marker_evaluation_with_extras(self):
249        """Extras are also evaluated as markers at resolution time."""
250        ad = pkg_resources.Environment([])
251        ws = WorkingSet([])
252        Foo = Distribution.from_filename(
253            "/foo_dir/Foo-1.2.dist-info",
254            metadata=Metadata(("METADATA", "Provides-Extra: baz\n"
255                               "Requires-Dist: quux; extra=='baz'"))
256        )
257        ad.add(Foo)
258        assert list(ws.resolve(parse_requirements("Foo"), ad)) == [Foo]
259        quux = Distribution.from_filename("/foo_dir/quux-1.0.dist-info")
260        ad.add(quux)
261        res = list(ws.resolve(parse_requirements("Foo[baz]"), ad))
262        assert res == [Foo, quux]
263
264    def test_marker_evaluation_with_extras_normlized(self):
265        """Extras are also evaluated as markers at resolution time."""
266        ad = pkg_resources.Environment([])
267        ws = WorkingSet([])
268        Foo = Distribution.from_filename(
269            "/foo_dir/Foo-1.2.dist-info",
270            metadata=Metadata(("METADATA", "Provides-Extra: baz-lightyear\n"
271                               "Requires-Dist: quux; extra=='baz-lightyear'"))
272        )
273        ad.add(Foo)
274        assert list(ws.resolve(parse_requirements("Foo"), ad)) == [Foo]
275        quux = Distribution.from_filename("/foo_dir/quux-1.0.dist-info")
276        ad.add(quux)
277        res = list(ws.resolve(parse_requirements("Foo[baz-lightyear]"), ad))
278        assert res == [Foo, quux]
279
280    def test_marker_evaluation_with_multiple_extras(self):
281        ad = pkg_resources.Environment([])
282        ws = WorkingSet([])
283        Foo = Distribution.from_filename(
284            "/foo_dir/Foo-1.2.dist-info",
285            metadata=Metadata(("METADATA", "Provides-Extra: baz\n"
286                               "Requires-Dist: quux; extra=='baz'\n"
287                               "Provides-Extra: bar\n"
288                               "Requires-Dist: fred; extra=='bar'\n"))
289        )
290        ad.add(Foo)
291        quux = Distribution.from_filename("/foo_dir/quux-1.0.dist-info")
292        ad.add(quux)
293        fred = Distribution.from_filename("/foo_dir/fred-0.1.dist-info")
294        ad.add(fred)
295        res = list(ws.resolve(parse_requirements("Foo[baz,bar]"), ad))
296        assert sorted(res) == [fred, quux, Foo]
297
298    def test_marker_evaluation_with_extras_loop(self):
299        ad = pkg_resources.Environment([])
300        ws = WorkingSet([])
301        a = Distribution.from_filename(
302            "/foo_dir/a-0.2.dist-info",
303            metadata=Metadata(("METADATA", "Requires-Dist: c[a]"))
304        )
305        b = Distribution.from_filename(
306            "/foo_dir/b-0.3.dist-info",
307            metadata=Metadata(("METADATA", "Requires-Dist: c[b]"))
308        )
309        c = Distribution.from_filename(
310            "/foo_dir/c-1.0.dist-info",
311            metadata=Metadata(("METADATA", "Provides-Extra: a\n"
312                               "Requires-Dist: b;extra=='a'\n"
313                               "Provides-Extra: b\n"
314                               "Requires-Dist: foo;extra=='b'"))
315        )
316        foo = Distribution.from_filename("/foo_dir/foo-0.1.dist-info")
317        for dist in (a, b, c, foo):
318            ad.add(dist)
319        res = list(ws.resolve(parse_requirements("a"), ad))
320        assert res == [a, c, b, foo]
321
322    def testDistroDependsOptions(self):
323        d = self.distRequires("""
324            Twisted>=1.5
325            [docgen]
326            ZConfig>=2.0
327            docutils>=0.3
328            [fastcgi]
329            fcgiapp>=0.1""")
330        self.checkRequires(d, "Twisted>=1.5")
331        self.checkRequires(
332            d, "Twisted>=1.5 ZConfig>=2.0 docutils>=0.3".split(), ["docgen"]
333        )
334        self.checkRequires(
335            d, "Twisted>=1.5 fcgiapp>=0.1".split(), ["fastcgi"]
336        )
337        self.checkRequires(
338            d, "Twisted>=1.5 ZConfig>=2.0 docutils>=0.3 fcgiapp>=0.1".split(),
339            ["docgen", "fastcgi"]
340        )
341        self.checkRequires(
342            d, "Twisted>=1.5 fcgiapp>=0.1 ZConfig>=2.0 docutils>=0.3".split(),
343            ["fastcgi", "docgen"]
344        )
345        with pytest.raises(pkg_resources.UnknownExtra):
346            d.requires(["foo"])
347
348
349class TestWorkingSet:
350    def test_find_conflicting(self):
351        ws = WorkingSet([])
352        Foo = Distribution.from_filename("/foo_dir/Foo-1.2.egg")
353        ws.add(Foo)
354
355        # create a requirement that conflicts with Foo 1.2
356        req = next(parse_requirements("Foo<1.2"))
357
358        with pytest.raises(VersionConflict) as vc:
359            ws.find(req)
360
361        msg = 'Foo 1.2 is installed but Foo<1.2 is required'
362        assert vc.value.report() == msg
363
364    def test_resolve_conflicts_with_prior(self):
365        """
366        A ContextualVersionConflict should be raised when a requirement
367        conflicts with a prior requirement for a different package.
368        """
369        # Create installation where Foo depends on Baz 1.0 and Bar depends on
370        # Baz 2.0.
371        ws = WorkingSet([])
372        md = Metadata(('depends.txt', "Baz==1.0"))
373        Foo = Distribution.from_filename("/foo_dir/Foo-1.0.egg", metadata=md)
374        ws.add(Foo)
375        md = Metadata(('depends.txt', "Baz==2.0"))
376        Bar = Distribution.from_filename("/foo_dir/Bar-1.0.egg", metadata=md)
377        ws.add(Bar)
378        Baz = Distribution.from_filename("/foo_dir/Baz-1.0.egg")
379        ws.add(Baz)
380        Baz = Distribution.from_filename("/foo_dir/Baz-2.0.egg")
381        ws.add(Baz)
382
383        with pytest.raises(VersionConflict) as vc:
384            ws.resolve(parse_requirements("Foo\nBar\n"))
385
386        msg = "Baz 1.0 is installed but Baz==2.0 is required by "
387        msg += repr(set(['Bar']))
388        assert vc.value.report() == msg
389
390
391class TestEntryPoints:
392    def assertfields(self, ep):
393        assert ep.name == "foo"
394        assert ep.module_name == "pkg_resources.tests.test_resources"
395        assert ep.attrs == ("TestEntryPoints",)
396        assert ep.extras == ("x",)
397        assert ep.load() is TestEntryPoints
398        expect = "foo = pkg_resources.tests.test_resources:TestEntryPoints [x]"
399        assert str(ep) == expect
400
401    def setup_method(self, method):
402        self.dist = Distribution.from_filename(
403            "FooPkg-1.2-py2.4.egg", metadata=Metadata(('requires.txt', '[x]')))
404
405    def testBasics(self):
406        ep = EntryPoint(
407            "foo", "pkg_resources.tests.test_resources", ["TestEntryPoints"],
408            ["x"], self.dist
409        )
410        self.assertfields(ep)
411
412    def testParse(self):
413        s = "foo = pkg_resources.tests.test_resources:TestEntryPoints [x]"
414        ep = EntryPoint.parse(s, self.dist)
415        self.assertfields(ep)
416
417        ep = EntryPoint.parse("bar baz=  spammity[PING]")
418        assert ep.name == "bar baz"
419        assert ep.module_name == "spammity"
420        assert ep.attrs == ()
421        assert ep.extras == ("ping",)
422
423        ep = EntryPoint.parse(" fizzly =  wocka:foo")
424        assert ep.name == "fizzly"
425        assert ep.module_name == "wocka"
426        assert ep.attrs == ("foo",)
427        assert ep.extras == ()
428
429        # plus in the name
430        spec = "html+mako = mako.ext.pygmentplugin:MakoHtmlLexer"
431        ep = EntryPoint.parse(spec)
432        assert ep.name == 'html+mako'
433
434    reject_specs = "foo", "x=a:b:c", "q=x/na", "fez=pish:tush-z", "x=f[a]>2"
435
436    @pytest.mark.parametrize("reject_spec", reject_specs)
437    def test_reject_spec(self, reject_spec):
438        with pytest.raises(ValueError):
439            EntryPoint.parse(reject_spec)
440
441    def test_printable_name(self):
442        """
443        Allow any printable character in the name.
444        """
445        # Create a name with all printable characters; strip the whitespace.
446        name = string.printable.strip()
447        spec = "{name} = module:attr".format(**locals())
448        ep = EntryPoint.parse(spec)
449        assert ep.name == name
450
451    def checkSubMap(self, m):
452        assert len(m) == len(self.submap_expect)
453        for key, ep in self.submap_expect.items():
454            assert m.get(key).name == ep.name
455            assert m.get(key).module_name == ep.module_name
456            assert sorted(m.get(key).attrs) == sorted(ep.attrs)
457            assert sorted(m.get(key).extras) == sorted(ep.extras)
458
459    submap_expect = dict(
460        feature1=EntryPoint('feature1', 'somemodule', ['somefunction']),
461        feature2=EntryPoint(
462            'feature2', 'another.module', ['SomeClass'], ['extra1', 'extra2']),
463        feature3=EntryPoint('feature3', 'this.module', extras=['something'])
464    )
465    submap_str = """
466            # define features for blah blah
467            feature1 = somemodule:somefunction
468            feature2 = another.module:SomeClass [extra1,extra2]
469            feature3 = this.module [something]
470    """
471
472    def testParseList(self):
473        self.checkSubMap(EntryPoint.parse_group("xyz", self.submap_str))
474        with pytest.raises(ValueError):
475            EntryPoint.parse_group("x a", "foo=bar")
476        with pytest.raises(ValueError):
477            EntryPoint.parse_group("x", ["foo=baz", "foo=bar"])
478
479    def testParseMap(self):
480        m = EntryPoint.parse_map({'xyz': self.submap_str})
481        self.checkSubMap(m['xyz'])
482        assert list(m.keys()) == ['xyz']
483        m = EntryPoint.parse_map("[xyz]\n" + self.submap_str)
484        self.checkSubMap(m['xyz'])
485        assert list(m.keys()) == ['xyz']
486        with pytest.raises(ValueError):
487            EntryPoint.parse_map(["[xyz]", "[xyz]"])
488        with pytest.raises(ValueError):
489            EntryPoint.parse_map(self.submap_str)
490
491    def testDeprecationWarnings(self):
492        ep = EntryPoint(
493            "foo", "pkg_resources.tests.test_resources", ["TestEntryPoints"],
494            ["x"]
495        )
496        with pytest.warns(pkg_resources.PkgResourcesDeprecationWarning):
497            ep.load(require=False)
498
499
500class TestRequirements:
501    def testBasics(self):
502        r = Requirement.parse("Twisted>=1.2")
503        assert str(r) == "Twisted>=1.2"
504        assert repr(r) == "Requirement.parse('Twisted>=1.2')"
505        assert r == Requirement("Twisted>=1.2")
506        assert r == Requirement("twisTed>=1.2")
507        assert r != Requirement("Twisted>=2.0")
508        assert r != Requirement("Zope>=1.2")
509        assert r != Requirement("Zope>=3.0")
510        assert r != Requirement("Twisted[extras]>=1.2")
511
512    def testOrdering(self):
513        r1 = Requirement("Twisted==1.2c1,>=1.2")
514        r2 = Requirement("Twisted>=1.2,==1.2c1")
515        assert r1 == r2
516        assert str(r1) == str(r2)
517        assert str(r2) == "Twisted==1.2c1,>=1.2"
518        assert (
519            Requirement("Twisted")
520            !=
521            Requirement("Twisted @ https://localhost/twisted.zip")
522        )
523
524    def testBasicContains(self):
525        r = Requirement("Twisted>=1.2")
526        foo_dist = Distribution.from_filename("FooPkg-1.3_1.egg")
527        twist11 = Distribution.from_filename("Twisted-1.1.egg")
528        twist12 = Distribution.from_filename("Twisted-1.2.egg")
529        assert parse_version('1.2') in r
530        assert parse_version('1.1') not in r
531        assert '1.2' in r
532        assert '1.1' not in r
533        assert foo_dist not in r
534        assert twist11 not in r
535        assert twist12 in r
536
537    def testOptionsAndHashing(self):
538        r1 = Requirement.parse("Twisted[foo,bar]>=1.2")
539        r2 = Requirement.parse("Twisted[bar,FOO]>=1.2")
540        assert r1 == r2
541        assert set(r1.extras) == set(("foo", "bar"))
542        assert set(r2.extras) == set(("foo", "bar"))
543        assert hash(r1) == hash(r2)
544        assert (
545            hash(r1)
546            ==
547            hash((
548                "twisted",
549                None,
550                packaging.specifiers.SpecifierSet(">=1.2"),
551                frozenset(["foo", "bar"]),
552                None
553            ))
554        )
555        assert (
556            hash(Requirement.parse("Twisted @ https://localhost/twisted.zip"))
557            ==
558            hash((
559                "twisted",
560                "https://localhost/twisted.zip",
561                packaging.specifiers.SpecifierSet(),
562                frozenset(),
563                None
564            ))
565        )
566
567    def testVersionEquality(self):
568        r1 = Requirement.parse("foo==0.3a2")
569        r2 = Requirement.parse("foo!=0.3a4")
570        d = Distribution.from_filename
571
572        assert d("foo-0.3a4.egg") not in r1
573        assert d("foo-0.3a1.egg") not in r1
574        assert d("foo-0.3a4.egg") not in r2
575
576        assert d("foo-0.3a2.egg") in r1
577        assert d("foo-0.3a2.egg") in r2
578        assert d("foo-0.3a3.egg") in r2
579        assert d("foo-0.3a5.egg") in r2
580
581    def testSetuptoolsProjectName(self):
582        """
583        The setuptools project should implement the setuptools package.
584        """
585
586        assert (
587            Requirement.parse('setuptools').project_name == 'setuptools')
588        # setuptools 0.7 and higher means setuptools.
589        assert (
590            Requirement.parse('setuptools == 0.7').project_name
591            == 'setuptools'
592        )
593        assert (
594            Requirement.parse('setuptools == 0.7a1').project_name
595            == 'setuptools'
596        )
597        assert (
598            Requirement.parse('setuptools >= 0.7').project_name
599            == 'setuptools'
600        )
601
602
603class TestParsing:
604    def testEmptyParse(self):
605        assert list(parse_requirements('')) == []
606
607    def testYielding(self):
608        for inp, out in [
609            ([], []), ('x', ['x']), ([[]], []), (' x\n y', ['x', 'y']),
610            (['x\n\n', 'y'], ['x', 'y']),
611        ]:
612            assert list(pkg_resources.yield_lines(inp)) == out
613
614    def testSplitting(self):
615        sample = """
616                    x
617                    [Y]
618                    z
619
620                    a
621                    [b ]
622                    # foo
623                    c
624                    [ d]
625                    [q]
626                    v
627                    """
628        assert (
629            list(pkg_resources.split_sections(sample))
630            ==
631            [
632                (None, ["x"]),
633                ("Y", ["z", "a"]),
634                ("b", ["c"]),
635                ("d", []),
636                ("q", ["v"]),
637            ]
638        )
639        with pytest.raises(ValueError):
640            list(pkg_resources.split_sections("[foo"))
641
642    def testSafeName(self):
643        assert safe_name("adns-python") == "adns-python"
644        assert safe_name("WSGI Utils") == "WSGI-Utils"
645        assert safe_name("WSGI  Utils") == "WSGI-Utils"
646        assert safe_name("Money$$$Maker") == "Money-Maker"
647        assert safe_name("peak.web") != "peak-web"
648
649    def testSafeVersion(self):
650        assert safe_version("1.2-1") == "1.2.post1"
651        assert safe_version("1.2 alpha") == "1.2.alpha"
652        assert safe_version("2.3.4 20050521") == "2.3.4.20050521"
653        assert safe_version("Money$$$Maker") == "Money-Maker"
654        assert safe_version("peak.web") == "peak.web"
655
656    def testSimpleRequirements(self):
657        assert (
658            list(parse_requirements('Twis-Ted>=1.2-1'))
659            ==
660            [Requirement('Twis-Ted>=1.2-1')]
661        )
662        assert (
663            list(parse_requirements('Twisted >=1.2, \\ # more\n<2.0'))
664            ==
665            [Requirement('Twisted>=1.2,<2.0')]
666        )
667        assert (
668            Requirement.parse("FooBar==1.99a3")
669            ==
670            Requirement("FooBar==1.99a3")
671        )
672        with pytest.raises(ValueError):
673            Requirement.parse(">=2.3")
674        with pytest.raises(ValueError):
675            Requirement.parse("x\\")
676        with pytest.raises(ValueError):
677            Requirement.parse("x==2 q")
678        with pytest.raises(ValueError):
679            Requirement.parse("X==1\nY==2")
680        with pytest.raises(ValueError):
681            Requirement.parse("#")
682
683    def test_requirements_with_markers(self):
684        assert (
685            Requirement.parse("foobar;os_name=='a'")
686            ==
687            Requirement.parse("foobar;os_name=='a'")
688        )
689        assert (
690            Requirement.parse("name==1.1;python_version=='2.7'")
691            !=
692            Requirement.parse("name==1.1;python_version=='3.6'")
693        )
694        assert (
695            Requirement.parse("name==1.0;python_version=='2.7'")
696            !=
697            Requirement.parse("name==1.2;python_version=='2.7'")
698        )
699        assert (
700            Requirement.parse("name[foo]==1.0;python_version=='3.6'")
701            !=
702            Requirement.parse("name[foo,bar]==1.0;python_version=='3.6'")
703        )
704
705    def test_local_version(self):
706        req, = parse_requirements('foo==1.0+org1')
707
708    def test_spaces_between_multiple_versions(self):
709        req, = parse_requirements('foo>=1.0, <3')
710        req, = parse_requirements('foo >= 1.0, < 3')
711
712    @pytest.mark.parametrize(
713        ['lower', 'upper'],
714        [
715            ('1.2-rc1', '1.2rc1'),
716            ('0.4', '0.4.0'),
717            ('0.4.0.0', '0.4.0'),
718            ('0.4.0-0', '0.4-0'),
719            ('0post1', '0.0post1'),
720            ('0pre1', '0.0c1'),
721            ('0.0.0preview1', '0c1'),
722            ('0.0c1', '0-rc1'),
723            ('1.2a1', '1.2.a.1'),
724            ('1.2.a', '1.2a'),
725        ],
726    )
727    def testVersionEquality(self, lower, upper):
728        assert parse_version(lower) == parse_version(upper)
729
730    torture = """
731        0.80.1-3 0.80.1-2 0.80.1-1 0.79.9999+0.80.0pre4-1
732        0.79.9999+0.80.0pre2-3 0.79.9999+0.80.0pre2-2
733        0.77.2-1 0.77.1-1 0.77.0-1
734        """
735
736    @pytest.mark.parametrize(
737        ['lower', 'upper'],
738        [
739            ('2.1', '2.1.1'),
740            ('2a1', '2b0'),
741            ('2a1', '2.1'),
742            ('2.3a1', '2.3'),
743            ('2.1-1', '2.1-2'),
744            ('2.1-1', '2.1.1'),
745            ('2.1', '2.1post4'),
746            ('2.1a0-20040501', '2.1'),
747            ('1.1', '02.1'),
748            ('3.2', '3.2.post0'),
749            ('3.2post1', '3.2post2'),
750            ('0.4', '4.0'),
751            ('0.0.4', '0.4.0'),
752            ('0post1', '0.4post1'),
753            ('2.1.0-rc1', '2.1.0'),
754            ('2.1dev', '2.1a0'),
755        ] + list(pairwise(reversed(torture.split()))),
756    )
757    def testVersionOrdering(self, lower, upper):
758        assert parse_version(lower) < parse_version(upper)
759
760    def testVersionHashable(self):
761        """
762        Ensure that our versions stay hashable even though we've subclassed
763        them and added some shim code to them.
764        """
765        assert (
766            hash(parse_version("1.0"))
767            ==
768            hash(parse_version("1.0"))
769        )
770
771
772class TestNamespaces:
773
774    ns_str = "__import__('pkg_resources').declare_namespace(__name__)\n"
775
776    @pytest.fixture
777    def symlinked_tmpdir(self, tmpdir):
778        """
779        Where available, return the tempdir as a symlink,
780        which as revealed in #231 is more fragile than
781        a natural tempdir.
782        """
783        if not hasattr(os, 'symlink'):
784            yield str(tmpdir)
785            return
786
787        link_name = str(tmpdir) + '-linked'
788        os.symlink(str(tmpdir), link_name)
789        try:
790            yield type(tmpdir)(link_name)
791        finally:
792            os.unlink(link_name)
793
794    @pytest.fixture(autouse=True)
795    def patched_path(self, tmpdir):
796        """
797        Patch sys.path to include the 'site-pkgs' dir. Also
798        restore pkg_resources._namespace_packages to its
799        former state.
800        """
801        saved_ns_pkgs = pkg_resources._namespace_packages.copy()
802        saved_sys_path = sys.path[:]
803        site_pkgs = tmpdir.mkdir('site-pkgs')
804        sys.path.append(str(site_pkgs))
805        try:
806            yield
807        finally:
808            pkg_resources._namespace_packages = saved_ns_pkgs
809            sys.path = saved_sys_path
810
811    issue591 = pytest.mark.xfail(platform.system() == 'Windows', reason="#591")
812
813    @issue591
814    def test_two_levels_deep(self, symlinked_tmpdir):
815        """
816        Test nested namespace packages
817        Create namespace packages in the following tree :
818            site-packages-1/pkg1/pkg2
819            site-packages-2/pkg1/pkg2
820        Check both are in the _namespace_packages dict and that their __path__
821        is correct
822        """
823        real_tmpdir = symlinked_tmpdir.realpath()
824        tmpdir = symlinked_tmpdir
825        sys.path.append(str(tmpdir / 'site-pkgs2'))
826        site_dirs = tmpdir / 'site-pkgs', tmpdir / 'site-pkgs2'
827        for site in site_dirs:
828            pkg1 = site / 'pkg1'
829            pkg2 = pkg1 / 'pkg2'
830            pkg2.ensure_dir()
831            (pkg1 / '__init__.py').write_text(self.ns_str, encoding='utf-8')
832            (pkg2 / '__init__.py').write_text(self.ns_str, encoding='utf-8')
833        import pkg1
834        assert "pkg1" in pkg_resources._namespace_packages
835        # attempt to import pkg2 from site-pkgs2
836        import pkg1.pkg2
837        # check the _namespace_packages dict
838        assert "pkg1.pkg2" in pkg_resources._namespace_packages
839        assert pkg_resources._namespace_packages["pkg1"] == ["pkg1.pkg2"]
840        # check the __path__ attribute contains both paths
841        expected = [
842            str(real_tmpdir / "site-pkgs" / "pkg1" / "pkg2"),
843            str(real_tmpdir / "site-pkgs2" / "pkg1" / "pkg2"),
844        ]
845        assert pkg1.pkg2.__path__ == expected
846
847    @issue591
848    def test_path_order(self, symlinked_tmpdir):
849        """
850        Test that if multiple versions of the same namespace package subpackage
851        are on different sys.path entries, that only the one earliest on
852        sys.path is imported, and that the namespace package's __path__ is in
853        the correct order.
854
855        Regression test for https://github.com/pypa/setuptools/issues/207
856        """
857
858        tmpdir = symlinked_tmpdir
859        site_dirs = (
860            tmpdir / "site-pkgs",
861            tmpdir / "site-pkgs2",
862            tmpdir / "site-pkgs3",
863        )
864
865        vers_str = "__version__ = %r"
866
867        for number, site in enumerate(site_dirs, 1):
868            if number > 1:
869                sys.path.append(str(site))
870            nspkg = site / 'nspkg'
871            subpkg = nspkg / 'subpkg'
872            subpkg.ensure_dir()
873            (nspkg / '__init__.py').write_text(self.ns_str, encoding='utf-8')
874            (subpkg / '__init__.py').write_text(
875                vers_str % number, encoding='utf-8')
876
877        import nspkg.subpkg
878        import nspkg
879        expected = [
880            str(site.realpath() / 'nspkg')
881            for site in site_dirs
882        ]
883        assert nspkg.__path__ == expected
884        assert nspkg.subpkg.__version__ == 1
885