python-color-math 0.2.12__tar.gz → 0.2.14__tar.gz

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 (80) hide show
  1. {python_color_math-0.2.12/python_color_math.egg-info → python_color_math-0.2.14}/PKG-INFO +4 -1
  2. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/__init__.py +7 -1
  3. python_color_math-0.2.14/color_math/ipython.py +183 -0
  4. python_color_math-0.2.14/color_math/jupyter.py +99 -0
  5. {python_color_math-0.2.12 → python_color_math-0.2.14}/pyproject.toml +3 -2
  6. {python_color_math-0.2.12 → python_color_math-0.2.14/python_color_math.egg-info}/PKG-INFO +4 -1
  7. {python_color_math-0.2.12 → python_color_math-0.2.14}/python_color_math.egg-info/SOURCES.txt +3 -0
  8. {python_color_math-0.2.12 → python_color_math-0.2.14}/python_color_math.egg-info/requires.txt +4 -0
  9. python_color_math-0.2.14/tests/test_ipython.py +89 -0
  10. {python_color_math-0.2.12 → python_color_math-0.2.14}/.pre-commit-hooks.yaml +0 -0
  11. {python_color_math-0.2.12 → python_color_math-0.2.14}/LICENSE +0 -0
  12. {python_color_math-0.2.12 → python_color_math-0.2.14}/MANIFEST.in +0 -0
  13. {python_color_math-0.2.12 → python_color_math-0.2.14}/README.md +0 -0
  14. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/__main__.py +0 -0
  15. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/adapters.py +0 -0
  16. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/colormath.default.json +0 -0
  17. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/completions.py +0 -0
  18. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/config.py +0 -0
  19. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/converters/__init__.py +0 -0
  20. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/converters/align.py +0 -0
  21. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/converters/block.py +0 -0
  22. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/converters/derivative.py +0 -0
  23. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/converters/equation.py +0 -0
  24. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/converters/generic.py +0 -0
  25. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/converters/integral.py +0 -0
  26. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/converters/limit.py +0 -0
  27. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/converters/matrix.py +0 -0
  28. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/converters/semantic.py +0 -0
  29. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/converters/unicode_converter.py +0 -0
  30. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/custom_definitions.py +0 -0
  31. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/gui.py +0 -0
  32. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/io.py +0 -0
  33. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/main.py +0 -0
  34. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/mcp.py +0 -0
  35. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/mcp_server.py +0 -0
  36. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/parsers/__init__.py +0 -0
  37. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/parsers/alignment.py +0 -0
  38. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/parsers/braket.py +0 -0
  39. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/parsers/constants.py +0 -0
  40. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/parsers/delimiters.py +0 -0
  41. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/parsers/differentials.py +0 -0
  42. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/parsers/dimensionless.py +0 -0
  43. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/parsers/frontmatter.py +0 -0
  44. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/parsers/latex_spans.py +0 -0
  45. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/parsers/markdown_scanner.py +0 -0
  46. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/parsers/math_parser.py +0 -0
  47. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/parsers/physics.py +0 -0
  48. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/parsers/scanner.py +0 -0
  49. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/parsers/taxonomy.py +0 -0
  50. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/parsers/units.py +0 -0
  51. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/parsers/variable_hash.py +0 -0
  52. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/py.typed +0 -0
  53. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/self_test.py +0 -0
  54. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/tutorial.py +0 -0
  55. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/undo.py +0 -0
  56. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/unicode.py +0 -0
  57. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/utils/__init__.py +0 -0
  58. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/utils/coloring.py +0 -0
  59. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/utils/latex_helpers.py +0 -0
  60. {python_color_math-0.2.12 → python_color_math-0.2.14}/color_math/utils/spans.py +0 -0
  61. {python_color_math-0.2.12 → python_color_math-0.2.14}/python_color_math/__init__.py +0 -0
  62. {python_color_math-0.2.12 → python_color_math-0.2.14}/python_color_math/__main__.py +0 -0
  63. {python_color_math-0.2.12 → python_color_math-0.2.14}/python_color_math.egg-info/dependency_links.txt +0 -0
  64. {python_color_math-0.2.12 → python_color_math-0.2.14}/python_color_math.egg-info/entry_points.txt +0 -0
  65. {python_color_math-0.2.12 → python_color_math-0.2.14}/python_color_math.egg-info/top_level.txt +0 -0
  66. {python_color_math-0.2.12 → python_color_math-0.2.14}/setup.cfg +0 -0
  67. {python_color_math-0.2.12 → python_color_math-0.2.14}/tests/test_adversarial.py +0 -0
  68. {python_color_math-0.2.12 → python_color_math-0.2.14}/tests/test_cli.py +0 -0
  69. {python_color_math-0.2.12 → python_color_math-0.2.14}/tests/test_config_tolerance.py +0 -0
  70. {python_color_math-0.2.12 → python_color_math-0.2.14}/tests/test_custom_definitions.py +0 -0
  71. {python_color_math-0.2.12 → python_color_math-0.2.14}/tests/test_extreme_adversarial.py +0 -0
  72. {python_color_math-0.2.12 → python_color_math-0.2.14}/tests/test_features.py +0 -0
  73. {python_color_math-0.2.12 → python_color_math-0.2.14}/tests/test_frontmatter.py +0 -0
  74. {python_color_math-0.2.12 → python_color_math-0.2.14}/tests/test_fuzz.py +0 -0
  75. {python_color_math-0.2.12 → python_color_math-0.2.14}/tests/test_gui.py +0 -0
  76. {python_color_math-0.2.12 → python_color_math-0.2.14}/tests/test_mcp_hardcore.py +0 -0
  77. {python_color_math-0.2.12 → python_color_math-0.2.14}/tests/test_mcp_server.py +0 -0
  78. {python_color_math-0.2.12 → python_color_math-0.2.14}/tests/test_physics.py +0 -0
  79. {python_color_math-0.2.12 → python_color_math-0.2.14}/tests/test_unicode.py +0 -0
  80. {python_color_math-0.2.12 → python_color_math-0.2.14}/tests/test_unicode_converter.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-color-math
