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

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

294 statements  

1""" 

2Load setuptools configuration from ``setup.cfg`` files. 

3 

4**API will be made private in the future** 

5 

6To read project metadata, consider using 

7``build.util.project_wheel_metadata`` (https://pypi.org/project/build/). 

8For simple scenarios, you can also try parsing the file directly 

9with the help of ``configparser``. 

10""" 

11 

12from __future__ import annotations 

13 

14import contextlib 

15import functools 

16import os 

17from collections import defaultdict 

18from functools import partial, wraps 

19from typing import ( 

20 TYPE_CHECKING, 

21 Any, 

22 Callable, 

23 Dict, 

24 Generic, 

25 Iterable, 

26 Iterator, 

27 List, 

28 Tuple, 

29 TypeVar, 

30 Union, 

31 cast, 

32) 

33 

34from packaging.markers import default_environment as marker_env 

35from packaging.requirements import InvalidRequirement, Requirement 

36from packaging.specifiers import SpecifierSet 

37from packaging.version import InvalidVersion, Version 

38 

39from .._path import StrPath 

40from ..errors import FileError, OptionError 

41from ..warnings import SetuptoolsDeprecationWarning 

42from . import expand 

43 

44if TYPE_CHECKING: 

45 from setuptools.dist import Distribution 

46 

47 from distutils.dist import DistributionMetadata 

48 

49SingleCommandOptions = Dict["str", Tuple["str", Any]] 

50"""Dict that associate the name of the options of a particular command to a 

51tuple. The first element of the tuple indicates the origin of the option value 

52(e.g. the name of the configuration file where it was read from), 

53while the second element of the tuple is the option value itself 

54""" 

55AllCommandOptions = Dict["str", SingleCommandOptions] # cmd name => its options 

56Target = TypeVar("Target", bound=Union["Distribution", "DistributionMetadata"]) 

57 

58 

59def read_configuration( 

60 filepath: StrPath, find_others=False, ignore_option_errors=False 

61) -> dict: 

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

63 

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

65 to get options from. 

66 

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

68 which could be on in various places. 

69 

70 :param bool ignore_option_errors: Whether to silently ignore 

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

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

73 If False exceptions are propagated as expected. 

74 

75 :rtype: dict 

76 """ 

77 from setuptools.dist import Distribution 

78 

79 dist = Distribution() 

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

81 handlers = _apply(dist, filepath, filenames, ignore_option_errors) 

82 return configuration_to_dict(handlers) 

83 

84 

85def apply_configuration(dist: Distribution, filepath: StrPath) -> Distribution: 

86 """Apply the configuration from a ``setup.cfg`` file into an existing 

87 distribution object. 

88 """ 

89 _apply(dist, filepath) 

90 dist._finalize_requires() 

91 return dist 

92 

93 

94def _apply( 

95 dist: Distribution, 

96 filepath: StrPath, 

97 other_files: Iterable[StrPath] = (), 

98 ignore_option_errors: bool = False, 

99) -> tuple[ConfigHandler, ...]: 

100 """Read configuration from ``filepath`` and applies to the ``dist`` object.""" 

101 from setuptools.dist import _Distribution 

102 

103 filepath = os.path.abspath(filepath) 

104 

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

106 raise FileError(f'Configuration file {filepath} does not exist.') 

107 

108 current_directory = os.getcwd() 

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

110 filenames = [*other_files, filepath] 

111 

112 try: 

113 # TODO: Temporary cast until mypy 1.12 is released with upstream fixes from typeshed 

114 _Distribution.parse_config_files(dist, filenames=cast(List[str], filenames)) 

115 handlers = parse_configuration( 

116 dist, dist.command_options, ignore_option_errors=ignore_option_errors 

117 ) 

118 dist._finalize_license_files() 

119 finally: 

120 os.chdir(current_directory) 

121 

122 return handlers 

123 

124 

125def _get_option(target_obj: Target, key: str): 

126 """ 

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

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

