test_dist.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. import io
  2. import collections
  3. import re
  4. import functools
  5. import urllib.request
  6. import urllib.parse
  7. from distutils.errors import DistutilsSetupError
  8. from setuptools.dist import (
  9. _get_unpatched,
  10. check_package_data,
  11. DistDeprecationWarning,
  12. )
  13. from setuptools import sic
  14. from setuptools import Distribution
  15. from .textwrap import DALS
  16. from .test_easy_install import make_nspkg_sdist
  17. import pytest
  18. def test_dist_fetch_build_egg(tmpdir):
  19. """
  20. Check multiple calls to `Distribution.fetch_build_egg` work as expected.
  21. """
  22. index = tmpdir.mkdir('index')
  23. index_url = urllib.parse.urljoin(
  24. 'file://', urllib.request.pathname2url(str(index)))
  25. def sdist_with_index(distname, version):
  26. dist_dir = index.mkdir(distname)
  27. dist_sdist = '%s-%s.tar.gz' % (distname, version)
  28. make_nspkg_sdist(str(dist_dir.join(dist_sdist)), distname, version)
  29. with dist_dir.join('index.html').open('w') as fp:
  30. fp.write(DALS(
  31. '''
  32. <!DOCTYPE html><html><body>
  33. <a href="{dist_sdist}" rel="internal">{dist_sdist}</a><br/>
  34. </body></html>
  35. '''
  36. ).format(dist_sdist=dist_sdist))
  37. sdist_with_index('barbazquux', '3.2.0')
  38. sdist_with_index('barbazquux-runner', '2.11.1')
  39. with tmpdir.join('setup.cfg').open('w') as fp:
  40. fp.write(DALS(
  41. '''
  42. [easy_install]
  43. index_url = {index_url}
  44. '''
  45. ).format(index_url=index_url))
  46. reqs = '''
  47. barbazquux-runner
  48. barbazquux
  49. '''.split()
  50. with tmpdir.as_cwd():
  51. dist = Distribution()
  52. dist.parse_config_files()
  53. resolved_dists = [
  54. dist.fetch_build_egg(r)
  55. for r in reqs
  56. ]
  57. assert [dist.key for dist in resolved_dists if dist] == reqs
  58. def test_dist__get_unpatched_deprecated():
  59. pytest.warns(DistDeprecationWarning, _get_unpatched, [""])
  60. def __read_test_cases():
  61. base = dict(
  62. name="package",
  63. version="0.0.1",
  64. author="Foo Bar",
  65. author_email="foo@bar.net",
  66. long_description="Long\ndescription",
  67. description="Short description",
  68. keywords=["one", "two"],
  69. )
  70. params = functools.partial(dict, base)
  71. test_cases = [
  72. ('Metadata version 1.0', params()),
  73. ('Metadata version 1.1: Provides', params(
  74. provides=['package'],
  75. )),
  76. ('Metadata version 1.1: Obsoletes', params(
  77. obsoletes=['foo'],
  78. )),
  79. ('Metadata version 1.1: Classifiers', params(
  80. classifiers=[
  81. 'Programming Language :: Python :: 3',
  82. 'Programming Language :: Python :: 3.7',
  83. 'License :: OSI Approved :: MIT License',
  84. ],
  85. )),
  86. ('Metadata version 1.1: Download URL', params(
  87. download_url='https://example.com',
  88. )),
  89. ('Metadata Version 1.2: Requires-Python', params(
  90. python_requires='>=3.7',
  91. )),
  92. pytest.param(
  93. 'Metadata Version 1.2: Project-Url',
  94. params(project_urls=dict(Foo='https://example.bar')),
  95. marks=pytest.mark.xfail(
  96. reason="Issue #1578: project_urls not read",
  97. ),
  98. ),
  99. ('Metadata Version 2.1: Long Description Content Type', params(
  100. long_description_content_type='text/x-rst; charset=UTF-8',
  101. )),
  102. pytest.param(
  103. 'Metadata Version 2.1: Provides Extra',
  104. params(provides_extras=['foo', 'bar']),
  105. marks=pytest.mark.xfail(reason="provides_extras not read"),
  106. ),
  107. ('Missing author', dict(
  108. name='foo',
  109. version='1.0.0',
  110. author_email='snorri@sturluson.name',
  111. )),
  112. ('Missing author e-mail', dict(
  113. name='foo',
  114. version='1.0.0',
  115. author='Snorri Sturluson',
  116. )),
  117. ('Missing author and e-mail', dict(
  118. name='foo',
  119. version='1.0.0',
  120. )),
  121. ('Bypass normalized version', dict(
  122. name='foo',
  123. version=sic('1.0.0a'),
  124. )),
  125. ]
  126. return test_cases
  127. @pytest.mark.parametrize('name,attrs', __read_test_cases())
  128. def test_read_metadata(name, attrs):
  129. dist = Distribution(attrs)
  130. metadata_out = dist.metadata
  131. dist_class = metadata_out.__class__
  132. # Write to PKG_INFO and then load into a new metadata object
  133. PKG_INFO = io.StringIO()
  134. metadata_out.write_pkg_file(PKG_INFO)
  135. PKG_INFO.seek(0)
  136. metadata_in = dist_class()
  137. metadata_in.read_pkg_file(PKG_INFO)
  138. tested_attrs = [
  139. ('name', dist_class.get_name),
  140. ('version', dist_class.get_version),
  141. ('author', dist_class.get_contact),
  142. ('author_email', dist_class.get_contact_email),
  143. ('metadata_version', dist_class.get_metadata_version),
  144. ('provides', dist_class.get_provides),
  145. ('description', dist_class.get_description),
  146. ('download_url', dist_class.get_download_url),
  147. ('keywords', dist_class.get_keywords),
  148. ('platforms', dist_class.get_platforms),
  149. ('obsoletes', dist_class.get_obsoletes),
  150. ('requires', dist_class.get_requires),
  151. ('classifiers', dist_class.get_classifiers),
  152. ('project_urls', lambda s: getattr(s, 'project_urls', {})),
  153. ('provides_extras', lambda s: getattr(s, 'provides_extras', set())),
  154. ]
  155. for attr, getter in tested_attrs:
  156. assert getter(metadata_in) == getter(metadata_out)
  157. def __maintainer_test_cases():
  158. attrs = {"name": "package",
  159. "version": "1.0",
  160. "description": "xxx"}
  161. def merge_dicts(d1, d2):
  162. d1 = d1.copy()
  163. d1.update(d2)
  164. return d1
  165. test_cases = [
  166. ('No author, no maintainer', attrs.copy()),
  167. ('Author (no e-mail), no maintainer', merge_dicts(
  168. attrs,
  169. {'author': 'Author Name'})),
  170. ('Author (e-mail), no maintainer', merge_dicts(
  171. attrs,
  172. {'author': 'Author Name',
  173. 'author_email': 'author@name.com'})),
  174. ('No author, maintainer (no e-mail)', merge_dicts(
  175. attrs,
  176. {'maintainer': 'Maintainer Name'})),
  177. ('No author, maintainer (e-mail)', merge_dicts(
  178. attrs,
  179. {'maintainer': 'Maintainer Name',
  180. 'maintainer_email': 'maintainer@name.com'})),
  181. ('Author (no e-mail), Maintainer (no-email)', merge_dicts(
  182. attrs,
  183. {'author': 'Author Name',
  184. 'maintainer': 'Maintainer Name'})),
  185. ('Author (e-mail), Maintainer (e-mail)', merge_dicts(
  186. attrs,
  187. {'author': 'Author Name',
  188. 'author_email': 'author@name.com',
  189. 'maintainer': 'Maintainer Name',
  190. 'maintainer_email': 'maintainer@name.com'})),
  191. ('No author (e-mail), no maintainer (e-mail)', merge_dicts(
  192. attrs,
  193. {'author_email': 'author@name.com',
  194. 'maintainer_email': 'maintainer@name.com'})),
  195. ('Author unicode', merge_dicts(
  196. attrs,
  197. {'author': '鉄沢寛'})),
  198. ('Maintainer unicode', merge_dicts(
  199. attrs,
  200. {'maintainer': 'Jan Łukasiewicz'})),
  201. ]
  202. return test_cases
  203. @pytest.mark.parametrize('name,attrs', __maintainer_test_cases())
  204. def test_maintainer_author(name, attrs, tmpdir):
  205. tested_keys = {
  206. 'author': 'Author',
  207. 'author_email': 'Author-email',
  208. 'maintainer': 'Maintainer',
  209. 'maintainer_email': 'Maintainer-email',
  210. }
  211. # Generate a PKG-INFO file
  212. dist = Distribution(attrs)
  213. fn = tmpdir.mkdir('pkg_info')
  214. fn_s = str(fn)
  215. dist.metadata.write_pkg_info(fn_s)
  216. with io.open(str(fn.join('PKG-INFO')), 'r', encoding='utf-8') as f:
  217. raw_pkg_lines = f.readlines()
  218. # Drop blank lines
  219. pkg_lines = list(filter(None, raw_pkg_lines))
  220. pkg_lines_set = set(pkg_lines)
  221. # Duplicate lines should not be generated
  222. assert len(pkg_lines) == len(pkg_lines_set)
  223. for fkey, dkey in tested_keys.items():
  224. val = attrs.get(dkey, None)
  225. if val is None:
  226. for line in pkg_lines:
  227. assert not line.startswith(fkey + ':')
  228. else:
  229. line = '%s: %s' % (fkey, val)
  230. assert line in pkg_lines_set
  231. def test_provides_extras_deterministic_order():
  232. extras = collections.OrderedDict()
  233. extras['a'] = ['foo']
  234. extras['b'] = ['bar']
  235. attrs = dict(extras_require=extras)
  236. dist = Distribution(attrs)
  237. assert dist.metadata.provides_extras == ['a', 'b']
  238. attrs['extras_require'] = collections.OrderedDict(
  239. reversed(list(attrs['extras_require'].items())))
  240. dist = Distribution(attrs)
  241. assert dist.metadata.provides_extras == ['b', 'a']
  242. CHECK_PACKAGE_DATA_TESTS = (
  243. # Valid.
  244. ({
  245. '': ['*.txt', '*.rst'],
  246. 'hello': ['*.msg'],
  247. }, None),
  248. # Not a dictionary.
  249. ((
  250. ('', ['*.txt', '*.rst']),
  251. ('hello', ['*.msg']),
  252. ), (
  253. "'package_data' must be a dictionary mapping package"
  254. " names to lists of string wildcard patterns"
  255. )),
  256. # Invalid key type.
  257. ({
  258. 400: ['*.txt', '*.rst'],
  259. }, (
  260. "keys of 'package_data' dict must be strings (got 400)"
  261. )),
  262. # Invalid value type.
  263. ({
  264. 'hello': str('*.msg'),
  265. }, (
  266. "\"values of 'package_data' dict\" "
  267. "must be a list of strings (got '*.msg')"
  268. )),
  269. # Invalid value type (generators are single use)
  270. ({
  271. 'hello': (x for x in "generator"),
  272. }, (
  273. "\"values of 'package_data' dict\" must be a list of strings "
  274. "(got <generator object"
  275. )),
  276. )
  277. @pytest.mark.parametrize(
  278. 'package_data, expected_message', CHECK_PACKAGE_DATA_TESTS)
  279. def test_check_package_data(package_data, expected_message):
  280. if expected_message is None:
  281. assert check_package_data(None, 'package_data', package_data) is None
  282. else:
  283. with pytest.raises(
  284. DistutilsSetupError, match=re.escape(expected_message)):
  285. check_package_data(None, str('package_data'), package_data)