3
- Version: 0.2.12
3
+ Version: 0.2.14
4
4
  Summary: A fast, local semantic LaTeX colorizer for Markdown, Jupyter, and LaTeX math equations.
5
5
  Author: Aditya
6
6
  License: MIT License
@@ -56,10 +56,13 @@ Requires-Dist: mcp>=1.0.0; extra == "mcp"
56
56
  Provides-Extra: tutorial
57
57
  Requires-Dist: rich>=13.0.0; extra == "tutorial"
58
58
  Requires-Dist: questionary>=2.0.0; extra == "tutorial"
59
+ Provides-Extra: jupyter
60
+ Requires-Dist: ipython>=8.0.0; extra == "jupyter"
59
61
  Provides-Extra: all
60
62
  Requires-Dist: mcp>=1.0.0; extra == "all"
61
63
  Requires-Dist: rich>=13.0.0; extra == "all"
62
64
  Requires-Dist: questionary>=2.0.0; extra == "all"
65
+ Requires-Dist: ipython>=8.0.0; extra == "all"
63
66
  Dynamic: license-file
64
67
 
65
68
  # 🎨 Python Color Math
@@ -5,13 +5,19 @@ from .config import ColorMathOptions
5
5
  from .converters.block import convert_text
6
6
  from .undo import uncolor_text
7
7
 
8
- __version__ = "0.2.12"
8
+ from .ipython import ColorMath, colormath, load_ipython_extension, unload_ipython_extension
9
+
10
+ __version__ = "0.2.14"
9
11
 
10
12
  __all__ = [
11
13
  "__version__",
14
+ "ColorMath",
12
15
  "ColorMathOptions",
16
+ "colormath",
13
17
  "convert_text",
14
18
  "detect_format",
19
+ "load_ipython_extension",
15
20
  "transform_document",
16
21
  "uncolor_text",
22
+ "unload_ipython_extension",
17
23
  ]
