Skip to content

Commit

Permalink
Merge pull request #369 from BCG-X-Official/dev/3.0rc1
Browse files Browse the repository at this point in the history
BUILD: release pytools 3.0rc1
  • Loading branch information
j-ittner authored Jun 10, 2024
2 parents 254400e + 3f62e70 commit df3f3cf
Show file tree
Hide file tree
Showing 13 changed files with 214 additions and 52 deletions.
10 changes: 6 additions & 4 deletions RELEASE_NOTES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,12 @@ Python 3.11, and drops support for Python versions.
generic type

- :mod:`pytools.viz`:
- API: new function :func:`.is_running_in_notebook` to check if the code is running
in a Jupyter or Colab notebook
- API: new property :attr:`.hex` for :class:`.RgbColor` and :class:`.RgbaColor` to
return the color as a hexadecimal string

- API: new class :class:`.HTMLStyle` for rendering HTML content with drawers
- API: new function :func:`.is_running_in_notebook` to check if the code is running
in a Jupyter or Colab notebook
- API: new property :attr:`.hex` for :class:`.RgbColor` and :class:`.RgbaColor` to
return the color as a hexadecimal string

- Various adjustments to maintain compatibility with recent Python versions

Expand Down
3 changes: 3 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ ignore_missing_imports = True

[mypy-google.*]
ignore_missing_imports = True

[mypy-IPython.*]
ignore_missing_imports = True
2 changes: 1 addition & 1 deletion src/pytools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
A collection of Python extensions and tools used in BCG GAMMA's open-source libraries.
"""

__version__ = "3.0rc0"
__version__ = "3.0rc1"
10 changes: 1 addition & 9 deletions src/pytools/expression/_expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import logging
from abc import ABCMeta, abstractmethod
from typing import Any, Dict, Optional, Tuple, TypeVar
from typing import Any, Optional, Tuple, TypeVar

import numpy as np
import numpy.typing as npt
Expand Down Expand Up @@ -113,14 +113,6 @@ def _repr_html_(self) -> str:
# to convert the expression to HTML for Jupyter notebooks
return f"<pre>{self}\n</pre>\n"

def _repr_mimebundle_(self, **kwargs: Any) -> Dict[str, Any]:
# get plain text and HTML representations of this object
# for use in Jupyter notebooks
return {
"text/plain": str(self),
"text/html": self._repr_html_(),
}


@inheritdoc(match="[see superclass]")
class Expression(HasExpressionRepr, metaclass=ABCMeta):
Expand Down
3 changes: 2 additions & 1 deletion src/pytools/viz/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""
A lean MVC framework for rendering basic visualizations in different styles, e.g.,
as `matplotlib` charts or as plain text.
as `matplotlib` charts, HTML, or as plain text.
"""

from ._html import *
from ._matplot import *
from ._notebook import *
from ._text import *
Expand Down
128 changes: 128 additions & 0 deletions src/pytools/viz/_html.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"""
Implementation of ``HTMLStyle``.
"""

from __future__ import annotations

import logging
import sys
from abc import ABCMeta
from io import StringIO
from typing import Any, Generic, TextIO, TypeVar, cast

from ..api import appenddoc, inheritdoc
from ._notebook import is_running_in_notebook
from ._viz import ColoredStyle
from .color import ColorScheme, RgbColor

log = logging.getLogger(__name__)

__all__ = [
"HTMLStyle",
]
#
# Type variables
#

T_ColorScheme = TypeVar("T_ColorScheme", bound=ColorScheme)


#
# Classes
#


@inheritdoc(match="[see superclass]")
class HTMLStyle(ColoredStyle[T_ColorScheme], Generic[T_ColorScheme], metaclass=ABCMeta):
"""
Abstract base class for styles for rendering output as HTML.
Supports color schemes, and is able to display output in a notebook (if running in
one), ``stdout``, or a given output stream.
"""

#: The output stream this style instance writes to; or ``None`` if output should
#: be displayed in a Jupyter notebook
out: TextIO | None

#: Whether the output should be displayed in a Jupyter notebook
_send_to_notebook: bool = False

@appenddoc(to=ColoredStyle.__init__)
def __init__(
self, *, colors: T_ColorScheme | None = None, out: TextIO | None = None
) -> None:
"""
:param out: the output stream this style instance writes to; if ``None`` and
running in a Jupyter notebook, output will be displayed in the notebook,
otherwise it will be written to ``stdout``
"""
super().__init__(colors=colors)