129 from an attribute directly. 

130 """ 

131 getter_name = f'get_{key}' 

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

133 getter = getattr(target_obj, getter_name, by_attribute) 

134 return getter() 

135 

136 

137def configuration_to_dict(handlers: tuple[ConfigHandler, ...]) -> dict: 

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

139 

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

141 usually from parse_configuration() 

142 

143 :rtype: dict 

144 """ 

145 config_dict: dict = defaultdict(dict) 

146 

147 for handler in handlers: 

148 for option in handler.set_options: 

149 value = _get_option(handler.target_obj, option) 

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

151 

152 return config_dict 

153 

154 

155def parse_configuration( 

156 distribution: Distribution, 

157 command_options: AllCommandOptions, 

158 ignore_option_errors=False, 

159) -> tuple[ConfigMetadataHandler, ConfigOptionsHandler]: 

160 """Performs additional parsing of configuration options 

161 for a distribution. 

162 

163 Returns a list of used option handlers. 

164 

165 :param Distribution distribution: 

166 :param dict command_options: 

167 :param bool ignore_option_errors: Whether to silently ignore 

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

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

170 If False exceptions are propagated as expected. 

171 :rtype: list 

172 """ 

173 with expand.EnsurePackagesDiscovered(distribution) as ensure_discovered: 

174 options = ConfigOptionsHandler( 

175 distribution, 

176 command_options, 

177 ignore_option_errors, 

178 ensure_discovered, 

179 ) 

180 

181 options.parse() 

182 if not distribution.package_dir: 

183 distribution.package_dir = options.package_dir # Filled by `find_packages` 

184 

185 meta = ConfigMetadataHandler( 

186 distribution.metadata, 

187 command_options, 

188 ignore_option_errors, 

189 ensure_discovered, 

190 distribution.package_dir, 

191 distribution.src_root, 

192 ) 

193 meta.parse() 

194 distribution._referenced_files.update( 

195 options._referenced_files, meta._referenced_files 

196 ) 

197 

198 return meta, options 

199 

200 

201def _warn_accidental_env_marker_misconfig(label: str, orig_value: str, parsed: list): 

202 """Because users sometimes misinterpret this configuration: 

203 

204 [options.extras_require] 

205 foo = bar;python_version<"4" 

206 

207 It looks like one requirement with an environment marker 

208 but because there is no newline, it's parsed as two requirements 

209 with a semicolon as separator. 

210 

211 Therefore, if: 

212 * input string does not contain a newline AND 

213 * parsed result contains two requirements AND 

214 * parsing of the two parts from the result ("<first>;<second>") 

215 leads in a valid Requirement with a valid marker 

216 a UserWarning is shown to inform the user about the possible problem. 

217 """ 

218 if "\n" in orig_value or len(parsed) != 2: 

219 return 

220 

221 markers = marker_env().keys() 

222 

223 try: 

224 req = Requirement(parsed[1]) 

225 if req.name in markers: 

226 _AmbiguousMarker.emit(field=label, req=parsed[1]) 

227 except InvalidRequirement as ex: 

228 if any(parsed[1].startswith(marker) for marker in markers): 

229 msg = _AmbiguousMarker.message(field=label, req=parsed[1]) 

230 raise InvalidRequirement(msg) from ex 

231 

232 

233class ConfigHandler(Generic[Target]): 

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

235 

236 section_prefix: str 

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

238 Must be provided by class heirs. 

239 

240 """ 

241 

242 aliases: dict[str, str] = {} 

243 """Options aliases. 

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

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

246 

247 """ 

248 

249 def __init__( 

250 self, 

251 target_obj: Target, 

252 options: AllCommandOptions, 

253 ignore_option_errors, 

254 ensure_discovered: expand.EnsurePackagesDiscovered, 

255 ): 

256 self.ignore_option_errors = ignore_option_errors 

257 self.target_obj = target_obj 

258 self.sections = dict(self._section_options(options)) 

259 self.set_options: list[str] = [] 

260 self.ensure_discovered = ensure_discovered 

261 self._referenced_files: set[str] = set() 

262 """After parsing configurations, this property will enumerate 