@@ -0,0 +1,183 @@
1
+ """IPython and Jupyter integration for color-math.
2
+
3
+ Enables seamless colorized math in Jupyter Notebook 7, JupyterLab, Classic
4
+ Notebook, and Google Colab via `%load_ext color_math`.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import shlex
10
+ from typing import Any
11
+
12
+ from .adapters import transform_document
13
+ from .config import ColorMathOptions, THEMES
14
+ from .converters.block import convert_math_block, convert_text
15
+
16
+ try:
17
+ from IPython.core.magic import Magics, cell_magic, line_magic, magics_class
18
+ from IPython.display import Markdown, Math, display
19
+ HAS_IPYTHON = True
20
+ except ImportError:
21
+ HAS_IPYTHON = False
22
+ Magics = object # type: ignore[misc,assignment]
23
+ def magics_class(cls: Any) -> Any: return cls
24
+ def line_magic(func: Any) -> Any: return func
25
+ def cell_magic(func: Any) -> Any: return func
26
+
27
+
28
+ def colormath(
29
+ text: str,
30
+ palette: dict[str, str] | None = None,
31
+ options: ColorMathOptions | None = None,
32
+ theme: str | None = None,
33
+ is_fragment: bool = False,
34
+ ) -> str:
35
+ """Convenience helper to colorize LaTeX math or Markdown math.
36
+
37
+ If is_fragment is True, text is treated as raw math without $ delimiters.
38
+ """
39
+ active_palette = THEMES.get(theme) if theme else palette
40
+ if is_fragment:
41
+ converted = convert_math_block(f"$${text}$$", palette=active_palette, options=options)
42
+ return converted[2:-2]
43
+ return convert_text(text, palette=active_palette, options=options)
44
+
45
+
46
+ if HAS_IPYTHON:
47
+ class ColorMath(Math):
48
+ """A drop-in replacement for IPython.display.Math that renders with semantic colors."""
49
+
50
+ def __init__(
51
+ self,
52
+ data: Any = None,
53
+ palette: dict[str, str] | None = None,
54
+ options: ColorMathOptions | None = None,
55
+ theme: str | None = None,
56
+ **kwargs: Any,
57
+ ) -> None:
58
+ raw = "" if data is None else str(data)
59
+ colored = colormath(raw, palette=palette, options=options, theme=theme, is_fragment=True)
60
+ super().__init__(data=colored, **kwargs)
61
+
62
+
63
+ @magics_class
64
+ class ColorMathMagics(Magics):
65
+ """IPython magics for semantic LaTeX colorization."""
66
+
67
+ def __init__(self, shell: Any) -> None:
68
+ super().__init__(shell)
69
+ self._auto_hook_active = False
70
+ self._orig_math_repr = None
71
+
72
+ @line_magic("color_math")
73
+ @line_magic("colormath")
74
+ def color_math_line(self, line: str) -> Any:
75
+ r"""Colorize and display a single LaTeX formula.
76
+
77
+ Usage:
78
+ %color_math \int_0^1 x^2 dx = \frac{1}{3}
79
+ %color_math --theme catppuccin \hat{H}\psi = E\psi
80
+ """
81
+ theme = None
82
+ raw_line = line.strip()
83
+ if raw_line.startswith("--theme "):
84
+ parts = shlex.split(raw_line)
85
+ if len(parts) >= 3 and parts[0] == "--theme":
86
+ theme = parts[1]
87
+ raw_line = " ".join(parts[2:])
88
+
89
+ if not raw_line:
90
+ display(Markdown("*Color Math: please provide a LaTeX expression.*"))
91
+ return None
92
+
93
+ # Render as display math
94
+ colored = colormath(raw_line, theme=theme, is_fragment=True)
95
+ return Math(colored)
96
+
97
+ @cell_magic("color_math")
98
+ @cell_magic("colormath")
99
+ def color_math_cell(self, line: str, cell: str) -> Any:
100
+ r"""Colorize and display a cell containing LaTeX or Markdown math.
101
+
102
+ Usage:
103
+ %%color_math
104
+ Here is the equation:
105
+ $$ \frac{df}{dx} = f'(x) $$
106
+ """
107
+ theme = None
108
+ args = shlex.split(line.strip()) if line.strip() else []
109
+ if len(args) >= 2 and args[0] == "--theme":
110
+ theme = args[1]
111
+
112
+ converted = colormath(cell, theme=theme, is_fragment=False)
113
+ display(Markdown(converted))
114
+
115
+ @line_magic("color_math_auto")
116
+ def color_math_auto(self, line: str) -> None:
117
+ """Toggle automatic semantic colorization for all IPython.display.Math outputs.
118
+
119
+ Usage:
120
+ %color_math_auto on
121
+ %color_math_auto off
122
+ %color_math_auto status
123
+ """
124
+ cmd = line.strip().lower()
125
+ if cmd in ("on", "enable", "1", "true"):
126
+ self.enable_auto_display()
127
+ print("Color Math: Automatic display coloring enabled for IPython.display.Math.")
128
+ elif cmd in ("off", "disable", "0", "false"):
129
+ self.disable_auto_display()
130
+ print("Color Math: Automatic display coloring disabled.")
131
+ else:
132
+ status = "ENABLED" if self._auto_hook_active else "DISABLED"
133
+ print(f"Color Math: Automatic display coloring is currently {status}.")
134
+
135
+ def enable_auto_display(self) -> None:
136
+ """Patch IPython.display.Math to automatically colorize its LaTeX payload."""
137
+ if self._auto_hook_active:
138
+ return
139
+ self._orig_math_repr = Math._repr_latex_
140
+
141
+ def _colorized_repr_latex(math_self: Math) -> str:
142
+ raw = self._orig_math_repr(math_self) # type: ignore[misc]
143
+ if not raw:
144
+ return raw
145
+ # Raw may be surrounded by $$ or not
146
+ if raw.startswith("$$") and raw.endswith("$$"):
147
+ body = raw[2:-2]
148
+ return f"$${colormath(body, is_fragment=True)}$$"
149
+ if raw.startswith("$") and raw.endswith("$"):
150
+ body = raw[1:-1]
151
+ return f"${colormath(body, is_fragment=True)}$"
152
+ return colormath(raw, is_fragment=True)
153
+
154
+ Math._repr_latex_ = _colorized_repr_latex # type: ignore[assignment]
155
+ self._auto_hook_active = True
156
+
157
+ def disable_auto_display(self) -> None:
158
+ """Restore original IPython.display.Math behavior."""
159
+ if not self._auto_hook_active:
160
+ return
161
+ if self._orig_math_repr is not None:
162
+ Math._repr_latex_ = self._orig_math_repr # type: ignore[assignment]
163
+ self._auto_hook_active = False
164
+
165
+
166
+ def load_ipython_extension(ipython: Any) -> None:
167
+ """Standard IPython extension loader."""
168
+ if not HAS_IPYTHON:
169
+ return
170
+ magics = ColorMathMagics(ipython)
171
+ ipython.register_magics(magics)
172
+ # Store reference so it can be cleanly unloaded
173
+ ipython.custom_exceptions = getattr(ipython, "custom_exceptions", {})
174
+ setattr(ipython, "_color_math_magics", magics)
175
+
176
+
177
+ def unload_ipython_extension(ipython: Any) -> None:
178
+ """Standard IPython extension unloader."""
179
+ if not HAS_IPYTHON:
180
+ return
181
+ magics = getattr(ipython, "_color_math_magics", None)
182
+ if magics is not None and hasattr(magics, "disable_auto_display"):
183
+ magics.disable_auto_display()
@@ -0,0 +1,99 @@
1
+ """Jupyter Server & Notebook 7 integration for color-math.
2
+
3
+ Provides automated pre-save formatting so that whenever a notebook is saved
4
+ in Jupyter Notebook 7 or JupyterLab, all LaTeX formulas in Markdown cells
5
+ are automatically colorized.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from .config import ColorMathOptions
15
+ from .converters.block import convert_text
16
+ from .undo import uncolor_text
17
+
18
+
19
+ HOOK_SNIPPET = """
20
+ # >>> color-math Jupyter pre-save hook >>>
21
+ try:
22
+ from color_math.jupyter import notebook_pre_save_hook
23
+ c.FileContentsManager.pre_save_hook = notebook_pre_save_hook
24
+ except ImportError:
25
+ pass
26
+ # <<< color-math Jupyter pre-save hook <<<
27
+ """
28
+
29
+
30
+ def notebook_pre_save_hook(
31
+ model: dict[str, Any],
32
+ os_path: str = "",
33
+ contents_manager: Any = None,
34
+ options: ColorMathOptions | None = None,
35
+ **kwargs: Any,
36
+ ) -> None:
37
+ """Jupyter FileContentsManager pre_save_hook.
38
+
39
+ Modifies the notebook model in-place before it is written to disk.
40
+ """
41
+ if model.get("type") != "notebook":
42
+ return
43
+
44
+ content = model.get("content")
45
+ if not isinstance(content, dict):
46
+ return
47
+
48
+ cells = content.get("cells")
49
+ if not isinstance(cells, list):
50
+ return
51
+
52
+ for cell in cells:
53
+ if not isinstance(cell, dict) or cell.get("cell_type") != "markdown":
54
+ continue
55
+
56
+ source = cell.get("source", "")
57
+ if isinstance(source, str):
58
+ joined = source
59
+ elif isinstance(source, list) and all(isinstance(part, str) for part in source):
60
+ joined = "".join(source)
61
+ else:
62
+ continue
63
+
64
+ converted = convert_text(joined, options=options)
65
+ if converted != joined:
66
+ if isinstance(source, str):
67
+ cell["source"] = converted
68
+ else:
69
+ cell["source"] = converted.splitlines(keepends=True) or ([""] if source else [])
70
+
71
+
72
+ def get_jupyter_config_dir() -> Path:
73
+ """Locate the active Jupyter user configuration directory."""
74
+ env_dir = os.environ.get("JUPYTER_CONFIG_DIR")
75
+ if env_dir:
76
+ return Path(env_dir)
77
+ return Path.home() / ".jupyter"
78
+
79
+
80
+ def install_jupyter_hook(config_dir: Path | str | None = None) -> Path:
81
+ """Install the pre-save hook into the user's Jupyter configuration."""
82
+ target_dir = Path(config_dir) if config_dir else get_jupyter_config_dir()
83
+ target_dir.mkdir(parents=True, exist_ok=True)
84
+
85
+ # In modern Jupyter (Notebook 7 / JupyterLab 4), jupyter_server_config.py is preferred
86
+ config_file = target_dir / "jupyter_server_config.py"
87
+ if not config_file.exists():
88
+ legacy_file = target_dir / "jupyter_notebook_config.py"
89
+ if legacy_file.exists():
90
+ config_file = legacy_file
91
+
92
+ existing_content = config_file.read_text(encoding="utf-8") if config_file.exists() else ""
93
+ if "color_math.jupyter" in existing_content:
94
+ return config_file
95
+
96
+ with open(config_file, "a", encoding="utf-8") as f:
97
+ f.write(HOOK_SNIPPET + "\n")
98
+
99
+ return config_file
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-color-math"
7
- version = "0.2.12"
7
+ version = "0.2.14"
8
8
  description = "A fast, local semantic LaTeX colorizer for Markdown, Jupyter, and LaTeX math equations."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -35,7 +35,8 @@ dependencies = []
