Merge branch 'main' into guide

This commit is contained in:
Jason Grace 2024-01-15 21:16:34 -05:00 committed by GitHub
commit 0a9d095372
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 267 additions and 217 deletions

View file

@ -36,6 +36,8 @@ logging.getLogger("PIL").setLevel(logging.INFO)
logging.getLogger("matplotlib").setLevel(logging.INFO)
config = ManimConfig().digest_parser(parser)
# TODO: to be used in the future - see PR #620
# https://github.com/ManimCommunity/manim/pull/620
frame = ManimFrame(config)

View file

@ -28,7 +28,7 @@ import numpy as np
from manim import constants
from manim.constants import RendererType
from manim.utils.color import ManimColor
from manim.utils.tex import TexTemplate, TexTemplateFromFile
from manim.utils.tex import TexTemplate
if TYPE_CHECKING:
from enum import EnumMeta
@ -833,7 +833,7 @@ class ManimConfig(MutableMapping):
# Handle --tex_template
if args.tex_template:
self.tex_template = TexTemplateFromFile(tex_filename=args.tex_template)
self.tex_template = TexTemplate.from_file(args.tex_template)
if (
self.renderer == RendererType.OPENGL
@ -1756,19 +1756,19 @@ class ManimConfig(MutableMapping):
if not hasattr(self, "_tex_template") or not self._tex_template:
fn = self._d["tex_template_file"]
if fn:
self._tex_template = TexTemplateFromFile(tex_filename=fn)
self._tex_template = TexTemplate.from_file(fn)
else:
self._tex_template = TexTemplate()
return self._tex_template
@tex_template.setter
def tex_template(self, val: TexTemplateFromFile | TexTemplate) -> None:
if isinstance(val, (TexTemplateFromFile, TexTemplate)):
def tex_template(self, val: TexTemplate) -> None:
if isinstance(val, TexTemplate):
self._tex_template = val
@property
def tex_template_file(self) -> Path:
"""File to read Tex template from (no flag). See :class:`.TexTemplateFromFile`."""
"""File to read Tex template from (no flag). See :class:`.TexTemplate`."""
return self._d["tex_template_file"]
@tex_template_file.setter
@ -1793,6 +1793,8 @@ class ManimConfig(MutableMapping):
self._d["plugins"] = value
# TODO: to be used in the future - see PR #620
# https://github.com/ManimCommunity/manim/pull/620
class ManimFrame(Mapping):
_OPTS: ClassVar[set[str]] = {
"pixel_width",

View file

@ -4,162 +4,132 @@ from __future__ import annotations
__all__ = [
"TexTemplate",
"TexTemplateFromFile",
]
import copy
import os
import re
import warnings
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from typing_extensions import Self
class TexTemplate:
"""TeX templates are used for creating Tex() and MathTex() objects.
from manim.typing import StrPath
Parameters
----------
tex_compiler
The TeX compiler to be used, e.g. ``latex``, ``pdflatex`` or ``lualatex``
output_format
The output format resulting from compilation, e.g. ``.dvi`` or ``.pdf``
documentclass
The command defining the documentclass, e.g. ``\\documentclass[preview]{standalone}``
preamble
The document's preamble, i.e. the part between ``\\documentclass`` and ``\\begin{document}``
placeholder_text
Text in the document that will be replaced by the expression to be rendered
post_doc_commands
Text (definitions, commands) to be inserted at right after ``\\begin{document}``, e.g. ``\\boldmath``
Attributes
----------
tex_compiler : :class:`str`
The TeX compiler to be used, e.g. ``latex``, ``pdflatex`` or ``lualatex``
output_format : :class:`str`
The output format resulting from compilation, e.g. ``.dvi`` or ``.pdf``
documentclass : :class:`str`
The command defining the documentclass, e.g. ``\\documentclass[preview]{standalone}``
preamble : :class:`str`
The document's preamble, i.e. the part between ``\\documentclass`` and ``\\begin{document}``
placeholder_text : :class:`str`
Text in the document that will be replaced by the expression to be rendered
post_doc_commands : :class:`str`
Text (definitions, commands) to be inserted at right after ``\\begin{document}``, e.g. ``\\boldmath``
"""
default_documentclass = r"\documentclass[preview]{standalone}"
default_preamble = r"""
\usepackage[english]{babel}
_DEFAULT_PREAMBLE = r"""\usepackage[english]{babel}
\usepackage{amsmath}
\usepackage{amssymb}
"""
default_placeholder_text = "YourTextHere"
default_tex_compiler = "latex"
default_output_format = ".dvi"
default_post_doc_commands = ""
\usepackage{amssymb}"""
def __init__(
self,
tex_compiler: str | None = None,
output_format: str | None = None,
documentclass: str | None = None,
preamble: str | None = None,
placeholder_text: str | None = None,
post_doc_commands: str | None = None,
**kwargs,
):
self.tex_compiler = (
tex_compiler
if tex_compiler is not None
else TexTemplate.default_tex_compiler
)
self.output_format = (
output_format
if output_format is not None
else TexTemplate.default_output_format
)
self.documentclass = (
documentclass
if documentclass is not None
else TexTemplate.default_documentclass
)
self.preamble = (
preamble if preamble is not None else TexTemplate.default_preamble
)
self.placeholder_text = (
placeholder_text
if placeholder_text is not None
else TexTemplate.default_placeholder_text
)
self.post_doc_commands = (
post_doc_commands
if post_doc_commands is not None
else TexTemplate.default_post_doc_commands
)
self._rebuild()
_BEGIN_DOCUMENT = r"\begin{document}"
_END_DOCUMENT = r"\end{document}"
def __eq__(self, other: TexTemplate) -> bool:
return (
self.body == other.body
and self.tex_compiler == other.tex_compiler
and self.output_format == other.output_format
and self.post_doc_commands == other.post_doc_commands
@dataclass(eq=True)
class TexTemplate:
"""TeX templates are used to create ``Tex`` and ``MathTex`` objects."""
_body: str = field(default="", init=False)
"""A custom body, can be set from a file."""
tex_compiler: str = "latex"
"""The TeX compiler to be used, e.g. ``latex``, ``pdflatex`` or ``lualatex``."""
output_format: str = ".dvi"
"""The output format resulting from compilation, e.g. ``.dvi`` or ``.pdf``."""
documentclass: str = r"\documentclass[preview]{standalone}"
r"""The command defining the documentclass, e.g. ``\documentclass[preview]{standalone}``."""
preamble: str = _DEFAULT_PREAMBLE
r"""The document's preamble, i.e. the part between ``\documentclass`` and ``\begin{document}``."""
placeholder_text: str = "YourTextHere"
"""Text in the document that will be replaced by the expression to be rendered."""
post_doc_commands: str = ""
r"""Text (definitions, commands) to be inserted at right after ``\begin{document}``, e.g. ``\boldmath``."""
@property
def body(self) -> str:
"""The entire TeX template."""
return self._body or "\n".join(
filter(
None,
[
self.documentclass,
self.preamble,
_BEGIN_DOCUMENT,
self.post_doc_commands,
self.placeholder_text,
_END_DOCUMENT,
],
)
)
def _rebuild(self):
"""Rebuilds the entire TeX template text from ``\\documentclass`` to ``\\end{document}`` according to all settings and choices."""
self.body = (
self.documentclass
+ "\n"
+ self.preamble
+ "\n"
+ r"\begin{document}"
+ "\n"
+ self.post_doc_commands
+ "\n"
+ self.placeholder_text
+ "\n"
+ "\n"
+ r"\end{document}"
+ "\n"
)
@body.setter
def body(self, value: str) -> None:
self._body = value
def add_to_preamble(self, txt: str, prepend: bool = False):
"""Adds stuff to the TeX template's preamble (e.g. definitions, packages). Text can be inserted at the beginning or at the end of the preamble.
@classmethod
def from_file(cls, file: StrPath = "tex_template.tex", **kwargs: Any) -> Self:
"""Create an instance by reading the content of a file.
Using the ``add_to_preamble`` and ``add_to_document`` methods on this instance
will have no effect, as the body is read from the file.
"""
instance = cls(**kwargs)
instance.body = Path(file).read_text(encoding="utf-8")
return instance
def add_to_preamble(self, txt: str, prepend: bool = False) -> Self:
r"""Adds text to the TeX template's preamble (e.g. definitions, packages). Text can be inserted at the beginning or at the end of the preamble.
Parameters
----------
txt
String containing the text to be added, e.g. ``\\usepackage{hyperref}``
String containing the text to be added, e.g. ``\usepackage{hyperref}``.
prepend
Whether the text should be added at the beginning of the preamble, i.e. right after ``\\documentclass``. Default is to add it at the end of the preamble, i.e. right before ``\\begin{document}``
Whether the text should be added at the beginning of the preamble, i.e. right after ``\documentclass``.
Default is to add it at the end of the preamble, i.e. right before ``\begin{document}``.
"""
if self._body:
warnings.warn(
"This TeX template was created with a fixed body, trying to add text the preamble will have no effect.",
UserWarning,
stacklevel=2,
)
if prepend:
self.preamble = txt + "\n" + self.preamble
else:
self.preamble += "\n" + txt
self._rebuild()
return self
def add_to_document(self, txt: str):
"""Adds txt to the TeX template just after \\begin{document}, e.g. ``\\boldmath``
def add_to_document(self, txt: str) -> Self:
r"""Adds text to the TeX template just after \begin{document}, e.g. ``\boldmath``.
Parameters
----------
txt
String containing the text to be added.
"""
self.post_doc_commands += "\n" + txt + "\n"
self._rebuild()
if self._body:
warnings.warn(
"This TeX template was created with a fixed body, trying to add text the document will have no effect.",
UserWarning,
stacklevel=2,
)
self.post_doc_commands += txt
return self
def get_texcode_for_expression(self, expression: str):
"""Inserts expression verbatim into TeX template.
def get_texcode_for_expression(self, expression: str) -> str:
r"""Inserts expression verbatim into TeX template.
Parameters
----------
expression
The string containing the expression to be typeset, e.g. ``$\\sqrt{2}$``
The string containing the expression to be typeset, e.g. ``$\sqrt{2}$``
Returns
-------
@ -168,102 +138,60 @@ class TexTemplate:
"""
return self.body.replace(self.placeholder_text, expression)
def _texcode_for_environment(self, environment: str):
"""Processes the tex_environment string to return the correct ``\\begin{environment}[extra]{extra}`` and
``\\end{environment}`` strings
Parameters
----------
environment
The tex_environment as a string. Acceptable formats include:
``{align*}``, ``align*``, ``{tabular}[t]{cccl}``, ``tabular}{cccl``, ``\\begin{tabular}[t]{cccl}``.
Returns
-------
Tuple[:class:`str`, :class:`str`]
A pair of strings representing the opening and closing of the tex environment, e.g.
``\\begin{tabular}{cccl}`` and ``\\end{tabular}``
"""
# If the environment starts with \begin, remove it
if environment[0:6] == r"\begin":
environment = environment[6:]
# If environment begins with { strip it
if environment[0] == r"{":
environment = environment[1:]
# The \begin command takes everything and closes with a brace
begin = r"\begin{" + environment
if (
begin[-1] != r"}" and begin[-1] != r"]"
): # If it doesn't end on } or ], assume missing }
begin += r"}"
# While the \end command terminates at the first closing brace
split_at_brace = re.split(r"}", environment, 1)
end = r"\end{" + split_at_brace[0] + r"}"
return begin, end
def get_texcode_for_expression_in_env(self, expression: str, environment: str):
r"""Inserts expression into TeX template wrapped in \begin{environment} and \end{environment}
def get_texcode_for_expression_in_env(
self, expression: str, environment: str
) -> str:
r"""Inserts expression into TeX template wrapped in ``\begin{environment}`` and ``\end{environment}``.
Parameters
----------
expression
The string containing the expression to be typeset, e.g. ``$\\sqrt{2}$``
The string containing the expression to be typeset, e.g. ``$\sqrt{2}$``.
environment
The string containing the environment in which the expression should be typeset, e.g. ``align*``
The string containing the environment in which the expression should be typeset, e.g. ``align*``.
Returns
-------
:class:`str`
LaTeX code based on template, containing the given expression inside its environment, ready for typesetting
"""
begin, end = self._texcode_for_environment(environment)
return self.body.replace(self.placeholder_text, f"{begin}\n{expression}\n{end}")
begin, end = _texcode_for_environment(environment)
return self.body.replace(
self.placeholder_text, "\n".join([begin, expression, end])
)
def copy(self) -> TexTemplate:
def copy(self) -> Self:
"""Create a deep copy of the TeX template instance."""
return copy.deepcopy(self)
class TexTemplateFromFile(TexTemplate):
"""A TexTemplate object created from a template file (default: tex_template.tex)
def _texcode_for_environment(environment: str) -> tuple[str, str]:
r"""Processes the tex_environment string to return the correct ``\begin{environment}[extra]{extra}`` and
``\end{environment}`` strings.
Parameters
----------
tex_filename
Path to a valid TeX template file
kwargs
Arguments for :class:`~.TexTemplate`.
environment
The tex_environment as a string. Acceptable formats include:
``{align*}``, ``align*``, ``{tabular}[t]{cccl}``, ``tabular}{cccl``, ``\begin{tabular}[t]{cccl}``.
Attributes
----------
template_file : :class:`str`
Path to a valid TeX template file
body : :class:`str`
Content of the TeX template file
tex_compiler : :class:`str`
The TeX compiler to be used, e.g. ``latex``, ``pdflatex`` or ``lualatex``
output_format : :class:`str`
The output format resulting from compilation, e.g. ``.dvi`` or ``.pdf``
Returns
-------
Tuple[:class:`str`, :class:`str`]
A pair of strings representing the opening and closing of the tex environment, e.g.
``\begin{tabular}{cccl}`` and ``\end{tabular}``
"""
def __init__(
self, *, tex_filename: str | os.PathLike = "tex_template.tex", **kwargs
):
self.template_file = Path(tex_filename)
super().__init__(**kwargs)
environment.removeprefix(r"\begin").removeprefix("{")
def _rebuild(self):
self.body = self.template_file.read_text()
# The \begin command takes everything and closes with a brace
begin = r"\begin{" + environment
# If it doesn't end on } or ], assume missing }
if not begin.endswith(("}", "]")):
begin += "}"
def file_not_mutable(self):
raise Exception("Cannot modify TexTemplate when using a template file.")
# While the \end command terminates at the first closing brace
split_at_brace = re.split("}", environment, 1)
end = r"\end{" + split_at_brace[0] + "}"
def add_to_preamble(self, txt, prepend=False):
self.file_not_mutable()
def add_to_document(self, txt):
self.file_not_mutable()
return begin, end

View file

@ -1,8 +1,8 @@
{"levelname": "INFO", "module": "logger_utils", "message": "Log file will be saved in <>"}
{"levelname": "INFO", "module": "tex_file_writing", "message": "Writing <> to <>"}
{"levelname": "ERROR", "module": "tex_file_writing", "message": "LaTeX compilation error: LaTeX Error: File `notapackage.sty' not found.\n"}
{"levelname": "ERROR", "module": "tex_file_writing", "message": "Context of error: \n\\documentclass[preview]{standalone}\n-> \\usepackage{notapackage}\n\\begin{document}\n\n\\begin{center}\n"}
{"levelname": "ERROR", "module": "tex_file_writing", "message": "Context of error: \n\\documentclass[preview]{standalone}\n-> \\usepackage{notapackage}\n\\begin{document}\n\\begin{center}\n\\frac{1}{0}\n"}
{"levelname": "INFO", "module": "tex_file_writing", "message": "You do not have package notapackage.sty installed."}
{"levelname": "INFO", "module": "tex_file_writing", "message": "Install notapackage.sty it using your LaTeX package manager, or check for typos."}
{"levelname": "ERROR", "module": "tex_file_writing", "message": "LaTeX compilation error: Emergency stop.\n"}
{"levelname": "ERROR", "module": "tex_file_writing", "message": "Context of error: \n\\documentclass[preview]{standalone}\n-> \\usepackage{notapackage}\n\\begin{document}\n\n\\begin{center}\n"}
{"levelname": "ERROR", "module": "tex_file_writing", "message": "Context of error: \n\\documentclass[preview]{standalone}\n-> \\usepackage{notapackage}\n\\begin{document}\n\\begin{center}\n\\frac{1}{0}\n"}

View file

@ -12,12 +12,12 @@ from manim.utils.color import RED
def test_MathTex():
MathTex("a^2 + b^2 = c^2")
assert Path(config.media_dir, "Tex", "eb38bdba08f46c80.svg").exists()
assert Path(config.media_dir, "Tex", "e4be163a00cf424f.svg").exists()
def test_SingleStringMathTex():
SingleStringMathTex("test")
assert Path(config.media_dir, "Tex", "5b2faa68ebf42d1e.svg").exists()
assert Path(config.media_dir, "Tex", "8ce17c7f5013209f.svg").exists()
@pytest.mark.parametrize( # : PT006
@ -31,7 +31,7 @@ def test_double_braces_testing(text_input, length_sub):
def test_tex():
Tex("The horse does not eat cucumber salad.")
assert Path(config.media_dir, "Tex", "f2e45e6e82d750e6.svg").exists()
assert Path(config.media_dir, "Tex", "c3945e23e546c95a.svg").exists()
def test_tex_temp_directory(tmpdir, monkeypatch):
@ -44,12 +44,12 @@ def test_tex_temp_directory(tmpdir, monkeypatch):
with tempconfig({"media_dir": "media"}):
Tex("The horse does not eat cucumber salad.")
assert Path("media", "Tex").exists()
assert Path("media", "Tex", "f2e45e6e82d750e6.svg").exists()
assert Path("media", "Tex", "c3945e23e546c95a.svg").exists()
def test_percent_char_rendering():
Tex(r"\%")
assert Path(config.media_dir, "Tex", "3f48edf8ebaf82c8.tex").exists()
assert Path(config.media_dir, "Tex", "4a583af4d19a3adf.tex").exists()
def test_tex_whitespace_arg():
@ -219,10 +219,10 @@ def test_tex_garbage_collection(tmpdir, monkeypatch):
Path(tmpdir, "media").mkdir()
with tempconfig({"media_dir": "media"}):
tex_without_log = Tex("Hello World!") # f7bc61042256dea9.tex
assert Path("media", "Tex", "f7bc61042256dea9.tex").exists()
assert not Path("media", "Tex", "f7bc61042256dea9.log").exists()
tex_without_log = Tex("Hello World!") # d771330b76d29ffb.tex
assert Path("media", "Tex", "d771330b76d29ffb.tex").exists()
assert not Path("media", "Tex", "d771330b76d29ffb.log").exists()
with tempconfig({"media_dir": "media", "no_latex_cleanup": True}):
tex_with_log = Tex("Hello World, again!") # 3ef79eaaa2d0b15b.tex
assert Path("media", "Tex", "3ef79eaaa2d0b15b.log").exists()
tex_with_log = Tex("Hello World, again!") # da27670a37b08799.tex
assert Path("media", "Tex", "da27670a37b08799.log").exists()

View file

@ -0,0 +1,118 @@
import pytest
from manim.utils.tex import TexTemplate
DEFAULT_BODY = r"""\documentclass[preview]{standalone}
\usepackage[english]{babel}
\usepackage{amsmath}
\usepackage{amssymb}
\begin{document}
YourTextHere
\end{document}"""
BODY_WITH_ADDED_PREAMBLE = r"""\documentclass[preview]{standalone}
\usepackage[english]{babel}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{testpackage}
\begin{document}
YourTextHere
\end{document}"""
BODY_WITH_PREPENDED_PREAMBLE = r"""\documentclass[preview]{standalone}
\usepackage{testpackage}
\usepackage[english]{babel}
\usepackage{amsmath}
\usepackage{amssymb}
\begin{document}
YourTextHere
\end{document}"""
BODY_WITH_ADDED_DOCUMENT = r"""\documentclass[preview]{standalone}
\usepackage[english]{babel}
\usepackage{amsmath}
\usepackage{amssymb}
\begin{document}
\boldmath
YourTextHere
\end{document}"""
BODY_REPLACE = r"""\documentclass[preview]{standalone}
\usepackage[english]{babel}
\usepackage{amsmath}
\usepackage{amssymb}
\begin{document}
\sqrt{2}
\end{document}"""
BODY_REPLACE_IN_ENV = r"""\documentclass[preview]{standalone}
\usepackage[english]{babel}
\usepackage{amsmath}
\usepackage{amssymb}
\begin{document}
\begin{align}
\sqrt{2}
\end{align}
\end{document}"""
def test_tex_template_default_body():
template = TexTemplate()
assert template.body == DEFAULT_BODY
def test_tex_template_preamble():
template = TexTemplate()
template.add_to_preamble(r"\usepackage{testpackage}")
assert template.body == BODY_WITH_ADDED_PREAMBLE
def test_tex_template_preprend_preamble():
template = TexTemplate()
template.add_to_preamble(r"\usepackage{testpackage}", prepend=True)
assert template.body == BODY_WITH_PREPENDED_PREAMBLE
def test_tex_template_document():
template = TexTemplate()
template.add_to_document(r"\boldmath")
assert template.body == BODY_WITH_ADDED_DOCUMENT
def test_tex_template_texcode_for_expression():
template = TexTemplate()
assert template.get_texcode_for_expression(r"\sqrt{2}") == BODY_REPLACE
def test_tex_template_texcode_for_expression_in_env():
template = TexTemplate()
assert (
template.get_texcode_for_expression_in_env(r"\sqrt{2}", environment="align")
== BODY_REPLACE_IN_ENV
)
def test_tex_template_fixed_body():
template = TexTemplate()
# Usually set when calling `from_file`
template.body = "dummy"
assert template.body == "dummy"
with pytest.warns(
UserWarning,
match="This TeX template was created with a fixed body, trying to add text the preamble will have no effect.",
):
template.add_to_preamble("dummys")
with pytest.warns(
UserWarning,
match="This TeX template was created with a fixed body, trying to add text the document will have no effect.",
):
template.add_to_document("dummy")

View file

@ -9,12 +9,12 @@ from manim import MathTex, SingleStringMathTex, Tex, config
def test_MathTex(using_opengl_renderer):
MathTex("a^2 + b^2 = c^2")
assert Path(config.media_dir, "Tex", "eb38bdba08f46c80.svg").exists()
assert Path(config.media_dir, "Tex", "e4be163a00cf424f.svg").exists()
def test_SingleStringMathTex(using_opengl_renderer):
SingleStringMathTex("test")
assert Path(config.media_dir, "Tex", "5b2faa68ebf42d1e.svg").exists()
assert Path(config.media_dir, "Tex", "8ce17c7f5013209f.svg").exists()
@pytest.mark.parametrize( # : PT006
@ -28,7 +28,7 @@ def test_double_braces_testing(using_opengl_renderer, text_input, length_sub):
def test_tex(using_opengl_renderer):
Tex("The horse does not eat cucumber salad.")
assert Path(config.media_dir, "Tex", "f2e45e6e82d750e6.svg").exists()
assert Path(config.media_dir, "Tex", "c3945e23e546c95a.svg").exists()
def test_tex_whitespace_arg(using_opengl_renderer):