You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

635 lines
19 KiB

4 years ago
  1. from __future__ import absolute_import, unicode_literals
  2. import io
  3. import os
  4. import sys
  5. import warnings
  6. import functools
  7. from collections import defaultdict
  8. from functools import partial
  9. from functools import wraps
  10. from importlib import import_module
  11. from distutils.errors import DistutilsOptionError, DistutilsFileError
  12. from setuptools.extern.packaging.version import LegacyVersion, parse
  13. from setuptools.extern.six import string_types, PY3
  14. __metaclass__ = type
  15. def read_configuration(
  16. filepath, find_others=False, ignore_option_errors=False):
  17. """Read given configuration file and returns options from it as a dict.
  18. :param str|unicode filepath: Path to configuration file
  19. to get options from.
  20. :param bool find_others: Whether to search for other configuration files
  21. which could be on in various places.
  22. :param bool ignore_option_errors: Whether to silently ignore
  23. options, values of which could not be resolved (e.g. due to exceptions
  24. in directives such as file:, attr:, etc.).
  25. If False exceptions are propagated as expected.
  26. :rtype: dict
  27. """
  28. from setuptools.dist import Distribution, _Distribution
  29. filepath = os.path.abspath(filepath)
  30. if not os.path.isfile(filepath):
  31. raise DistutilsFileError(
  32. 'Configuration file %s does not exist.' % filepath)
  33. current_directory = os.getcwd()
  34. os.chdir(os.path.dirname(filepath))
  35. try:
  36. dist = Distribution()
  37. filenames = dist.find_config_files() if find_others else []
  38. if filepath not in filenames:
  39. filenames.append(filepath)
  40. _Distribution.parse_config_files(dist, filenames=filenames)
  41. handlers = parse_configuration(
  42. dist, dist.command_options,
  43. ignore_option_errors=ignore_option_errors)
  44. finally:
  45. os.chdir(current_directory)
  46. return configuration_to_dict(handlers)
  47. def _get_option(target_obj, key):
  48. """
  49. Given a target object and option key, get that option from
  50. the target object, either through a get_{key} method or
  51. from an attribute directly.
  52. """
  53. getter_name = 'get_{key}'.format(**locals())
  54. by_attribute = functools.partial(getattr, target_obj, key)
  55. getter = getattr(target_obj, getter_name, by_attribute)
  56. return getter()
  57. def configuration_to_dict(handlers):
  58. """Returns configuration data gathered by given handlers as a dict.
  59. :param list[ConfigHandler] handlers: Handlers list,
  60. usually from parse_configuration()
  61. :rtype: dict
  62. """
  63. config_dict = defaultdict(dict)
  64. for handler in handlers:
  65. for option in handler.set_options:
  66. value = _get_option(handler.target_obj, option)
  67. config_dict[handler.section_prefix][option] = value
  68. return config_dict
  69. def parse_configuration(
  70. distribution, command_options, ignore_option_errors=False):
  71. """Performs additional parsing of configuration options
  72. for a distribution.
  73. Returns a list of used option handlers.
  74. :param Distribution distribution:
  75. :param dict command_options:
  76. :param bool ignore_option_errors: Whether to silently ignore
  77. options, values of which could not be resolved (e.g. due to exceptions
  78. in directives such as file:, attr:, etc.).
  79. If False exceptions are propagated as expected.
  80. :rtype: list
  81. """
  82. options = ConfigOptionsHandler(
  83. distribution, command_options, ignore_option_errors)
  84. options.parse()
  85. meta = ConfigMetadataHandler(
  86. distribution.metadata, command_options, ignore_option_errors,
  87. distribution.package_dir)
  88. meta.parse()
  89. return meta, options
  90. class ConfigHandler:
  91. """Handles metadata supplied in configuration files."""
  92. section_prefix = None
  93. """Prefix for config sections handled by this handler.
  94. Must be provided by class heirs.
  95. """
  96. aliases = {}
  97. """Options aliases.
  98. For compatibility with various packages. E.g.: d2to1 and pbr.
  99. Note: `-` in keys is replaced with `_` by config parser.
  100. """
  101. def __init__(self, target_obj, options, ignore_option_errors=False):
  102. sections = {}
  103. section_prefix = self.section_prefix
  104. for section_name, section_options in options.items():
  105. if not section_name.startswith(section_prefix):
  106. continue
  107. section_name = section_name.replace(section_prefix, '').strip('.')
  108. sections[section_name] = section_options
  109. self.ignore_option_errors = ignore_option_errors
  110. self.target_obj = target_obj
  111. self.sections = sections
  112. self.set_options = []
  113. @property
  114. def parsers(self):
  115. """Metadata item name to parser function mapping."""
  116. raise NotImplementedError(
  117. '%s must provide .parsers property' % self.__class__.__name__)
  118. def __setitem__(self, option_name, value):
  119. unknown = tuple()
  120. target_obj = self.target_obj
  121. # Translate alias into real name.
  122. option_name = self.aliases.get(option_name, option_name)
  123. current_value = getattr(target_obj, option_name, unknown)
  124. if current_value is unknown:
  125. raise KeyError(option_name)
  126. if current_value:
  127. # Already inhabited. Skipping.
  128. return
  129. skip_option = False
  130. parser = self.parsers.get(option_name)
  131. if parser:
  132. try:
  133. value = parser(value)
  134. except Exception:
  135. skip_option = True
  136. if not self.ignore_option_errors:
  137. raise
  138. if skip_option:
  139. return
  140. setter = getattr(target_obj, 'set_%s' % option_name, None)
  141. if setter is None:
  142. setattr(target_obj, option_name, value)
  143. else:
  144. setter(value)
  145. self.set_options.append(option_name)
  146. @classmethod
  147. def _parse_list(cls, value, separator=','):
  148. """Represents value as a list.
  149. Value is split either by separator (defaults to comma) or by lines.
  150. :param value:
  151. :param separator: List items separator character.
  152. :rtype: list
  153. """
  154. if isinstance(value, list): # _get_parser_compound case
  155. return value
  156. if '\n' in value:
  157. value = value.splitlines()
  158. else:
  159. value = value.split(separator)
  160. return [chunk.strip() for chunk in value if chunk.strip()]
  161. @classmethod
  162. def _parse_dict(cls, value):
  163. """Represents value as a dict.
  164. :param value:
  165. :rtype: dict
  166. """
  167. separator = '='
  168. result = {}
  169. for line in cls._parse_list(value):
  170. key, sep, val = line.partition(separator)
  171. if sep != separator:
  172. raise DistutilsOptionError(
  173. 'Unable to parse option value to dict: %s' % value)
  174. result[key.strip()] = val.strip()
  175. return result
  176. @classmethod
  177. def _parse_bool(cls, value):
  178. """Represents value as boolean.
  179. :param value:
  180. :rtype: bool
  181. """
  182. value = value.lower()
  183. return value in ('1', 'true', 'yes')
  184. @classmethod
  185. def _parse_file(cls, value):
  186. """Represents value as a string, allowing including text
  187. from nearest files using `file:` directive.
  188. Directive is sandboxed and won't reach anything outside
  189. directory with setup.py.
  190. Examples:
  191. file: LICENSE
  192. file: README.rst, CHANGELOG.md, src/file.txt
  193. :param str value:
  194. :rtype: str
  195. """
  196. include_directive = 'file:'
  197. if not isinstance(value, string_types):
  198. return value
  199. if not value.startswith(include_directive):
  200. return value
  201. spec = value[len(include_directive):]
  202. filepaths = (os.path.abspath(path.strip()) for path in spec.split(','))
  203. return '\n'.join(
  204. cls._read_file(path)
  205. for path in filepaths
  206. if (cls._assert_local(path) or True)
  207. and os.path.isfile(path)
  208. )
  209. @staticmethod
  210. def _assert_local(filepath):
  211. if not filepath.startswith(os.getcwd()):
  212. raise DistutilsOptionError(
  213. '`file:` directive can not access %s' % filepath)
  214. @staticmethod
  215. def _read_file(filepath):
  216. with io.open(filepath, encoding='utf-8') as f:
  217. return f.read()
  218. @classmethod
  219. def _parse_attr(cls, value, package_dir=None):
  220. """Represents value as a module attribute.
  221. Examples:
  222. attr: package.attr
  223. attr: package.module.attr
  224. :param str value:
  225. :rtype: str
  226. """
  227. attr_directive = 'attr:'
  228. if not value.startswith(attr_directive):
  229. return value
  230. attrs_path = value.replace(attr_directive, '').strip().split('.')
  231. attr_name = attrs_path.pop()
  232. module_name = '.'.join(attrs_path)
  233. module_name = module_name or '__init__'
  234. parent_path = os.getcwd()
  235. if package_dir:
  236. if attrs_path[0] in package_dir:
  237. # A custom path was specified for the module we want to import
  238. custom_path = package_dir[attrs_path[0]]
  239. parts = custom_path.rsplit('/', 1)
  240. if len(parts) > 1:
  241. parent_path = os.path.join(os.getcwd(), parts[0])
  242. module_name = parts[1]
  243. else:
  244. module_name = custom_path
  245. elif '' in package_dir:
  246. # A custom parent directory was specified for all root modules
  247. parent_path = os.path.join(os.getcwd(), package_dir[''])
  248. sys.path.insert(0, parent_path)
  249. try:
  250. module = import_module(module_name)
  251. value = getattr(module, attr_name)
  252. finally:
  253. sys.path = sys.path[1:]
  254. return value
  255. @classmethod
  256. def _get_parser_compound(cls, *parse_methods):
  257. """Returns parser function to represents value as a list.
  258. Parses a value applying given methods one after another.
  259. :param parse_methods:
  260. :rtype: callable
  261. """
  262. def parse(value):
  263. parsed = value
  264. for method in parse_methods:
  265. parsed = method(parsed)
  266. return parsed
  267. return parse
  268. @classmethod
  269. def _parse_section_to_dict(cls, section_options, values_parser=None):
  270. """Parses section options into a dictionary.
  271. Optionally applies a given parser to values.
  272. :param dict section_options:
  273. :param callable values_parser:
  274. :rtype: dict
  275. """
  276. value = {}
  277. values_parser = values_parser or (lambda val: val)
  278. for key, (_, val) in section_options.items():
  279. value[key] = values_parser(val)
  280. return value
  281. def parse_section(self, section_options):
  282. """Parses configuration file section.
  283. :param dict section_options:
  284. """
  285. for (name, (_, value)) in section_options.items():
  286. try:
  287. self[name] = value
  288. except KeyError:
  289. pass # Keep silent for a new option may appear anytime.
  290. def parse(self):
  291. """Parses configuration file items from one
  292. or more related sections.
  293. """
  294. for section_name, section_options in self.sections.items():
  295. method_postfix = ''
  296. if section_name: # [section.option] variant
  297. method_postfix = '_%s' % section_name
  298. section_parser_method = getattr(
  299. self,
  300. # Dots in section names are tranlsated into dunderscores.
  301. ('parse_section%s' % method_postfix).replace('.', '__'),
  302. None)
  303. if section_parser_method is None:
  304. raise DistutilsOptionError(
  305. 'Unsupported distribution option section: [%s.%s]' % (
  306. self.section_prefix, section_name))
  307. section_parser_method(section_options)
  308. def _deprecated_config_handler(self, func, msg, warning_class):
  309. """ this function will wrap around parameters that are deprecated
  310. :param msg: deprecation message
  311. :param warning_class: class of warning exception to be raised
  312. :param func: function to be wrapped around
  313. """
  314. @wraps(func)
  315. def config_handler(*args, **kwargs):
  316. warnings.warn(msg, warning_class)
  317. return func(*args, **kwargs)
  318. return config_handler
  319. class ConfigMetadataHandler(ConfigHandler):
  320. section_prefix = 'metadata'
  321. aliases = {
  322. 'home_page': 'url',
  323. 'summary': 'description',
  324. 'classifier': 'classifiers',
  325. 'platform': 'platforms',
  326. }
  327. strict_mode = False
  328. """We need to keep it loose, to be partially compatible with
  329. `pbr` and `d2to1` packages which also uses `metadata` section.
  330. """
  331. def __init__(self, target_obj, options, ignore_option_errors=False,
  332. package_dir=None):
  333. super(ConfigMetadataHandler, self).__init__(target_obj, options,
  334. ignore_option_errors)
  335. self.package_dir = package_dir
  336. @property
  337. def parsers(self):
  338. """Metadata item name to parser function mapping."""
  339. parse_list = self._parse_list
  340. parse_file = self._parse_file
  341. parse_dict = self._parse_dict
  342. return {
  343. 'platforms': parse_list,
  344. 'keywords': parse_list,
  345. 'provides': parse_list,
  346. 'requires': self._deprecated_config_handler(parse_list,
  347. "The requires parameter is deprecated, please use " +
  348. "install_requires for runtime dependencies.",
  349. DeprecationWarning),
  350. 'obsoletes': parse_list,
  351. 'classifiers': self._get_parser_compound(parse_file, parse_list),
  352. 'license': parse_file,
  353. 'description': parse_file,
  354. 'long_description': parse_file,
  355. 'version': self._parse_version,
  356. 'project_urls': parse_dict,
  357. }
  358. def _parse_version(self, value):
  359. """Parses `version` option value.
  360. :param value:
  361. :rtype: str
  362. """
  363. version = self._parse_file(value)
  364. if version != value:
  365. version = version.strip()
  366. # Be strict about versions loaded from file because it's easy to
  367. # accidentally include newlines and other unintended content
  368. if isinstance(parse(version), LegacyVersion):
  369. tmpl = (
  370. 'Version loaded from {value} does not '
  371. 'comply with PEP 440: {version}'
  372. )
  373. raise DistutilsOptionError(tmpl.format(**locals()))
  374. return version
  375. version = self._parse_attr(value, self.package_dir)
  376. if callable(version):
  377. version = version()
  378. if not isinstance(version, string_types):
  379. if hasattr(version, '__iter__'):
  380. version = '.'.join(map(str, version))
  381. else:
  382. version = '%s' % version
  383. return version
  384. class ConfigOptionsHandler(ConfigHandler):
  385. section_prefix = 'options'
  386. @property
  387. def parsers(self):
  388. """Metadata item name to parser function mapping."""
  389. parse_list = self._parse_list
  390. parse_list_semicolon = partial(self._parse_list, separator=';')
  391. parse_bool = self._parse_bool
  392. parse_dict = self._parse_dict
  393. return {
  394. 'zip_safe': parse_bool,
  395. 'use_2to3': parse_bool,
  396. 'include_package_data': parse_bool,
  397. 'package_dir': parse_dict,
  398. 'use_2to3_fixers': parse_list,
  399. 'use_2to3_exclude_fixers': parse_list,
  400. 'convert_2to3_doctests': parse_list,
  401. 'scripts': parse_list,
  402. 'eager_resources': parse_list,
  403. 'dependency_links': parse_list,
  404. 'namespace_packages': parse_list,
  405. 'install_requires': parse_list_semicolon,
  406. 'setup_requires': parse_list_semicolon,
  407. 'tests_require': parse_list_semicolon,
  408. 'packages': self._parse_packages,
  409. 'entry_points': self._parse_file,
  410. 'py_modules': parse_list,
  411. }
  412. def _parse_packages(self, value):
  413. """Parses `packages` option value.
  414. :param value:
  415. :rtype: list
  416. """
  417. find_directives = ['find:', 'find_namespace:']
  418. trimmed_value = value.strip()
  419. if trimmed_value not in find_directives:
  420. return self._parse_list(value)
  421. findns = trimmed_value == find_directives[1]
  422. if findns and not PY3:
  423. raise DistutilsOptionError(
  424. 'find_namespace: directive is unsupported on Python < 3.3')
  425. # Read function arguments from a dedicated section.
  426. find_kwargs = self.parse_section_packages__find(
  427. self.sections.get('packages.find', {}))
  428. if findns:
  429. from setuptools import find_namespace_packages as find_packages
  430. else:
  431. from setuptools import find_packages
  432. return find_packages(**find_kwargs)
  433. def parse_section_packages__find(self, section_options):
  434. """Parses `packages.find` configuration file section.
  435. To be used in conjunction with _parse_packages().
  436. :param dict section_options:
  437. """
  438. section_data = self._parse_section_to_dict(
  439. section_options, self._parse_list)
  440. valid_keys = ['where', 'include', 'exclude']
  441. find_kwargs = dict(
  442. [(k, v) for k, v in section_data.items() if k in valid_keys and v])
  443. where = find_kwargs.get('where')
  444. if where is not None:
  445. find_kwargs['where'] = where[0] # cast list to single val
  446. return find_kwargs
  447. def parse_section_entry_points(self, section_options):
  448. """Parses `entry_points` configuration file section.
  449. :param dict section_options:
  450. """
  451. parsed = self._parse_section_to_dict(section_options, self._parse_list)
  452. self['entry_points'] = parsed
  453. def _parse_package_data(self, section_options):
  454. parsed = self._parse_section_to_dict(section_options, self._parse_list)
  455. root = parsed.get('*')
  456. if root:
  457. parsed[''] = root
  458. del parsed['*']
  459. return parsed
  460. def parse_section_package_data(self, section_options):
  461. """Parses `package_data` configuration file section.
  462. :param dict section_options:
  463. """
  464. self['package_data'] = self._parse_package_data(section_options)
  465. def parse_section_exclude_package_data(self, section_options):
  466. """Parses `exclude_package_data` configuration file section.
  467. :param dict section_options:
  468. """
  469. self['exclude_package_data'] = self._parse_package_data(
  470. section_options)
  471. def parse_section_extras_require(self, section_options):
  472. """Parses `extras_require` configuration file section.
  473. :param dict section_options:
  474. """
  475. parse_list = partial(self._parse_list, separator=';')
  476. self['extras_require'] = self._parse_section_to_dict(
  477. section_options, parse_list)
  478. def parse_section_data_files(self, section_options):
  479. """Parses `data_files` configuration file section.
  480. :param dict section_options:
  481. """
  482. parsed = self._parse_section_to_dict(section_options, self._parse_list)
  483. self['data_files'] = [(k, v) for k, v in parsed.items()]