35
35
  [project.optional-dependencies]
36
36
  mcp = ["mcp>=1.0.0"]
37
37
  tutorial = ["rich>=13.0.0", "questionary>=2.0.0"]
38
- all = ["mcp>=1.0.0", "rich>=13.0.0", "questionary>=2.0.0"]
38
+ jupyter = ["ipython>=8.0.0"]
39
+ all = ["mcp>=1.0.0", "rich>=13.0.0", "questionary>=2.0.0", "ipython>=8.0.0"]
39
40
 
40
41
  [project.urls]
41
42
  Homepage = "https://github.com/36ty-blip/python-color-math"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-color-math
3
- Version: 0.2.12
3
+ Version: 0.2.14
4
4
  Summary: A fast, local semantic LaTeX colorizer for Markdown, Jupyter, and LaTeX math equations.
5
5
  Author: Aditya
6
6
  License: MIT License
@@ -56,10 +56,13 @@ Requires-Dist: mcp>=1.0.0; extra == "mcp"
56
56
  Provides-Extra: tutorial
57
57
  Requires-Dist: rich>=13.0.0; extra == "tutorial"
58
58
  Requires-Dist: questionary>=2.0.0; extra == "tutorial"
59
+ Provides-Extra: jupyter
60
+ Requires-Dist: ipython>=8.0.0; extra == "jupyter"
59
61
  Provides-Extra: all
