diff --git a/.gitignore b/.gitignore index acbe050d..233804ad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*.ipynb .pytype .DS_Store .vscode diff --git a/CHANGELOG.md b/CHANGELOG.md index 835e00ca..ca48bd07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.2.4] Unreleased +## [1.3.0] Unreleased ### Changed - Updated `markdown.Heading.create()` to work with subclassing. +- Console now transparently works with Jupyter ## [1.2.3] - 2020-05-24 diff --git a/rich/__init__.py b/rich/__init__.py index 4c7011a7..f1d726d5 100644 --- a/rich/__init__.py +++ b/rich/__init__.py @@ -7,18 +7,27 @@ if TYPE_CHECKING: _console: Optional["Console"] = None +def get_console() -> "Console": + """Get a global Console instance. + + Returns: + Console: A console instance. + """ + global _console + if _console is None: + from .console import Console + + _console = Console() + + return _console + + def print( *objects: Any, sep=" ", end="\n", file: IO[str] = None, flush: bool = False, ): from .console import Console - global _console - if _console is None: - from ._global import console - - _console = console - - write_console = _console if file is None else Console(file=file) + write_console = get_console() if file is None else Console(file=file) return write_console.print(*objects, sep=sep, end=end) diff --git a/rich/_global.py b/rich/_global.py deleted file mode 100644 index a4134772..00000000 --- a/rich/_global.py +++ /dev/null @@ -1,6 +0,0 @@ -"""A global instance of a Console.""" - -from .console import Console -from . import jupyter - -console = jupyter.get_console() if jupyter.is_jupyter() else Console() diff --git a/rich/console.py b/rich/console.py index 457ce16e..52154aa5 100644 --- a/rich/console.py +++ b/rich/console.py @@ -44,7 +44,6 @@ from .highlighter import NullHighlighter, ReprHighlighter from .pretty import Pretty from .style import Style from .tabulate import tabulate_mapping -from . import jupyter from . import highlighter from . import themes from .pretty import Pretty @@ -193,6 +192,25 @@ class ConsoleDimensions(NamedTuple): height: int +def _is_jupyter() -> bool: + """Check if we're running in a Jupyter notebook.""" + try: + from IPython.display import display + except ImportError: + return False + try: + get_ipython # type: ignore + except NameError: + return False + shell = get_ipython().__class__.__name__ # type: ignore + if shell == "ZMQInteractiveShell": + return True # Jupyter notebook or qtconsole + elif shell == "TerminalInteractiveShell": + return False # Terminal running IPython + else: + return False # Other type (?) + + COLOR_SYSTEMS = { "standard": ColorSystem.STANDARD, "256": ColorSystem.EIGHT_BIT, @@ -229,6 +247,8 @@ class Console: Args: color_system (str, optional): The color system supported by your terminal, either ``"standard"``, ``"256"`` or ``"truecolor"``. Leave as ``"auto"`` to autodetect. + force_terminal (bool, optional): Force the Console to write control codes even when a terminal is not detected. Defaults to False. + force_jupyter (bool, optional): Force the Console to write to Jupyter even when Jupyter is not detected. Defaults to False theme (Theme, optional): An optional style theme object, or ``None`` for default theme. file (IO, optional): A file object where the console should write to. Defaults to stdoutput. width (int, optional): The width of the terminal. Leave as default to auto-detect width. @@ -246,10 +266,12 @@ class Console: def __init__( self, + *, color_system: Optional[ Literal["auto", "standard", "256", "truecolor", "windows"] ] = "auto", force_terminal: bool = False, + force_jupyter: bool = False, theme: Theme = None, file: IO[str] = None, width: int = None, @@ -264,6 +286,10 @@ class Console: log_time_format: str = "[%X]", highlighter: Optional["HighlighterType"] = ReprHighlighter(), ): + self.is_jupyter = force_jupyter or _is_jupyter() + if self.is_jupyter: + width = width or 93 + height = height or 100 self._styles = themes.DEFAULT.styles if theme is None else theme.styles self._width = width self._height = height @@ -398,9 +424,6 @@ class Console: Returns: ConsoleDimensions: A named tuple containing the dimensions. """ - if jupyter.is_jupyter(): - return ConsoleDimensions(93, 100) - if self._width is not None and self._height is not None: return ConsoleDimensions(self._width, self._height) @@ -833,8 +856,10 @@ class Console: """Check if the buffer may be rendered.""" with self._lock: if self._buffer_index == 0: - if jupyter.is_jupyter(): - jupyter.display(self._buffer) + if self.is_jupyter: + from .jupyter import display + + display(self._buffer) del self._buffer[:] else: text = self._render_buffer() diff --git a/rich/jupyter.py b/rich/jupyter.py index 4b51d424..a6ed9ad4 100644 --- a/rich/jupyter.py +++ b/rich/jupyter.py @@ -2,45 +2,19 @@ import io from typing import Any, Iterable, IO, List, Optional, TYPE_CHECKING, Union # from .console import Console as BaseConsole +from .__init__ import get_console from .segment import Segment from .style import Style from .terminal_theme import DEFAULT_TERMINAL_THEME if TYPE_CHECKING: - from .console import Console + from .console import Console, RenderableType JUPYTER_HTML_FORMAT = """\
{code}
""" -_console: Optional["Console"] = None - - -def get_console() -> "Console": - from .console import Console - - global _console - if _console is None: - _console = Console(width=100) - return _console - - -def is_jupyter() -> bool: - """Check if we're running in a Jupyter notebook.""" - try: - get_ipython # type: ignore - except NameError: - return False - shell = get_ipython().__class__.__name__ # type: ignore - if shell == "ZMQInteractiveShell": - return True # Jupyter notebook or qtconsole - elif shell == "TerminalInteractiveShell": - return False # Terminal running IPython - else: - return False # Other type (?) - - class JupyterRenderable: """A shim to write html to Jupyter notebook.""" @@ -48,7 +22,7 @@ class JupyterRenderable: self.html = html @classmethod - def render(self, rich_renderable) -> str: + def render(self, rich_renderable: "RenderableType") -> str: console = get_console() segments = console.render(rich_renderable, console.options) html = _render_segments(segments) @@ -100,3 +74,9 @@ def display(segments: Iterable[Segment]) -> None: html = _render_segments(segments) jupyter_renderable = JupyterRenderable(html) ipython_display(jupyter_renderable) + + +def print(*args, **kwargs) -> None: + """Proxy for Console print.""" + console = get_console() + return console.print(*args, **kwargs) diff --git a/rich/progress.py b/rich/progress.py index f3161c93..11083837 100644 --- a/rich/progress.py +++ b/rich/progress.py @@ -25,6 +25,7 @@ from typing import ( Union, ) +from . import get_console from .bar import Bar from .console import Console, JustifyValues, RenderGroup, RenderableType from .highlighter import Highlighter @@ -387,7 +388,7 @@ class Progress: TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), TimeRemainingColumn(), ) - self.console = console or Console(file=sys.stdout) + self.console = console or get_console() self.auto_refresh = auto_refresh self.refresh_per_second = refresh_per_second self.speed_estimate_period = speed_estimate_period diff --git a/tests/test_rich_print.py b/tests/test_rich_print.py index c8e21ec5..cccb38af 100644 --- a/tests/test_rich_print.py +++ b/tests/test_rich_print.py @@ -5,15 +5,18 @@ import rich from rich.console import Console -def test_rich_print(): - output = io.StringIO() +def test_get_console(): + console = rich.get_console() + assert isinstance(console, Console) - assert rich._console is None - backup_file = sys.stdout + +def test_rich_print(): + console = rich.get_console() + output = io.StringIO() + backup_file = console.file try: - sys.stdout = output + console.file = output rich.print("foo") - assert isinstance(rich._console, Console) assert output.getvalue() == "foo\n" finally: - sys.stdout = backup_file + console.file = backup_file