test_resources.py 31 KB

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