Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/setuptools/config.py: 24%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

282 statements  

1from __future__ import absolute_import, unicode_literals 

2import io 

3import os 

4import sys 

5 

6import warnings 

7import functools 

8from collections import defaultdict 

9from functools import partial 

10from functools import wraps 

11from importlib import import_module 

12 

13from distutils.errors import DistutilsOptionError, DistutilsFileError 

14from setuptools.extern.packaging.version import LegacyVersion, parse 

15from setuptools.extern.packaging.specifiers import SpecifierSet 

16from setuptools.extern.six import string_types, PY3 

17 

18 

19__metaclass__ = type 

20 

21 

22def read_configuration( 

23 filepath, find_others=False, ignore_option_errors=False): 

24 """Read given configuration file and returns options from it as a dict. 

25 

26 :param str|unicode filepath: Path to configuration file 

27 to get options from. 

28 

29 :param bool find_others: Whether to search for other configuration files 

30 which could be on in various places. 

31 

32 :param bool ignore_option_errors: Whether to silently ignore 

33 options, values of which could not be resolved (e.g. due to exceptions 

34 in directives such as file:, attr:, etc.). 

35 If False exceptions are propagated as expected. 

36 

37 :rtype: dict 

38 """ 

39 from setuptools.dist import Distribution, _Distribution 

40 

41 filepath = os.path.abspath(filepath) 

42 

43 if not os.path.isfile(filepath): 

44 raise DistutilsFileError( 

45 'Configuration file %s does not exist.' % filepath) 

46 

47 current_directory = os.getcwd() 

48 os.chdir(os.path.dirname(filepath)) 

49 

50 try: 

51 dist = Distribution() 

52 

53 filenames = dist.find_config_files() if find_others else [] 

54 if filepath not in filenames: 

55 filenames.append(filepath) 

56 

57 _Distribution.parse_config_files(dist, filenames=filenames) 

58 

59 handlers = parse_configuration( 

60 dist, dist.command_options, 

61 ignore_option_errors=ignore_option_errors) 

62 

63 finally: 

64 os.chdir(current_directory) 

65 

66 return configuration_to_dict(handlers) 

67 

68 

69def _get_option(target_obj, key): 

70 """ 

71 Given a target object and option key, get that option from 

72 the target object, either through a get_{key} method or 

73 from an attribute directly. 

74 """ 

75 getter_name = 'get_{key}'.format(**locals()) 

76 by_attribute = functools.partial(getattr, target_obj, key) 

77 getter = getattr(target_obj, getter_name, by_attribute) 

78 return getter() 

79 

80 

81def configuration_to_dict(handlers): 

82 """Returns configuration data gathered by given handlers as a dict. 

83 

84 :param list[ConfigHandler] handlers: Handlers list, 

85 usually from parse_configuration() 

86 

87 :rtype: dict 

88 """ 

89 config_dict = defaultdict(dict) 

90 

91 for handler in handlers: 

92 for option in handler.set_options: 

93 value = _get_option(handler.target_obj, option) 

94 config_dict[handler.section_prefix][option] = value 

95 

96 return config_dict 

97 

98 

99def parse_configuration( 

100 distribution, command_options, ignore_option_errors=False): 

101 """Performs additional parsing of configuration options 

102 for a distribution. 

103 

104 Returns a list of used option handlers. 

105 

106 :param Distribution distribution: 

107 :param dict command_options: 

108 :param bool ignore_option_errors: Whether to silently ignore 

109 options, values of which could not be resolved (e.g. due to exceptions 

110 in directives such as file:, attr:, etc.). 

111 If False exceptions are propagated as expected. 

112 :rtype: list 

113 """ 

114 options = ConfigOptionsHandler( 

115 distribution, command_options, ignore_option_errors) 

116 options.parse() 

117 

118 meta = ConfigMetadataHandler( 

119 distribution.metadata, command_options, ignore_option_errors, 

120 distribution.package_dir) 

121 meta.parse() 

122 

123 return meta, options 

124 

125 

126class ConfigHandler: 

127 """Handles metadata supplied in configuration files.""" 

128 

129 section_prefix = None 