263 all files referenced by the "file:" directive. Private API for setuptools only. 

264 """ 

265 

266 @classmethod 

267 def _section_options( 

268 cls, options: AllCommandOptions 

269 ) -> Iterator[tuple[str, SingleCommandOptions]]: 

270 for full_name, value in options.items(): 

271 pre, sep, name = full_name.partition(cls.section_prefix) 

272 if pre: 

273 continue 

274 yield name.lstrip('.'), value 

275 

276 @property 

277 def parsers(self): 

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

279 raise NotImplementedError( 

280 '%s must provide .parsers property' % self.__class__.__name__ 

281 ) 

282 

283 def __setitem__(self, option_name, value) -> None: 

284 target_obj = self.target_obj 

285 

286 # Translate alias into real name. 

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

288 

289 try: 

290 current_value = getattr(target_obj, option_name) 

291 except AttributeError as e: 

292 raise KeyError(option_name) from e 

293 

294 if current_value: 

295 # Already inhabited. Skipping. 

296 return 

297 

298 try: 

299 parsed = self.parsers.get(option_name, lambda x: x)(value) 

300 except (Exception,) * self.ignore_option_errors: 

301 return 

302 

303 simple_setter = functools.partial(target_obj.__setattr__, option_name) 

304 setter = getattr(target_obj, 'set_%s' % option_name, simple_setter) 

305 setter(parsed) 

306 

307 self.set_options.append(option_name) 

308 

309 @classmethod 

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

311 """Represents value as a list. 

312 

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

314 

315 :param value: 

316 :param separator: List items separator character. 

317 :rtype: list 

318 """ 

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

320 return value 

321 

322 if '\n' in value: 

323 value = value.splitlines() 

324 else: 

325 value = value.split(separator) 

326 

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

328 

329 @classmethod 

330 def _parse_dict(cls, value): 

331 """Represents value as a dict. 

332 

333 :param value: 

334 :rtype: dict 

335 """ 

336 separator = '=' 

337 result = {} 

338 for line in cls._parse_list(value): 

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

340 if sep != separator: 

341 raise OptionError(f"Unable to parse option value to dict: {value}") 

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

343 

344 return result 

345 

346 @classmethod 

347 def _parse_bool(cls, value): 

348 """Represents value as boolean. 

349 

350 :param value: 

351 :rtype: bool 

352 """ 

353 value = value.lower() 

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

355 

356 @classmethod 

357 def _exclude_files_parser(cls, key): 

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

359 are not files. 

360 

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

362 more informative. 

363 

364 :param key: 

365 :rtype: callable 

366 """ 

367 

368 def parser(value): 

369 exclude_directive = 'file:' 

370 if value.startswith(exclude_directive): 

371 raise ValueError( 

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

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

374 ) 

375 return value 

376 

377 return parser 

378 

379 def _parse_file(self, value, root_dir: StrPath): 

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

381 from nearest files using `file:` directive. 

382 

383 Directive is sandboxed and won't reach anything outside 

384 directory with setup.py. 

385 

386 Examples: 

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

388 

389 :param str value: 

390 :rtype: str 

391 """ 

392 include_directive = 'file:' 

393 

394 if not isinstance(value, str): 

395 return value 

396 

397 if not value.startswith(include_directive): 

398 return value 

399 

400 spec = value[len(include_directive) :] 

401 filepaths = [path.strip() for path in spec.split(',')] 

402 self._referenced_files.update(filepaths) 

403 return expand.read_files(filepaths, root_dir) 

404 

405 def _parse_attr(self, value, package_dir, root_dir: StrPath): 

406 """Represents value as a module attribute. 

