manim 0.17.0__py3-none-any.whl → 0.19.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. manim/__init__.py +11 -6
  2. manim/__main__.py +62 -19
  3. manim/_config/__init__.py +10 -9
  4. manim/_config/cli_colors.py +26 -9
  5. manim/_config/default.cfg +1 -3
  6. manim/_config/logger_utils.py +23 -13
  7. manim/_config/utils.py +662 -468
  8. manim/animation/animation.py +164 -18
  9. manim/animation/changing.py +34 -23
  10. manim/animation/composition.py +265 -67
  11. manim/animation/creation.py +208 -26
  12. manim/animation/fading.py +16 -18
  13. manim/animation/growing.py +35 -15
  14. manim/animation/indication.py +150 -76
  15. manim/animation/movement.py +56 -22
  16. manim/animation/numbers.py +64 -6
  17. manim/animation/rotation.py +78 -7
  18. manim/animation/specialized.py +6 -7
  19. manim/animation/speedmodifier.py +13 -10
  20. manim/animation/transform.py +14 -11
  21. manim/animation/transform_matching_parts.py +3 -4
  22. manim/animation/updaters/mobject_update_utils.py +152 -30
  23. manim/animation/updaters/update.py +10 -7
  24. manim/camera/camera.py +182 -118
  25. manim/camera/mapping_camera.py +34 -3
  26. manim/camera/moving_camera.py +95 -74
  27. manim/camera/multi_camera.py +23 -15
  28. manim/camera/three_d_camera.py +70 -52
  29. manim/cli/__init__.py +17 -0
  30. manim/cli/cfg/group.py +76 -44
  31. manim/cli/checkhealth/checks.py +192 -0
  32. manim/cli/checkhealth/commands.py +90 -0
  33. manim/cli/default_group.py +158 -25
  34. manim/cli/init/commands.py +33 -25
  35. manim/cli/plugins/commands.py +16 -3
  36. manim/cli/render/commands.py +72 -60
  37. manim/cli/render/ease_of_access_options.py +4 -3
  38. manim/cli/render/global_options.py +59 -17
  39. manim/cli/render/output_options.py +6 -5
  40. manim/cli/render/render_options.py +98 -33
  41. manim/constants.py +109 -59
  42. manim/data_structures.py +31 -0
  43. manim/mobject/frame.py +8 -5
  44. manim/mobject/geometry/__init__.py +1 -0
  45. manim/mobject/geometry/arc.py +277 -135
  46. manim/mobject/geometry/boolean_ops.py +32 -31
  47. manim/mobject/geometry/labeled.py +376 -0
  48. manim/mobject/geometry/line.py +192 -87
  49. manim/mobject/geometry/polygram.py +224 -58
  50. manim/mobject/geometry/shape_matchers.py +61 -25
  51. manim/mobject/geometry/tips.py +122 -48
  52. manim/mobject/graph.py +1027 -419
  53. manim/mobject/graphing/coordinate_systems.py +533 -278
  54. manim/mobject/graphing/functions.py +53 -32
  55. manim/mobject/graphing/number_line.py +123 -65
  56. manim/mobject/graphing/probability.py +88 -62
  57. manim/mobject/graphing/scale.py +33 -19
  58. manim/mobject/logo.py +118 -28
  59. manim/mobject/matrix.py +87 -83
  60. manim/mobject/mobject.py +912 -442
  61. manim/mobject/opengl/dot_cloud.py +16 -5
  62. manim/mobject/opengl/opengl_compatibility.py +4 -2
  63. manim/mobject/opengl/opengl_geometry.py +254 -153
  64. manim/mobject/opengl/opengl_image_mobject.py +3 -1
  65. manim/mobject/opengl/opengl_mobject.py +779 -482
  66. manim/mobject/opengl/opengl_point_cloud_mobject.py +41 -14
  67. manim/mobject/opengl/opengl_surface.py +14 -92
  68. manim/mobject/opengl/opengl_three_dimensions.py +12 -8
  69. manim/mobject/opengl/opengl_vectorized_mobject.py +98 -100
  70. manim/mobject/svg/brace.py +173 -41
  71. manim/mobject/svg/svg_mobject.py +139 -53
  72. manim/mobject/table.py +61 -68
  73. manim/mobject/text/code_mobject.py +193 -539
  74. manim/mobject/text/numbers.py +81 -34
  75. manim/mobject/text/tex_mobject.py +130 -78
  76. manim/mobject/text/text_mobject.py +288 -164
  77. manim/mobject/three_d/polyhedra.py +111 -13
  78. manim/mobject/three_d/three_d_utils.py +17 -8
  79. manim/mobject/three_d/three_dimensions.py +239 -106
  80. manim/mobject/types/image_mobject.py +50 -30
  81. manim/mobject/types/point_cloud_mobject.py +120 -75
  82. manim/mobject/types/vectorized_mobject.py +841 -408
  83. manim/mobject/value_tracker.py +105 -38
  84. manim/mobject/vector_field.py +50 -31
  85. manim/opengl/__init__.py +3 -3
  86. manim/plugins/__init__.py +14 -1
  87. manim/plugins/plugins_flags.py +10 -14
  88. manim/renderer/cairo_renderer.py +65 -50
  89. manim/renderer/opengl_renderer.py +89 -69
  90. manim/renderer/opengl_renderer_window.py +39 -18
  91. manim/renderer/shader.py +123 -87
  92. manim/renderer/shader_wrapper.py +44 -28
  93. manim/renderer/vectorized_mobject_rendering.py +38 -10
  94. manim/scene/moving_camera_scene.py +32 -3
  95. manim/scene/scene.py +507 -242
  96. manim/scene/scene_file_writer.py +371 -220
  97. manim/scene/section.py +20 -16
  98. manim/scene/three_d_scene.py +14 -22
  99. manim/scene/vector_space_scene.py +223 -129
  100. manim/scene/zoomed_scene.py +46 -41
  101. manim/typing.py +990 -0
  102. manim/utils/bezier.py +1823 -371
  103. manim/utils/caching.py +12 -5
  104. manim/utils/color/AS2700.py +236 -0
  105. manim/utils/color/BS381.py +318 -0
  106. manim/utils/color/DVIPSNAMES.py +96 -0
  107. manim/utils/color/SVGNAMES.py +179 -0
  108. manim/utils/color/X11.py +533 -0
  109. manim/utils/color/XKCD.py +952 -0
  110. manim/utils/color/__init__.py +61 -0
  111. manim/utils/color/core.py +1667 -0
  112. manim/utils/color/manim_colors.py +218 -0
  113. manim/utils/commands.py +48 -20
  114. manim/utils/config_ops.py +39 -19
  115. manim/utils/debug.py +8 -7
  116. manim/utils/deprecation.py +86 -39
  117. manim/utils/docbuild/__init__.py +17 -0
  118. manim/utils/docbuild/autoaliasattr_directive.py +236 -0
  119. manim/utils/docbuild/autocolor_directive.py +99 -0
  120. manim/utils/docbuild/manim_directive.py +94 -41
  121. manim/utils/docbuild/module_parsing.py +245 -0
  122. manim/utils/exceptions.py +6 -0
  123. manim/utils/family.py +5 -3
  124. manim/utils/family_ops.py +17 -4
  125. manim/utils/file_ops.py +27 -17
  126. manim/utils/hashing.py +55 -45
  127. manim/utils/images.py +13 -7
  128. manim/utils/ipython_magic.py +13 -7
  129. manim/utils/iterables.py +163 -120
  130. manim/utils/module_ops.py +66 -24
  131. manim/utils/opengl.py +77 -24
  132. manim/utils/parameter_parsing.py +32 -0
  133. manim/utils/paths.py +30 -33
  134. manim/utils/polylabel.py +235 -0
  135. manim/utils/qhull.py +218 -0
  136. manim/utils/rate_functions.py +98 -32
  137. manim/utils/simple_functions.py +25 -33
  138. manim/utils/sounds.py +7 -1
  139. manim/utils/space_ops.py +188 -115
  140. manim/utils/testing/__init__.py +17 -0
  141. manim/utils/testing/_frames_testers.py +13 -8
  142. manim/utils/testing/_show_diff.py +5 -3
  143. manim/utils/testing/_test_class_makers.py +34 -18
  144. manim/utils/testing/frames_comparison.py +37 -19
  145. manim/utils/tex.py +130 -198
  146. manim/utils/tex_file_writing.py +77 -47
  147. manim/utils/tex_templates.py +2 -1
  148. manim/utils/unit.py +6 -5
  149. {manim-0.17.0.dist-info → manim-0.19.1.dist-info}/METADATA +64 -65
  150. manim-0.19.1.dist-info/RECORD +220 -0
  151. {manim-0.17.0.dist-info → manim-0.19.1.dist-info}/WHEEL +1 -1
  152. manim-0.19.1.dist-info/entry_points.txt +3 -0
  153. {manim-0.17.0.dist-info → manim-0.19.1.dist-info/licenses}/LICENSE.community +1 -1
  154. manim/cli/new/group.py +0 -189
  155. manim/communitycolors.py +0 -9
  156. manim/gui/__init__.py +0 -0
  157. manim/gui/gui.py +0 -82
  158. manim/plugins/import_plugins.py +0 -43
  159. manim/utils/color.py +0 -552
  160. manim-0.17.0.dist-info/RECORD +0 -206
  161. manim-0.17.0.dist-info/entry_points.txt +0 -4
  162. /manim/cli/{new → checkhealth}/__init__.py +0 -0
  163. {manim-0.17.0.dist-info → manim-0.19.1.dist-info/licenses}/LICENSE +0 -0
