|
8 | 8 | from typing import Optional |
9 | 9 | from typing import Sequence |
10 | 10 | from typing import TextIO |
| 11 | +from typing import TYPE_CHECKING |
11 | 12 |
|
12 | 13 | from ..compat import assert_never |
13 | 14 | from .wcwidth import wcswidth |
14 | 15 |
|
15 | 16 |
|
| 17 | +if TYPE_CHECKING: |
| 18 | + from pygments.formatter import Formatter |
| 19 | + from pygments.lexer import Lexer |
| 20 | + |
| 21 | + |
16 | 22 | # This code was initially copied from py 1.8.1, file _io/terminalwriter.py. |
17 | 23 |
|
18 | 24 |
|
@@ -194,58 +200,76 @@ def _write_source(self, lines: Sequence[str], indents: Sequence[str] = ()) -> No |
194 | 200 | for indent, new_line in zip(indents, new_lines): |
195 | 201 | self.line(indent + new_line) |
196 | 202 |
|
| 203 | + def _get_pygments_lexer( |
| 204 | + self, lexer: Literal["python", "diff"] |
| 205 | + ) -> Optional["Lexer"]: |
| 206 | + try: |
| 207 | + if lexer == "python": |
| 208 | + from pygments.lexers.python import PythonLexer |
| 209 | + |
| 210 | + return PythonLexer() |
| 211 | + elif lexer == "diff": |
| 212 | + from pygments.lexers.diff import DiffLexer |
| 213 | + |
| 214 | + return DiffLexer() |
| 215 | + else: |
| 216 | + assert_never(lexer) |
| 217 | + except ModuleNotFoundError: |
| 218 | + return None |
| 219 | + |
| 220 | + def _get_pygments_formatter(self) -> Optional["Formatter"]: |
| 221 | + try: |
| 222 | + import pygments.util |
| 223 | + except ModuleNotFoundError: |
| 224 | + return None |
| 225 | + |
| 226 | + from _pytest.config.exceptions import UsageError |
| 227 | + |
| 228 | + theme = os.getenv("PYTEST_THEME") |
| 229 | + theme_mode = os.getenv("PYTEST_THEME_MODE", "dark") |
| 230 | + |
| 231 | + try: |
| 232 | + from pygments.formatters.terminal import TerminalFormatter |
| 233 | + |
| 234 | + return TerminalFormatter(bg=theme_mode, style=theme) |
| 235 | + |
| 236 | + except pygments.util.ClassNotFound as e: |
| 237 | + raise UsageError( |
| 238 | + f"PYTEST_THEME environment variable has an invalid value: '{theme}'. " |
| 239 | + "Hint: See available pygments styles with `pygmentize -L styles`." |
| 240 | + ) from e |
| 241 | + except pygments.util.OptionError as e: |
| 242 | + raise UsageError( |
| 243 | + f"PYTEST_THEME_MODE environment variable has an invalid value: '{theme_mode}'. " |
| 244 | + "The allowed values are 'dark' (default) and 'light'." |
| 245 | + ) from e |
| 246 | + |
197 | 247 | def _highlight( |
198 | 248 | self, source: str, lexer: Literal["diff", "python"] = "python" |
199 | 249 | ) -> str: |
200 | 250 | """Highlight the given source if we have markup support.""" |
201 | | - from _pytest.config.exceptions import UsageError |
202 | | - |
203 | 251 | if not source or not self.hasmarkup or not self.code_highlight: |
204 | 252 | return source |
205 | 253 |
|
206 | | - try: |
207 | | - from pygments.formatters.terminal import TerminalFormatter |
| 254 | + pygments_lexer = self._get_pygments_lexer(lexer) |
| 255 | + if pygments_lexer is None: |
| 256 | + return source |
208 | 257 |
|
209 | | - if lexer == "python": |
210 | | - from pygments.lexers.python import PythonLexer as Lexer |
211 | | - elif lexer == "diff": |
212 | | - from pygments.lexers.diff import DiffLexer as Lexer |
213 | | - else: |
214 | | - assert_never(lexer) |
215 | | - from pygments import highlight |
216 | | - import pygments.util |
217 | | - except ImportError: |
| 258 | + pygments_formatter = self._get_pygments_formatter() |
| 259 | + if pygments_formatter is None: |
218 | 260 | return source |
219 | | - else: |
220 | | - try: |
221 | | - highlighted: str = highlight( |
222 | | - source, |
223 | | - Lexer(), |
224 | | - TerminalFormatter( |
225 | | - bg=os.getenv("PYTEST_THEME_MODE", "dark"), |
226 | | - style=os.getenv("PYTEST_THEME"), |
227 | | - ), |
228 | | - ) |
229 | | - # pygments terminal formatter may add a newline when there wasn't one. |
230 | | - # We don't want this, remove. |
231 | | - if highlighted[-1] == "\n" and source[-1] != "\n": |
232 | | - highlighted = highlighted[:-1] |
233 | | - |
234 | | - # Some lexers will not set the initial color explicitly |
235 | | - # which may lead to the previous color being propagated to the |
236 | | - # start of the expression, so reset first. |
237 | | - return "\x1b[0m" + highlighted |
238 | | - except pygments.util.ClassNotFound as e: |
239 | | - raise UsageError( |
240 | | - "PYTEST_THEME environment variable had an invalid value: '{}'. " |
241 | | - "Only valid pygment styles are allowed.".format( |
242 | | - os.getenv("PYTEST_THEME") |
243 | | - ) |
244 | | - ) from e |
245 | | - except pygments.util.OptionError as e: |
246 | | - raise UsageError( |
247 | | - "PYTEST_THEME_MODE environment variable had an invalid value: '{}'. " |
248 | | - "The only allowed values are 'dark' and 'light'.".format( |
249 | | - os.getenv("PYTEST_THEME_MODE") |
250 | | - ) |
251 | | - ) from e |
| 261 | + |
| 262 | + from pygments import highlight |
| 263 | + |
| 264 | + highlighted: str = highlight(source, pygments_lexer, pygments_formatter) |
| 265 | + # pygments terminal formatter may add a newline when there wasn't one. |
| 266 | + # We don't want this, remove. |
| 267 | + if highlighted[-1] == "\n" and source[-1] != "\n": |
| 268 | + highlighted = highlighted[:-1] |
| 269 | + |
| 270 | + # Some lexers will not set the initial color explicitly |
| 271 | + # which may lead to the previous color being propagated to the |
| 272 | + # start of the expression, so reset first. |
| 273 | + highlighted = "\x1b[0m" + highlighted |
| 274 | + |
| 275 | + return highlighted |
0 commit comments