python (3.11.7)
       1  import os.path
       2  import platform
       3  import re
       4  import sys
       5  import textwrap
       6  from abc import ABC, abstractmethod
       7  from pathlib import Path
       8  from typing import (
       9      Any,
      10      Dict,
      11      Iterable,
      12      List,
      13      NamedTuple,
      14      Optional,
      15      Sequence,
      16      Set,
      17      Tuple,
      18      Type,
      19      Union,
      20  )
      21  
      22  from pip._vendor.pygments.lexer import Lexer
      23  from pip._vendor.pygments.lexers import get_lexer_by_name, guess_lexer_for_filename
      24  from pip._vendor.pygments.style import Style as PygmentsStyle
      25  from pip._vendor.pygments.styles import get_style_by_name
      26  from pip._vendor.pygments.token import (
      27      Comment,
      28      Error,
      29      Generic,
      30      Keyword,
      31      Name,
      32      Number,
      33      Operator,
      34      String,
      35      Token,
      36      Whitespace,
      37  )
      38  from pip._vendor.pygments.util import ClassNotFound
      39  
      40  from pip._vendor.rich.containers import Lines
      41  from pip._vendor.rich.padding import Padding, PaddingDimensions
      42  
      43  from ._loop import loop_first
      44  from .cells import cell_len
      45  from .color import Color, blend_rgb
      46  from .console import Console, ConsoleOptions, JustifyMethod, RenderResult
      47  from .jupyter import JupyterMixin
      48  from .measure import Measurement
      49  from .segment import Segment, Segments
      50  from .style import Style, StyleType
      51  from .text import Text
      52  
      53  TokenType = Tuple[str, ...]
      54  
      55  WINDOWS = platform.system() == "Windows"
      56  DEFAULT_THEME = "monokai"
      57  
      58  # The following styles are based on https://github.com/pygments/pygments/blob/master/pygments/formatters/terminal.py
      59  # A few modifications were made
      60  
      61  ANSI_LIGHT: Dict[TokenType, Style] = {
      62      Token: Style(),
      63      Whitespace: Style(color="white"),
      64      Comment: Style(dim=True),
      65      Comment.Preproc: Style(color="cyan"),
      66      Keyword: Style(color="blue"),
      67      Keyword.Type: Style(color="cyan"),
      68      Operator.Word: Style(color="magenta"),
      69      Name.Builtin: Style(color="cyan"),
      70      Name.Function: Style(color="green"),
      71      Name.Namespace: Style(color="cyan", underline=True),
      72      Name.Class: Style(color="green", underline=True),
      73      Name.Exception: Style(color="cyan"),
      74      Name.Decorator: Style(color="magenta", bold=True),
      75      Name.Variable: Style(color="red"),
      76      Name.Constant: Style(color="red"),
      77      Name.Attribute: Style(color="cyan"),
      78      Name.Tag: Style(color="bright_blue"),
      79      String: Style(color="yellow"),
      80      Number: Style(color="blue"),
      81      Generic.Deleted: Style(color="bright_red"),
      82      Generic.Inserted: Style(color="green"),
      83      Generic.Heading: Style(bold=True),
      84      Generic.Subheading: Style(color="magenta", bold=True),
      85      Generic.Prompt: Style(bold=True),
      86      Generic.Error: Style(color="bright_red"),
      87      Error: Style(color="red", underline=True),
      88  }
      89  
      90  ANSI_DARK: Dict[TokenType, Style] = {
      91      Token: Style(),
      92      Whitespace: Style(color="bright_black"),
      93      Comment: Style(dim=True),
      94      Comment.Preproc: Style(color="bright_cyan"),
      95      Keyword: Style(color="bright_blue"),
      96      Keyword.Type: Style(color="bright_cyan"),
      97      Operator.Word: Style(color="bright_magenta"),
      98      Name.Builtin: Style(color="bright_cyan"),
      99      Name.Function: Style(color="bright_green"),
     100      Name.Namespace: Style(color="bright_cyan", underline=True),
     101      Name.Class: Style(color="bright_green", underline=True),
     102      Name.Exception: Style(color="bright_cyan"),
     103      Name.Decorator: Style(color="bright_magenta", bold=True),
     104      Name.Variable: Style(color="bright_red"),
     105      Name.Constant: Style(color="bright_red"),
     106      Name.Attribute: Style(color="bright_cyan"),
     107      Name.Tag: Style(color="bright_blue"),
     108      String: Style(color="yellow"),
     109      Number: Style(color="bright_blue"),
     110      Generic.Deleted: Style(color="bright_red"),
     111      Generic.Inserted: Style(color="bright_green"),
     112      Generic.Heading: Style(bold=True),
     113      Generic.Subheading: Style(color="bright_magenta", bold=True),
     114      Generic.Prompt: Style(bold=True),
     115      Generic.Error: Style(color="bright_red"),
     116      Error: Style(color="red", underline=True),
     117  }
     118  
     119  RICH_SYNTAX_THEMES = {"ansi_light": ANSI_LIGHT, "ansi_dark": ANSI_DARK}
     120  NUMBERS_COLUMN_DEFAULT_PADDING = 2
     121  
     122  
     123  class ESC[4;38;5;81mSyntaxTheme(ESC[4;38;5;149mABC):
     124      """Base class for a syntax theme."""
     125  
     126      @abstractmethod
     127      def get_style_for_token(self, token_type: TokenType) -> Style:
     128          """Get a style for a given Pygments token."""
     129          raise NotImplementedError  # pragma: no cover
     130  
     131      @abstractmethod
     132      def get_background_style(self) -> Style:
     133          """Get the background color."""
     134          raise NotImplementedError  # pragma: no cover
     135  
     136  
     137  class ESC[4;38;5;81mPygmentsSyntaxTheme(ESC[4;38;5;149mSyntaxTheme):
     138      """Syntax theme that delegates to Pygments theme."""
     139  
     140      def __init__(self, theme: Union[str, Type[PygmentsStyle]]) -> None:
     141          self._style_cache: Dict[TokenType, Style] = {}
     142          if isinstance(theme, str):
     143              try:
     144                  self._pygments_style_class = get_style_by_name(theme)
     145              except ClassNotFound:
     146                  self._pygments_style_class = get_style_by_name("default")
     147          else:
     148              self._pygments_style_class = theme
     149  
     150          self._background_color = self._pygments_style_class.background_color
     151          self._background_style = Style(bgcolor=self._background_color)
     152  
     153      def get_style_for_token(self, token_type: TokenType) -> Style:
     154          """Get a style from a Pygments class."""
     155          try:
     156              return self._style_cache[token_type]
     157          except KeyError:
     158              try:
     159                  pygments_style = self._pygments_style_class.style_for_token(token_type)
     160              except KeyError:
     161                  style = Style.null()
     162              else:
     163                  color = pygments_style["color"]
     164                  bgcolor = pygments_style["bgcolor"]
     165                  style = Style(
     166                      color="#" + color if color else "#000000",
     167                      bgcolor="#" + bgcolor if bgcolor else self._background_color,
     168                      bold=pygments_style["bold"],
     169                      italic=pygments_style["italic"],
     170                      underline=pygments_style["underline"],
     171                  )
     172              self._style_cache[token_type] = style
     173          return style
     174  
     175      def get_background_style(self) -> Style:
     176          return self._background_style
     177  
     178  
     179  class ESC[4;38;5;81mANSISyntaxTheme(ESC[4;38;5;149mSyntaxTheme):
     180      """Syntax theme to use standard colors."""
     181  
     182      def __init__(self, style_map: Dict[TokenType, Style]) -> None:
     183          self.style_map = style_map
     184          self._missing_style = Style.null()
     185          self._background_style = Style.null()
     186          self._style_cache: Dict[TokenType, Style] = {}
     187  
     188      def get_style_for_token(self, token_type: TokenType) -> Style:
     189          """Look up style in the style map."""
     190          try:
     191              return self._style_cache[token_type]
     192          except KeyError:
     193              # Styles form a hierarchy
     194              # We need to go from most to least specific
     195              # e.g. ("foo", "bar", "baz") to ("foo", "bar")  to ("foo",)
     196              get_style = self.style_map.get
     197              token = tuple(token_type)
     198              style = self._missing_style
     199              while token:
     200                  _style = get_style(token)
     201                  if _style is not None:
     202                      style = _style
     203                      break
     204                  token = token[:-1]
     205              self._style_cache[token_type] = style
     206              return style
     207  
     208      def get_background_style(self) -> Style:
     209          return self._background_style
     210  
     211  
     212  SyntaxPosition = Tuple[int, int]
     213  
     214  
     215  class ESC[4;38;5;81m_SyntaxHighlightRange(ESC[4;38;5;149mNamedTuple):
     216      """
     217      A range to highlight in a Syntax object.
     218      `start` and `end` are 2-integers tuples, where the first integer is the line number
     219      (starting from 1) and the second integer is the column index (starting from 0).
     220      """
     221  
     222      style: StyleType
     223      start: SyntaxPosition
     224      end: SyntaxPosition
     225  
     226  
     227  class ESC[4;38;5;81mSyntax(ESC[4;38;5;149mJupyterMixin):
     228      """Construct a Syntax object to render syntax highlighted code.
     229  
     230      Args:
     231          code (str): Code to highlight.
     232          lexer (Lexer | str): Lexer to use (see https://pygments.org/docs/lexers/)
     233          theme (str, optional): Color theme, aka Pygments style (see https://pygments.org/docs/styles/#getting-a-list-of-available-styles). Defaults to "monokai".
     234          dedent (bool, optional): Enable stripping of initial whitespace. Defaults to False.
     235          line_numbers (bool, optional): Enable rendering of line numbers. Defaults to False.
     236          start_line (int, optional): Starting number for line numbers. Defaults to 1.
     237          line_range (Tuple[int | None, int | None], optional): If given should be a tuple of the start and end line to render.
     238              A value of None in the tuple indicates the range is open in that direction.
     239          highlight_lines (Set[int]): A set of line numbers to highlight.
     240          code_width: Width of code to render (not including line numbers), or ``None`` to use all available width.
     241          tab_size (int, optional): Size of tabs. Defaults to 4.
     242          word_wrap (bool, optional): Enable word wrapping.
     243          background_color (str, optional): Optional background color, or None to use theme color. Defaults to None.
     244          indent_guides (bool, optional): Show indent guides. Defaults to False.
     245          padding (PaddingDimensions): Padding to apply around the syntax. Defaults to 0 (no padding).
     246      """
     247  
     248      _pygments_style_class: Type[PygmentsStyle]
     249      _theme: SyntaxTheme
     250  
     251      @classmethod
     252      def get_theme(cls, name: Union[str, SyntaxTheme]) -> SyntaxTheme:
     253          """Get a syntax theme instance."""
     254          if isinstance(name, SyntaxTheme):
     255              return name
     256          theme: SyntaxTheme
     257          if name in RICH_SYNTAX_THEMES:
     258              theme = ANSISyntaxTheme(RICH_SYNTAX_THEMES[name])
     259          else:
     260              theme = PygmentsSyntaxTheme(name)
     261          return theme
     262  
     263      def __init__(
     264          self,
     265          code: str,
     266          lexer: Union[Lexer, str],
     267          *,
     268          theme: Union[str, SyntaxTheme] = DEFAULT_THEME,
     269          dedent: bool = False,
     270          line_numbers: bool = False,
     271          start_line: int = 1,
     272          line_range: Optional[Tuple[Optional[int], Optional[int]]] = None,
     273          highlight_lines: Optional[Set[int]] = None,
     274          code_width: Optional[int] = None,
     275          tab_size: int = 4,
     276          word_wrap: bool = False,
     277          background_color: Optional[str] = None,
     278          indent_guides: bool = False,
     279          padding: PaddingDimensions = 0,
     280      ) -> None:
     281          self.code = code
     282          self._lexer = lexer
     283          self.dedent = dedent
     284          self.line_numbers = line_numbers
     285          self.start_line = start_line
     286          self.line_range = line_range
     287          self.highlight_lines = highlight_lines or set()
     288          self.code_width = code_width
     289          self.tab_size = tab_size
     290          self.word_wrap = word_wrap
     291          self.background_color = background_color
     292          self.background_style = (
     293              Style(bgcolor=background_color) if background_color else Style()
     294          )
     295          self.indent_guides = indent_guides
     296          self.padding = padding
     297  
     298          self._theme = self.get_theme(theme)
     299          self._stylized_ranges: List[_SyntaxHighlightRange] = []
     300  
     301      @classmethod
     302      def from_path(
     303          cls,
     304          path: str,
     305          encoding: str = "utf-8",
     306          lexer: Optional[Union[Lexer, str]] = None,
     307          theme: Union[str, SyntaxTheme] = DEFAULT_THEME,
     308          dedent: bool = False,
     309          line_numbers: bool = False,
     310          line_range: Optional[Tuple[int, int]] = None,
     311          start_line: int = 1,
     312          highlight_lines: Optional[Set[int]] = None,
     313          code_width: Optional[int] = None,
     314          tab_size: int = 4,
     315          word_wrap: bool = False,
     316          background_color: Optional[str] = None,
     317          indent_guides: bool = False,
     318          padding: PaddingDimensions = 0,
     319      ) -> "Syntax":
     320          """Construct a Syntax object from a file.
     321  
     322          Args:
     323              path (str): Path to file to highlight.
     324              encoding (str): Encoding of file.
     325              lexer (str | Lexer, optional): Lexer to use. If None, lexer will be auto-detected from path/file content.
     326              theme (str, optional): Color theme, aka Pygments style (see https://pygments.org/docs/styles/#getting-a-list-of-available-styles). Defaults to "emacs".
     327              dedent (bool, optional): Enable stripping of initial whitespace. Defaults to True.
     328              line_numbers (bool, optional): Enable rendering of line numbers. Defaults to False.
     329              start_line (int, optional): Starting number for line numbers. Defaults to 1.
     330              line_range (Tuple[int, int], optional): If given should be a tuple of the start and end line to render.
     331              highlight_lines (Set[int]): A set of line numbers to highlight.
     332              code_width: Width of code to render (not including line numbers), or ``None`` to use all available width.
     333              tab_size (int, optional): Size of tabs. Defaults to 4.
     334              word_wrap (bool, optional): Enable word wrapping of code.
     335              background_color (str, optional): Optional background color, or None to use theme color. Defaults to None.
     336              indent_guides (bool, optional): Show indent guides. Defaults to False.
     337              padding (PaddingDimensions): Padding to apply around the syntax. Defaults to 0 (no padding).
     338  
     339          Returns:
     340              [Syntax]: A Syntax object that may be printed to the console
     341          """
     342          code = Path(path).read_text(encoding=encoding)
     343  
     344          if not lexer:
     345              lexer = cls.guess_lexer(path, code=code)
     346  
     347          return cls(
     348              code,
     349              lexer,
     350              theme=theme,
     351              dedent=dedent,
     352              line_numbers=line_numbers,
     353              line_range=line_range,
     354              start_line=start_line,
     355              highlight_lines=highlight_lines,
     356              code_width=code_width,
     357              tab_size=tab_size,
     358              word_wrap=word_wrap,
     359              background_color=background_color,
     360              indent_guides=indent_guides,
     361              padding=padding,
     362          )
     363  
     364      @classmethod
     365      def guess_lexer(cls, path: str, code: Optional[str] = None) -> str:
     366          """Guess the alias of the Pygments lexer to use based on a path and an optional string of code.
     367          If code is supplied, it will use a combination of the code and the filename to determine the
     368          best lexer to use. For example, if the file is ``index.html`` and the file contains Django
     369          templating syntax, then "html+django" will be returned. If the file is ``index.html``, and no
     370          templating language is used, the "html" lexer will be used. If no string of code
     371          is supplied, the lexer will be chosen based on the file extension..
     372  
     373          Args:
     374               path (AnyStr): The path to the file containing the code you wish to know the lexer for.
     375               code (str, optional): Optional string of code that will be used as a fallback if no lexer
     376                  is found for the supplied path.
     377  
     378          Returns:
     379              str: The name of the Pygments lexer that best matches the supplied path/code.
     380          """
     381          lexer: Optional[Lexer] = None
     382          lexer_name = "default"
     383          if code:
     384              try:
     385                  lexer = guess_lexer_for_filename(path, code)
     386              except ClassNotFound:
     387                  pass
     388  
     389          if not lexer:
     390              try:
     391                  _, ext = os.path.splitext(path)
     392                  if ext:
     393                      extension = ext.lstrip(".").lower()
     394                      lexer = get_lexer_by_name(extension)
     395              except ClassNotFound:
     396                  pass
     397  
     398          if lexer:
     399              if lexer.aliases:
     400                  lexer_name = lexer.aliases[0]
     401              else:
     402                  lexer_name = lexer.name
     403  
     404          return lexer_name
     405  
     406      def _get_base_style(self) -> Style:
     407          """Get the base style."""
     408          default_style = self._theme.get_background_style() + self.background_style
     409          return default_style
     410  
     411      def _get_token_color(self, token_type: TokenType) -> Optional[Color]:
     412          """Get a color (if any) for the given token.
     413  
     414          Args:
     415              token_type (TokenType): A token type tuple from Pygments.
     416  
     417          Returns:
     418              Optional[Color]: Color from theme, or None for no color.
     419          """
     420          style = self._theme.get_style_for_token(token_type)
     421          return style.color
     422  
     423      @property
     424      def lexer(self) -> Optional[Lexer]:
     425          """The lexer for this syntax, or None if no lexer was found.
     426  
     427          Tries to find the lexer by name if a string was passed to the constructor.
     428          """
     429  
     430          if isinstance(self._lexer, Lexer):
     431              return self._lexer
     432          try:
     433              return get_lexer_by_name(
     434                  self._lexer,
     435                  stripnl=False,
     436                  ensurenl=True,
     437                  tabsize=self.tab_size,
     438              )
     439          except ClassNotFound:
     440              return None
     441  
     442      def highlight(
     443          self,
     444          code: str,
     445          line_range: Optional[Tuple[Optional[int], Optional[int]]] = None,
     446      ) -> Text:
     447          """Highlight code and return a Text instance.
     448  
     449          Args:
     450              code (str): Code to highlight.
     451              line_range(Tuple[int, int], optional): Optional line range to highlight.
     452  
     453          Returns:
     454              Text: A text instance containing highlighted syntax.
     455          """
     456  
     457          base_style = self._get_base_style()
     458          justify: JustifyMethod = (
     459              "default" if base_style.transparent_background else "left"
     460          )
     461  
     462          text = Text(
     463              justify=justify,
     464              style=base_style,
     465              tab_size=self.tab_size,
     466              no_wrap=not self.word_wrap,
     467          )
     468          _get_theme_style = self._theme.get_style_for_token
     469  
     470          lexer = self.lexer
     471  
     472          if lexer is None:
     473              text.append(code)
     474          else:
     475              if line_range:
     476                  # More complicated path to only stylize a portion of the code
     477                  # This speeds up further operations as there are less spans to process
     478                  line_start, line_end = line_range
     479  
     480                  def line_tokenize() -> Iterable[Tuple[Any, str]]:
     481                      """Split tokens to one per line."""
     482                      assert lexer  # required to make MyPy happy - we know lexer is not None at this point
     483  
     484                      for token_type, token in lexer.get_tokens(code):
     485                          while token:
     486                              line_token, new_line, token = token.partition("\n")
     487                              yield token_type, line_token + new_line
     488  
     489                  def tokens_to_spans() -> Iterable[Tuple[str, Optional[Style]]]:
     490                      """Convert tokens to spans."""
     491                      tokens = iter(line_tokenize())
     492                      line_no = 0
     493                      _line_start = line_start - 1 if line_start else 0
     494  
     495                      # Skip over tokens until line start
     496                      while line_no < _line_start:
     497                          try:
     498                              _token_type, token = next(tokens)
     499                          except StopIteration:
     500                              break
     501                          yield (token, None)
     502                          if token.endswith("\n"):
     503                              line_no += 1
     504                      # Generate spans until line end
     505                      for token_type, token in tokens:
     506                          yield (token, _get_theme_style(token_type))
     507                          if token.endswith("\n"):
     508                              line_no += 1
     509                              if line_end and line_no >= line_end:
     510                                  break
     511  
     512                  text.append_tokens(tokens_to_spans())
     513  
     514              else:
     515                  text.append_tokens(
     516                      (token, _get_theme_style(token_type))
     517                      for token_type, token in lexer.get_tokens(code)
     518                  )
     519              if self.background_color is not None:
     520                  text.stylize(f"on {self.background_color}")
     521  
     522          if self._stylized_ranges:
     523              self._apply_stylized_ranges(text)
     524  
     525          return text
     526  
     527      def stylize_range(
     528          self, style: StyleType, start: SyntaxPosition, end: SyntaxPosition
     529      ) -> None:
     530          """
     531          Adds a custom style on a part of the code, that will be applied to the syntax display when it's rendered.
     532          Line numbers are 1-based, while column indexes are 0-based.
     533  
     534          Args:
     535              style (StyleType): The style to apply.
     536              start (Tuple[int, int]): The start of the range, in the form `[line number, column index]`.
     537              end (Tuple[int, int]): The end of the range, in the form `[line number, column index]`.
     538          """
     539          self._stylized_ranges.append(_SyntaxHighlightRange(style, start, end))
     540  
     541      def _get_line_numbers_color(self, blend: float = 0.3) -> Color:
     542          background_style = self._theme.get_background_style() + self.background_style
     543          background_color = background_style.bgcolor
     544          if background_color is None or background_color.is_system_defined:
     545              return Color.default()
     546          foreground_color = self._get_token_color(Token.Text)
     547          if foreground_color is None or foreground_color.is_system_defined:
     548              return foreground_color or Color.default()
     549          new_color = blend_rgb(
     550              background_color.get_truecolor(),
     551              foreground_color.get_truecolor(),
     552              cross_fade=blend,
     553          )
     554          return Color.from_triplet(new_color)
     555  
     556      @property
     557      def _numbers_column_width(self) -> int:
     558          """Get the number of characters used to render the numbers column."""
     559          column_width = 0
     560          if self.line_numbers:
     561              column_width = (
     562                  len(str(self.start_line + self.code.count("\n")))
     563                  + NUMBERS_COLUMN_DEFAULT_PADDING
     564              )
     565          return column_width
     566  
     567      def _get_number_styles(self, console: Console) -> Tuple[Style, Style, Style]:
     568          """Get background, number, and highlight styles for line numbers."""
     569          background_style = self._get_base_style()
     570          if background_style.transparent_background:
     571              return Style.null(), Style(dim=True), Style.null()
     572          if console.color_system in ("256", "truecolor"):
     573              number_style = Style.chain(
     574                  background_style,
     575                  self._theme.get_style_for_token(Token.Text),
     576                  Style(color=self._get_line_numbers_color()),
     577                  self.background_style,
     578              )
     579              highlight_number_style = Style.chain(
     580                  background_style,
     581                  self._theme.get_style_for_token(Token.Text),
     582                  Style(bold=True, color=self._get_line_numbers_color(0.9)),
     583                  self.background_style,
     584              )
     585          else:
     586              number_style = background_style + Style(dim=True)
     587              highlight_number_style = background_style + Style(dim=False)
     588          return background_style, number_style, highlight_number_style
     589  
     590      def __rich_measure__(
     591          self, console: "Console", options: "ConsoleOptions"
     592      ) -> "Measurement":
     593          _, right, _, left = Padding.unpack(self.padding)
     594          padding = left + right
     595          if self.code_width is not None:
     596              width = self.code_width + self._numbers_column_width + padding + 1
     597              return Measurement(self._numbers_column_width, width)
     598          lines = self.code.splitlines()
     599          width = (
     600              self._numbers_column_width
     601              + padding
     602              + (max(cell_len(line) for line in lines) if lines else 0)
     603          )
     604          if self.line_numbers:
     605              width += 1
     606          return Measurement(self._numbers_column_width, width)
     607  
     608      def __rich_console__(
     609          self, console: Console, options: ConsoleOptions
     610      ) -> RenderResult:
     611          segments = Segments(self._get_syntax(console, options))
     612          if self.padding:
     613              yield Padding(
     614                  segments, style=self._theme.get_background_style(), pad=self.padding
     615              )
     616          else:
     617              yield segments
     618  
     619      def _get_syntax(
     620          self,
     621          console: Console,
     622          options: ConsoleOptions,
     623      ) -> Iterable[Segment]:
     624          """
     625          Get the Segments for the Syntax object, excluding any vertical/horizontal padding
     626          """
     627          transparent_background = self._get_base_style().transparent_background
     628          code_width = (
     629              (
     630                  (options.max_width - self._numbers_column_width - 1)
     631                  if self.line_numbers
     632                  else options.max_width
     633              )
     634              if self.code_width is None
     635              else self.code_width
     636          )
     637  
     638          ends_on_nl, processed_code = self._process_code(self.code)
     639          text = self.highlight(processed_code, self.line_range)
     640  
     641          if not self.line_numbers and not self.word_wrap and not self.line_range:
     642              if not ends_on_nl:
     643                  text.remove_suffix("\n")
     644              # Simple case of just rendering text
     645              style = (
     646                  self._get_base_style()
     647                  + self._theme.get_style_for_token(Comment)
     648                  + Style(dim=True)
     649                  + self.background_style
     650              )
     651              if self.indent_guides and not options.ascii_only:
     652                  text = text.with_indent_guides(self.tab_size, style=style)
     653                  text.overflow = "crop"
     654              if style.transparent_background:
     655                  yield from console.render(
     656                      text, options=options.update(width=code_width)
     657                  )
     658              else:
     659                  syntax_lines = console.render_lines(
     660                      text,
     661                      options.update(width=code_width, height=None, justify="left"),
     662                      style=self.background_style,
     663                      pad=True,
     664                      new_lines=True,
     665                  )
     666                  for syntax_line in syntax_lines:
     667                      yield from syntax_line
     668              return
     669  
     670          start_line, end_line = self.line_range or (None, None)
     671          line_offset = 0
     672          if start_line:
     673              line_offset = max(0, start_line - 1)
     674          lines: Union[List[Text], Lines] = text.split("\n", allow_blank=ends_on_nl)
     675          if self.line_range:
     676              if line_offset > len(lines):
     677                  return
     678              lines = lines[line_offset:end_line]
     679  
     680          if self.indent_guides and not options.ascii_only:
     681              style = (
     682                  self._get_base_style()
     683                  + self._theme.get_style_for_token(Comment)
     684                  + Style(dim=True)
     685                  + self.background_style
     686              )
     687              lines = (
     688                  Text("\n")
     689                  .join(lines)
     690                  .with_indent_guides(self.tab_size, style=style + Style(italic=False))
     691                  .split("\n", allow_blank=True)
     692              )
     693  
     694          numbers_column_width = self._numbers_column_width
     695          render_options = options.update(width=code_width)
     696  
     697          highlight_line = self.highlight_lines.__contains__
     698          _Segment = Segment
     699          new_line = _Segment("\n")
     700  
     701          line_pointer = "> " if options.legacy_windows else "❱ "
     702  
     703          (
     704              background_style,
     705              number_style,
     706              highlight_number_style,
     707          ) = self._get_number_styles(console)
     708  
     709          for line_no, line in enumerate(lines, self.start_line + line_offset):
     710              if self.word_wrap:
     711                  wrapped_lines = console.render_lines(
     712                      line,
     713                      render_options.update(height=None, justify="left"),
     714                      style=background_style,
     715                      pad=not transparent_background,
     716                  )
     717              else:
     718                  segments = list(line.render(console, end=""))
     719                  if options.no_wrap:
     720                      wrapped_lines = [segments]
     721                  else:
     722                      wrapped_lines = [
     723                          _Segment.adjust_line_length(
     724                              segments,
     725                              render_options.max_width,
     726                              style=background_style,
     727                              pad=not transparent_background,
     728                          )
     729                      ]
     730  
     731              if self.line_numbers:
     732                  wrapped_line_left_pad = _Segment(
     733                      " " * numbers_column_width + " ", background_style
     734                  )
     735                  for first, wrapped_line in loop_first(wrapped_lines):
     736                      if first:
     737                          line_column = str(line_no).rjust(numbers_column_width - 2) + " "
     738                          if highlight_line(line_no):
     739                              yield _Segment(line_pointer, Style(color="red"))
     740                              yield _Segment(line_column, highlight_number_style)
     741                          else:
     742                              yield _Segment("  ", highlight_number_style)
     743                              yield _Segment(line_column, number_style)
     744                      else:
     745                          yield wrapped_line_left_pad
     746                      yield from wrapped_line
     747                      yield new_line
     748              else:
     749                  for wrapped_line in wrapped_lines:
     750                      yield from wrapped_line
     751                      yield new_line
     752  
     753      def _apply_stylized_ranges(self, text: Text) -> None:
     754          """
     755          Apply stylized ranges to a text instance,
     756          using the given code to determine the right portion to apply the style to.
     757  
     758          Args:
     759              text (Text): Text instance to apply the style to.
     760          """
     761          code = text.plain
     762          newlines_offsets = [
     763              # Let's add outer boundaries at each side of the list:
     764              0,
     765              # N.B. using "\n" here is much faster than using metacharacters such as "^" or "\Z":
     766              *[
     767                  match.start() + 1
     768                  for match in re.finditer("\n", code, flags=re.MULTILINE)
     769              ],
     770              len(code) + 1,
     771          ]
     772  
     773          for stylized_range in self._stylized_ranges:
     774              start = _get_code_index_for_syntax_position(
     775                  newlines_offsets, stylized_range.start
     776              )
     777              end = _get_code_index_for_syntax_position(
     778                  newlines_offsets, stylized_range.end
     779              )
     780              if start is not None and end is not None:
     781                  text.stylize(stylized_range.style, start, end)
     782  
     783      def _process_code(self, code: str) -> Tuple[bool, str]:
     784          """
     785          Applies various processing to a raw code string
     786          (normalises it so it always ends with a line return, dedents it if necessary, etc.)
     787  
     788          Args:
     789              code (str): The raw code string to process
     790  
     791          Returns:
     792              Tuple[bool, str]: the boolean indicates whether the raw code ends with a line return,
     793                  while the string is the processed code.
     794          """
     795          ends_on_nl = code.endswith("\n")
     796          processed_code = code if ends_on_nl else code + "\n"
     797          processed_code = (
     798              textwrap.dedent(processed_code) if self.dedent else processed_code
     799          )
     800          processed_code = processed_code.expandtabs(self.tab_size)
     801          return ends_on_nl, processed_code
     802  
     803  
     804  def _get_code_index_for_syntax_position(
     805      newlines_offsets: Sequence[int], position: SyntaxPosition
     806  ) -> Optional[int]:
     807      """
     808      Returns the index of the code string for the given positions.
     809  
     810      Args:
     811          newlines_offsets (Sequence[int]): The offset of each newline character found in the code snippet.
     812          position (SyntaxPosition): The position to search for.
     813  
     814      Returns:
     815          Optional[int]: The index of the code string for this position, or `None`
     816              if the given position's line number is out of range (if it's the column that is out of range
     817              we silently clamp its value so that it reaches the end of the line)
     818      """
     819      lines_count = len(newlines_offsets)
     820  
     821      line_number, column_index = position
     822      if line_number > lines_count or len(newlines_offsets) < (line_number + 1):
     823          return None  # `line_number` is out of range
     824      line_index = line_number - 1
     825      line_length = newlines_offsets[line_index + 1] - newlines_offsets[line_index] - 1
     826      # If `column_index` is out of range: let's silently clamp it:
     827      column_index = min(line_length, column_index)
     828      return newlines_offsets[line_index] + column_index
     829  
     830  
     831  if __name__ == "__main__":  # pragma: no cover
     832      import argparse
     833      import sys
     834  
     835      parser = argparse.ArgumentParser(
     836          description="Render syntax to the console with Rich"
     837      )
     838      parser.add_argument(
     839          "path",
     840          metavar="PATH",
     841          help="path to file, or - for stdin",
     842      )
     843      parser.add_argument(
     844          "-c",
     845          "--force-color",
     846          dest="force_color",
     847          action="store_true",
     848          default=None,
     849          help="force color for non-terminals",
     850      )
     851      parser.add_argument(
     852          "-i",
     853          "--indent-guides",
     854          dest="indent_guides",
     855          action="store_true",
     856          default=False,
     857          help="display indent guides",
     858      )
     859      parser.add_argument(
     860          "-l",
     861          "--line-numbers",
     862          dest="line_numbers",
     863          action="store_true",
     864          help="render line numbers",
     865      )
     866      parser.add_argument(
     867          "-w",
     868          "--width",
     869          type=int,
     870          dest="width",
     871          default=None,
     872          help="width of output (default will auto-detect)",
     873      )
     874      parser.add_argument(
     875          "-r",
     876          "--wrap",
     877          dest="word_wrap",
     878          action="store_true",
     879          default=False,
     880          help="word wrap long lines",
     881      )
     882      parser.add_argument(
     883          "-s",
     884          "--soft-wrap",
     885          action="store_true",
     886          dest="soft_wrap",
     887          default=False,
     888          help="enable soft wrapping mode",
     889      )
     890      parser.add_argument(
     891          "-t", "--theme", dest="theme", default="monokai", help="pygments theme"
     892      )
     893      parser.add_argument(
     894          "-b",
     895          "--background-color",
     896          dest="background_color",
     897          default=None,
     898          help="Override background color",
     899      )
     900      parser.add_argument(
     901          "-x",
     902          "--lexer",
     903          default=None,
     904          dest="lexer_name",
     905          help="Lexer name",
     906      )
     907      parser.add_argument(
     908          "-p", "--padding", type=int, default=0, dest="padding", help="Padding"
     909      )
     910      parser.add_argument(
     911          "--highlight-line",
     912          type=int,
     913          default=None,
     914          dest="highlight_line",
     915          help="The line number (not index!) to highlight",
     916      )
     917      args = parser.parse_args()
     918  
     919      from pip._vendor.rich.console import Console
     920  
     921      console = Console(force_terminal=args.force_color, width=args.width)
     922  
     923      if args.path == "-":
     924          code = sys.stdin.read()
     925          syntax = Syntax(
     926              code=code,
     927              lexer=args.lexer_name,
     928              line_numbers=args.line_numbers,
     929              word_wrap=args.word_wrap,
     930              theme=args.theme,
     931              background_color=args.background_color,
     932              indent_guides=args.indent_guides,
     933              padding=args.padding,
     934              highlight_lines={args.highlight_line},
     935          )
     936      else:
     937          syntax = Syntax.from_path(
     938              args.path,
     939              lexer=args.lexer_name,
     940              line_numbers=args.line_numbers,
     941              word_wrap=args.word_wrap,
     942              theme=args.theme,
     943              background_color=args.background_color,
     944              indent_guides=args.indent_guides,
     945              padding=args.padding,
     946              highlight_lines={args.highlight_line},
     947          )
     948      console.print(syntax, soft_wrap=args.soft_wrap)