output_line.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  2. # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
  3. # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt
  4. from __future__ import annotations
  5. import warnings
  6. from collections.abc import Sequence
  7. from typing import Any, NamedTuple, TypeVar
  8. from astroid import nodes
  9. from pylint.constants import PY38_PLUS
  10. from pylint.interfaces import UNDEFINED, Confidence
  11. from pylint.message.message import Message
  12. from pylint.testutils.constants import UPDATE_OPTION
  13. _T = TypeVar("_T")
  14. class MessageTest(NamedTuple):
  15. msg_id: str
  16. line: int | None = None
  17. node: nodes.NodeNG | None = None
  18. args: Any | None = None
  19. confidence: Confidence | None = UNDEFINED
  20. col_offset: int | None = None
  21. end_line: int | None = None
  22. end_col_offset: int | None = None
  23. """Used to test messages produced by pylint.
  24. Class name cannot start with Test as pytest doesn't allow constructors in test classes.
  25. """
  26. class OutputLine(NamedTuple):
  27. symbol: str
  28. lineno: int
  29. column: int
  30. end_lineno: int | None
  31. end_column: int | None
  32. object: str
  33. msg: str
  34. confidence: str
  35. @classmethod
  36. def from_msg(cls, msg: Message, check_endline: bool = True) -> OutputLine:
  37. """Create an OutputLine from a Pylint Message."""
  38. column = cls._get_column(msg.column)
  39. end_line = cls._get_py38_none_value(msg.end_line, check_endline)
  40. end_column = cls._get_py38_none_value(msg.end_column, check_endline)
  41. return cls(
  42. msg.symbol,
  43. msg.line,
  44. column,
  45. end_line,
  46. end_column,
  47. msg.obj or "",
  48. msg.msg.replace("\r\n", "\n"),
  49. msg.confidence.name,
  50. )
  51. @staticmethod
  52. def _get_column(column: str | int) -> int:
  53. """Handle column numbers except for python < 3.8.
  54. The ast parser in those versions doesn't return them.
  55. """
  56. if not PY38_PLUS:
  57. # We check the column only for the new better ast parser introduced in python 3.8
  58. return 0 # pragma: no cover
  59. return int(column)
  60. @staticmethod
  61. def _get_py38_none_value(value: _T, check_endline: bool) -> _T | None:
  62. """Used to make end_line and end_column None as indicated by our version
  63. compared to `min_pyver_end_position`.
  64. """
  65. if not check_endline:
  66. return None # pragma: no cover
  67. return value
  68. @classmethod
  69. def from_csv(
  70. cls, row: Sequence[str] | str, check_endline: bool = True
  71. ) -> OutputLine:
  72. """Create an OutputLine from a comma separated list (the functional tests
  73. expected output .txt files).
  74. """
  75. if isinstance(row, str):
  76. row = row.split(",")
  77. # noinspection PyBroadException
  78. # pylint: disable = too-many-try-statements
  79. try:
  80. column = cls._get_column(row[2])
  81. if len(row) == 5:
  82. warnings.warn(
  83. "In pylint 3.0 functional tests expected output should always include the "
  84. "expected confidence level, expected end_line and expected end_column. "
  85. "An OutputLine should thus have a length of 8.",
  86. DeprecationWarning,
  87. stacklevel=2,
  88. )
  89. return cls(
  90. row[0],
  91. int(row[1]),
  92. column,
  93. None,
  94. None,
  95. row[3],
  96. row[4],
  97. UNDEFINED.name,
  98. )
  99. if len(row) == 6:
  100. warnings.warn(
  101. "In pylint 3.0 functional tests expected output should always include the "
  102. "expected end_line and expected end_column. An OutputLine should thus have "
  103. "a length of 8.",
  104. DeprecationWarning,
  105. stacklevel=2,
  106. )
  107. return cls(
  108. row[0], int(row[1]), column, None, None, row[3], row[4], row[5]
  109. )
  110. if len(row) == 8:
  111. end_line = cls._get_py38_none_value(row[3], check_endline)
  112. end_column = cls._get_py38_none_value(row[4], check_endline)
  113. return cls(
  114. row[0],
  115. int(row[1]),
  116. column,
  117. cls._value_to_optional_int(end_line),
  118. cls._value_to_optional_int(end_column),
  119. row[5],
  120. row[6],
  121. row[7],
  122. )
  123. raise IndexError
  124. except Exception: # pylint: disable=broad-except
  125. warnings.warn(
  126. "Expected 'msg-symbolic-name:42:27:MyClass.my_function:The message:"
  127. f"CONFIDENCE' but we got '{':'.join(row)}'. Try updating the expected"
  128. f" output with:\npython tests/test_functional.py {UPDATE_OPTION}",
  129. UserWarning,
  130. )
  131. return cls("", 0, 0, None, None, "", "", "")
  132. def to_csv(self) -> tuple[str, str, str, str, str, str, str, str]:
  133. """Convert an OutputLine to a tuple of string to be written by a
  134. csv-writer.
  135. """
  136. return (
  137. str(self.symbol),
  138. str(self.lineno),
  139. str(self.column),
  140. str(self.end_lineno),
  141. str(self.end_column),
  142. str(self.object),
  143. str(self.msg),
  144. str(self.confidence),
  145. )
  146. @staticmethod
  147. def _value_to_optional_int(value: str | None) -> int | None:
  148. """Checks if a (stringified) value should be None or a Python integer."""
  149. if value == "None" or not value:
  150. return None
  151. return int(value)