if out is None: # pragma: no cover
if is_running_in_notebook():
self.out = StringIO()
self._send_to_notebook = True
else:
self.out = sys.stdout
self._send_to_notebook = False
else:
self.out = out
self._send_to_notebook = False

@classmethod
def get_default_style_name(cls) -> str:
"""[see superclass]"""
return "html"

@staticmethod
def rgb_to_css(rgb: RgbColor) -> str:
"""
Convert an RGB color to its CSS representation in the form ``rgb(r,g,b)``,
where ``r``, ``g``, and ``b`` are integers in the range 0-255.
:param rgb: the RGB color
:return: the CSS representation of the color
"""
rgb_0_to_255 = ",".join(str(int(luminance * 255)) for luminance in rgb)
return f"rgb({rgb_0_to_255})"

def start_drawing(self, *, title: str, **kwargs: Any) -> None:
"""[see superclass]"""
super().start_drawing(title=title, **kwargs)

# we start a section, setting the colors
print(
'<style type="text/css"></style>'
f'<div style="'
f"color:{self.rgb_to_css(self.colors.foreground)};" # noqa: E702
f"background-color:{self.rgb_to_css(self.colors.background)};" # noqa: E702
"display:inline-block;"
f'">',
file=self.out,
)

# print the title
print(self.render_title(title=title), file=self.out)

def finalize_drawing(self, **kwargs: Any) -> None:
"""[see superclass]"""
super().finalize_drawing()
# we close the section
print("</div>", file=self.out)

# if we are in a notebook, display the HTML
if self._send_to_notebook:
from IPython.display import HTML, display

display(HTML(cast(StringIO, self.out).getvalue()))

# noinspection PyMethodMayBeStatic
def render_title(self, title: str) -> str:
"""
Render the title of the drawing as HTML.
:param title: the title of the drawing
:return: the HTML code of the title
"""
return f"<h2>{title}</h2>"
2 changes: 1 addition & 1 deletion src/pytools/viz/_text.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Text styles for the GAMMA visualization library.
Text styles for the visualization library.
"""

import logging
Expand Down
72 changes: 50 additions & 22 deletions src/pytools/viz/_viz.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from abc import ABCMeta, abstractmethod
from multiprocessing import Lock
from typing import (
AbstractSet,
Any,
Callable,
Dict,
Expand Down Expand Up @@ -169,7 +170,7 @@ def __init__(self, *, colors: Optional[T_ColorScheme] = None) -> None:
self._colors = colors or cast(T_ColorScheme, ColorScheme.DEFAULT)

__init__.__doc__ = cast(str, __init__.__doc__).replace(
"%%COLORS_DEFAULT%%", repr(ColorScheme.DEFAULT)
"%%COLORS_DEFAULT%%", type(ColorScheme.DEFAULT).__name__
)

@classmethod
Expand Down Expand Up @@ -221,28 +222,21 @@ class Drawer(Generic[T_Model, T_Style], metaclass=ABCMeta):
#: The :class:`.DrawingStyle` used by this drawer.
style: T_Style

#: The name of the default drawing style.
DEFAULT_STYLE = "matplot"
#: The class-level named styles for this drawer type.
__named_styles: Optional[Dict[str, Callable[[], T_Style]]] = None

def __init__(self, style: Optional[Union[T_Style, str]] = None) -> None:
"""
:param style: the style to be used for drawing; either as a
:class:`.DrawingStyle` instance, or as the name of a default style.
Permissible names include ``"matplot"`` for a style supporting `matplotlib`,
and ``"text"`` if text rendering is supported (default: ``"%DEFAULT%"``)
:class:`.DrawingStyle` instance, or as the name of a named style supported
by this drawer type; if not specified, the default style will be used
as returned by :meth:`get_default_style`
"""

def _get_style_factory(_style_name: str) -> Callable[..., T_Style]:
# get the named style from the style dict
try:
return self.get_named_styles()[_style_name]
except KeyError:
raise KeyError(f"unknown named style: {_style_name}")

if style is None:
self.style = _get_style_factory(Drawer.DEFAULT_STYLE)()
self.style = self.get_default_style()
elif isinstance(style, str):
self.style = _get_style_factory(style)()
self.style = self.get_style(style)
elif isinstance(style, DrawingStyle):
self.style = style
else:
Expand All @@ -251,22 +245,47 @@ def _get_style_factory(_style_name: str) -> Callable[..., T_Style]:
f"{DrawingStyle.__name__}"
)

__init__.__doc__ = cast(str, __init__.__doc__).replace("%DEFAULT%", DEFAULT_STYLE)
@classmethod
def _get_named_style_lookup(cls) -> Dict[str, Callable[[], T_Style]]:
"""
Get a mapping of names to style factories for all named styles recognized by
this drawer's initializer.
A factory is a class or function with no mandatory parameters.
"""

if cls.__named_styles is None:
# Lazily initialize the named styles lookup table.
cls.__named_styles = {
name: style
for style_class in cls.get_style_classes()
for name, style in style_class.get_named_styles().items()
}

return cls.__named_styles

@classmethod
def get_named_styles(cls) -> Dict[str, Callable[..., T_Style]]:
def get_named_styles(cls) -> AbstractSet[str]:
"""
Get a mapping of names to style factories for all named styles recognized by
this drawer's initializer.
A factory is a class or function with no mandatory parameters.
"""

return {
name: style
for style_class in cls.get_style_classes()
for name, style in style_class.get_named_styles().items()
}
return cls._get_named_style_lookup().keys()

@classmethod
def get_style(cls, name: str) -> T_Style:
"""
Get a style object by name.
:param name: the name of the style
:return: the style object
:raises KeyError: if the style name is not recognized
"""
style_factory: Callable[[], T_Style] = cls._get_named_style_lookup()[name]
return style_factory()

@classmethod
@abstractmethod
Expand All @@ -278,6 +297,15 @@ def get_style_classes(cls) -> Iterable[Type[T_Style]]:
"""
pass