@@ -1,9 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Callable
3
+ from collections.abc import Callable
4
+ from typing import Any
4
5
 
6
+ from manim.renderer.cairo_renderer import CairoRenderer
7
+ from manim.renderer.opengl_renderer import OpenGLRenderer
5
8
  from manim.scene.scene import Scene
6
9
  from manim.scene.scene_file_writer import SceneFileWriter
10
+ from manim.typing import PixelArray, StrPath
7
11
 
8
12
  from ._frames_testers import _FramesTester
9
13
 
@@ -11,13 +15,14 @@ from ._frames_testers import _FramesTester
11
15
  def _make_test_scene_class(
12
16
  base_scene: type[Scene],
13
17
  construct_test: Callable[[Scene], None],
14
- test_renderer,
18
+ test_renderer: CairoRenderer | OpenGLRenderer | None,
15
19
  ) -> type[Scene]:
16
- class _TestedScene(base_scene):
17
- def __init__(self, *args, **kwargs):
18
- super().__init__(renderer=test_renderer, *args, **kwargs)
20
+ # TODO: Get the type annotation right for the base_scene argument.
21
+ class _TestedScene(base_scene): # type: ignore[valid-type, misc]
22
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
23
+ super().__init__(*args, renderer=test_renderer, **kwargs)
19
24
 
