# Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ruff: noqa: F821, RUF012 """Highlight printed TVM script.""" import functools import os import shutil import subprocess import sys import warnings from typing import Any, Union def cprint( printable: Any | str, style: str | None = None, black_format: bool = False, ) -> None: """Print TVMScript string with Pygments highlight and Black auto-formatting. Parameters ---------- printable : Union[IRModule, PrimFunc, str] The TVMScript to be printed style : str, optional Pygmentize printing style, auto-detected if None. black_format: bool If true, use the formatter Black to format the TVMScript Notes ----- The style parameter follows the Pygments style names or Style objects. Three built-in styles are extended: "light", "dark" and "ansi". By default, "light" will be used for notebook environment and terminal style will be "ansi" for better style consistency. As an fallback when the optional Pygment library is not installed, plain text will be printed with a one-time warning to suggest installing the Pygment library. Other Pygment styles can be found in https://pygments.org/styles/ The default pygmentize style can also be set with the environment variable "TVM_PYGMENTIZE_STYLE". """ if hasattr(printable, "script") and callable(getattr(printable, "script")): printable = printable.script() elif not isinstance(printable, str): raise TypeError( f"Only can print strings or objects with `script` method, but got: {type(printable)}" ) if black_format: printable = _format(printable) is_in_notebook = "ipykernel" in sys.modules # in notebook env (support html display). style = _get_pygments_style(style, is_in_notebook) if style is None: print(printable) return # pylint: disable=import-outside-toplevel from pygments import highlight from pygments.formatters import HtmlFormatter, Terminal256Formatter from pygments.lexers.python import Python3Lexer if is_in_notebook: from IPython import display # pylint: disable=import-outside-toplevel formatter = HtmlFormatter(style=style) formatter.noclasses = True # inline styles html = highlight(printable, Python3Lexer(), formatter) display.display(display.HTML(html)) else: print(highlight(printable, Python3Lexer(), Terminal256Formatter(style=style))) @functools.lru_cache def _get_formatter(formatter: str | None = None): def get_ruff_formatter(): if shutil.which("ruff") is None: return None def formatter(code_str): proc = subprocess.Popen( ["ruff", "format", "--stdin-filename=TVMScript"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, encoding="utf-8", ) stdout, _stderr = proc.communicate(code_str) return stdout return formatter def get_black_formatter(): try: # pylint: disable=import-outside-toplevel import black except ImportError: return None def formatter(code_str): return black.format_str(code_str, mode=black.FileMode()) return formatter def get_fallback_formatter(): def formatter(code_str): with warnings.catch_warnings(): warnings.simplefilter("once", UserWarning) ruff_install_cmd = sys.executable + " -m pip install ruff" black_install_cmd = ( sys.executable + ' -m pip install "black==22.3.0" --upgrade --user' ) warnings.warn( f"Neither the 'ruff' formatter nor the 'black' formatter is available. " f"To print formatted TVM script, please a formatter. \n" f"To install ruff: {ruff_install_cmd}\n" f"To install black: {black_install_cmd}", category=UserWarning, ) return code_str return formatter # formatter = "black" if formatter is None: options = [get_ruff_formatter, get_black_formatter] elif formatter == "ruff": options = [get_ruff_formatter] elif formatter == "black": options = [get_black_formatter] else: raise ValueError(f"Unknown formatter: {formatter}") for option in options: func = option() if func is not None: return func return get_fallback_formatter() def _format(code_str: str, formatter: str | None = None) -> str: """Format a code string using Black. Parameters ---------- code_str: str The string containing Python/TVMScript code to format formatter: Optional[str] The formatter to use. Can specify `ruff`, `black`, or auto-select by passing `None`. Returns ------- formatted: str The formatted Python/TVMScript code """ return _get_formatter(formatter)(code_str) def _get_pygments_style( style: str | None, is_in_notebook: bool ) -> Union["pygments.style.Style", str] | None: """Select a pygments style to use Parameters ---------- style: str The style specifier to use. If None, auto-select a style. is_in_notebook: bool Whether python is currently running in a jupyter notebook. Used for automatic selection. Returns ------- style: Optional[Union['pygments.style.Style',str]] If pygments is installed, the style object or string, suitable for use as the "style" argument to pygments formatters. If pygments is not installed, returns None. """ try: # pylint: disable=import-outside-toplevel import pygments from packaging import version from pygments.style import Style from pygments.token import Comment, Keyword, Name, Number, Operator, String if version.parse(pygments.__version__) < version.parse("2.4.0"): raise ImportError("Required Pygments version >= 2.4.0 but got " + pygments.__version__) except ImportError as err: if err.name == "packaging": name = "packaging" elif err.name == "pygments": name = "Pygments>=2.4.0" else: raise ValueError(f'Package "{err.name}" should not be used') with warnings.catch_warnings(): warnings.simplefilter("once", UserWarning) install_cmd = sys.executable + f' -m pip install "{name}" --upgrade --user' warnings.warn( str(err) + "\n" + f"To print highlighted TVM script, please install {name}:\n" + install_cmd, category=UserWarning, ) return None class JupyterLight(Style): """A Jupyter-Notebook-like Pygments style configuration (aka. "light")""" background_color = "" styles = { Keyword: "bold #008000", Keyword.Type: "nobold #008000", Name.Function: "#0000FF", Name.Class: "bold #0000FF", Name.Decorator: "#AA22FF", String: "#BA2121", Number: "#008000", Operator: "bold #AA22FF", Operator.Word: "bold #008000", Comment: "italic #007979", } class VSCDark(Style): """A VSCode-Dark-like Pygments style configuration (aka. "dark")""" background_color = "" styles = { Keyword: "bold #c586c0", Keyword.Type: "#82aaff", Keyword.Namespace: "#4ec9b0", Name.Class: "bold #569cd6", Name.Function: "bold #dcdcaa", Name.Decorator: "italic #fe4ef3", String: "#ce9178", Number: "#b5cea8", Operator: "#bbbbbb", Operator.Word: "#569cd6", Comment: "italic #6a9956", } class AnsiTerminalDefault(Style): """The default style for terminal display with ANSI colors (aka. "ansi")""" background_color = "" styles = { Keyword: "bold ansigreen", Keyword.Type: "nobold ansigreen", Name.Class: "bold ansiblue", Name.Function: "bold ansiblue", Name.Decorator: "italic ansibrightmagenta", String: "ansiyellow", Number: "ansibrightgreen", Operator: "bold ansimagenta", Operator.Word: "bold ansigreen", Comment: "italic ansibrightblack", } if style == "light": return JupyterLight elif style == "dark": return VSCDark elif style == "ansi": return AnsiTerminalDefault if style is not None: return style style_from_environment = os.environ.get("TVM_PYGMENTIZE_STYLE", "").strip() if style_from_environment: return style_from_environment if is_in_notebook: return JupyterLight return AnsiTerminalDefault