@classmethod
@abstractmethod
def get_default_style(cls) -> T_Style:
"""
Get the default style for this drawer.
:return: the default style for this drawer
"""

def draw(self, data: T_Model, *, title: str) -> None:
"""
Render the data using the style associated with this drawer.
Expand Down
5 changes: 5 additions & 0 deletions src/pytools/viz/dendrogram/_draw.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ def get_style_classes(cls) -> Iterable[Type[DendrogramStyle]]:
DendrogramReportStyle,
]

@classmethod
def get_default_style(cls) -> DendrogramStyle:
"""[see superclass]"""
return DendrogramMatplotStyle()

def get_style_kwargs(self, data: LinkageTree) -> Dict[str, Any]:
"""[see superclass]"""
return dict(
Expand Down
5 changes: 5 additions & 0 deletions src/pytools/viz/distribution/_distribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,11 @@ def get_style_classes(cls) -> Iterable[Type[ECDFStyle]]:
ECDFMatplotStyle,
]

@classmethod
def get_default_style(cls) -> ECDFStyle:
"""[see superclass]"""
return ECDFMatplotStyle()

def _draw(self, data: Union[Sequence[float], ArrayLike]) -> None:
ecdf = self._ecdf(data=data)
x_label = getattr(data, "name", "value")
Expand Down
9 changes: 6 additions & 3 deletions src/pytools/viz/matrix/_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,7 @@ def __init__(
colormap_normalize: Optional[Normalize] = None,
colorbar_major_formatter: Optional[Formatter] = None,
colorbar_minor_formatter: Optional[Formatter] = None,
cell_format: Union[str, Formatter, Callable[..., str], None] = None,
# todo: change to Callable[[Any], str] once sphinx "unhashable type" bug is
# fixed
cell_format: Union[str, Formatter, Callable[[Any], str], None] = None,
nan_substitute: Optional[float] = None,
) -> None:
"""
Expand Down Expand Up @@ -548,6 +546,11 @@ def get_style_classes(cls) -> Iterable[Type[MatrixStyle]]:
MatrixReportStyle,
]

@classmethod
def get_default_style(cls) -> MatrixStyle:
"""[see superclass]"""
return MatrixMatplotStyle()

def get_style_kwargs(self, data: Matrix[Any]) -> Dict[str, Any]:
"""[see superclass]"""
return dict(
Expand Down
11 changes: 3 additions & 8 deletions test/test/pytools/test_expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,16 +109,11 @@ def test_expression_repr_html() -> None:
- f((1 | 2) >> 'x' % x, abc=-5) * f((1 | 2) >> 'x' % x, abc=-5)
)
)"""
expected_html_expression = f"<pre>{expected_formatted_expression}\n</pre>\n"
# test if the string representation is generated as expected
assert str(expr) == expected_formatted_expression

# test if the html representation is generated as expected
assert expr._repr_html_() == expected_html_expression

# test if the mimebundle representation is generated as expected
assert expr._repr_mimebundle_() == {
"text/html": expected_html_expression,
"text/plain": expected_formatted_expression,
}
assert expr._repr_html_() == f"<pre>{expected_formatted_expression}\n</pre>\n"


def test_expression() -> None:
Expand Down
Loading

0 comments on commit df3f3cf

Please sign in to comment.