check.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. """distutils.command.check
  2. Implements the Distutils 'check' command.
  3. """
  4. import contextlib
  5. from typing import ClassVar
  6. from ..core import Command
  7. from ..errors import DistutilsSetupError
  8. with contextlib.suppress(ImportError):
  9. import docutils.frontend
  10. import docutils.nodes
  11. import docutils.parsers.rst
  12. import docutils.utils
  13. class SilentReporter(docutils.utils.Reporter):
  14. def __init__(
  15. self,
  16. source,
  17. report_level,
  18. halt_level,
  19. stream=None,
  20. debug=False,
  21. encoding='ascii',
  22. error_handler='replace',
  23. ):
  24. self.messages = []
  25. super().__init__(
  26. source, report_level, halt_level, stream, debug, encoding, error_handler
  27. )
  28. def system_message(self, level, message, *children, **kwargs):
  29. self.messages.append((level, message, children, kwargs))
  30. return docutils.nodes.system_message(
  31. message, *children, level=level, type=self.levels[level], **kwargs
  32. )
  33. class check(Command):
  34. """This command checks the meta-data of the package."""
  35. description = "perform some checks on the package"
  36. user_options: ClassVar[list[tuple[str, str, str]]] = [
  37. ('metadata', 'm', 'Verify meta-data'),
  38. (
  39. 'restructuredtext',
  40. 'r',
  41. 'Checks if long string meta-data syntax are reStructuredText-compliant',
  42. ),
  43. ('strict', 's', 'Will exit with an error if a check fails'),
  44. ]
  45. boolean_options: ClassVar[list[str]] = ['metadata', 'restructuredtext', 'strict']
  46. def initialize_options(self):
  47. """Sets default values for options."""
  48. self.restructuredtext = False
  49. self.metadata = 1
  50. self.strict = False
  51. self._warnings = 0
  52. def finalize_options(self):
  53. pass
  54. def warn(self, msg):
  55. """Counts the number of warnings that occurs."""
  56. self._warnings += 1
  57. return Command.warn(self, msg)
  58. def run(self):
  59. """Runs the command."""
  60. # perform the various tests
  61. if self.metadata:
  62. self.check_metadata()
  63. if self.restructuredtext:
  64. if 'docutils' in globals():
  65. try:
  66. self.check_restructuredtext()
  67. except TypeError as exc:
  68. raise DistutilsSetupError(str(exc))
  69. elif self.strict:
  70. raise DistutilsSetupError('The docutils package is needed.')
  71. # let's raise an error in strict mode, if we have at least
  72. # one warning
  73. if self.strict and self._warnings > 0:
  74. raise DistutilsSetupError('Please correct your package.')
  75. def check_metadata(self):
  76. """Ensures that all required elements of meta-data are supplied.
  77. Required fields:
  78. name, version
  79. Warns if any are missing.
  80. """
  81. metadata = self.distribution.metadata
  82. missing = [
  83. attr for attr in ('name', 'version') if not getattr(metadata, attr, None)
  84. ]
  85. if missing:
  86. self.warn("missing required meta-data: {}".format(', '.join(missing)))
  87. def check_restructuredtext(self):
  88. """Checks if the long string fields are reST-compliant."""
  89. data = self.distribution.get_long_description()
  90. for warning in self._check_rst_data(data):
  91. line = warning[-1].get('line')
  92. if line is None:
  93. warning = warning[1]
  94. else:
  95. warning = f'{warning[1]} (line {line})'
  96. self.warn(warning)
  97. def _check_rst_data(self, data):
  98. """Returns warnings when the provided data doesn't compile."""
  99. # the include and csv_table directives need this to be a path
  100. source_path = self.distribution.script_name or 'setup.py'
  101. parser = docutils.parsers.rst.Parser()
  102. settings = docutils.frontend.OptionParser(
  103. components=(docutils.parsers.rst.Parser,)
  104. ).get_default_values()
  105. settings.tab_width = 4
  106. settings.pep_references = None
  107. settings.rfc_references = None
  108. reporter = SilentReporter(
  109. source_path,
  110. settings.report_level,
  111. settings.halt_level,
  112. stream=settings.warning_stream,
  113. debug=settings.debug,
  114. encoding=settings.error_encoding,
  115. error_handler=settings.error_encoding_error_handler,
  116. )
  117. document = docutils.nodes.document(settings, reporter, source=source_path)
  118. document.note_source(source_path, -1)
  119. try:
  120. parser.parse(data, document)
  121. except (AttributeError, TypeError) as e:
  122. reporter.messages.append((
  123. -1,
  124. f'Could not finish the parsing: {e}.',
  125. '',
  126. {},
  127. ))
  128. return reporter.messages