130 """Prefix for config sections handled by this handler. 

131 Must be provided by class heirs. 

132 

133 """ 

134 

135 aliases = {} 

136 """Options aliases. 

137 For compatibility with various packages. E.g.: d2to1 and pbr. 

138 Note: `-` in keys is replaced with `_` by config parser. 

139 

140 """ 

141 

142 def __init__(self, target_obj, options, ignore_option_errors=False): 

143 sections = {} 

144 

145 section_prefix = self.section_prefix 

146 for section_name, section_options in options.items(): 

147 if not section_name.startswith(section_prefix): 

148 continue 

149 

150 section_name = section_name.replace(section_prefix, '').strip('.') 

151 sections[section_name] = section_options 

152 

153 self.ignore_option_errors = ignore_option_errors 

154 self.target_obj = target_obj 

155 self.sections = sections 

156 self.set_options = [] 

157 

158 @property 

159 def parsers(self): 

160 """Metadata item name to parser function mapping.""" 

161 raise NotImplementedError( 

162 '%s must provide .parsers property' % self.__class__.__name__) 

163 

164 def __setitem__(self, option_name, value): 

165 unknown = tuple() 

166 target_obj = self.target_obj 

167 

168 # Translate alias into real name. 

169 option_name = self.aliases.get(option_name, option_name) 

170 

171 current_value = getattr(target_obj, option_name, unknown) 

172 

173 if current_value is unknown: 

174 raise KeyError(option_name) 

175 

176 if current_value: 

177 # Already inhabited. Skipping. 

178 return 

179 

180 skip_option = False 

181 parser = self.parsers.get(option_name) 

182 if parser: 

183 try: 

184 value = parser(value) 

185 

186 except Exception: 

187 skip_option = True 

188 if not self.ignore_option_errors: 

189 raise 

190 

191 if skip_option: 

192 return 

193 

194 setter = getattr(target_obj, 'set_%s' % option_name, None) 

195 if setter is None: 

196 setattr(target_obj, option_name, value) 

197 else: 

198 setter(value) 

199 

200 self.set_options.append(option_name) 

201 

202 @classmethod 

203 def _parse_list(cls, value, separator=','): 

204 """Represents value as a list. 

205 

206 Value is split either by separator (defaults to comma) or by lines. 

207 

208 :param value: 

209 :param separator: List items separator character. 

210 :rtype: list 

211 """ 

212 if isinstance(value, list): # _get_parser_compound case 

213 return value 

214 

215 if '\n' in value: 

216 value = value.splitlines() 

217 else: 

218 value = value.split(separator) 

219 

220 return [chunk.strip() for chunk in value if chunk.strip()] 

221 

222 @classmethod 

223 def _parse_dict(cls, value): 

224 """Represents value as a dict. 

225 

226 :param value: 

227 :rtype: dict 

228 """ 

229 separator = '=' 

230 result = {} 

231 for line in cls._parse_list(value): 

232 key, sep, val = line.partition(separator) 

233 if sep != separator: 

234 raise DistutilsOptionError( 

235 'Unable to parse option value to dict: %s' % value) 

236 result[key.strip()] = val.strip() 

237 

238 return result 

239 

240 @classmethod 

241 def _parse_bool(cls, value): 

242 """Represents value as boolean. 

243 

244 :param value: 

245 :rtype: bool 

246 """ 

247 value = value.lower() 

248 return value in ('1', 'true', 'yes') 

249 

250 @classmethod 

251 def _exclude_files_parser(cls, key): 

252 """Returns a parser function to make sure field inputs 

253 are not files. 

254 

255 Parses a value after getting the key so error messages are 

256 more informative. 

257 

258 :param key: 

259 :rtype: callable 

260 """ 

261 def parser(value): 

262 exclude_directive = 'file:' 

263 if value.startswith(exclude_directive): 

264 raise ValueError( 

265 'Only strings are accepted for the {0} field, ' 

266 'files are not accepted'.format(key)) 

267 return value 

268 return parser 

269 

270 @classmethod 

271 def _parse_file(cls, value): 