60
62
  Requires-Dist: mcp>=1.0.0; extra == "all"
61
63
  Requires-Dist: rich>=13.0.0; extra == "all"
62
64
  Requires-Dist: questionary>=2.0.0; extra == "all"
65
+ Requires-Dist: ipython>=8.0.0; extra == "all"
63
66
  Dynamic: license-file
64
67
 
65
68
  # 🎨 Python Color Math
@@ -12,6 +12,8 @@ color_math/config.py
12
12
  color_math/custom_definitions.py
13
13
  color_math/gui.py
14
14
  color_math/io.py
15
+ color_math/ipython.py
16
+ color_math/jupyter.py
15
17
  color_math/main.py
16
18
  color_math/mcp.py
17
19
  color_math/mcp_server.py
@@ -68,6 +70,7 @@ tests/test_features.py
68
70
  tests/test_frontmatter.py
69
71
  tests/test_fuzz.py
70
72
  tests/test_gui.py
73
+ tests/test_ipython.py
71
74
  tests/test_mcp_hardcore.py
72
75
  tests/test_mcp_server.py
73
76
  tests/test_physics.py
@@ -3,6 +3,10 @@
3
3
  mcp>=1.0.0
4
4
  rich>=13.0.0
5
5
  questionary>=2.0.0
6
+ ipython>=8.0.0
7
+
8
+ [jupyter]
9
+ ipython>=8.0.0
6
10
 