407 

408 Examples: 

409 attr: package.attr 

410 attr: package.module.attr 

411 

412 :param str value: 

413 :rtype: str 

414 """ 

415 attr_directive = 'attr:' 

416 if not value.startswith(attr_directive): 

417 return value 

418 

419 attr_desc = value.replace(attr_directive, '') 

420 

421 # Make sure package_dir is populated correctly, so `attr:` directives can work 

422 package_dir.update(self.ensure_discovered.package_dir) 

423 return expand.read_attr(attr_desc, package_dir, root_dir) 

424 

425 @classmethod 

426 def _get_parser_compound(cls, *parse_methods): 

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

428 

429 Parses a value applying given methods one after another. 

430 

431 :param parse_methods: 

432 :rtype: callable 

433 """ 

434 

435 def parse(value): 

436 parsed = value 

437 

438 for method in parse_methods: 

439 parsed = method(parsed) 

440 

441 return parsed 

442 

443 return parse 

444 

445 @classmethod 

446 def _parse_section_to_dict_with_key(cls, section_options, values_parser): 

447 """Parses section options into a dictionary. 

448 

449 Applies a given parser to each option in a section. 

450 

451 :param dict section_options: 

452 :param callable values_parser: function with 2 args corresponding to key, value 

453 :rtype: dict 

454 """ 

455 value = {} 

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

457 value[key] = values_parser(key, val) 

458 return value 

459 

460 @classmethod 

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

462 """Parses section options into a dictionary. 

463 

464 Optionally applies a given parser to each value. 

465 

466 :param dict section_options: 

467 :param callable values_parser: function with 1 arg corresponding to option value 

468 :rtype: dict 

469 """ 

470 parser = (lambda _, v: values_parser(v)) if values_parser else (lambda _, v: v) 

471 return cls._parse_section_to_dict_with_key(section_options, parser) 

472 

473 def parse_section(self, section_options): 

474 """Parses configuration file section. 

475 

476 :param dict section_options: 

477 """ 

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

479 with contextlib.suppress(KeyError): 

480 # Keep silent for a new option may appear anytime. 

481 self[name] = value 

482 

483 def parse(self) -> None: 

484 """Parses configuration file items from one 

485 or more related sections. 

486 

487 """ 

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

489 method_postfix = '' 

490 if section_name: # [section.option] variant 

491 method_postfix = '_%s' % section_name 

492 

493 section_parser_method: Callable | None = getattr( 

494 self, 

495 # Dots in section names are translated into dunderscores. 

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

497 None, 

498 ) 

499 

500 if section_parser_method is None: 

501 raise OptionError( 

502 "Unsupported distribution option section: " 

503 f"[{self.section_prefix}.{section_name}]" 

504 ) 

505 

506 section_parser_method(section_options) 

507 

508 def _deprecated_config_handler(self, func, msg, **kw): 

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

510 

511 :param msg: deprecation message 

512 :param func: function to be wrapped around 

513 """ 

514 

515 @wraps(func) 

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

517 kw.setdefault("stacklevel", 2) 

518 _DeprecatedConfig.emit("Deprecated config in `setup.cfg`", msg, **kw) 

519 return func(*args, **kwargs) 

520 

521 return config_handler 

522 

523 

524class ConfigMetadataHandler(ConfigHandler["DistributionMetadata"]): 

525 section_prefix = 'metadata' 

526 

527 aliases = { 

528 'home_page': 'url', 

529 'summary': 'description', 

530 'classifier': 'classifiers', 

531 'platform': 'platforms', 

532 } 

533 

534 strict_mode = False 

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

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

537 

538 """ 

539 

540 def __init__( 

541 self, 

542 target_obj: DistributionMetadata, 

543 options: AllCommandOptions, 

544 ignore_option_errors: bool, 

545 ensure_discovered: expand.EnsurePackagesDiscovered, 

546 package_dir: dict | None = None, 

547 root_dir: StrPath = os.curdir, 

548 ): 