272 """Represents value as a string, allowing including text 

273 from nearest files using `file:` directive. 

274 

275 Directive is sandboxed and won't reach anything outside 

276 directory with setup.py. 

277 

278 Examples: 

279 file: README.rst, CHANGELOG.md, src/file.txt 

280 

281 :param str value: 

282 :rtype: str 

283 """ 

284 include_directive = 'file:' 

285 

286 if not isinstance(value, string_types): 

287 return value 

288 

289 if not value.startswith(include_directive): 

290 return value 

291 

292 spec = value[len(include_directive):] 

293 filepaths = (os.path.abspath(path.strip()) for path in spec.split(',')) 

294 return '\n'.join( 

295 cls._read_file(path) 

296 for path in filepaths 

297 if (cls._assert_local(path) or True) 

298 and os.path.isfile(path) 

299 ) 

300 

301 @staticmethod 

302 def _assert_local(filepath): 

303 if not filepath.startswith(os.getcwd()): 

304 raise DistutilsOptionError( 

305 '`file:` directive can not access %s' % filepath) 

306 

307 @staticmethod 

308 def _read_file(filepath): 

309 with io.open(filepath, encoding='utf-8') as f: 

310 return f.read() 

311 

312 @classmethod 

313 def _parse_attr(cls, value, package_dir=None): 

314 """Represents value as a module attribute. 

315 

316 Examples: 

317 attr: package.attr 

318 attr: package.module.attr 

319 

320 :param str value: 

321 :rtype: str 

322 """ 

323 attr_directive = 'attr:' 

324 if not value.startswith(attr_directive): 

325 return value 

326 

327 attrs_path = value.replace(attr_directive, '').strip().split('.') 

328 attr_name = attrs_path.pop() 

329 

330 module_name = '.'.join(attrs_path) 

331 module_name = module_name or '__init__' 

332 

333 parent_path = os.getcwd() 

334 if package_dir: 

335 if attrs_path[0] in package_dir: 

336 # A custom path was specified for the module we want to import 

337 custom_path = package_dir[attrs_path[0]] 

338 parts = custom_path.rsplit('/', 1) 

339 if len(parts) > 1: 

340 parent_path = os.path.join(os.getcwd(), parts[0]) 

341 module_name = parts[1] 

342 else: 

343 module_name = custom_path 

344 elif '' in package_dir: 

345 # A custom parent directory was specified for all root modules 

346 parent_path = os.path.join(os.getcwd(), package_dir['']) 

347 sys.path.insert(0, parent_path) 

348 try: 

349 module = import_module(module_name) 

350 value = getattr(module, attr_name) 

351 

352 finally: 

353 sys.path = sys.path[1:] 

354 

355 return value 

356 

357 @classmethod 

358 def _get_parser_compound(cls, *parse_methods): 

359 """Returns parser function to represents value as a list. 

360 

361 Parses a value applying given methods one after another. 

362 

363 :param parse_methods: 

364 :rtype: callable 

365 """ 

366 def parse(value): 

367 parsed = value 

368 

369 for method in parse_methods: 

370 parsed = method(parsed) 

371 

372 return parsed 

373 

374 return parse 

375 

376 @classmethod 

377 def _parse_section_to_dict(cls, section_options, values_parser=None): 

378 """Parses section options into a dictionary. 

379 

380 Optionally applies a given parser to values. 

381 

382 :param dict section_options: 

383 :param callable values_parser: 

384 :rtype: dict 

385 """ 

386 value = {} 

387 values_parser = values_parser or (lambda val: val) 

388 for key, (_, val) in section_options.items(): 

389 value[key] = values_parser(val) 

390 return value 

391 

392 def parse_section(self, section_options): 

393 """Parses configuration file section. 

394 

395 :param dict section_options: 

396 """ 

397 for (name, (_, value)) in section_options.items(): 

398 try: 

399 self[name] = value 

400 

401 except KeyError: 

402 pass # Keep silent for a new option may appear anytime. 

403 

404 def parse(self): 

405 """Parses configuration file items from one 

406 or more related sections. 

407 

408 """ 

409 for section_name, section_options in self.sections.items(): 

410 

411 method_postfix = '' 