7
11
  [mcp]
8
12
  mcp>=1.0.0
@@ -0,0 +1,89 @@
1
+ """Tests for IPython and Jupyter integrations."""
2
+
3
+ import unittest
4
+ from IPython.core.interactiveshell import InteractiveShell
5
+ from IPython.display import Math
6
+
7
+ from color_math.ipython import (
8
+ ColorMath,
9
+ colormath,
10
+ load_ipython_extension,
11
+ unload_ipython_extension,
12
+ )
13
+ from color_math.jupyter import notebook_pre_save_hook
14
+
15
+
16
+ class TestIPythonIntegration(unittest.TestCase):
17
+ def setUp(self):
18
+ self.shell = InteractiveShell.instance()
19
+
20
+ def test_colormath_helper(self):
21
+ # Fragment test (no dollar signs)
22
+ frag = colormath(r"\int_0^1 x^2 dx = \frac{1}{3}", is_fragment=True)
23
+ self.assertIn(r"\textcolor{#e0af68}{\int", frag)
24
+ self.assertIn(r"\textcolor{white}{=}", frag)
25
+
26
+ # Full markdown test
27
+ md = colormath("Let $f(x) = x^2$.")
28
+ self.assertIn(r"$\textcolor{#7aa2f7}{f}(x)", md)
29
+
30
+ def test_colormath_display_object(self):
31
+ obj = ColorMath(r"\frac{df}{dx} = f'(x)")
32
+ self.assertIsInstance(obj, Math)
33
+ self.assertIn(r"\textcolor{#7aa2f7}{f'}(x)", obj.data)
34
+
35
+ def test_load_and_unload_extension(self):
36
+ load_ipython_extension(self.shell)
37
+ self.assertTrue(self.shell.find_magic("color_math") is not None)
38
+ self.assertTrue(self.shell.find_magic("colormath") is not None)
39
+ self.assertTrue(self.shell.find_magic("color_math_auto") is not None)
40
+
41
+ # Execute line magic
42
+ result = self.shell.run_line_magic("color_math", r"\int x^2 dx")
43
+ self.assertIsInstance(result, Math)
44
+ self.assertIn(r"\textcolor{#e0af68}{\int", result.data)
45
+
46
+ # Execute cell magic
47
+ cell_body = "The equation is:\n$$ \\psi + \\omega $$"
48
+ self.shell.run_cell_magic("color_math", "", cell_body)
49
+
50
+ # Test auto hook
51
+ self.shell.run_line_magic("color_math_auto", "on")
52
+ # Creating a standard IPython Math object should now produce colored latex in _repr_latex_
53
+ standard_math = Math(r"\int x^2 dx")
54
+ repr_latex = standard_math._repr_latex_()
55
+ self.assertIn(r"\textcolor{#e0af68}{\int", repr_latex)
56
+
57
+ self.shell.run_line_magic("color_math_auto", "off")
58
+ unload_ipython_extension(self.shell)
59
+
60
+ def test_jupyter_pre_save_hook(self):
61
+ model = {
62
+ "type": "notebook",
63
+ "content": {
64
+ "cells": [
65
+ {
66
+ "cell_type": "markdown",
67
+ "source": "Calculate $$\\int_0^1 x dx = \\frac{1}{2}$$.",
68
+ },
69
+ {
70
+ "cell_type": "code",
71
+ "source": "x = 1\nprint(x)",
72
+ },
73
+ ]
74
+ },
75
+ }
76
+
77
+ notebook_pre_save_hook(model)
78
+
79
+ # Markdown cell was transformed
80
+ md_cell = model["content"]["cells"][0]
81
+ self.assertIn(r"\textcolor{#e0af68}{\int", md_cell["source"])
82
+
83
+ # Code cell was untouched
84
+ code_cell = model["content"]["cells"][1]
85
+ self.assertEqual(code_cell["source"], "x = 1\nprint(x)")
86
+
87
+
88
+ if __name__ == "__main__":
89
+ unittest.main()