Skip to content

Commit

Permalink
Frontend refactor -- ipython_wrapper for decorator output, setup work…
Browse files Browse the repository at this point in the history
… for ipython-algorithmic codegen (#164)

* this blocks other half

* rename

* default style

* docstring

* better wording

* suggestions

* docs

* doesn't exist in python 3.7

* ci

* mv for blame

* move back

* blame stuff

* another move

* changes

* comment

* moved to top level

* move to top level

* Update src/latexify/frontend.py

Co-authored-by: Yusuke Oda <[email protected]>

* Update src/latexify/ipython_wrappers.py

Co-authored-by: Yusuke Oda <[email protected]>

* suggestions

* fix sig

* same as in gen latex

* fix cdot

* Update src/latexify/ipython_wrappers.py

Co-authored-by: Yusuke Oda <[email protected]>

* Update src/latexify/ipython_wrappers.py

Co-authored-by: Yusuke Oda <[email protected]>

* Update src/latexify/ipython_wrappers.py

Co-authored-by: Yusuke Oda <[email protected]>

* Update src/latexify/ipython_wrappers.py

Co-authored-by: Yusuke Oda <[email protected]>

* import cast

Co-authored-by: Zibing Zhang <[email protected]>
Co-authored-by: Yusuke Oda <[email protected]>
  • Loading branch information
3 people authored Dec 13, 2022
1 parent 22e79a7 commit 144073b
Show file tree
Hide file tree
Showing 7 changed files with 369 additions and 209 deletions.
6 changes: 4 additions & 2 deletions src/integration_tests/algorithmic_style_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import textwrap
from typing import Any, Callable

from latexify import frontend
from latexify import generate_latex


def check_algorithm(
Expand All @@ -24,7 +24,9 @@ def check_algorithm(
# def fn(...):
# ...
# latexified = get_latex(fn, style=ALGORITHM, **kwargs)
latexified = frontend.get_latex(fn, style=frontend.Style.ALGORITHMIC, **kwargs)
latexified = generate_latex.get_latex(
fn, style=generate_latex.Style.ALGORITHMIC, **kwargs
)
assert latexified == latex


Expand Down
6 changes: 3 additions & 3 deletions src/latexify/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
except Exception:
__version__ = ""

from latexify import frontend
from latexify import frontend, generate_latex

Style = frontend.Style
Style = generate_latex.Style

get_latex = frontend.get_latex
get_latex = generate_latex.get_latex

function = frontend.function
expression = frontend.expression
177 changes: 56 additions & 121 deletions src/latexify/frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,145 +2,74 @@

from __future__ import annotations

import enum
from collections.abc import Callable
from typing import Any, overload

from latexify import codegen
from latexify import config as cfg
from latexify import exceptions, parser, transformers
from latexify import ipython_wrappers


class Style(enum.Enum):
EXPRESSION = "expression"
FUNCTION = "function"
ALGORITHMIC = "algorithmic"
@overload
def algorithmic(
fn: Callable[..., Any], **kwargs: Any
) -> ipython_wrappers.LatexifiedAlgorithm:
...


@overload
def algorithmic(
**kwargs: Any,
) -> Callable[[Callable[..., Any]], ipython_wrappers.LatexifiedAlgorithm]:
...


def get_latex(
fn: Callable[..., Any],
*,
style: Style = Style.FUNCTION,
config: cfg.Config | None = None,
**kwargs,
) -> str:
"""Obtains LaTeX description from the function's source.
def algorithmic(
fn: Callable[..., Any] | None = None, **kwargs: Any
) -> ipython_wrappers.LatexifiedAlgorithm | Callable[
[Callable[..., Any]], ipython_wrappers.LatexifiedAlgorithm
]:
"""Attach LaTeX pretty-printing to the given function.
This function works with or without specifying the target function as the
positional argument. The following two syntaxes works similarly.
- latexify.algorithmic(alg, **kwargs)
- latexify.algorithmic(**kwargs)(alg)
Args:
fn: Reference to a function to analyze.
style: Style of the LaTeX description, the default is FUNCTION.
config: Use defined Config object, if it is None, it will be automatic assigned
with default value.
**kwargs: Dict of Config field values that could be defined individually
by users.
fn: Callable to be wrapped.
**kwargs: Arguments to control behavior. See also get_latex().
Returns:
Generated LaTeX description.
Raises:
latexify.exceptions.LatexifyError: Something went wrong during conversion.
- If `fn` is passed, returns the wrapped function.
- Otherwise, returns the wrapper function with given settings.
"""
if style == Style.EXPRESSION:
kwargs["use_signature"] = kwargs.get("use_signature", False)

merged_config = cfg.Config.defaults().merge(config=config, **kwargs)

# Obtains the source AST.
tree = parser.parse_function(fn)

# Applies AST transformations.

if merged_config.prefixes is not None:
tree = transformers.PrefixTrimmer(merged_config.prefixes).visit(tree)
if merged_config.identifiers is not None:
tree = transformers.IdentifierReplacer(merged_config.identifiers).visit(tree)
if merged_config.reduce_assignments:
tree = transformers.AssignmentReducer().visit(tree)
if merged_config.expand_functions is not None:
tree = transformers.FunctionExpander(merged_config.expand_functions).visit(tree)

# Generates LaTeX.
if style == Style.ALGORITHMIC:
return codegen.AlgorithmicCodegen(
use_math_symbols=merged_config.use_math_symbols,
use_set_symbols=merged_config.use_set_symbols,
).visit(tree)
else:
return codegen.FunctionCodegen(
use_math_symbols=merged_config.use_math_symbols,
use_signature=merged_config.use_signature,
use_set_symbols=merged_config.use_set_symbols,
).visit(tree)


class LatexifiedFunction:
"""Function with latex representation."""

_fn: Callable[..., Any]
_latex: str | None
_error: str | None

def __init__(self, fn, **kwargs):
self._fn = fn
try:
self._latex = get_latex(fn, **kwargs)
self._error = None
except exceptions.LatexifyError as e:
self._latex = None
self._error = f"{type(e).__name__}: {str(e)}"

@property
def __doc__(self):
return self._fn.__doc__

@__doc__.setter
def __doc__(self, val):
self._fn.__doc__ = val

@property
def __name__(self):
return self._fn.__name__

@__name__.setter
def __name__(self, val):
self._fn.__name__ = val

def __call__(self, *args):
return self._fn(*args)

def __str__(self):
return self._latex if self._latex is not None else self._error

def _repr_html_(self):
"""IPython hook to display HTML visualization."""
return (
'<span style="color: red;">' + self._error + "</span>"
if self._error is not None
else None
)

def _repr_latex_(self):
"""IPython hook to display LaTeX visualization."""
return (
r"$$ \displaystyle " + self._latex + " $$"
if self._latex is not None
else self._error
)
if fn is not None:
return ipython_wrappers.LatexifiedAlgorithm(fn, **kwargs)

def wrapper(f):
return ipython_wrappers.LatexifiedAlgorithm(f, **kwargs)

return wrapper


@overload
def function(fn: Callable[..., Any], **kwargs: Any) -> LatexifiedFunction:
def function(
fn: Callable[..., Any], **kwargs: Any
) -> ipython_wrappers.LatexifiedFunction:
...


@overload
def function(**kwargs: Any) -> Callable[[Callable[..., Any]], LatexifiedFunction]:
def function(
**kwargs: Any,
) -> Callable[[Callable[..., Any]], ipython_wrappers.LatexifiedFunction]:
...


def function(
fn: Callable[..., Any] | None = None, **kwargs: Any
) -> LatexifiedFunction | Callable[[Callable[..., Any]], LatexifiedFunction]:
) -> ipython_wrappers.LatexifiedFunction | Callable[
[Callable[..., Any]], ipython_wrappers.LatexifiedFunction
]:
"""Attach LaTeX pretty-printing to the given function.
This function works with or without specifying the target function as the positional
Expand All @@ -157,33 +86,39 @@ def function(
- Otherwise, returns the wrapper function with given settings.
"""
if fn is not None:
return LatexifiedFunction(fn, **kwargs)
return ipython_wrappers.LatexifiedFunction(fn, **kwargs)

def wrapper(f):
return LatexifiedFunction(f, **kwargs)
return ipython_wrappers.LatexifiedFunction(f, **kwargs)

return wrapper


@overload
def expression(fn: Callable[..., Any], **kwargs: Any) -> LatexifiedFunction:
def expression(
fn: Callable[..., Any], **kwargs: Any
) -> ipython_wrappers.LatexifiedFunction:
...


@overload
def expression(**kwargs: Any) -> Callable[[Callable[..., Any]], LatexifiedFunction]:
def expression(
**kwargs: Any,
) -> Callable[[Callable[..., Any]], ipython_wrappers.LatexifiedFunction]:
...


def expression(
fn: Callable[..., Any] | None = None, **kwargs: Any
) -> LatexifiedFunction | Callable[[Callable[..., Any]], LatexifiedFunction]:
) -> ipython_wrappers.LatexifiedFunction | Callable[
[Callable[..., Any]], ipython_wrappers.LatexifiedFunction
]:
"""Attach LaTeX pretty-printing to the given function.
This function is a shortcut for `latexify.function` with the default parameter
`use_signature=False`.
"""
kwargs["style"] = Style.EXPRESSION
kwargs["use_signature"] = kwargs.get("use_signature", False)
if fn is not None:
return function(fn, **kwargs)
else:
Expand Down
83 changes: 0 additions & 83 deletions src/latexify/frontend_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,89 +5,6 @@
from latexify import frontend


def test_get_latex_identifiers() -> None:
def myfn(myvar):
return 3 * myvar

identifiers = {"myfn": "f", "myvar": "x"}

latex_without_flag = r"\mathrm{myfn}(\mathrm{myvar}) = 3 \cdot \mathrm{myvar}"
latex_with_flag = r"f(x) = 3 \cdot x"

assert frontend.get_latex(myfn) == latex_without_flag
assert frontend.get_latex(myfn, identifiers=identifiers) == latex_with_flag


def test_get_latex_prefixes() -> None:
abc = object()

def f(x):
return abc.d + x.y.z.e

latex_without_flag = r"f(x) = \mathrm{abc}.d + x.y.z.e"
latex_with_flag1 = r"f(x) = d + x.y.z.e"
latex_with_flag2 = r"f(x) = \mathrm{abc}.d + y.z.e"
latex_with_flag3 = r"f(x) = \mathrm{abc}.d + z.e"
latex_with_flag4 = r"f(x) = d + e"

assert frontend.get_latex(f) == latex_without_flag
assert frontend.get_latex(f, prefixes=set()) == latex_without_flag
assert frontend.get_latex(f, prefixes={"abc"}) == latex_with_flag1
assert frontend.get_latex(f, prefixes={"x"}) == latex_with_flag2
assert frontend.get_latex(f, prefixes={"x.y"}) == latex_with_flag3
assert frontend.get_latex(f, prefixes={"abc", "x.y.z"}) == latex_with_flag4
assert frontend.get_latex(f, prefixes={"abc", "x", "x.y.z"}) == latex_with_flag4


def test_get_latex_reduce_assignments() -> None:
def f(x):
y = 3 * x
return y

latex_without_flag = r"\begin{array}{l} y = 3 \cdot x \\ f(x) = y \end{array}"
latex_with_flag = r"f(x) = 3 \cdot x"

assert frontend.get_latex(f) == latex_without_flag
assert frontend.get_latex(f, reduce_assignments=False) == latex_without_flag
assert frontend.get_latex(f, reduce_assignments=True) == latex_with_flag


def test_get_latex_use_math_symbols() -> None:
def f(alpha):
return alpha

latex_without_flag = r"f(\mathrm{alpha}) = \mathrm{alpha}"
latex_with_flag = r"f(\alpha) = \alpha"

assert frontend.get_latex(f) == latex_without_flag
assert frontend.get_latex(f, use_math_symbols=False) == latex_without_flag
assert frontend.get_latex(f, use_math_symbols=True) == latex_with_flag


def test_get_latex_use_signature() -> None:
def f(x):
return x

latex_without_flag = "x"
latex_with_flag = r"f(x) = x"

assert frontend.get_latex(f) == latex_with_flag
assert frontend.get_latex(f, use_signature=False) == latex_without_flag
assert frontend.get_latex(f, use_signature=True) == latex_with_flag


def test_get_latex_use_set_symbols() -> None:
def f(x, y):
return x & y

latex_without_flag = r"f(x, y) = x \mathbin{\&} y"
latex_with_flag = r"f(x, y) = x \cap y"

assert frontend.get_latex(f) == latex_without_flag
assert frontend.get_latex(f, use_set_symbols=False) == latex_without_flag
assert frontend.get_latex(f, use_set_symbols=True) == latex_with_flag


def test_function() -> None:
def f(x):
return x
Expand Down
Loading

0 comments on commit 144073b

Please sign in to comment.