Skip to content

Commit af68c09

Browse files
Yusuke OdaShigekiKarita
Yusuke Oda
andauthored
Remove action enum, and add Latexify specific exceptions (#68)
* refactoring * add _repr_html_ * font -> span * add more description about exceptions * Update src/latexify/exceptions.py Co-authored-by: Shigeki Karita <6745326+ShigekiKarita@users.noreply.github.com> Co-authored-by: Shigeki Karita <6745326+ShigekiKarita@users.noreply.github.com>
1 parent 1df1a44 commit af68c09

File tree

5 files changed

+86
-21
lines changed

5 files changed

+86
-21
lines changed

src/latexify/constants.py

-9
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,8 @@
1414
"""Latexify module-level constants"""
1515

1616
import ast
17-
from typing import NamedTuple
1817

1918

20-
class Actions(NamedTuple):
21-
"""A class which holds supported actions as constants"""
22-
23-
SET_BOUNDS = "set_bounds"
24-
25-
26-
actions = Actions()
27-
2819
PREFIXES = ["math", "numpy", "np"]
2920

3021
BUILTIN_CALLEES = {

src/latexify/exceptions.py

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Exceptions used in Latexify."""
2+
3+
4+
class LatexifyError(Exception):
5+
"""Base class of all Latexify exceptions.
6+
7+
Subclasses of this exception does not mean incorrect use of the library by the user,
8+
but informs users that Latexify went into something wrong during compiling the given
9+
functions.
10+
These functions are usually captured by the frontend functions (e.g., `with_latex`)
11+
to prevent destroying the entire program.
12+
Errors caused by the wrong inputs should raise built-in exceptions.
13+
"""
14+
15+
...
16+
17+
18+
class LatexifyNotSupportedError(LatexifyError):
19+
"""Some subtree in the AST is not supported by the current implementation.
20+
21+
This error is raised when the library discovered incompatible syntaxes due to lack
22+
of the implementation. Possibly this error would be resolved in the future.
23+
"""
24+
25+
...
26+
27+
28+
class LatexifySyntaxError(LatexifyError):
29+
"""Some subtree in the AST is not supported.
30+
31+
This error is raised when the library discovered syntaxes that are not possible to
32+
be processed anymore. This error is essential, and wouldn't be resolved in the
33+
future.
34+
"""
35+
36+
...

src/latexify/frontend.py

+28-4
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import dill
1212

13-
from latexify import latexify_visitor
13+
from latexify import exceptions, latexify_visitor
1414
from latexify.transformers.identifier_replacer import IdentifierReplacer
1515

1616

@@ -40,6 +40,9 @@ def get_latex(
4040
4141
Returns:
4242
Generatee LaTeX description.
43+
44+
Raises:
45+
latexify.exceptions.LatexifyError: Something went wrong during conversion.
4346
"""
4447
try:
4548
source = inspect.getsource(fn)
@@ -67,9 +70,18 @@ def get_latex(
6770
class LatexifiedFunction:
6871
"""Function with latex representation."""
6972

73+
_fn: Callable[..., Any]
74+
_latex: str | None
75+
_error: str | None
76+
7077
def __init__(self, fn, **kwargs):
7178
self._fn = fn
72-
self._str = get_latex(fn, **kwargs)
79+
try:
80+
self._latex = get_latex(fn, **kwargs)
81+
self._error = None
82+
except exceptions.LatexifyError as e:
83+
self._latex = None
84+
self._error = f"{type(e).__name__}: {str(e)}"
7385

7486
@property
7587
def __doc__(self):
@@ -91,11 +103,23 @@ def __call__(self, *args):
91103
return self._fn(*args)
92104

93105
def __str__(self):
94-
return self._str
106+
return self._latex if self._latex is not None else self._error
107+
108+
def _repr_html_(self):
109+
"""IPython hook to display HTML visualization."""
110+
return (
111+
'<span style="color: red;">' + self._error + "</span>"
112+
if self._error is not None
113+
else None
114+
)
95115

96116
def _repr_latex_(self):
97117
"""IPython hook to display LaTeX visualization."""
98-
return r"$$ \displaystyle " + self._str + " $$"
118+
return (
119+
r"$$ \displaystyle " + self._latex + " $$"
120+
if self._latex is not None
121+
else self._error
122+
)
99123

100124

101125
def with_latex(*args, **kwargs) -> Callable[[Callable[..., Any]], LatexifiedFunction]:

src/latexify/latexify_visitor.py

+10-8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from latexify import constants
99
from latexify import math_symbols
1010
from latexify import node_visitor_base
11+
from latexify import exceptions
1112

1213

1314
class LatexifyVisitor(node_visitor_base.NodeVisitorBase):
@@ -45,8 +46,10 @@ def __init__(
4546

4647
self.assign_var = {}
4748

48-
def generic_visit(self, node, action):
49-
return str(node)
49+
def generic_visit(self, node, action) -> str:
50+
raise exceptions.LatexifyNotSupportedError(
51+
f"Unsupported AST: {type(node).__name__}"
52+
)
5053

5154
def visit_Module(self, node, action): # pylint: disable=invalid-name
5255
return self.visit(node.body[0], "multi_lines")
@@ -78,7 +81,7 @@ def visit_FunctionDef(self, node, action): # pylint: disable=invalid-name
7881
elif isinstance(el, ast.Return):
7982
break
8083
if body_str == "":
81-
raise ValueError("`return` missing")
84+
raise exceptions.LatexifySyntaxError("`return` missing")
8285

8386
return name_str, arg_strs, assign_vars, body_str
8487

@@ -126,7 +129,7 @@ def visit_Call(self, node, action): # pylint: disable=invalid-name
126129
def _decorated_lstr_and_arg(node, callee_str, lstr):
127130
"""Decorates lstr and get its associated arguments"""
128131
if callee_str == "sum" and isinstance(node.args[0], ast.GeneratorExp):
129-
generator_info = self.visit(node.args[0], constants.actions.SET_BOUNDS)
132+
generator_info = self.visit(node.args[0], "set_bounds")
130133
arg_str, comprehension = generator_info
131134
var_comp, args_comp = comprehension
132135
if len(args_comp) == 1:
@@ -293,14 +296,13 @@ def visit_If(self, node, action): # pylint: disable=invalid-name
293296
return latex + r", & \mathrm{otherwise} \end{array} \right."
294297

295298
def visit_GeneratorExp_set_bounds(self, node): # pylint: disable=invalid-name
296-
action = constants.actions.SET_BOUNDS
297299
output = self.visit(node.elt)
298300
comprehensions = [
299-
self.visit(generator, action) for generator in node.generators
301+
self.visit(generator, "set_bounds") for generator in node.generators
300302
]
301303
if len(comprehensions) == 1:
302304
return output, comprehensions[0]
303-
raise TypeError(
305+
raise exceptions.LatexifyNotSupportedError(
304306
"visit_GeneratorExp_sum() supports a single for clause"
305307
"but {} were given".format(len(comprehensions))
306308
)
@@ -351,6 +353,6 @@ def visit_comprehension_set_bounds(self, node): # pylint: disable=invalid-name
351353
args = [self.visit(arg) for arg in node.iter.args]
352354
if len(args) in (1, 2):
353355
return var, args
354-
raise TypeError(
356+
raise exceptions.LatexifyNotSupportedError(
355357
"Comprehension for sum only supports range func " "with 1 or 2 args"
356358
)

src/latexify/latexify_visitor_test.py

+12
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
11
"""Tests for latexify.latexify_visitor."""
22

33
import ast
4+
from latexify import exceptions
45
import pytest
56

67
from latexify.latexify_visitor import LatexifyVisitor
78

89

10+
def test_generic_visit() -> None:
11+
class UnknownNode(ast.AST):
12+
pass
13+
14+
with pytest.raises(
15+
exceptions.LatexifyNotSupportedError,
16+
match=r"^Unsupported AST: UnknownNode$",
17+
):
18+
LatexifyVisitor().visit(UnknownNode())
19+
20+
921
@pytest.mark.parametrize(
1022
"code,latex",
1123
[

0 commit comments

Comments
 (0)