549 super().__init__(target_obj, options, ignore_option_errors, ensure_discovered) 

550 self.package_dir = package_dir 

551 self.root_dir = root_dir 

552 

553 @property 

554 def parsers(self): 

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

556 parse_list = self._parse_list 

557 parse_file = partial(self._parse_file, root_dir=self.root_dir) 

558 parse_dict = self._parse_dict 

559 exclude_files_parser = self._exclude_files_parser 

560 

561 return { 

562 'platforms': parse_list, 

563 'keywords': parse_list, 

564 'provides': parse_list, 

565 'obsoletes': parse_list, 

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

567 'license': exclude_files_parser('license'), 

568 'license_files': parse_list, 

569 'description': parse_file, 

570 'long_description': parse_file, 

571 'version': self._parse_version, 

572 'project_urls': parse_dict, 

573 } 

574 

575 def _parse_version(self, value): 

576 """Parses `version` option value. 

577 

578 :param value: 

579 :rtype: str 

580 

581 """ 

582 version = self._parse_file(value, self.root_dir) 

583 

584 if version != value: 

585 version = version.strip() 

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

587 # accidentally include newlines and other unintended content 

588 try: 

589 Version(version) 

590 except InvalidVersion as e: 

591 raise OptionError( 

592 f'Version loaded from {value} does not ' 

593 f'comply with PEP 440: {version}' 

594 ) from e 

595 

596 return version 

597 

598 return expand.version(self._parse_attr(value, self.package_dir, self.root_dir)) 

599 

600 

601class ConfigOptionsHandler(ConfigHandler["Distribution"]): 

602 section_prefix = 'options' 

603 

604 def __init__( 

605 self, 

606 target_obj: Distribution, 

607 options: AllCommandOptions, 

608 ignore_option_errors: bool, 

609 ensure_discovered: expand.EnsurePackagesDiscovered, 

610 ): 

611 super().__init__(target_obj, options, ignore_option_errors, ensure_discovered) 

612 self.root_dir = target_obj.src_root 

613 self.package_dir: dict[str, str] = {} # To be filled by `find_packages` 

614 

615 @classmethod 

616 def _parse_list_semicolon(cls, value): 

617 return cls._parse_list(value, separator=';') 

618 

619 def _parse_file_in_root(self, value): 

620 return self._parse_file(value, root_dir=self.root_dir) 

621 

622 def _parse_requirements_list(self, label: str, value: str): 

623 # Parse a requirements list, either by reading in a `file:`, or a list. 

624 parsed = self._parse_list_semicolon(self._parse_file_in_root(value)) 

625 _warn_accidental_env_marker_misconfig(label, value, parsed) 

626 # Filter it to only include lines that are not comments. `parse_list` 

627 # will have stripped each line and filtered out empties. 

628 return [line for line in parsed if not line.startswith("#")] 

629 

630 @property 

631 def parsers(self): 

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

633 parse_list = self._parse_list 

634 parse_bool = self._parse_bool 

635 parse_dict = self._parse_dict 

636 parse_cmdclass = self._parse_cmdclass 

637 

638 return { 

639 'zip_safe': parse_bool, 

640 'include_package_data': parse_bool, 

641 'package_dir': parse_dict, 

642 'scripts': parse_list, 

643 'eager_resources': parse_list, 

644 'dependency_links': parse_list, 

645 'namespace_packages': self._deprecated_config_handler( 

646 parse_list, 

647 "The namespace_packages parameter is deprecated, " 

648 "consider using implicit namespaces instead (PEP 420).", 

649 # TODO: define due date, see setuptools.dist:check_nsp. 

650 ), 

651 'install_requires': partial( 

652 self._parse_requirements_list, "install_requires" 

653 ), 

654 'setup_requires': self._parse_list_semicolon, 

655 'packages': self._parse_packages, 

656 'entry_points': self._parse_file_in_root, 

657 'py_modules': parse_list, 

658 'python_requires': SpecifierSet, 

659 'cmdclass': parse_cmdclass, 

660 } 