412 if section_name: # [section.option] variant 

413 method_postfix = '_%s' % section_name 

414 

415 section_parser_method = getattr( 

416 self, 

417 # Dots in section names are translated into dunderscores. 

418 ('parse_section%s' % method_postfix).replace('.', '__'), 

419 None) 

420 

421 if section_parser_method is None: 

422 raise DistutilsOptionError( 

423 'Unsupported distribution option section: [%s.%s]' % ( 

424 self.section_prefix, section_name)) 

425 

426 section_parser_method(section_options) 

427 

428 def _deprecated_config_handler(self, func, msg, warning_class): 

429 """ this function will wrap around parameters that are deprecated 

430 

431 :param msg: deprecation message 

432 :param warning_class: class of warning exception to be raised 

433 :param func: function to be wrapped around 

434 """ 

435 @wraps(func) 

436 def config_handler(*args, **kwargs): 

437 warnings.warn(msg, warning_class) 

438 return func(*args, **kwargs) 

439 

440 return config_handler 

441 

442 

443class ConfigMetadataHandler(ConfigHandler): 

444 

445 section_prefix = 'metadata' 

446 

447 aliases = { 

448 'home_page': 'url', 

449 'summary': 'description', 

450 'classifier': 'classifiers', 

451 'platform': 'platforms', 

452 } 

453 

454 strict_mode = False 

455 """We need to keep it loose, to be partially compatible with 

456 `pbr` and `d2to1` packages which also uses `metadata` section. 

457 

458 """ 

459 

460 def __init__(self, target_obj, options, ignore_option_errors=False, 

461 package_dir=None): 

462 super(ConfigMetadataHandler, self).__init__(target_obj, options, 

463 ignore_option_errors) 

464 self.package_dir = package_dir 

465 

466 @property 

467 def parsers(self): 

468 """Metadata item name to parser function mapping.""" 

469 parse_list = self._parse_list 

470 parse_file = self._parse_file 

471 parse_dict = self._parse_dict 

472 exclude_files_parser = self._exclude_files_parser 

473 

474 return { 

475 'platforms': parse_list, 

476 'keywords': parse_list, 

477 'provides': parse_list, 

478 'requires': self._deprecated_config_handler( 

479 parse_list, 

480 "The requires parameter is deprecated, please use " 

481 "install_requires for runtime dependencies.", 

482 DeprecationWarning), 

483 'obsoletes': parse_list, 

484 'classifiers': self._get_parser_compound(parse_file, parse_list), 

485 'license': exclude_files_parser('license'), 

486 'license_files': parse_list, 

487 'description': parse_file, 

488 'long_description': parse_file, 

489 'version': self._parse_version, 

490 'project_urls': parse_dict, 

491 } 

492 

493 def _parse_version(self, value): 

494 """Parses `version` option value. 

495 

496 :param value: 

497 :rtype: str 

498 

499 """ 

500 version = self._parse_file(value) 

501 

502 if version != value: 

503 version = version.strip() 

504 # Be strict about versions loaded from file because it's easy to 

505 # accidentally include newlines and other unintended content 

506 if isinstance(parse(version), LegacyVersion): 

507 tmpl = ( 

508 'Version loaded from {value} does not ' 

509 'comply with PEP 440: {version}' 

510 ) 

511 raise DistutilsOptionError(tmpl.format(**locals())) 

512 

513 return version 

514 

515 version = self._parse_attr(value, self.package_dir) 

516 

517 if callable(version): 

518 version = version() 

519 

520 if not isinstance(version, string_types): 

521 if hasattr(version, '__iter__'): 

522 version = '.'.join(map(str, version)) 

523 else: 

524 version = '%s' % version 

525 

526 return version 

527 

528 

529class ConfigOptionsHandler(ConfigHandler): 

530 

531 section_prefix = 'options' 

532 

533 @property 

534 def parsers(self): 

535 """Metadata item name to parser function mapping.""" 

536 parse_list = self._parse_list 

537 parse_list_semicolon = partial(self._parse_list, separator=';') 

538 parse_bool = self._parse_bool 

539 parse_dict = self._parse_dict 

