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