Skip to content

Commit

Permalink
Merge pull request #104 from e10v/dev
Browse files Browse the repository at this point in the history
Drop support for old extras and Chart class
  • Loading branch information
e10v authored Jul 23, 2023
2 parents d8ea9a0 + 0087838 commit b75b121
Show file tree
Hide file tree
Showing 10 changed files with 813 additions and 761 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
enable-pep582: false
cache: true
- name: Install dependencies
run: pdm sync -G complete -G test
run: pdm sync -G test
- name: Test
run: pdm run test
- name: Convert coverage report
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
enable-pep582: false
cache: true
- name: Install dependencies
run: pdm sync -G complete
run: pdm sync
- name: Build and publish
env:
PDM_PUBLISH_PASSWORD: ${{ secrets.PYPI_TOKEN }}
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ pip install rico

**rico** has no dependencies other than the standard Python packages.

For Markdown support:
* install [markdown-it-py](https://github.com/executablebooks/markdown-it-py),
* or install [Python Markdown](https://github.com/Python-Markdown/),
* or set your own Markdown renderer using `rico.set_config`.

### Deprecated

Optional additional dependencies were required to support the following content types:
Expand Down
1,378 changes: 756 additions & 622 deletions pdm.lock

Large diffs are not rendered by default.

12 changes: 3 additions & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,6 @@ classifiers = [
source = "https://github.com/e10v/rico"
"release notes" = "https://github.com/e10v/rico/releases"

[project.optional-dependencies]
altair = ["altair>=4.2", "vl-convert-python>=0.8"]
markdown = ["markdown>=3.3"]
pyplot = ["matplotlib>=3.5"]
seaborn = ["seaborn>=0.12"]
complete = ["rico[altair,markdown,pyplot,seaborn]"]


[build-system]
requires = ["pdm-backend"]
Expand All @@ -51,7 +44,8 @@ build-backend = "pdm.backend"
[tool.pdm]
[tool.pdm.dev-dependencies]
lint = ["ruff", "pyright"]
test = ["pytest", "coverage[toml]", "markdown-it-py", "markdown"]
test = ["pytest", "coverage[toml]", "markdown-it-py", "markdown", "matplotlib"]
examples = ["pandas", "altair"]

[tool.pdm.scripts]
test = "coverage run -m pytest"
Expand Down Expand Up @@ -89,7 +83,7 @@ select = [
]
ignore = [
"ANN101", "ANN102", "ANN204", "ANN401", "B006", "N817", "PGH003", "PT001",
"PT011", "SLF001", "TRY003",
"PT011", "RUF015", "SLF001", "TRY003",
]

[tool.ruff.per-file-ignores]
Expand Down
1 change: 0 additions & 1 deletion src/rico/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from rico._config import config_context, get_config, set_config
from rico._container import Div, Doc
from rico._content import (
Chart,
Code,
ContentBase,
HTML,
Expand Down
1 change: 0 additions & 1 deletion src/rico/_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ def __init__(self, *objects: Any, class_: str | None = None):
append_markdown = _append(rico._content.Markdown, rico._content.Markdown.__init__)
append_image = _append(rico._content.Image, rico._content.Image.__init__)
append_plot = _append(rico._content.Plot, rico._content.Plot.__init__)
append_chart = _append(rico._content.Chart, rico._content.Chart.__init__)
append = _append(rico._content.Obj, rico._content.Obj.__init__)


Expand Down
86 changes: 18 additions & 68 deletions src/rico/_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import io
from typing import TYPE_CHECKING
import urllib.request
import warnings
import xml.etree.ElementTree as ET

import rico._config
Expand All @@ -20,22 +19,11 @@
_ReprResult = str | bytes | dict[str, str | bytes]


try:
import altair as alt
import vl_convert as vlc
except ImportError:
alt = None

try:
import matplotlib.pyplot as plt
except ImportError:
plt = None

try:
import seaborn.objects as so
except ImportError:
so = None


class ContentBase:
container: ET.Element
Expand Down Expand Up @@ -251,74 +239,36 @@ def __init__(
):
"""Create an HTML element from a plot object and wrap it in a <div> container.
The supported plot types are the following:
- Matplotlib Pyplot Axes and Figure,
- [Deprecated] Altair Chart,
- [Deprecated] Seaborn Plot (seaborn.objects interface).
Deprecated:
Support of the Seaborn plots in this class/method is deprecated
and will be removed in version 0.4.0.
Use the rico.Obj or obj.append indstead.
Args:
obj: The plot object.
obj: The Matplotlib Pyplot Axes or Figure object.
format: Image format.
class_: The container class attribute.
**kwargs: Keyword arguments passed to the function
which converts the object to an image.
**kwargs: Keyword arguments passed to the `savefig` method.
Raises:
TypeError: The plot type is not supported
or a required extra package is not installed.
ImportError: Matplotlib is not installed.
TypeError: The object type should be an instance of plt.Figure or plt.Axes.
"""
if plt is None:
raise ImportError("Matplotlib is not installed.")

if not isinstance(obj, plt.Figure | plt.Axes): # type: ignore
raise TypeError(
"The object type should be an instance of plt.Figure or plt.Axes.")

if format is None:
format = rico._config.get_config("image_format") # noqa: A001

if plt is not None and isinstance(obj, plt.Axes):
if isinstance(obj, plt.Axes):
obj = obj.figure

if plt is not None and isinstance(obj, plt.Figure): # type: ignore
stream = io.StringIO() if format == "svg" else io.BytesIO()
obj.savefig(stream, format=format, **kwargs)
image = stream.getvalue()
elif so is not None and isinstance(obj, so.Plot):
warnings.warn(
"Support of the Seaborn plots in this class/method is deprecated "
"and will be removed in version 0.4.0. "
"Use the rico.Obj or obj.append indstead.",
DeprecationWarning,
stacklevel=2,
)
stream = io.StringIO() if format == "svg" else io.BytesIO()
obj.save(stream, format=format, **kwargs)
image = stream.getvalue()
elif alt is not None and isinstance(obj, alt.TopLevelMixin):
warnings.warn(
"Support of the Altair plots in this class/method is deprecated "
"and will be removed in version 0.4.0. "
"Use the rico.Obj or obj.append indstead.",
DeprecationWarning,
stacklevel=2,
)
convert = vlc.vegalite_to_svg if format == "svg" else vlc.vegalite_to_png # type: ignore # noqa: E501
image = convert( # type: ignore
obj.to_json(), # type: ignore
vl_version="_".join(alt.SCHEMA_VERSION.split(".")[:2]),
**kwargs,
)
else:
error_msg = (
f"The plot type {type(obj)} is not supported "
"or a required extra package is not installed."
)
raise TypeError(error_msg)

mime_subtype = "svg+xml" if format == "svg" else format
content = Image(data=image, mime_subtype=mime_subtype, class_=class_) # type: ignore # noqa: E501
self.container = content.container
stream = io.StringIO() if format == "svg" else io.BytesIO()
obj.savefig(stream, format=format, **kwargs)
image = stream.getvalue()

Chart = Plot
mime_subtype = "svg+xml" if format == "svg" else "png"
content = Image(data=image, mime_subtype=mime_subtype, class_=class_)
self.container = content.container


class Script(ContentBase):
Expand Down
29 changes: 16 additions & 13 deletions tests/test__container.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import xml.etree.ElementTree as ET

import altair as alt
import matplotlib.pyplot as plt
import pytest

import rico._config
Expand Down Expand Up @@ -108,20 +108,23 @@ def test_div_append_image(div_container: rico._container.Div):
assert str(div_container) == f"<div>{content}</div>"


altair_chart = alt.Chart(
alt.Data(values=[
{"x": "A", "y": 5},
{"x": "B", "y": 3},
{"x": "C", "y": 6},
{"x": "D", "y": 7},
{"x": "E", "y": 2},
]),
).mark_bar().encode(x="x:N", y="y:Q")
pyplot_figure, pyplot_axes = plt.subplots() # type: ignore
pyplot_axes.plot([1, 2, 3, 4], [1, 4, 2, 3]) # type: ignore

def test_div_append_plot(div_container: rico._container.Div):
div_container.append_plot(altair_chart)
content = rico._content.Plot(altair_chart)
assert str(div_container) == f"<div>{content}</div>"
div_container.append_plot(pyplot_figure)

div = div_container.container[0]
assert isinstance(div, ET.Element)
assert div.tag == "div"
assert div.attrib == {}
assert div.text is None
assert div.tail is None
assert len(div) == 1

img = tuple(div)[0]
assert isinstance(img, ET.Element)
assert img.tag == "img"


def test_div_append(div_container: rico._container.Div):
Expand Down
58 changes: 13 additions & 45 deletions tests/test__content.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@
import unittest.mock
import xml.etree.ElementTree as ET

import altair as alt
import matplotlib.pyplot as plt
import pytest
import seaborn.objects as so

import rico._config
import rico._content
Expand All @@ -23,22 +21,10 @@


def test_import_error():
with unittest.mock.patch.dict("sys.modules", {"altair": None}):
importlib.reload(rico._content)
assert rico._content.alt is None

with unittest.mock.patch.dict("sys.modules", {"vl_convert": None}):
importlib.reload(rico._content)
assert rico._content.alt is None

with unittest.mock.patch.dict("sys.modules", {"matplotlib.pyplot": None}):
importlib.reload(rico._content)
assert rico._content.plt is None

with unittest.mock.patch.dict("sys.modules", {"seaborn.objects": None}):
importlib.reload(rico._content)
assert rico._content.so is None

importlib.reload(rico._content)


Expand Down Expand Up @@ -363,25 +349,13 @@ def test_image_png(data: str | bytes):
assert len(img) == 0


altair_chart = alt.Chart(
alt.Data(values=[
{"x": "A", "y": 5},
{"x": "B", "y": 3},
{"x": "C", "y": 6},
{"x": "D", "y": 7},
{"x": "E", "y": 2},
]),
).mark_bar().encode(x="x:N", y="y:Q")

pyplot_figure, pyplot_axes = plt.subplots() # type: ignore
pyplot_axes.plot([1, 2, 3, 4], [1, 4, 2, 3]) # type: ignore

seaborn_plot = so.Plot({"x": [1, 2, 3, 4], "y": [1, 4, 2, 3]}) # type: ignore

@pytest.mark.parametrize(
"plot",
[altair_chart, pyplot_axes, pyplot_figure, seaborn_plot],
ids=["altair", "pyplot_axes", "pyplot_figure", "seaborn_plot"],
[pyplot_axes, pyplot_figure],
ids=["pyplot_axes", "pyplot_figure"],
)
@pytest.mark.parametrize("format", [None, "png"], ids=["svg", "png"])
def test_plot_complete(plot: Any, format: Literal["svg", "png"] | None): # noqa: A002
Expand All @@ -400,23 +374,17 @@ def test_plot_complete(plot: Any, format: Literal["svg", "png"] | None): # noqa
assert img.tag == "img"


@pytest.mark.parametrize(
("module", "err_plot", "plot"),
[
("alt", altair_chart, seaborn_plot),
("plt", pyplot_axes, altair_chart),
("so", seaborn_plot, pyplot_axes),
],
ids=["alt", "plt", "so"],
)
def test_plot_error(module: str, err_plot: Any, plot: Any):
with unittest.mock.patch.object(rico._content, module, None):
with pytest.raises(TypeError):
rico._content.Plot(err_plot)

content = rico._content.Plot(plot, class_="row")
div = content.container
assert isinstance(div, ET.Element)
def test_plot_import_error():
with (
unittest.mock.patch.object(rico._content, "plt", None),
pytest.raises(ImportError),
):
rico._content.Plot(pyplot_axes)


def test_plot_type_error():
with pytest.raises(TypeError):
rico._content.Plot("text")


@pytest.mark.parametrize("defer", [True, False], ids=["defer", "not defer"])
Expand Down

0 comments on commit b75b121

Please sign in to comment.