661 

662 def _parse_cmdclass(self, value): 

663 package_dir = self.ensure_discovered.package_dir 

664 return expand.cmdclass(self._parse_dict(value), package_dir, self.root_dir) 

665 

666 def _parse_packages(self, value): 

667 """Parses `packages` option value. 

668 

669 :param value: 

670 :rtype: list 

671 """ 

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

673 trimmed_value = value.strip() 

674 

675 if trimmed_value not in find_directives: 

676 return self._parse_list(value) 

677 

678 # Read function arguments from a dedicated section. 

679 find_kwargs = self.parse_section_packages__find( 

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

681 ) 

682 

683 find_kwargs.update( 

684 namespaces=(trimmed_value == find_directives[1]), 

685 root_dir=self.root_dir, 

686 fill_package_dir=self.package_dir, 

687 ) 

688 

689 return expand.find_packages(**find_kwargs) 

690 

691 def parse_section_packages__find(self, section_options): 

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

693 

694 To be used in conjunction with _parse_packages(). 

695 

696 :param dict section_options: 

697 """ 

698 section_data = self._parse_section_to_dict(section_options, self._parse_list) 

699 

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

701 

702 find_kwargs = dict([ 

703 (k, v) for k, v in section_data.items() if k in valid_keys and v 

704 ]) 

705 

706 where = find_kwargs.get('where') 

707 if where is not None: 

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

709 

710 return find_kwargs 

711 

712 def parse_section_entry_points(self, section_options): 

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

714 

715 :param dict section_options: 

716 """ 

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

718 self['entry_points'] = parsed 

719 

720 def _parse_package_data(self, section_options): 

721 package_data = self._parse_section_to_dict(section_options, self._parse_list) 

722 return expand.canonic_package_data(package_data) 

723 

724 def parse_section_package_data(self, section_options): 

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

726 

727 :param dict section_options: 

728 """ 

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

730 

731 def parse_section_exclude_package_data(self, section_options): 

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

733 

734 :param dict section_options: 

735 """ 

736 self['exclude_package_data'] = self._parse_package_data(section_options) 

737 

738 def parse_section_extras_require(self, section_options): 

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

740 

741 :param dict section_options: 

742 """ 

743 parsed = self._parse_section_to_dict_with_key( 

744 section_options, 

745 lambda k, v: self._parse_requirements_list(f"extras_require[{k}]", v), 

746 ) 

747 

748 self['extras_require'] = parsed 

749 

750 def parse_section_data_files(self, section_options): 

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

752 

753 :param dict section_options: 

754 """ 

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

756 self['data_files'] = expand.canonic_data_files(parsed, self.root_dir) 

757 

758 

759class _AmbiguousMarker(SetuptoolsDeprecationWarning): 

760 _SUMMARY = "Ambiguous requirement marker." 

761 _DETAILS = """ 

762 One of the parsed requirements in `{field}` looks like a valid environment marker: 

763 

764 {req!r} 

765 

766 Please make sure that the configuration file is correct. 

767 You can use dangling lines to avoid this problem. 

768 """ 

769 _SEE_DOCS = "userguide/declarative_config.html#opt-2" 

770 # TODO: should we include due_date here? Initially introduced in 6 Aug 2022. 

771 # Does this make sense with latest version of packaging? 

772 

773 @classmethod 

774 def message(cls, **kw): 

775 docs = f"https://setuptools.pypa.io/en/latest/{cls._SEE_DOCS}" 

776 return cls._format(cls._SUMMARY, cls._DETAILS, see_url=docs, format_args=kw) 

777 

778 

779class _DeprecatedConfig(SetuptoolsDeprecationWarning): 

780 _SEE_DOCS = "userguide/declarative_config.html"