20
- def construct(self):
25
+ def construct(self) -> None:
21
26
  construct_test(self)
22
27
 
23
28
  # Manim hack to render the very last frame (normally the last frame is not the very end of the animation)
@@ -28,7 +33,7 @@ def _make_test_scene_class(
28
33
  return _TestedScene
29
34
 
30
35
 
31
- def _make_test_renderer_class(from_renderer):
36
+ def _make_test_renderer_class(from_renderer: type) -> Any:
32
37
  # Just for inheritance.
33
38
  class _TestRenderer(from_renderer):
34
39
  pass
@@ -39,39 +44,50 @@ def _make_test_renderer_class(from_renderer):
39
44
  class DummySceneFileWriter(SceneFileWriter):
40
45
  """Delegate of SceneFileWriter used to test the frames."""
41
46
 
42
- def __init__(self, renderer, scene_name, **kwargs):
47
+ def __init__(
48
+ self,
49
+ renderer: CairoRenderer | OpenGLRenderer,
50
+ scene_name: str,
51
+ **kwargs: Any,
52
+ ) -> None:
43
53
  super().__init__(renderer, scene_name, **kwargs)
44
54
  self.i = 0
45
55
 
46
- def init_output_directories(self, scene_name):
56
+ def init_output_directories(self, scene_name: str) -> None:
47
57
  pass
48
58
 
49
- def add_partial_movie_file(self, hash_animation):
59
+ def add_partial_movie_file(self, hash_animation: str | None) -> None:
50
60
  pass
51
61
 
52
- def begin_animation(self, allow_write=True):
62
+ def begin_animation(
63
+ self, allow_write: bool = True, file_path: StrPath | None = None
64
+ ) -> Any:
53
65
  pass
54
66
 
55
- def end_animation(self, allow_write):
67
+ def end_animation(self, allow_write: bool = False) -> None:
56
68
  pass
57
69
 
58
- def combine_to_movie(self):
70
+ def combine_to_movie(self) -> None:
59
71
  pass
60
72
 
61
- def combine_to_section_videos(self):
73
+ def combine_to_section_videos(self) -> None:
62
74
  pass
63
75
 
64
- def clean_cache(self):
76
+ def clean_cache(self) -> None:
65
77
  pass
66
78
 
67
- def write_frame(self, frame_or_renderer):
79
+ def write_frame(
80
+ self, frame_or_renderer: PixelArray | OpenGLRenderer, num_frames: int = 1
81
+ ) -> None:
68
82
  self.i += 1
69
83
 
70
84
 
71
85
  def _make_scene_file_writer_class(tester: _FramesTester) -> type[SceneFileWriter]:
72
86
  class TestSceneFileWriter(DummySceneFileWriter):
73
- def write_frame(self, frame_or_renderer):
87
+ def write_frame(
88
+ self, frame_or_renderer: PixelArray | OpenGLRenderer, num_frames: int = 1
89
+ ) -> None:
74
90
  tester.check_frame(self.i, frame_or_renderer)
75
- super().write_frame(frame_or_renderer)
91
+ super().write_frame(frame_or_renderer, num_frames=num_frames)
76
92
 
77
93
  return TestSceneFileWriter
@@ -2,9 +2,12 @@ from __future__ import annotations
2
2
 
3
3
  import functools
4
4
  import inspect
5
+ from collections.abc import Callable
5
6
  from pathlib import Path
6
- from typing import Callable
7
+ from typing import Any
7
8
 
9
+ import cairo
10
+ import pytest
8
11
  from _pytest.fixtures import FixtureRequest
9
12
 
10
13
  from manim import Scene
@@ -12,7 +15,9 @@ from manim._config import tempconfig
12
15
  from manim._config.utils import ManimConfig
13
16
  from manim.camera.three_d_camera import ThreeDCamera
14
17
  from manim.renderer.cairo_renderer import CairoRenderer
18
+ from manim.renderer.opengl_renderer import OpenGLRenderer
15
19
  from manim.scene.three_d_scene import ThreeDScene
20
+ from manim.typing import StrPath
16
21
 
17
22
  from ._frames_testers import _ControlDataWriter, _FramesTester
18
23
  from ._test_class_makers import (
@@ -25,16 +30,17 @@ from ._test_class_makers import (
25
30
  SCENE_PARAMETER_NAME = "scene"
26
31
  _tests_root_dir_path = Path(__file__).absolute().parents[2]
27
32
  PATH_CONTROL_DATA = _tests_root_dir_path / Path("control_data", "graphical_units_data")
33
+ MIN_CAIRO_VERSION = 11800
28
34
 
29
35
 
30
36
  def frames_comparison(
31
- func=None,
37
+ func: Callable | None = None,
32
38
  *,
33
39
  last_frame: bool = True,
34
- renderer_class=CairoRenderer,
35
- base_scene=Scene,
36
- **custom_config,
37
- ):
40
+ renderer_class: type[CairoRenderer | OpenGLRenderer] = CairoRenderer,
41
+ base_scene: type[Scene] = Scene,
42
+ **custom_config: Any,
43
+ ) -> Callable:
38
44
  """Compares the frames generated by the test with control frames previously registered.
39
45
 
40
46
  If there is no control frames for this test, the test will fail. To generate
@@ -57,7 +63,7 @@ def frames_comparison(
57
63
  If the scene has a moving animation, then the test must set last_frame to False.
58
64
  """
59
65
 
60
- def decorator_maker(tested_scene_construct):
66
+ def decorator_maker(tested_scene_construct: Callable) -> Callable:
61
67
  if (
62
68
  SCENE_PARAMETER_NAME
63
69
  not in inspect.getfullargspec(tested_scene_construct).args
@@ -76,11 +82,20 @@ def frames_comparison(
76
82
  "There is no module test name indicated for the graphical unit test. You have to declare __module_test__ in the test file.",
77
83
  )
78
84
  module_name = tested_scene_construct.__globals__.get("__module_test__")
85
+ assert isinstance(module_name, str)
79
86
  test_name = tested_scene_construct.__name__[len("test_") :]
80
87
 
81
88
  @functools.wraps(tested_scene_construct)
82
89
  # The "request" parameter is meant to be used as a fixture by pytest. See below.
83
- def wrapper(*args, request: FixtureRequest, tmp_path, **kwargs):
90
+ def wrapper(
91
+ *args: Any, request: FixtureRequest, tmp_path: StrPath, **kwargs: Any
92
+ ) -> None:
93
+ # check for cairo version
94
+ if (
95
+ renderer_class is CairoRenderer
96
+ and cairo.cairo_version() < MIN_CAIRO_VERSION
97
+ ):
98
+ pytest.skip("Cairo version is too old. Skipping cairo graphical tests.")
84
99
  # Wraps the test_function to a construct method, to "freeze" the eventual additional arguments (parametrizations fixtures).
85
100
  construct = functools.partial(tested_scene_construct, *args, **kwargs)
86
101
 
@@ -137,13 +152,13 @@ def frames_comparison(
137
152
  inspect.Parameter("tmp_path", inspect.Parameter.KEYWORD_ONLY),
138
153
  ]
139
154
  new_sig = old_sig.replace(parameters=parameters)
140
- wrapper.__signature__ = new_sig
155
+ wrapper.__signature__ = new_sig # type: ignore[attr-defined]
141
156
 
142
157
  # Reach a bit into pytest internals to hoist the marks from our wrapped
143
158
  # function.
144
- setattr(wrapper, "pytestmark", [])
159
+ wrapper.pytestmark = [] # type: ignore[attr-defined]
145
160
  new_marks = getattr(tested_scene_construct, "pytestmark", [])
146
- wrapper.pytestmark = new_marks
161
+ wrapper.pytestmark = new_marks # type: ignore[attr-defined]
147
162
  return wrapper
148
163
 
149
164
  # Case where the decorator is called with and without parentheses.
@@ -183,9 +198,10 @@ def _make_test_comparing_frames(
183
198
  Callable[[], None]
184
199
  The pytest test.
185
200
  """
186
-
187
201
  if is_set_test_data_test:
188
- frames_tester = _ControlDataWriter(file_path, size_frame=size_frame)
202
+ frames_tester: _FramesTester = _ControlDataWriter(
203
+ file_path, size_frame=size_frame
204
+ )
189
205
  else:
190
206
  frames_tester = _FramesTester(file_path, show_diff=show_diff)
191
207
 
@@ -196,7 +212,7 @@ def _make_test_comparing_frames(
196
212
  )
197
213
  testRenderer = _make_test_renderer_class(renderer_class)
198
214
 
199
- def real_test():
215
+ def real_test() -> None:
200
216
  with frames_tester.testing():
201
217
  sceneTested = _make_test_scene_class(
202
218
  base_scene=base_scene,
@@ -205,11 +221,13 @@ def _make_test_comparing_frames(
205
221
  # If you pass a custom renderer to the Scene, the Camera class given as an argument in the Scene
206
222
  # is not passed to the renderer. See __init__ of Scene.
207
223
  # This potentially prevents OpenGL testing.
208
- test_renderer=testRenderer(file_writer_class=file_writer_class)
209
- if base_scene is not ThreeDScene
210
- else testRenderer(
211
- file_writer_class=file_writer_class,
212
- camera_class=ThreeDCamera,
224
+ test_renderer=(
225
+ testRenderer(file_writer_class=file_writer_class)
226
+ if base_scene is not ThreeDScene
227
+ else testRenderer(
228
+ file_writer_class=file_writer_class,
229
+ camera_class=ThreeDCamera,
230
+ )
213
231
  ), # testRenderer(file_writer_class=file_writer_class),
214
232
  )
215
233
  scene_tested = sceneTested(skip_animations=True)
manim/utils/tex.py CHANGED
@@ -4,160 +4,135 @@ from __future__ import annotations
4
4
 
5
5
  __all__ = [
6
6
  "TexTemplate",
7
- "TexTemplateFromFile",
8
7
  ]
9
8
 
10
9
  import copy
11
- import os
12
10
  import re
11
+ import warnings
12
+ from dataclasses import dataclass, field
13
13
  from pathlib import Path
14
+ from typing import TYPE_CHECKING, Any
14
15
 
16
+ if TYPE_CHECKING:
17
+ from typing_extensions import Self
15
18
 
16
- class TexTemplate:
17
- """TeX templates are used for creating Tex() and MathTex() objects.
18
-
19
- Parameters
20
- ----------
21
- tex_compiler
22
- The TeX compiler to be used, e.g. ``latex``, ``pdflatex`` or ``lualatex``
23
- output_format
24
- The output format resulting from compilation, e.g. ``.dvi`` or ``.pdf``
25
- documentclass
26
- The command defining the documentclass, e.g. ``\\documentclass[preview]{standalone}``
27
- preamble
28
- The document's preamble, i.e. the part between ``\\documentclass`` and ``\\begin{document}``
29
- placeholder_text
30
- Text in the document that will be replaced by the expression to be rendered
31
- post_doc_commands
32
- Text (definitions, commands) to be inserted at right after ``\\begin{document}``, e.g. ``\\boldmath``
33
-
34
- Attributes
35
- ----------
36
- tex_compiler : :class:`str`
37
- The TeX compiler to be used, e.g. ``latex``, ``pdflatex`` or ``lualatex``
38
- output_format : :class:`str`
39
- The output format resulting from compilation, e.g. ``.dvi`` or ``.pdf``
40
- documentclass : :class:`str`
41
- The command defining the documentclass, e.g. ``\\documentclass[preview]{standalone}``
42
- preamble : :class:`str`
43
- The document's preamble, i.e. the part between ``\\documentclass`` and ``\\begin{document}``
44
- placeholder_text : :class:`str`
45
- Text in the document that will be replaced by the expression to be rendered
46
- post_doc_commands : :class:`str`
47
- Text (definitions, commands) to be inserted at right after ``\\begin{document}``, e.g. ``\\boldmath``
48
- """
19
+ from manim.typing import StrPath
49
20
 
50
- default_documentclass = r"\documentclass[preview]{standalone}"
51
- default_preamble = r"""
52
- \usepackage[english]{babel}
21
+ _DEFAULT_PREAMBLE = r"""\usepackage[english]{babel}
53
22
  \usepackage{amsmath}
54
- \usepackage{amssymb}
55
- """
56
- default_placeholder_text = "YourTextHere"
57
- default_tex_compiler = "latex"
58
- default_output_format = ".dvi"
59
- default_post_doc_commands = ""
60
-
61
- def __init__(
62
- self,
63
- tex_compiler: str | None = None,
64
- output_format: str | None = None,
65
- documentclass: str | None = None,
66
- preamble: str | None = None,
67
- placeholder_text: str | None = None,
68
- post_doc_commands: str | None = None,
69
- **kwargs,
70
- ):
71
- self.tex_compiler = (
72
- tex_compiler
73
- if tex_compiler is not None
74
- else TexTemplate.default_tex_compiler
75
- )
76
- self.output_format = (
77
- output_format
78
- if output_format is not None
79
- else TexTemplate.default_output_format
80
- )
81
- self.documentclass = (
82
- documentclass
83
- if documentclass is not None
84
- else TexTemplate.default_documentclass
85
- )
86
- self.preamble = (
87
- preamble if preamble is not None else TexTemplate.default_preamble
88
- )
89
- self.placeholder_text = (
90
- placeholder_text
91
- if placeholder_text is not None
92
- else TexTemplate.default_placeholder_text
93
- )
94
- self.post_doc_commands = (
95
- post_doc_commands
96
- if post_doc_commands is not None
97
- else TexTemplate.default_post_doc_commands
98
- )
99
- self._rebuild()
100
-
101
- def __eq__(self, other: TexTemplate) -> bool:
102
- return (
103
- self.body == other.body
104
- and self.tex_compiler == other.tex_compiler
105
- and self.output_format == other.output_format
106
- and self.post_doc_commands == other.post_doc_commands
107
- )
23
+ \usepackage{amssymb}"""
24
+
25
+ _BEGIN_DOCUMENT = r"\begin{document}"
26
+ _END_DOCUMENT = r"\end{document}"
27
+
108
28
 
109
- def _rebuild(self):
110
- """Rebuilds the entire TeX template text from ``\\documentclass`` to ``\\end{document}`` according to all settings and choices."""
111
- self.body = (
112
- self.documentclass
113
- + "\n"
114
- + self.preamble
115
- + "\n"
116
- + r"\begin{document}"
117
- + "\n"
118
- + self.post_doc_commands
119
- + "\n"
120
- + self.placeholder_text
121
- + "\n"
122
- + "\n"
123
- + r"\end{document}"
124
- + "\n"
29
+ @dataclass(eq=True)
30
+ class TexTemplate:
31
+ """TeX templates are used to create ``Tex`` and ``MathTex`` objects."""
32
+
33
+ _body: str = field(default="", init=False)
34
+ """A custom body, can be set from a file."""
35
+
36
+ tex_compiler: str = "latex"
37
+ """The TeX compiler to be used, e.g. ``latex``, ``pdflatex`` or ``lualatex``."""
38
+
39
+ description: str = ""
40
+ """A description of the template"""
41
+
42
+ output_format: str = ".dvi"
43
+ """The output format resulting from compilation, e.g. ``.dvi`` or ``.pdf``."""
44
+
45
+ documentclass: str = r"\documentclass[preview]{standalone}"
46
+ r"""The command defining the documentclass, e.g. ``\documentclass[preview]{standalone}``."""
47
+
48
+ preamble: str = _DEFAULT_PREAMBLE
49
+ r"""The document's preamble, i.e. the part between ``\documentclass`` and ``\begin{document}``."""
50
+
51
+ placeholder_text: str = "YourTextHere"
52
+ """Text in the document that will be replaced by the expression to be rendered."""
53
+
54
+ post_doc_commands: str = ""
55
+ r"""Text (definitions, commands) to be inserted at right after ``\begin{document}``, e.g. ``\boldmath``."""
56
+
57
+ @property
58
+ def body(self) -> str:
59
+ """The entire TeX template."""
60
+ return self._body or "\n".join(
61
+ filter(
62
+ None,
63
+ [
64
+ self.documentclass,
65
+ self.preamble,
66
+ _BEGIN_DOCUMENT,
67
+ self.post_doc_commands,
68
+ self.placeholder_text,
69
+ _END_DOCUMENT,
70
+ ],
71
+ )
125
72
  )
126
73
 
127
- def add_to_preamble(self, txt: str, prepend: bool = False):
128
- """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.
74
+ @body.setter
75
+ def body(self, value: str) -> None:
76
+ self._body = value
77
+
78
+ @classmethod
79
+ def from_file(cls, file: StrPath = "tex_template.tex", **kwargs: Any) -> Self:
80
+ """Create an instance by reading the content of a file.
81
+
82
+ Using the ``add_to_preamble`` and ``add_to_document`` methods on this instance
83
+ will have no effect, as the body is read from the file.
84
+ """
85
+ instance = cls(**kwargs)
86
+ instance.body = Path(file).read_text(encoding="utf-8")
87
+ return instance
88
+
89
+ def add_to_preamble(self, txt: str, prepend: bool = False) -> Self:
90
+ 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.
129
91
 
130
92
  Parameters
131
93
  ----------
132
94
  txt
133
- String containing the text to be added, e.g. ``\\usepackage{hyperref}``
95
+ String containing the text to be added, e.g. ``\usepackage{hyperref}``.
134
96
  prepend
135
- 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}``
97
+ Whether the text should be added at the beginning of the preamble, i.e. right after ``\documentclass``.
98
+ Default is to add it at the end of the preamble, i.e. right before ``\begin{document}``.
136
99
  """
100
+ if self._body:
101
+ warnings.warn(
102
+ "This TeX template was created with a fixed body, trying to add text the preamble will have no effect.",
103
+ UserWarning,
104
+ stacklevel=2,
105
+ )
137
106
  if prepend:
138
107
  self.preamble = txt + "\n" + self.preamble
139
108
  else:
140
109
  self.preamble += "\n" + txt
141
- self._rebuild()
110
+ return self
142
111
 
143
- def add_to_document(self, txt: str):
144
- """Adds txt to the TeX template just after \\begin{document}, e.g. ``\\boldmath``
112
+ def add_to_document(self, txt: str) -> Self:
113
+ r"""Adds text to the TeX template just after \begin{document}, e.g. ``\boldmath``.
145
114
 
146
115
  Parameters
147
116
  ----------
148
117
  txt
149
118
  String containing the text to be added.
150
119
  """
151
- self.post_doc_commands += "\n" + txt + "\n"
152
- self._rebuild()
153
-
154
- def get_texcode_for_expression(self, expression: str):
155
- """Inserts expression verbatim into TeX template.
120
+ if self._body:
121
+ warnings.warn(
122
+ "This TeX template was created with a fixed body, trying to add text the document will have no effect.",
123
+ UserWarning,
124
+ stacklevel=2,
125
+ )
126
+ self.post_doc_commands += txt
127
+ return self
128
+
129
+ def get_texcode_for_expression(self, expression: str) -> str:
130
+ r"""Inserts expression verbatim into TeX template.
156
131
 
157
132
  Parameters
158
133
  ----------
159
134
  expression
160
- The string containing the expression to be typeset, e.g. ``$\\sqrt{2}$``
135
+ The string containing the expression to be typeset, e.g. ``$\sqrt{2}$``
161
136
 
162
137
  Returns
163
138
  -------
@@ -166,102 +141,59 @@ class TexTemplate:
166
141
  """
167
142
  return self.body.replace(self.placeholder_text, expression)
168
143
 
169
- def _texcode_for_environment(self, environment: str):
170
- """Processes the tex_environment string to return the correct ``\\begin{environment}[extra]{extra}`` and
171
- ``\\end{environment}`` strings
172
-
173
- Parameters
174
- ----------
175
- environment
176
- The tex_environment as a string. Acceptable formats include:
177
- ``{align*}``, ``align*``, ``{tabular}[t]{cccl}``, ``tabular}{cccl``, ``\\begin{tabular}[t]{cccl}``.
178
-
179
- Returns
180
- -------
181
- Tuple[:class:`str`, :class:`str`]
182
- A pair of strings representing the opening and closing of the tex environment, e.g.
183
- ``\\begin{tabular}{cccl}`` and ``\\end{tabular}``
184
- """
185
-
186
- # If the environment starts with \begin, remove it
187
- if environment[0:6] == r"\begin":
188
- environment = environment[6:]
189
-
190
- # If environment begins with { strip it
191
- if environment[0] == r"{":
192
- environment = environment[1:]
193
-
194
- # The \begin command takes everything and closes with a brace
195
- begin = r"\begin{" + environment
196
- if (
197
- begin[-1] != r"}" and begin[-1] != r"]"
198
- ): # If it doesn't end on } or ], assume missing }
199
- begin += r"}"
200
-
201
- # While the \end command terminates at the first closing brace
202
- split_at_brace = re.split(r"}", environment, 1)
203
- end = r"\end{" + split_at_brace[0] + r"}"
204
-
205
- return begin, end
206
-
207
- def get_texcode_for_expression_in_env(self, expression: str, environment: str):
208
- r"""Inserts expression into TeX template wrapped in \begin{environment} and \end{environment}
144
+ def get_texcode_for_expression_in_env(
145
+ self, expression: str, environment: str
146
+ ) -> str:
147
+ r"""Inserts expression into TeX template wrapped in ``\begin{environment}`` and ``\end{environment}``.
209
148
 
210
149
  Parameters
211
150
  ----------
212
151
  expression
213
- The string containing the expression to be typeset, e.g. ``$\\sqrt{2}$``
152
+ The string containing the expression to be typeset, e.g. ``$\sqrt{2}$``.
214
153
  environment
215
- The string containing the environment in which the expression should be typeset, e.g. ``align*``
154
+ The string containing the environment in which the expression should be typeset, e.g. ``align*``.
216
155
 
217
156
  Returns
218
157
  -------
219
158
  :class:`str`
220
159
  LaTeX code based on template, containing the given expression inside its environment, ready for typesetting
221
160
  """
222
- begin, end = self._texcode_for_environment(environment)
223
- return self.body.replace(self.placeholder_text, f"{begin}\n{expression}\n{end}")
161
+ begin, end = _texcode_for_environment(environment)
162
+ return self.body.replace(
163
+ self.placeholder_text, "\n".join([begin, expression, end])
164
+ )
224
165
 
225
- def copy(self) -> TexTemplate:
166
+ def copy(self) -> Self:
167
+ """Create a deep copy of the TeX template instance."""
226
168
  return copy.deepcopy(self)
227
169
 
228
170
 
229
- class TexTemplateFromFile(TexTemplate):
230
- """A TexTemplate object created from a template file (default: tex_template.tex)
171
+ def _texcode_for_environment(environment: str) -> tuple[str, str]:
172
+ r"""Processes the tex_environment string to return the correct ``\begin{environment}[extra]{extra}`` and
173
+ ``\end{environment}`` strings.
231
174
 
232
175
  Parameters
233
176
  ----------
234
- tex_filename
235
- Path to a valid TeX template file
236
- kwargs
237
- Arguments for :class:`~.TexTemplate`.
238
-
239
- Attributes
240
- ----------
241
- template_file : :class:`str`
242
- Path to a valid TeX template file
243
- body : :class:`str`
244
- Content of the TeX template file
245
- tex_compiler : :class:`str`
246
- The TeX compiler to be used, e.g. ``latex``, ``pdflatex`` or ``lualatex``
247
- output_format : :class:`str`
248
- The output format resulting from compilation, e.g. ``.dvi`` or ``.pdf``
177
+ environment
178
+ The tex_environment as a string. Acceptable formats include:
179
+ ``{align*}``, ``align*``, ``{tabular}[t]{cccl}``, ``tabular}{cccl``, ``\begin{tabular}[t]{cccl}``.
180
+
181
+ Returns
182
+ -------
183
+ Tuple[:class:`str`, :class:`str`]
184
+ A pair of strings representing the opening and closing of the tex environment, e.g.
185
+ ``\begin{tabular}{cccl}`` and ``\end{tabular}``
249
186
  """
187
+ environment = environment.removeprefix(r"\begin").removeprefix("{")
250
188
 
251
- def __init__(
252
- self, *, tex_filename: str | os.PathLike = "tex_template.tex", **kwargs
253
- ):
254
- self.template_file = Path(tex_filename)
255
- super().__init__(**kwargs)
256
-
257
- def _rebuild(self):
258
- self.body = self.template_file.read_text()
259
-
260
- def file_not_mutable(self):
261
- raise Exception("Cannot modify TexTemplate when using a template file.")
189
+ # The \begin command takes everything and closes with a brace
190
+ begin = r"\begin{" + environment
191
+ # If it doesn't end on } or ], assume missing }
192
+ if not begin.endswith(("}", "]")):
193
+ begin += "}"
262
194
 
263
- def add_to_preamble(self, txt, prepend=False):
264
- self.file_not_mutable()
195
+ # While the \end command terminates at the first closing brace
196
+ split_at_brace = re.split("}", environment, maxsplit=1)
197
+ end = r"\end{" + split_at_brace[0] + "}"
265
198
 
266
- def add_to_document(self, txt):
267
- self.file_not_mutable()
199
+ return begin, end