diff --git a/CHANGELOG.md b/CHANGELOG.md index c5b1877289..a994765609 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Removed `typing_extensions` from runtime dependencies https://github.com/Textualize/rich/pull/3763 - Live objects (including Progress) may now be nested https://github.com/Textualize/rich/pull/3768 +- Added padding property to Syntax which returns a tuple of four integers https://github.com/Textualize/rich/pull/3782 ### Fixed - Fixed extraction of recursive exceptions https://github.com/Textualize/rich/pull/3772 +- Fixed padding applied to Syntax https://github.com/Textualize/rich/pull/3782 ### Added diff --git a/rich/syntax.py b/rich/syntax.py index cff8fd235d..5e17b48f12 100644 --- a/rich/syntax.py +++ b/rich/syntax.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import re import sys @@ -224,6 +226,17 @@ class _SyntaxHighlightRange(NamedTuple): style_before: bool = False +class PaddingProperty: + """Descriptor to get and set padding.""" + + def __get__(self, obj: Syntax, objtype: Type[Syntax]) -> Tuple[int, int, int, int]: + """Space around the Syntax.""" + return obj._padding + + def __set__(self, obj: Syntax, padding: PaddingDimensions) -> None: + obj._padding = Padding.unpack(padding) + + class Syntax(JupyterMixin): """Construct a Syntax object to render syntax highlighted code. @@ -293,11 +306,13 @@ def __init__( Style(bgcolor=background_color) if background_color else Style() ) self.indent_guides = indent_guides - self.padding = padding + self._padding = Padding.unpack(padding) self._theme = self.get_theme(theme) self._stylized_ranges: List[_SyntaxHighlightRange] = [] + padding = PaddingProperty() + @classmethod def from_path( cls, @@ -371,8 +386,8 @@ def guess_lexer(cls, path: str, code: Optional[str] = None) -> str: is supplied, the lexer will be chosen based on the file extension.. Args: - path (AnyStr): The path to the file containing the code you wish to know the lexer for. - code (str, optional): Optional string of code that will be used as a fallback if no lexer + path (AnyStr): The path to the file containing the code you wish to know the lexer for. + code (str, optional): Optional string of code that will be used as a fallback if no lexer is found for the supplied path. Returns: @@ -607,7 +622,7 @@ def _get_number_styles(self, console: Console) -> Tuple[Style, Style, Style]: def __rich_measure__( self, console: "Console", options: "ConsoleOptions" ) -> "Measurement": - _, right, _, left = Padding.unpack(self.padding) + _, right, _, left = self.padding padding = left + right if self.code_width is not None: width = self.code_width + self._numbers_column_width + padding + 1 @@ -626,7 +641,7 @@ def __rich_console__( self, console: Console, options: ConsoleOptions ) -> RenderResult: segments = Segments(self._get_syntax(console, options)) - if self.padding: + if any(self.padding): yield Padding(segments, style=self._get_base_style(), pad=self.padding) else: yield segments @@ -640,15 +655,19 @@ def _get_syntax( Get the Segments for the Syntax object, excluding any vertical/horizontal padding """ transparent_background = self._get_base_style().transparent_background + _pad_top, pad_right, _pad_bottom, pad_left = self.padding + horizontal_padding = pad_left + pad_right code_width = ( ( (options.max_width - self._numbers_column_width - 1) if self.line_numbers else options.max_width ) + - horizontal_padding if self.code_width is None else self.code_width ) + code_width = max(0, code_width) ends_on_nl, processed_code = self._process_code(self.code) text = self.highlight(processed_code, self.line_range) diff --git a/tests/test_syntax.py b/tests/test_syntax.py index 04edb9cc3e..3537ab5b6b 100644 --- a/tests/test_syntax.py +++ b/tests/test_syntax.py @@ -2,6 +2,7 @@ import os import sys import tempfile +from importlib.metadata import Distribution import pytest from pygments.lexers import PythonLexer @@ -19,7 +20,6 @@ ) from .render import render -from importlib.metadata import Distribution PYGMENTS_VERSION = Distribution.from_name("pygments").version OLD_PYGMENTS = PYGMENTS_VERSION == "2.13.0" @@ -40,7 +40,7 @@ def loop_first_last(values: Iterable[T]) -> Iterable[Tuple[bool, bool, T]]: yield first, True, previous_value''' -def test_blank_lines(): +def test_blank_lines() -> None: code = "\n\nimport this\n\n" syntax = Syntax( code, lexer="python", theme="ascii_light", code_width=30, line_numbers=True @@ -53,7 +53,7 @@ def test_blank_lines(): ) -def test_python_render(): +def test_python_render() -> None: syntax = Panel.fit( Syntax( CODE, @@ -72,7 +72,7 @@ def test_python_render(): assert rendered_syntax == expected -def test_python_render_simple(): +def test_python_render_simple() -> None: syntax = Syntax( CODE, lexer="python", @@ -87,7 +87,7 @@ def test_python_render_simple(): assert rendered_syntax == expected -def test_python_render_simple_passing_lexer_instance(): +def test_python_render_simple_passing_lexer_instance() -> None: syntax = Syntax( CODE, lexer=PythonLexer(), @@ -103,7 +103,7 @@ def test_python_render_simple_passing_lexer_instance(): @pytest.mark.skipif(OLD_PYGMENTS, reason="Pygments changed their tokenizer") -def test_python_render_simple_indent_guides(): +def test_python_render_simple_indent_guides() -> None: syntax = Syntax( CODE, lexer="python", @@ -120,7 +120,7 @@ def test_python_render_simple_indent_guides(): @pytest.mark.skipif(OLD_PYGMENTS, reason="Pygments changed their tokenizer") -def test_python_render_line_range_indent_guides(): +def test_python_render_line_range_indent_guides() -> None: syntax = Syntax( CODE, lexer="python", @@ -137,7 +137,7 @@ def test_python_render_line_range_indent_guides(): assert rendered_syntax == expected -def test_python_render_indent_guides(): +def test_python_render_indent_guides() -> None: syntax = Panel.fit( Syntax( CODE, @@ -157,19 +157,19 @@ def test_python_render_indent_guides(): assert rendered_syntax == expected -def test_pygments_syntax_theme_non_str(): +def test_pygments_syntax_theme_non_str() -> None: from pygments.style import Style as PygmentsStyle style = PygmentsSyntaxTheme(PygmentsStyle()) assert style.get_background_style().bgcolor == Color.parse("#ffffff") -def test_pygments_syntax_theme(): +def test_pygments_syntax_theme() -> None: style = PygmentsSyntaxTheme("default") assert style.get_style_for_token("abc") == Style.parse("none") -def test_get_line_color_none(): +def test_get_line_color_none() -> None: style = PygmentsSyntaxTheme("default") style._background_style = Style(bgcolor=None) syntax = Syntax( @@ -185,7 +185,7 @@ def test_get_line_color_none(): assert syntax._get_line_numbers_color() == Color.default() -def test_highlight_background_color(): +def test_highlight_background_color() -> None: syntax = Syntax( CODE, lexer="python", @@ -199,7 +199,7 @@ def test_highlight_background_color(): assert syntax.highlight(CODE).style == Style.parse("on red") -def test_get_number_styles(): +def test_get_number_styles() -> None: syntax = Syntax(CODE, "python", theme="monokai", line_numbers=True) console = Console(color_system="windows") assert syntax._get_number_styles(console=console) == ( @@ -209,7 +209,7 @@ def test_get_number_styles(): ) -def test_get_style_for_token(): +def test_get_style_for_token() -> None: # from pygments.style import Style as PygmentsStyle # pygments_style = PygmentsStyle() from pygments.style import Token @@ -230,7 +230,7 @@ def test_get_style_for_token(): assert syntax._get_line_numbers_color() == Color.default() -def test_option_no_wrap(): +def test_option_no_wrap() -> None: syntax = Syntax( CODE, lexer="python", @@ -247,7 +247,7 @@ def test_option_no_wrap(): assert rendered_syntax == expected -def test_syntax_highlight_ranges(): +def test_syntax_highlight_ranges() -> None: syntax = Syntax( CODE, lexer="python", @@ -302,7 +302,7 @@ def test_syntax_highlight_ranges(): assert rendered_syntax == expected -def test_ansi_theme(): +def test_ansi_theme() -> None: style = Style(color="red") theme = ANSISyntaxTheme({("foo", "bar"): style}) assert theme.get_style_for_token(("foo", "bar", "baz")) == style @@ -315,7 +315,7 @@ def test_ansi_theme(): @skip_windows_permission_error -def test_from_path(): +def test_from_path() -> None: fh, path = tempfile.mkstemp("example.py") try: os.write(fh, b"import this\n") @@ -328,7 +328,7 @@ def test_from_path(): @skip_windows_permission_error -def test_from_path_unknown_lexer(): +def test_from_path_unknown_lexer() -> None: fh, path = tempfile.mkstemp("example.nosuchtype") try: os.write(fh, b"import this\n") @@ -340,7 +340,7 @@ def test_from_path_unknown_lexer(): @skip_windows_permission_error -def test_from_path_lexer_override(): +def test_from_path_lexer_override() -> None: fh, path = tempfile.mkstemp("example.nosuchtype") try: os.write(fh, b"import this\n") @@ -352,7 +352,7 @@ def test_from_path_lexer_override(): @skip_windows_permission_error -def test_from_path_lexer_override_invalid_lexer(): +def test_from_path_lexer_override_invalid_lexer() -> None: fh, path = tempfile.mkstemp("example.nosuchtype") try: os.write(fh, b"import this\n") @@ -363,7 +363,7 @@ def test_from_path_lexer_override_invalid_lexer(): os.remove(path) -def test_syntax_guess_lexer(): +def test_syntax_guess_lexer() -> None: assert Syntax.guess_lexer("banana.py") == "python" assert Syntax.guess_lexer("banana.py", "import this") == "python" assert Syntax.guess_lexer("banana.html", "hello") == "html" @@ -371,7 +371,7 @@ def test_syntax_guess_lexer(): assert Syntax.guess_lexer("banana.html", "{{something|filter:3}}") == "html+django" -def test_syntax_padding(): +def test_syntax_padding() -> None: syntax = Syntax("x = 1", lexer="python", padding=(1, 3)) console = Console( width=20, @@ -387,7 +387,7 @@ def test_syntax_padding(): ) -def test_syntax_measure(): +def test_syntax_measure() -> None: console = Console() code = Syntax("Hello, World", "python") assert code.__rich_measure__(console, console.options) == Measurement(0, 12) @@ -402,7 +402,7 @@ def test_syntax_measure(): assert code.__rich_measure__(console, console.options) == Measurement(3, 24) -def test_background_color_override_includes_padding(): +def test_background_color_override_includes_padding() -> None: """Regression test for https://github.com/Textualize/rich/issues/3295""" syntax = Syntax( @@ -419,6 +419,22 @@ def test_background_color_override_includes_padding(): ) +def test_padding_plus_wrap() -> None: + """Regression test for https://github.com/Textualize/rich/issues/3727""" + console = Console(width=24, file=io.StringIO(), legacy_windows=False) + syntax = Syntax( + "'Hello, World. This should wrap.'", + lexer="python", + padding=(0, 3), + word_wrap=True, + ) + console.print(syntax) + output = console.file.getvalue() + print(repr(output)) + expected = " 'Hello, World. \n This should wrap.' \n" + assert output == expected + + if __name__ == "__main__": syntax = Panel.fit( Syntax(