rubberize 0.1.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Chito Peralta
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.2
2
+ Name: rubberize
3
+ Version: 0.1.0
4
+ Summary: Turn Python calculations into well-formatted, math-rich documents.
5
+ Author-email: Chito Peralta <chitoangeloperalta@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Chito Peralta
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ Project-URL: repository, https://github.com/chitoperalta/rubberize
28
+ Project-URL: homepage, https://github.com/chitoperalta/rubberize
29
+ Keywords: latex,markdown,jupyter,notebook,math,engineering
30
+ Requires-Python: >=3.11
31
+ Description-Content-Type: text/markdown
32
+ License-File: LICENSE
33
+ Requires-Dist: markdown<4.0,>=3.7
34
+ Requires-Dist: titlecase<3.0.0,>=2.4.1
35
+ Provides-Extra: notebook
36
+ Requires-Dist: ipython<10.0.0,>=9.0.2; extra == "notebook"
37
+ Requires-Dist: playwright<2.0.0,>=1.51.0; extra == "notebook"
38
+ Provides-Extra: dev
39
+ Requires-Dist: ipython<10.0.0,>=9.0.2; extra == "dev"
40
+ Requires-Dist: playwright<2.0.0,>=1.51.0; extra == "dev"
41
+ Requires-Dist: jupyterlab<5.0.0,>=4.3.6; extra == "dev"
42
+ Requires-Dist: pint<0.25.0,>=0.24.4; extra == "dev"
43
+ Requires-Dist: sympy<2.0.0,>=1.13.3; extra == "dev"
44
+ Requires-Dist: numpy<3.0.0,>=2.2.4; extra == "dev"
45
+
46
+ # Rubberize
47
+
48
+ **Turn Python calculations into well-formatted, math-rich documents.**
49
+
50
+ ## Installation
51
+
52
+ Install Rubberize with `pip`:
53
+
54
+ ```bash
55
+ pip install rubberize
56
+ ```
57
+
58
+ Rubberize is primarily built for Jupyter. To enable notebook magics:
59
+
60
+ ```bash
61
+ pip install rubberize[notebook]
62
+ ```
63
+
64
+ ## Basic Usage
65
+
66
+ ### In Modules
67
+
68
+ Here's a quick example of how to use Rubberize:
69
+
70
+ ```python
71
+ import rubberize
72
+
73
+ source = """\
74
+ a + b
75
+ a - b
76
+ """
77
+
78
+ namespace = {"a": 1, "b": 2}
79
+ stmts_latex = rubberize.latexer(source, namespace)
80
+ ```
81
+
82
+ `stmts_latex` will be a list of `StmtLatex` objects that contain LaTeX representations of each statement.
83
+
84
+ ### In Notebooks
85
+
86
+ Rubberize is more impressive in notebooks (assuming `rubberize[notebooks]` is installed). In a Code cell, use `%%tap` and your code in the cell will be displayed as math notation, along with substitutions, results, and comments.
87
+
88
+ ## Contributing
89
+
90
+ To set up a development environment, install the supported dependencies:
91
+
92
+ ```bash
93
+ pip install rubberize[dev]
94
+ ```
95
+
96
+ ## License
97
+
98
+ MIT License © 2025 [Chito Peralta](mailto:chitoangeloperalta@gmail.com)
99
+
@@ -0,0 +1,54 @@
1
+ # Rubberize
2
+
3
+ **Turn Python calculations into well-formatted, math-rich documents.**
4
+
5
+ ## Installation
6
+
7
+ Install Rubberize with `pip`:
8
+
9
+ ```bash
10
+ pip install rubberize
11
+ ```
12
+
13
+ Rubberize is primarily built for Jupyter. To enable notebook magics:
14
+
15
+ ```bash
16
+ pip install rubberize[notebook]
17
+ ```
18
+
19
+ ## Basic Usage
20
+
21
+ ### In Modules
22
+
23
+ Here's a quick example of how to use Rubberize:
24
+
25
+ ```python
26
+ import rubberize
27
+
28
+ source = """\
29
+ a + b
30
+ a - b
31
+ """
32
+
33
+ namespace = {"a": 1, "b": 2}
34
+ stmts_latex = rubberize.latexer(source, namespace)
35
+ ```
36
+
37
+ `stmts_latex` will be a list of `StmtLatex` objects that contain LaTeX representations of each statement.
38
+
39
+ ### In Notebooks
40
+
41
+ Rubberize is more impressive in notebooks (assuming `rubberize[notebooks]` is installed). In a Code cell, use `%%tap` and your code in the cell will be displayed as math notation, along with substitutions, results, and comments.
42
+
43
+ ## Contributing
44
+
45
+ To set up a development environment, install the supported dependencies:
46
+
47
+ ```bash
48
+ pip install rubberize[dev]
49
+ ```
50
+
51
+ ## License
52
+
53
+ MIT License © 2025 [Chito Peralta](mailto:chitoangeloperalta@gmail.com)
54
+
@@ -0,0 +1,40 @@
1
+ [project]
2
+ name = "rubberize"
3
+ version = "0.1.0"
4
+ description = "Turn Python calculations into well-formatted, math-rich documents."
5
+ authors = [
6
+ {name = "Chito Peralta", email = "chitoangeloperalta@gmail.com"}
7
+ ]
8
+ keywords = ["latex", "markdown", "jupyter", "notebook", "math", "engineering"]
9
+ license = {file = "LICENSE"}
10
+ readme = "README.md"
11
+ requires-python = ">=3.11"
12
+ dependencies = [
13
+ "markdown>=3.7,<4.0",
14
+ "titlecase>=2.4.1,<3.0.0"
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ notebook = [
19
+ "ipython>=9.0.2,<10.0.0",
20
+ "playwright>=1.51.0,<2.0.0"
21
+ ]
22
+ dev = [
23
+ "ipython>=9.0.2,<10.0.0",
24
+ "playwright>=1.51.0,<2.0.0",
25
+ "jupyterlab>=4.3.6,<5.0.0",
26
+ "pint>=0.24.4,<0.25.0",
27
+ "sympy>=1.13.3,<2.0.0",
28
+ "numpy>=2.2.4,<3.0.0",
29
+ ]
30
+
31
+ [project.urls]
32
+ repository = "https://github.com/chitoperalta/rubberize"
33
+ homepage = "https://github.com/chitoperalta/rubberize"
34
+
35
+ [build-system]
36
+ requires = ["setuptools", "wheel"]
37
+ build-backend = "setuptools.build_meta"
38
+
39
+ [tool.setuptools]
40
+ packages = ["rubberize"]
@@ -0,0 +1,38 @@
1
+ """Rubberize turns Python calculations into well-formatted, math-rich
2
+ documents.
3
+ """
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ from rubberize.config import config
8
+
9
+ from rubberize.latexer import (
10
+ latexer,
11
+ CalcSheet,
12
+ Table,
13
+ register_call_converter,
14
+ register_object_converter,
15
+ )
16
+
17
+ try:
18
+ # If Pint is installed:
19
+ from rubberize.latexer import register_units_latex
20
+ except ImportError:
21
+ pass
22
+
23
+ from rubberize.render import render
24
+
25
+ try:
26
+ # If IPython is installed:
27
+ from rubberize.load_ipython_extension import load_ipython_extension
28
+ except ImportError:
29
+ pass
30
+
31
+ try:
32
+ # If exporter deps are installed:
33
+ from rubberize.export_notebook import (
34
+ export_notebook_to_html,
35
+ export_notebook_to_pdf,
36
+ )
37
+ except ImportError:
38
+ pass
@@ -0,0 +1,203 @@
1
+ """Config system.
2
+
3
+ The configuration is a singleton instance of `_Config`.
4
+ """
5
+
6
+ import ast
7
+ import json
8
+ from contextlib import contextmanager
9
+ from dataclasses import dataclass, field, asdict
10
+ from pathlib import Path
11
+ from typing import Any, Iterable, Literal, Optional
12
+
13
+ from rubberize import exceptions
14
+
15
+
16
+ @dataclass
17
+ class _ConfigDefaults: # pylint: disable=too-many-instance-attributes
18
+ """Default config."""
19
+
20
+ # Name
21
+ use_subscripts: bool = True
22
+ use_symbols: bool = True
23
+
24
+ # Display modes
25
+ show_definition: bool = True
26
+ show_substitution: bool = True
27
+ show_result: bool = True
28
+
29
+ # String display options
30
+ str_font: Literal["", "bf", "it", "rm", "sf", "tt"] = ""
31
+
32
+ # Number display options
33
+ num_format: Literal["FIX", "SCI", "GEN", "ENG"] = "FIX"
34
+ num_format_prec: int = 2
35
+ num_format_max_digits: int = 15
36
+ num_format_e_not: bool = False
37
+ thousands_separator: Literal["", " ", ",", ".", "'"] = " "
38
+ decimal_marker: Literal[".", ","] = "."
39
+
40
+ use_polar: bool = False
41
+ use_polar_deg: bool = True
42
+
43
+ use_inline_units: bool = True
44
+ use_dms_units: bool = False
45
+ use_fif_units: bool = False
46
+ fif_prec: int = 16
47
+
48
+ # Expressions
49
+ wrap_indices: bool = True
50
+ convert_special_funcs: bool = True
51
+ use_contextual_mult: bool = True
52
+ hidden_modules: set[str] = field(
53
+ default_factory=lambda: {"math", "sp", "np", "ureg"}
54
+ )
55
+ show_list_as_col: bool = True
56
+ show_tuple_as_col: bool = False
57
+ show_set_as_col: bool = False
58
+ show_dict_as_col: bool = True
59
+
60
+ # Statements
61
+ multiline: bool = False
62
+
63
+
64
+ class _Config(_ConfigDefaults):
65
+ """Singleton global configuration."""
66
+
67
+ def __init__(self):
68
+ super().__init__()
69
+ self.load()
70
+
71
+ def set(self, **kwargs: bool | int | Iterable[str]) -> None:
72
+ """Update multiple config values passed as keyword arguments."""
73
+
74
+ for key, value in kwargs.items():
75
+ if not hasattr(self, key):
76
+ raise AttributeError(f"Invalid config key: {key}")
77
+
78
+ if key == "hidden_modules":
79
+ if not isinstance(value, (set, list, tuple)):
80
+ raise TypeError(f"Invalid {key} type: {type(value)}")
81
+ value = set(value)
82
+
83
+ setattr(self, key, value)
84
+
85
+ def load(self, *args: str, path: Optional[str | Path] = None) -> None:
86
+ """Load config from defaults or a specified JSON file.
87
+
88
+ If a file exists for a given `path`, its contents are loaded and
89
+ used to update the config. Otherwise, default values are used.
90
+
91
+ Args:
92
+ *args: If provided, only the specified keys are updated.
93
+ path: Path to the JSON file. If `None`, the default values
94
+ are used.
95
+ """
96
+
97
+ data = asdict(_ConfigDefaults())
98
+
99
+ if path is not None:
100
+ path = Path(path)
101
+ if path.exists():
102
+ with path.open("r", encoding="utf-8") as f:
103
+ data.update(json.load(f))
104
+
105
+ if args:
106
+ data = {k: data[k] for k in args if k in data}
107
+ self.set(**data)
108
+
109
+ def reset(self, *args: str) -> None:
110
+ """Reset the config or only the specified keys to defaults."""
111
+
112
+ self.load(*args, path=None)
113
+
114
+ def save(self, path: str | Path) -> None:
115
+ """Save the current config to a JSON file."""
116
+
117
+ config_dict = asdict(self)
118
+ config_dict["hidden_modules"] = list(config_dict["hidden_modules"])
119
+
120
+ path = Path(path)
121
+ path.parent.mkdir(parents=True, exist_ok=True)
122
+
123
+ with path.open("w", encoding="utf-8") as f:
124
+ json.dump(config_dict, f, indent=4)
125
+
126
+ def add_hidden_module(self, *modules: str) -> None:
127
+ """Add one or more modules to `hidden_modules`."""
128
+
129
+ self.hidden_modules.update(modules)
130
+
131
+ def remove_hidden_module(self, *modules: str) -> None:
132
+ """Remove one or more modules from `hidden_modules`."""
133
+
134
+ self.hidden_modules.difference_update(modules)
135
+
136
+ @contextmanager
137
+ def override(self, **kwargs: bool | int | Iterable[str]):
138
+ """Temporarily override config values within a context."""
139
+
140
+ original_values = {key: getattr(self, key) for key in kwargs}
141
+
142
+ try:
143
+ self.set(**kwargs)
144
+ yield
145
+ finally:
146
+ self.set(**original_values)
147
+
148
+
149
+ # fmt: off
150
+ _KEYWORDS: dict[str, dict[str, Any]] = {
151
+ "none": {"show_definition": False, "show_substitution": False, "show_result": False},
152
+ "all": {"show_definition": True, "show_substitution": True, "show_result": True},
153
+ "def": {"show_definition": True, "show_substitution": False, "show_result": False},
154
+ "sub": {"show_definition": False, "show_substitution": True, "show_result": False},
155
+ "res": {"show_definition": False, "show_substitution": False, "show_result": True},
156
+ "nodef": {"show_definition": False, "show_substitution": True, "show_result": True},
157
+ "nosub": {"show_definition": True, "show_substitution": False, "show_result": True},
158
+ "nores": {"show_definition": True, "show_substitution": True, "show_result": False},
159
+ "line": {"multiline": False},
160
+ "stack": {"multiline": True},
161
+ "fix": {"num_format": "FIX"},
162
+ "sci": {"num_format": "SCI"},
163
+ "gen": {"num_format": "GEN"},
164
+ "eng": {"num_format": "ENG"},
165
+ "0": {"num_format_prec": 0},
166
+ "1": {"num_format_prec": 1},
167
+ "2": {"num_format_prec": 2},
168
+ "3": {"num_format_prec": 3},
169
+ "4": {"num_format_prec": 4},
170
+ "5": {"num_format_prec": 5},
171
+ "6": {"num_format_prec": 6},
172
+ }
173
+ # fmt: on
174
+
175
+
176
+ def parse_modifiers(modifiers: list[str]) -> dict[str, Any]:
177
+ """Parse a list of modifiers to an overrides dict for use with
178
+ `config.override()`.
179
+ """
180
+
181
+ override_dict = {}
182
+ for modifier in modifiers:
183
+ modifier = modifier.removeprefix("@")
184
+
185
+ if modifier == "hide":
186
+ return {"hide": ...}
187
+ if modifier == "endhide":
188
+ return {"endhide": ...}
189
+
190
+ if "=" in modifier:
191
+ key, value = modifier.split("=", 1)
192
+ override_dict[key] = ast.literal_eval(value)
193
+ elif modifier in _KEYWORDS:
194
+ override_dict.update(_KEYWORDS[modifier])
195
+ else:
196
+ raise exceptions.RubberizeKeywordError(
197
+ f"Unknown keyword: '{modifier}'"
198
+ )
199
+ return override_dict
200
+
201
+
202
+ # Singleton instance
203
+ config = _Config()
@@ -0,0 +1,33 @@
1
+ """Custom exceptions for the library."""
2
+
3
+
4
+ class RubberizeError(Exception):
5
+ """Base class for all rubberize exceptions."""
6
+
7
+
8
+ class RubberizeNotImplementedError(RubberizeError):
9
+ """This error is raised when the user attempts to use a feature that
10
+ is currently not implemented (but may be implemented in the future).
11
+ """
12
+
13
+
14
+ class RubberizeTypeError(RubberizeError, TypeError):
15
+ """This error is raised when the user attempts to pass the wrong
16
+ argument type.
17
+ """
18
+
19
+
20
+ class RubberizeValueError(RubberizeError, ValueError):
21
+ """This error is raised when the user attempts to pass the wrong
22
+ argument value of correct type.
23
+ """
24
+
25
+
26
+ class RubberizeSyntaxError(RubberizeError, SyntaxError):
27
+ """This error is raised when the user uses a feature incorrectly, or
28
+ attempts to use a feature that will not be implemented.
29
+ """
30
+
31
+
32
+ class RubberizeKeywordError(RubberizeError, KeyError):
33
+ """This error is raised when the user uses an unknown mapping key."""
@@ -0,0 +1,189 @@
1
+ """Functions to export IPython notebooks."""
2
+
3
+ import asyncio
4
+ import subprocess
5
+ import sys
6
+ import tempfile
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ import nest_asyncio
11
+ from playwright.sync_api import sync_playwright, Error
12
+ from playwright.async_api import async_playwright
13
+
14
+
15
+ _MATHJAX_WAITER = """
16
+ (function() {
17
+ if (typeof MathJax !== 'undefined' && MathJax.Hub) {
18
+ MathJax.Hub.Queue(["Typeset", MathJax.Hub]);
19
+
20
+ return new Promise((resolve, reject) => {
21
+ MathJax.Hub.Queue(() => {
22
+ if (MathJax.Hub.Queue.length === 0) {
23
+ setTimeout(() => {
24
+ resolve(true);
25
+ }, 500); // Wait for 500ms before resolve
26
+ } else {
27
+ reject('MathJax is still rendering');
28
+ }
29
+ });
30
+ });
31
+ }
32
+ return false;
33
+ })()
34
+ """
35
+
36
+
37
+ def export_notebook_to_html(
38
+ path: str | Path,
39
+ output: Optional[str | Path] = None,
40
+ *,
41
+ no_input: bool = True,
42
+ ) -> None:
43
+ """Export a Jupyter notebook to HTML using `nbconvert`.
44
+
45
+ Args:
46
+ path: The path to the notebook to convert.
47
+ output: Optional output path. If `None`, uses the input path but
48
+ with file extension changed to `.html`.
49
+ no_input: Whether to exclude input cells in the output. Defaults
50
+ to `True`.
51
+ """
52
+
53
+ path = Path(path)
54
+ output = Path(output) if output else path.with_suffix(".html")
55
+
56
+ subprocess.run(
57
+ [
58
+ "jupyter",
59
+ "nbconvert",
60
+ "--to",
61
+ "html",
62
+ "--no-input" if no_input else "",
63
+ str(path),
64
+ "--output",
65
+ str(output),
66
+ ],
67
+ check=True,
68
+ stdout=subprocess.DEVNULL,
69
+ stderr=subprocess.DEVNULL,
70
+ )
71
+
72
+
73
+ def _html_to_pdf(path: str | Path, output: Optional[str | Path] = None) -> None:
74
+ """Export an HTML file to PDF.
75
+
76
+ Use Playwright to open the HTML file, wait for MathJax to render,
77
+ and print it to PDF.
78
+ """
79
+
80
+ path = Path(path)
81
+ output = Path(output) if output else path.with_suffix(".pdf")
82
+
83
+ try:
84
+ _html_to_pdf_sync(path, output)
85
+ except Error:
86
+ if sys.platform.startswith("win"):
87
+ # Fix for windows, probably.
88
+ asyncio.set_event_loop_policy(
89
+ asyncio.WindowsSelectorEventLoopPolicy() # type: ignore
90
+ )
91
+ loop = asyncio.get_running_loop()
92
+ if loop.is_running():
93
+ # Use nest_asyncio to allow nested loops (e.g. in Jupyter)
94
+ nest_asyncio.apply()
95
+ new_loop = asyncio.new_event_loop()
96
+ new_loop.run_until_complete(_html_to_pdf_async(path, output))
97
+ new_loop.close()
98
+ else:
99
+ loop.run_until_complete(_html_to_pdf_async(path, output))
100
+
101
+
102
+ def _html_to_pdf_sync(path: Path, output: str | Path) -> None:
103
+ """Sync version of Playwright routine."""
104
+
105
+ with sync_playwright() as p:
106
+ browser = p.chromium.launch(headless=True)
107
+ try:
108
+ page = browser.new_page()
109
+ page.goto(f"file:///{path.resolve().as_posix()}")
110
+ page.wait_for_function(_MATHJAX_WAITER)
111
+ page.pdf(path=output, prefer_css_page_size=True, outline=True)
112
+ finally:
113
+ browser.close()
114
+
115
+
116
+ async def _html_to_pdf_async(path: Path, output: str | Path) -> None:
117
+ """Async version of Playwright routine."""
118
+
119
+ async with async_playwright() as p:
120
+ browser = await p.chromium.launch(headless=True)
121
+ try:
122
+ page = await browser.new_page()
123
+ await page.goto(f"file:///{path.resolve().as_posix()}")
124
+ await page.wait_for_function(_MATHJAX_WAITER)
125
+ await page.pdf(path=output, prefer_css_page_size=True, outline=True)
126
+ finally:
127
+ await browser.close()
128
+
129
+
130
+ def export_notebook_to_pdf(
131
+ path: str | Path,
132
+ output: Optional[str | Path] = None,
133
+ *,
134
+ no_input: bool = True,
135
+ ) -> None:
136
+ """Export a Jupyter notebook to PDF using nbconvert and Playwright.
137
+ if a directory is supplied as input, all notebooks in the directory
138
+ will be exported.
139
+
140
+ Args:
141
+ path: The path to the notebook or directory to convert.
142
+ output: Optional output path. If `None`, uses the input path but
143
+ with file extension changed to `.pdf`, or if input path is a
144
+ directory, the output will be saved in a new sibling dir
145
+ named `{{dir}}_pdf`.
146
+ no_input: Whether to exclude input cells in the output. Defaults
147
+ to `True`.
148
+ """
149
+
150
+ path = Path(path)
151
+
152
+ if path.is_dir():
153
+ output = Path(output) if output else path.parent / f"{path.name}_pdf"
154
+ output.mkdir(parents=True, exist_ok=True)
155
+
156
+ notebooks = list(path.glob("*.ipynb"))
157
+ if not notebooks:
158
+ print(f"No notebooks found in {path.name}")
159
+ return
160
+
161
+ for notebook in notebooks:
162
+ output_pdf = output / notebook.with_suffix(".pdf").name
163
+ export_notebook_to_pdf(notebook, output_pdf, no_input=no_input)
164
+
165
+ print(
166
+ f"\nAll notebooks in {path.name} converted."
167
+ f"PDFs saved to: {output}"
168
+ )
169
+
170
+ elif path.is_file() and path.suffix == ".ipynb":
171
+ # Handle single notebook file
172
+ output = Path(output) if output else path.with_suffix(".pdf")
173
+
174
+ print(f"Converting: {path}")
175
+
176
+ # Create a temp file for the HTML output
177
+ with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as tmp:
178
+ tmp_path = Path(tmp.name)
179
+
180
+ try:
181
+ export_notebook_to_html(path, tmp_path, no_input=no_input)
182
+ _html_to_pdf(tmp_path, output)
183
+ print(f"PDF saved as: {output}")
184
+ finally:
185
+ # Ensure deletion of temp file
186
+ tmp_path.unlink(missing_ok=True)
187
+
188
+ else:
189
+ print(f"Invalid input: {path} is not a notebook or directory.")
@@ -0,0 +1,27 @@
1
+ """Loads the calcsetter extension features into IPython."""
2
+
3
+ import os
4
+
5
+ from IPython.core.display import display_html
6
+ from IPython.core.interactiveshell import InteractiveShell
7
+
8
+ from rubberize.magics import RubberizeMagics
9
+
10
+
11
+ def load_ipython_extension(ipython: InteractiveShell) -> None:
12
+ """Load the IPython extension.
13
+
14
+ This function is not to be called directly. It executed by the
15
+ %load_ext line magic in the IPython InteractiveShell. It registers
16
+ the magics of the library.
17
+ """
18
+ ipython.register_magics(RubberizeMagics)
19
+ _load_css(os.path.join(os.path.dirname(__file__), "static", "styles.css"))
20
+
21
+
22
+ def _load_css(path: str) -> None:
23
+ """Load a CSS stylesheet into IPython."""
24
+
25
+ with open(path, "r", encoding="utf-8") as file:
26
+ css = file.read()
27
+ display_html(f"<style>{css}</style>", raw=True)
@@ -0,0 +1,118 @@
1
+ """The class for all magic commands that the library adds to the
2
+ Interactive Shell. Called by `load_ipython_extension()`
3
+ """
4
+
5
+ from typing import Any, Optional
6
+
7
+ from IPython.core.display import display_html
8
+ from IPython.core.interactiveshell import InteractiveShell
9
+ from IPython.core.magic import (
10
+ Magics,
11
+ cell_magic,
12
+ magics_class,
13
+ needs_local_scope,
14
+ )
15
+ from IPython.core.magic_arguments import (
16
+ argument,
17
+ magic_arguments,
18
+ parse_argstring,
19
+ )
20
+ from IPython.utils.capture import capture_output
21
+
22
+ import rubberize.vendor.ast_comments as ast_c
23
+
24
+ from rubberize.config import config
25
+ from rubberize.latexer.latexer import latexer
26
+ from rubberize.render.render import render
27
+ from rubberize.config import parse_modifiers
28
+
29
+
30
+ @magics_class
31
+ class RubberizeMagics(Magics):
32
+ """Contains IPython magics to be loaded."""
33
+
34
+ @magic_arguments()
35
+ @argument(
36
+ "--dead",
37
+ action="store_true",
38
+ help="dead cell -- the cell is not run; only rendered.",
39
+ )
40
+ @cell_magic
41
+ def ast(self, line: str, cell: str) -> None:
42
+ """Magic to run the cell and dump it as AST for debugging."""
43
+
44
+ assert isinstance(self.shell, InteractiveShell)
45
+
46
+ args = parse_argstring(self.tap, line)
47
+ if not args.dead:
48
+ with capture_output():
49
+ run_result = self.shell.run_cell(cell)
50
+ if not run_result.success:
51
+ return None
52
+
53
+ # Ignore line magics
54
+ cleaned_cell = "\n".join(
55
+ l for l in cell.split("\n") if not l.startswith("%")
56
+ )
57
+
58
+ return print(ast_c.dump(ast_c.parse(cleaned_cell), indent=4))
59
+
60
+ @magic_arguments()
61
+ @argument(
62
+ "modifiers",
63
+ nargs="*",
64
+ help="keyword shortcuts or config option assignments for the cell",
65
+ )
66
+ @argument(
67
+ "--grid",
68
+ "-g",
69
+ action="store_true",
70
+ help="render the cell in a grid and without descriptions",
71
+ )
72
+ @argument(
73
+ "--html",
74
+ "-h",
75
+ action="store_true",
76
+ help="print the html output for debugging",
77
+ )
78
+ @argument(
79
+ "--dead",
80
+ action="store_true",
81
+ help="dead cell -- the cell is not run; only rendered.",
82
+ )
83
+ @needs_local_scope
84
+ @cell_magic
85
+ def tap(
86
+ self, line: str, cell: str, local_ns: Optional[dict[str, Any]]
87
+ ) -> None:
88
+ """Magic to run the cell and render it as mathematical notation.
89
+ It supports line args to customize the output.
90
+ """
91
+
92
+ assert isinstance(self.shell, InteractiveShell)
93
+
94
+ args = parse_argstring(self.tap, line)
95
+ override = parse_modifiers(args.modifiers)
96
+ if "hide" in override:
97
+ return None
98
+
99
+ if not args.dead:
100
+ with capture_output():
101
+ run_result = self.shell.run_cell(cell)
102
+ if not run_result.success:
103
+ return None
104
+ else:
105
+ local_ns = None
106
+
107
+ # Ignore line magics
108
+ cleaned_cell = "\n".join(
109
+ l for l in cell.split("\n") if not l.startswith("%")
110
+ )
111
+
112
+ with config.override(**override):
113
+ latex_list = latexer(cleaned_cell, local_ns)
114
+ cell_html = render(latex_list, local_ns, grid=args.grid)
115
+
116
+ if args.html:
117
+ return print(cell_html, "\n")
118
+ return display_html(cell_html, raw=True)
@@ -0,0 +1,123 @@
1
+ """Utility functions used throughout the library."""
2
+
3
+ import re
4
+ import textwrap
5
+ from typing import Any, Callable, Literal, Optional
6
+
7
+
8
+ _FlagsType = re.RegexFlag | Literal[0]
9
+
10
+
11
+ def find_and_sub(
12
+ pattern: str | re.Pattern[str],
13
+ repl: str | Callable[[re.Match[str]], str],
14
+ string: str,
15
+ group: int = 1,
16
+ flags: _FlagsType = 0,
17
+ ) -> tuple[list[str | Any], str]:
18
+ """Find occurrences of a pattern in a string, extract matches from
19
+ the specified capturing group, and perform a substitution.
20
+
21
+ Args:
22
+ pattern: The regex pattern to search for.
23
+ repl: The replacement string or a function that takes a match
24
+ object and returns a replacement string.
25
+ string: The input string to search within.
26
+ group: The capturing group whose matches should be extracted.
27
+ flags: Regex flags modifying the search behavior.
28
+
29
+ Returns:
30
+ A tuple containing a list of matches from the capturing group,
31
+ and the string after performing the substitution.
32
+ """
33
+ matches = [m.group(group) for m in re.finditer(pattern, string, flags)]
34
+ return matches, re.sub(pattern, repl, string, flags)
35
+
36
+
37
+ def wrap_delims(
38
+ text: str,
39
+ delims: tuple[str, str],
40
+ *,
41
+ force_block: bool = False,
42
+ spaced_line: bool = False,
43
+ indent: int = 4,
44
+ ) -> str:
45
+ """Wrap text with a pair of delimiters, optionally formatting it as
46
+ a block.
47
+
48
+ Default behavior, for delims `"{{...}}"`:
49
+ - `""` -> `"{{}}"`
50
+ - `"text 1\\ntext 2"` -> `"{{\\n text 1\\n text 2\\n}}"`
51
+ - `"text"` -> `"{{text}}"`
52
+
53
+ Args:
54
+ text: The text to be wrapped.
55
+ delims: A tuple specifying the opening and closing delimiters.
56
+ force_block: If True, forces block formatting even for single
57
+ line input.
58
+ spaced_line: If True, adds spaces between delimiters and text
59
+ when inline.
60
+ indent: The number of spaces to indent each line when using
61
+ block format.
62
+
63
+ Returns:
64
+ The text wrapped with the given delimiters.
65
+ """
66
+
67
+ if not text:
68
+ return delims[0] + delims[1]
69
+ if force_block or ("\n" in text):
70
+ text = textwrap.indent(text, " " * indent)
71
+ return delims[0] + "\n" + text + "\n" + delims[1]
72
+ if spaced_line:
73
+ return delims[0] + " " + text + " " + delims[1]
74
+ return delims[0] + text + delims[1]
75
+
76
+
77
+ def html_tag(
78
+ tag: str,
79
+ content: str | list[str],
80
+ *,
81
+ force_block=False,
82
+ indent: int = 4,
83
+ **kwargs: Optional[str],
84
+ ) -> str:
85
+ """Wrap content in an HTML tag. Supports attributes and block
86
+ formatting.
87
+
88
+ Notes:
89
+ - If `content` is a list, it is joined into a single string with
90
+ line breaks.
91
+ - Attributes with a leading underscore (`_class`) are stripped of
92
+ the underscore.
93
+
94
+ Args:
95
+ tag: The HTML tag name.
96
+ content: The text or list of strings to wrap within the tag.
97
+ force_block: If True, forces block formatting even for single
98
+ line content.
99
+ indent: The number of spaces to indent block content.
100
+ **kwargs: Optional HTML attributes as key-value pairs.
101
+
102
+ Returns:
103
+ A string representing the formatted HTML element.
104
+ """
105
+
106
+ if isinstance(content, list):
107
+ content = "\n".join(content)
108
+
109
+ open_tag = f"<{tag}"
110
+ for attr, value in kwargs.items():
111
+ open_tag += (
112
+ f' {attr.lstrip("_")}="{value}"' if value is not None else ""
113
+ )
114
+ open_tag += ">"
115
+ close_tag = f"</{tag}>"
116
+
117
+ return wrap_delims(
118
+ content,
119
+ (open_tag, close_tag),
120
+ force_block=force_block,
121
+ spaced_line=False,
122
+ indent=indent,
123
+ )
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.2
2
+ Name: rubberize
3
+ Version: 0.1.0
4
+ Summary: Turn Python calculations into well-formatted, math-rich documents.
5
+ Author-email: Chito Peralta <chitoangeloperalta@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Chito Peralta
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ Project-URL: repository, https://github.com/chitoperalta/rubberize
28
+ Project-URL: homepage, https://github.com/chitoperalta/rubberize
29
+ Keywords: latex,markdown,jupyter,notebook,math,engineering
30
+ Requires-Python: >=3.11
31
+ Description-Content-Type: text/markdown
32
+ License-File: LICENSE
33
+ Requires-Dist: markdown<4.0,>=3.7
34
+ Requires-Dist: titlecase<3.0.0,>=2.4.1
35
+ Provides-Extra: notebook
36
+ Requires-Dist: ipython<10.0.0,>=9.0.2; extra == "notebook"
37
+ Requires-Dist: playwright<2.0.0,>=1.51.0; extra == "notebook"
38
+ Provides-Extra: dev
39
+ Requires-Dist: ipython<10.0.0,>=9.0.2; extra == "dev"
40
+ Requires-Dist: playwright<2.0.0,>=1.51.0; extra == "dev"
41
+ Requires-Dist: jupyterlab<5.0.0,>=4.3.6; extra == "dev"
42
+ Requires-Dist: pint<0.25.0,>=0.24.4; extra == "dev"
43
+ Requires-Dist: sympy<2.0.0,>=1.13.3; extra == "dev"
44
+ Requires-Dist: numpy<3.0.0,>=2.2.4; extra == "dev"
45
+
46
+ # Rubberize
47
+
48
+ **Turn Python calculations into well-formatted, math-rich documents.**
49
+
50
+ ## Installation
51
+
52
+ Install Rubberize with `pip`:
53
+
54
+ ```bash
55
+ pip install rubberize
56
+ ```
57
+
58
+ Rubberize is primarily built for Jupyter. To enable notebook magics:
59
+
60
+ ```bash
61
+ pip install rubberize[notebook]
62
+ ```
63
+
64
+ ## Basic Usage
65
+
66
+ ### In Modules
67
+
68
+ Here's a quick example of how to use Rubberize:
69
+
70
+ ```python
71
+ import rubberize
72
+
73
+ source = """\
74
+ a + b
75
+ a - b
76
+ """
77
+
78
+ namespace = {"a": 1, "b": 2}
79
+ stmts_latex = rubberize.latexer(source, namespace)
80
+ ```
81
+
82
+ `stmts_latex` will be a list of `StmtLatex` objects that contain LaTeX representations of each statement.
83
+
84
+ ### In Notebooks
85
+
86
+ Rubberize is more impressive in notebooks (assuming `rubberize[notebooks]` is installed). In a Code cell, use `%%tap` and your code in the cell will be displayed as math notation, along with substitutions, results, and comments.
87
+
88
+ ## Contributing
89
+
90
+ To set up a development environment, install the supported dependencies:
91
+
92
+ ```bash
93
+ pip install rubberize[dev]
94
+ ```
95
+
96
+ ## License
97
+
98
+ MIT License © 2025 [Chito Peralta](mailto:chitoangeloperalta@gmail.com)
99
+
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ rubberize/__init__.py
5
+ rubberize/config.py
6
+ rubberize/exceptions.py
7
+ rubberize/export_notebook.py
8
+ rubberize/load_ipython_extension.py
9
+ rubberize/magics.py
10
+ rubberize/utils.py
11
+ rubberize.egg-info/PKG-INFO
12
+ rubberize.egg-info/SOURCES.txt
13
+ rubberize.egg-info/dependency_links.txt
14
+ rubberize.egg-info/requires.txt
15
+ rubberize.egg-info/top_level.txt
@@ -0,0 +1,14 @@
1
+ markdown<4.0,>=3.7
2
+ titlecase<3.0.0,>=2.4.1
3
+
4
+ [dev]
5
+ ipython<10.0.0,>=9.0.2
6
+ playwright<2.0.0,>=1.51.0
7
+ jupyterlab<5.0.0,>=4.3.6
8
+ pint<0.25.0,>=0.24.4
9
+ sympy<2.0.0,>=1.13.3
10
+ numpy<3.0.0,>=2.2.4
11
+
12
+ [notebook]
13
+ ipython<10.0.0,>=9.0.2
14
+ playwright<2.0.0,>=1.51.0
@@ -0,0 +1 @@
1
+ rubberize
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+