540 

541 return { 

542 'zip_safe': parse_bool, 

543 'use_2to3': parse_bool, 

544 'include_package_data': parse_bool, 

545 'package_dir': parse_dict, 

546 'use_2to3_fixers': parse_list, 

547 'use_2to3_exclude_fixers': parse_list, 

548 'convert_2to3_doctests': parse_list, 

549 'scripts': parse_list, 

550 'eager_resources': parse_list, 

551 'dependency_links': parse_list, 

552 'namespace_packages': parse_list, 

553 'install_requires': parse_list_semicolon, 

554 'setup_requires': parse_list_semicolon, 

555 'tests_require': parse_list_semicolon, 

556 'packages': self._parse_packages, 

557 'entry_points': self._parse_file, 

558 'py_modules': parse_list, 

559 'python_requires': SpecifierSet, 

560 } 

561 

562 def _parse_packages(self, value): 

563 """Parses `packages` option value. 

564 

565 :param value: 

566 :rtype: list 

567 """ 

568 find_directives = ['find:', 'find_namespace:'] 

569 trimmed_value = value.strip() 

570 

571 if trimmed_value not in find_directives: 

572 return self._parse_list(value) 

573 

574 findns = trimmed_value == find_directives[1] 

575 if findns and not PY3: 

576 raise DistutilsOptionError( 

577 'find_namespace: directive is unsupported on Python < 3.3') 

578 

579 # Read function arguments from a dedicated section. 

580 find_kwargs = self.parse_section_packages__find( 

581 self.sections.get('packages.find', {})) 

582 

583 if findns: 

584 from setuptools import find_namespace_packages as find_packages 

585 else: 

586 from setuptools import find_packages 

587 

588 return find_packages(**find_kwargs) 

589 

590 def parse_section_packages__find(self, section_options): 

591 """Parses `packages.find` configuration file section. 

592 

593 To be used in conjunction with _parse_packages(). 

594 

595 :param dict section_options: 

596 """ 

597 section_data = self._parse_section_to_dict( 

598 section_options, self._parse_list) 

599 

600 valid_keys = ['where', 'include', 'exclude'] 

601 

602 find_kwargs = dict( 

603 [(k, v) for k, v in section_data.items() if k in valid_keys and v]) 

604 

605 where = find_kwargs.get('where') 

606 if where is not None: 

607 find_kwargs['where'] = where[0] # cast list to single val 

608 

609 return find_kwargs 

610 

611 def parse_section_entry_points(self, section_options): 

612 """Parses `entry_points` configuration file section. 

613 

614 :param dict section_options: 

615 """ 

616 parsed = self._parse_section_to_dict(section_options, self._parse_list) 

617 self['entry_points'] = parsed 

618 

619 def _parse_package_data(self, section_options): 

620 parsed = self._parse_section_to_dict(section_options, self._parse_list) 

621 

622 root = parsed.get('*') 

623 if root: 

624 parsed[''] = root 

625 del parsed['*'] 

626 

627 return parsed 

628 

629 def parse_section_package_data(self, section_options): 

630 """Parses `package_data` configuration file section. 

631 

632 :param dict section_options: 

633 """ 

634 self['package_data'] = self._parse_package_data(section_options) 

635 

636 def parse_section_exclude_package_data(self, section_options): 

637 """Parses `exclude_package_data` configuration file section. 

638 

639 :param dict section_options: 

640 """ 

641 self['exclude_package_data'] = self._parse_package_data( 

642 section_options) 

643 

644 def parse_section_extras_require(self, section_options): 

645 """Parses `extras_require` configuration file section. 

646 

647 :param dict section_options: 

648 """ 

649 parse_list = partial(self._parse_list, separator=';') 

650 self['extras_require'] = self._parse_section_to_dict( 

651 section_options, parse_list) 

652 

653 def parse_section_data_files(self, section_options): 

654 """Parses `data_files` configuration file section. 

655 

656 :param dict section_options: 

657 """ 

658 parsed = self._parse_section_to_dict(section_options, self._parse_list) 

659 self['data_files'] = [(k, v) for k, v in parsed.items()]