plot3 0.7.0__tar.gz → 0.8.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.
- {plot3-0.7.0 → plot3-0.8.0}/PKG-INFO +2 -1
- plot3-0.8.0/plot3/__version__.py +1 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/geoms.py +1 -1
- {plot3-0.7.0 → plot3-0.8.0}/plot3/jupyter.py +9 -3
- {plot3-0.7.0 → plot3-0.8.0}/plot3/masking.py +172 -55
- {plot3-0.7.0 → plot3-0.8.0}/plot3.egg-info/PKG-INFO +2 -1
- {plot3-0.7.0 → plot3-0.8.0}/plot3.egg-info/requires.txt +1 -0
- {plot3-0.7.0 → plot3-0.8.0}/pyproject.toml +3 -0
- plot3-0.8.0/tests/test_masking.py +437 -0
- plot3-0.7.0/plot3/__version__.py +0 -1
- plot3-0.7.0/tests/test_masking.py +0 -240
- {plot3-0.7.0 → plot3-0.8.0}/LICENSE +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/README.md +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/__init__.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/aesexpr.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/build.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/calculus.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/compose.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/contour.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/craft.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/encode.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/expr.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/flip.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/function.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/ggplot.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/io.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/kde.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/latexin.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/mathtext.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/payload.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/remote.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/scales.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/scaling.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/special.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/stat2d.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/static.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/stats3d.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/table.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/themes.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3/viewer.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3.egg-info/SOURCES.txt +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3.egg-info/dependency_links.txt +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/plot3.egg-info/top_level.txt +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/setup.cfg +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_2d_density.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_3d.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_animation.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_annotate.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_api_public.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_array_backend.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_arrow.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_build_contract.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_contour.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_everyday_ggplot2.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_expr.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_facets_compose.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_fast_paths.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_geom_function.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_geoms_ds.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_ggplot2_extras2.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_ggplot2_parity.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_ggplot_extras.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_grammar.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_latex_input.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_lidar_scene.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_math_layers.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_mathtext.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_payload.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_payload_display.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_point_cloud.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_r_oracle_parity.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_readme.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_remote_bridge.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_review_fixes.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_save_menu.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_scales_api.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_scales_themes.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_special.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_stat2d.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_static_save.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_stats_semantic.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_step3_geoms.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_table_backends.py +0 -0
- {plot3-0.7.0 → plot3-0.8.0}/tests/test_titles_theme.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plot3
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: ggplot2 grammar of graphics for Python: interactive WebGL figures and journal-ready PNG, SVG, and PDF
|
|
5
5
|
Author: Rigoberto Leyva Salmeron
|
|
6
6
|
License-Expression: MIT
|
|
@@ -17,6 +17,7 @@ Description-Content-Type: text/markdown
|
|
|
17
17
|
License-File: LICENSE
|
|
18
18
|
Requires-Dist: numpy>=1.24
|
|
19
19
|
Requires-Dist: pandas>=2.0
|
|
20
|
+
Requires-Dist: tidysyntax<0.2,>=0.1
|
|
20
21
|
Provides-Extra: pdf
|
|
21
22
|
Requires-Dist: cairosvg>=2.7; extra == "pdf"
|
|
22
23
|
Provides-Extra: dev
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.8.0"
|
|
@@ -1798,7 +1798,7 @@ class facet_wrap:
|
|
|
1798
1798
|
# Accept "~cyl", ". ~ cyl", "cyl ~ ."
|
|
1799
1799
|
parts = [p.strip() for p in name.split("~")]
|
|
1800
1800
|
candidates = [p for p in parts if p and p != "."]
|
|
1801
|
-
if len(candidates) != 1:
|
|
1801
|
+
if len(candidates) != 1 or "+" in candidates[0]:
|
|
1802
1802
|
raise ValueError(
|
|
1803
1803
|
"facet_wrap() currently accepts a single facet column "
|
|
1804
1804
|
f"(got {facets!r})"
|
|
@@ -22,6 +22,7 @@ from plot3.masking import (
|
|
|
22
22
|
is_mask_transformer,
|
|
23
23
|
is_tidy3_backtick_transformer,
|
|
24
24
|
plot3_backtick_transform,
|
|
25
|
+
plot3_formula_transform,
|
|
25
26
|
plot3_layer_transform,
|
|
26
27
|
)
|
|
27
28
|
|
|
@@ -62,16 +63,21 @@ def enable_r_style(ipython: Any | None = None) -> bool:
|
|
|
62
63
|
# When tidy3 is not loaded, leave tidy3 sentinel unset; plot3 AST
|
|
63
64
|
# still rewrites __tidy3_bt__ if tidy3's preparser ran first.
|
|
64
65
|
|
|
65
|
-
# Source preparser:
|
|
66
|
+
# Source preparser: when tidy3 already rewrites backticks, run only
|
|
67
|
+
# plot3's own formula rewrites (^ in geom_function, ~ in facets), after it.
|
|
66
68
|
for attr in ("input_transformers_cleanup", "input_transformers_post"):
|
|
67
69
|
transformers = getattr(ipython, attr, None)
|
|
68
70
|
if not isinstance(transformers, list):
|
|
69
71
|
continue
|
|
70
|
-
has_tidy3_bt = any(is_tidy3_backtick_transformer(t) for t in transformers)
|
|
71
72
|
transformers[:] = [
|
|
72
73
|
t for t in transformers if not is_backtick_transformer(t)
|
|
73
74
|
]
|
|
74
|
-
|
|
75
|
+
tidy3_at = [
|
|
76
|
+
i for i, t in enumerate(transformers) if is_tidy3_backtick_transformer(t)
|
|
77
|
+
]
|
|
78
|
+
if tidy3_at:
|
|
79
|
+
transformers.insert(tidy3_at[-1] + 1, plot3_formula_transform)
|
|
80
|
+
else:
|
|
75
81
|
transformers.insert(0, plot3_backtick_transform)
|
|
76
82
|
|
|
77
83
|
# AST pass for bare names inside aes / facet_wrap.
|
|
@@ -30,11 +30,16 @@ from __future__ import annotations
|
|
|
30
30
|
|
|
31
31
|
import ast
|
|
32
32
|
import builtins
|
|
33
|
+
import dataclasses
|
|
33
34
|
import io
|
|
34
35
|
import re
|
|
35
36
|
import tokenize
|
|
36
37
|
from typing import Any, Iterable
|
|
37
38
|
|
|
39
|
+
import tidysyntax
|
|
40
|
+
from tidysyntax import repairs
|
|
41
|
+
from tidysyntax.scanner import Spans, apply
|
|
42
|
+
|
|
38
43
|
# Sentinels (plot3 own + tidy3 compatibility).
|
|
39
44
|
BT_NAME = "__plot3_bt__"
|
|
40
45
|
TIDY3_BT_NAME = "__tidy3_bt__"
|
|
@@ -108,69 +113,121 @@ _PASSTHROUGH_KW = frozenset(
|
|
|
108
113
|
|
|
109
114
|
_FORMULA_CALLS = frozenset({"geom_function", "geom_vector_field"})
|
|
110
115
|
|
|
111
|
-
|
|
116
|
+
_FACETS = frozenset({"facet_wrap", "facet_grid"})
|
|
117
|
+
|
|
118
|
+
# plot3's part of the notebook syntax tidy3, tidy3a, plot3 and stats3 share
|
|
119
|
+
# (tidysyntax). ``drv ~ cyl`` and ``~`my col``` are not Python, so they are
|
|
120
|
+
# quoted in any facet call; ``^`` in geom_function (XOR in Python) and ``~cyl``
|
|
121
|
+
# in a facet (bitwise not) are valid Python, so they are rewritten only where
|
|
122
|
+
# the call is plot3's: a notebook's own geom_function keeps XOR.
|
|
123
|
+
DIALECT = tidysyntax.Dialect(
|
|
124
|
+
"plot3",
|
|
125
|
+
formula_calls=_FACETS,
|
|
126
|
+
formula_keywords={"facets"},
|
|
127
|
+
formula_backticks="strip",
|
|
128
|
+
power_calls=_FORMULA_CALLS,
|
|
129
|
+
unary_formula_calls=_FACETS,
|
|
130
|
+
)
|
|
131
|
+
_POWER_ONLY = dataclasses.replace(DIALECT, formula_calls=(), formula_keywords=(), unary_formula_calls=())
|
|
132
|
+
_FACETS_ONLY = dataclasses.replace(DIALECT, power_calls=())
|
|
133
|
+
|
|
134
|
+
_SESSION = object() # default: the running IPython's namespace, if any
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _session_namespace(namespace: Any) -> dict[str, Any] | None:
|
|
138
|
+
if namespace is not _SESSION:
|
|
139
|
+
return namespace
|
|
140
|
+
try:
|
|
141
|
+
from IPython import get_ipython
|
|
142
|
+
|
|
143
|
+
ip = get_ipython()
|
|
144
|
+
except Exception:
|
|
145
|
+
return None
|
|
146
|
+
return getattr(ip, "user_ns", None) if ip is not None else None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _plot3_bt(source: str) -> str:
|
|
150
|
+
"""tidysyntax's ``__r_name__`` placeholder as plot3's ``__plot3_bt__``, outside strings and comments."""
|
|
151
|
+
if tidysyntax.NAME not in source:
|
|
152
|
+
return source
|
|
153
|
+
spans = Spans.of(source)
|
|
154
|
+
edits = [
|
|
155
|
+
(m.start(), m.end(), BT_NAME)
|
|
156
|
+
for m in re.finditer(re.escape(tidysyntax.NAME) + r"(?=\s*\()", source)
|
|
157
|
+
if m.start() in spans
|
|
158
|
+
]
|
|
159
|
+
return apply(source, edits)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _repair(source: str) -> str:
|
|
163
|
+
"""The rewrites of text that is not Python: ``drv ~ cyl``, ``~`my col``` and backticks."""
|
|
164
|
+
return _plot3_bt(repairs.backticks(repairs.quote_formulas(source, [DIALECT])))
|
|
112
165
|
|
|
113
166
|
|
|
114
|
-
def
|
|
167
|
+
def rewrite_facet_formulas(source: str, *, unary: str = "owned", namespace: Any = _SESSION) -> str:
|
|
168
|
+
"""Quote R formulas inside ``facet_wrap`` / ``facet_grid`` before parsing.
|
|
169
|
+
|
|
170
|
+
``facet_grid(drv ~ cyl)`` → ``facet_grid('drv ~ cyl')``: ``~`` with a left
|
|
171
|
+
side is not Python, so it is quoted in any facet call, as is a ``~`` formula
|
|
172
|
+
holding a backtick name. ``facet_wrap(~cyl)`` → ``facet_wrap('~cyl')``
|
|
173
|
+
only where the facet call is plot3's, since ``~cyl`` is Python's bitwise
|
|
174
|
+
not (``unary="owned"``); ``unary="never"`` leaves such formulas for a
|
|
175
|
+
later pass. Backtick names inside the formula lose their backticks
|
|
176
|
+
(``~`my col``` → ``'~my col'``).
|
|
177
|
+
"""
|
|
178
|
+
source = repairs.quote_formulas(source, [DIALECT])
|
|
179
|
+
if unary == "never":
|
|
180
|
+
return source
|
|
181
|
+
return tidysyntax.reinterpret(source, [_FACETS_ONLY], _session_namespace(namespace))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def rewrite_formulas(source: str, namespace: Any = _SESSION) -> str:
|
|
185
|
+
"""The text rewrites plot3 needs even when tidy3 handles backticks:
|
|
186
|
+
``^`` inside ``geom_function`` and ``~`` formulas inside facets.
|
|
187
|
+
|
|
188
|
+
``drv ~ cyl`` is quoted first, since the cell must parse before the
|
|
189
|
+
ownership of ``geom_function`` and ``facet_wrap`` can be read."""
|
|
190
|
+
source = repairs.quote_formulas(source, [DIALECT])
|
|
191
|
+
return tidysyntax.reinterpret(source, [DIALECT], _session_namespace(namespace))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def rewrite_formula_carets(source: str, namespace: Any = _SESSION) -> str:
|
|
115
195
|
"""Inside ``geom_function(...)``, turn ``^`` into ``**`` before Python parses.
|
|
116
196
|
|
|
117
197
|
``2*x^3`` and ``(2*x)^3`` are the same tree once Python has parsed ``^``
|
|
118
|
-
as xor, so the rewrite has to happen on the text.
|
|
198
|
+
as xor, so the rewrite has to happen on the text. Only plot3's own
|
|
199
|
+
``geom_function`` is rewritten: in a notebook's own function of that
|
|
200
|
+
name, ``^`` stays XOR.
|
|
119
201
|
"""
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return source
|
|
126
|
-
starts = [0]
|
|
127
|
-
for line in source.splitlines(keepends=True):
|
|
128
|
-
starts.append(starts[-1] + len(line))
|
|
129
|
-
hits: list[int] = []
|
|
130
|
-
depth = 0 # paren depth inside the current geom_function call
|
|
131
|
-
armed = False # just saw the name geom_function
|
|
132
|
-
for tok in tokens:
|
|
133
|
-
if depth == 0:
|
|
134
|
-
if tok.type == tokenize.NAME and tok.string in _FORMULA_CALLS:
|
|
135
|
-
armed = True
|
|
136
|
-
continue
|
|
137
|
-
if armed and tok.type == tokenize.OP and tok.string == "(":
|
|
138
|
-
depth = 1
|
|
139
|
-
armed = False
|
|
140
|
-
continue
|
|
141
|
-
if tok.type == tokenize.OP:
|
|
142
|
-
if tok.string in "([{":
|
|
143
|
-
depth += 1
|
|
144
|
-
elif tok.string in ")]}":
|
|
145
|
-
depth -= 1
|
|
146
|
-
elif tok.string == "^":
|
|
147
|
-
row, col = tok.start
|
|
148
|
-
hits.append(starts[row - 1] + col)
|
|
149
|
-
for pos in reversed(hits):
|
|
150
|
-
source = source[:pos] + "**" + source[pos + 1 :]
|
|
151
|
-
return source
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
def rewrite_backticks(source: str) -> str:
|
|
155
|
-
"""Preparse backticks, and ``^`` inside ``geom_function``, before parsing.
|
|
202
|
+
return tidysyntax.reinterpret(source, [_POWER_ONLY], _session_namespace(namespace))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def rewrite_backticks(source: str, namespace: Any = _SESSION) -> str:
|
|
206
|
+
"""Preparse backticks, ``^`` inside ``geom_function``, and facet formulas.
|
|
156
207
|
|
|
157
208
|
`` `col name` `` → ``__plot3_bt__("col name")``. ``^`` → ``**`` only
|
|
158
|
-
inside
|
|
209
|
+
inside plot3's ``geom_function(...)``, where it means power. Backticks
|
|
210
|
+
inside string literals and comments stay as written. The text that is
|
|
211
|
+
not Python goes first, so the cell parses when the ownership of
|
|
212
|
+
``geom_function`` and ``facet_wrap`` is read.
|
|
159
213
|
"""
|
|
160
|
-
source
|
|
161
|
-
if "`" not in source:
|
|
162
|
-
return source
|
|
163
|
-
return _BT_RE.sub(lambda m: f"{BT_NAME}({m.group(1)!r})", source)
|
|
214
|
+
return tidysyntax.reinterpret(_repair(source), [DIALECT], _session_namespace(namespace))
|
|
164
215
|
|
|
165
216
|
|
|
166
|
-
def
|
|
167
|
-
"""
|
|
168
|
-
if
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
217
|
+
def _parse_dedented(source: str) -> tuple[ast.Module, list[int]] | None:
|
|
218
|
+
"""Parse *source* with indented ``+`` lines moved to column 0 (they are
|
|
219
|
+
joined later); the shift per line maps positions back. ``None`` if the
|
|
220
|
+
cell does not parse, in which case nothing valid is reinterpreted."""
|
|
221
|
+
raw = source.split("\n")
|
|
222
|
+
shift = [len(line) - len(line.lstrip()) if line.lstrip().startswith("+") else 0 for line in raw]
|
|
223
|
+
try:
|
|
224
|
+
tree = ast.parse("\n".join(line[n:] for line, n in zip(raw, shift)))
|
|
225
|
+
except (SyntaxError, ValueError):
|
|
226
|
+
return None
|
|
227
|
+
return tree, shift
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _as_lines(src: str, out: str, lines: list[str]) -> list[str]:
|
|
174
231
|
if out == src:
|
|
175
232
|
return lines
|
|
176
233
|
if out.endswith("\n"):
|
|
@@ -179,6 +236,28 @@ def plot3_backtick_transform(lines: list[str]) -> list[str]:
|
|
|
179
236
|
return parts or [out]
|
|
180
237
|
|
|
181
238
|
|
|
239
|
+
def plot3_backtick_transform(lines: list[str]) -> list[str]:
|
|
240
|
+
"""IPython input transformer: backticks, ``^`` inside geom_function, and
|
|
241
|
+
``~`` formulas inside facets."""
|
|
242
|
+
if not lines:
|
|
243
|
+
return lines
|
|
244
|
+
src = "".join(lines)
|
|
245
|
+
if "`" not in src and "^" not in src and "~" not in src:
|
|
246
|
+
return lines
|
|
247
|
+
return _as_lines(src, rewrite_backticks(src), lines)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def plot3_formula_transform(lines: list[str]) -> list[str]:
|
|
251
|
+
"""IPython input transformer used beside tidy3's backtick preparser:
|
|
252
|
+
``^`` inside geom_function and ``~`` formulas inside facets only."""
|
|
253
|
+
if not lines:
|
|
254
|
+
return lines
|
|
255
|
+
src = "".join(lines)
|
|
256
|
+
if "^" not in src and "~" not in src:
|
|
257
|
+
return lines
|
|
258
|
+
return _as_lines(src, rewrite_formulas(src), lines)
|
|
259
|
+
|
|
260
|
+
|
|
182
261
|
_OPENERS = frozenset("([{")
|
|
183
262
|
_CLOSERS = frozenset(")]}")
|
|
184
263
|
_SKIP_TOKENS = frozenset({
|
|
@@ -229,7 +308,7 @@ def _value_start(tokens: list[tokenize.TokenInfo]) -> tuple[int, int]:
|
|
|
229
308
|
return tokens[0].start
|
|
230
309
|
|
|
231
310
|
|
|
232
|
-
def join_layer_lines(source: str) -> str | None:
|
|
311
|
+
def join_layer_lines(source: str, namespace: Any = _SESSION) -> str | None:
|
|
233
312
|
"""Join ``+`` lines that add plot3 layers to the statement above them.
|
|
234
313
|
|
|
235
314
|
In a notebook, ``ggplot(df, aes(...))`` followed by lines that start
|
|
@@ -240,6 +319,12 @@ def join_layer_lines(source: str) -> str | None:
|
|
|
240
319
|
lines; the expression gets parentheses on the lines as typed, so error
|
|
241
320
|
line numbers still match. Returns ``None`` when nothing needs joining,
|
|
242
321
|
or when the result would not parse. Pipes (``>>``) are tidy3's.
|
|
322
|
+
|
|
323
|
+
An indented ``+`` line is not Python, so it joins by name. An unindented
|
|
324
|
+
one is a valid unary-plus statement, so it joins only when the name after
|
|
325
|
+
``+`` is plot3's and the statement above starts with a plot3 call or a
|
|
326
|
+
value not known to be something else: after ``x = 2``, or with the
|
|
327
|
+
notebook's own ``geom_point``, ``+ geom_point()`` stays a statement.
|
|
243
328
|
"""
|
|
244
329
|
if not source or "+" not in source:
|
|
245
330
|
return None
|
|
@@ -253,6 +338,36 @@ def join_layer_lines(source: str) -> str | None:
|
|
|
253
338
|
logical = _logical_lines(analysed)
|
|
254
339
|
if not logical:
|
|
255
340
|
return None
|
|
341
|
+
parsed = _parse_dedented(_repair(source))
|
|
342
|
+
owners = tidysyntax.Owners(parsed[0], _session_namespace(namespace), [DIALECT]) if parsed is not None else None
|
|
343
|
+
|
|
344
|
+
def plot3_name(name: str, row: int) -> bool:
|
|
345
|
+
return owners is not None and DIALECT.owns_module(owners.module(name, (row, 0)))
|
|
346
|
+
|
|
347
|
+
def plot3_head(tokens: list[tokenize.TokenInfo]) -> bool:
|
|
348
|
+
start = _value_start(tokens)
|
|
349
|
+
rest = [t for t in tokens if t.start >= start]
|
|
350
|
+
while rest and rest[0].string == "(":
|
|
351
|
+
rest = rest[1:]
|
|
352
|
+
if owners is None or not rest or rest[0].type != tokenize.NAME:
|
|
353
|
+
return False
|
|
354
|
+
name, row = rest[0].string, rest[0].start[0]
|
|
355
|
+
if len(rest) > 1 and rest[1].string in ("(", "."):
|
|
356
|
+
return plot3_name(name, row) # ggplot(...), p3.ggplot(...)
|
|
357
|
+
# A variable: refused only when its value is known not to be plot3's.
|
|
358
|
+
bound = [package for end, package in owners.bindings.get(name, ()) if end <= (row, 0)]
|
|
359
|
+
if bound:
|
|
360
|
+
return bound[-1] is None or DIALECT.owns_module(bound[-1])
|
|
361
|
+
if owners.context is not None and name in owners.context:
|
|
362
|
+
return DIALECT.owns_module(tidysyntax.module_of(owners.context[name]))
|
|
363
|
+
return True
|
|
364
|
+
|
|
365
|
+
def may_join(tokens: list[tokenize.TokenInfo], head: list[tokenize.TokenInfo]) -> bool:
|
|
366
|
+
"""Whether joining this ``+`` line changes valid Python, and may: see above."""
|
|
367
|
+
row = tokens[0].start[0]
|
|
368
|
+
if shift[row - 1]:
|
|
369
|
+
return True # indented: not Python on its own
|
|
370
|
+
return plot3_name(tokens[1].string, row) and plot3_head(head)
|
|
256
371
|
|
|
257
372
|
def is_layer(tokens: list[tokenize.TokenInfo]) -> bool:
|
|
258
373
|
return (
|
|
@@ -273,6 +388,7 @@ def join_layer_lines(source: str) -> str | None:
|
|
|
273
388
|
and head.start[1] == 0
|
|
274
389
|
and head.type != tokenize.OP
|
|
275
390
|
and head.string not in {"if", "for", "while", "with", "def", "class", "try", "elif", "else", "except", "finally", "return", "import", "from"}
|
|
391
|
+
and may_join(tokens, previous[0])
|
|
276
392
|
):
|
|
277
393
|
previous.append(tokens)
|
|
278
394
|
else:
|
|
@@ -618,7 +734,7 @@ def apply_masking(
|
|
|
618
734
|
backticks: bool = True,
|
|
619
735
|
) -> str:
|
|
620
736
|
"""Apply backtick rewrite + AST masking; return unparsed source (tests)."""
|
|
621
|
-
text =
|
|
737
|
+
text = rewrite_formulas(source)
|
|
622
738
|
if backticks:
|
|
623
739
|
text = rewrite_backticks(text)
|
|
624
740
|
tree = ast.parse(text)
|
|
@@ -637,7 +753,8 @@ def is_mask_transformer(obj: Any) -> bool:
|
|
|
637
753
|
def is_backtick_transformer(obj: Any) -> bool:
|
|
638
754
|
return (
|
|
639
755
|
getattr(obj, "__module__", "") in {"plot3.masking", "plot3.jupyter"}
|
|
640
|
-
and getattr(obj, "__name__", "")
|
|
756
|
+
and getattr(obj, "__name__", "")
|
|
757
|
+
in {"plot3_backtick_transform", "plot3_formula_transform"}
|
|
641
758
|
)
|
|
642
759
|
|
|
643
760
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plot3
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: ggplot2 grammar of graphics for Python: interactive WebGL figures and journal-ready PNG, SVG, and PDF
|
|
5
5
|
Author: Rigoberto Leyva Salmeron
|
|
6
6
|
License-Expression: MIT
|
|
@@ -17,6 +17,7 @@ Description-Content-Type: text/markdown
|
|
|
17
17
|
License-File: LICENSE
|
|
18
18
|
Requires-Dist: numpy>=1.24
|
|
19
19
|
Requires-Dist: pandas>=2.0
|
|
20
|
+
Requires-Dist: tidysyntax<0.2,>=0.1
|
|
20
21
|
Provides-Extra: pdf
|
|
21
22
|
Requires-Dist: cairosvg>=2.7; extra == "pdf"
|
|
22
23
|
Provides-Extra: dev
|
|
@@ -22,6 +22,9 @@ classifiers = [
|
|
|
22
22
|
dependencies = [
|
|
23
23
|
"numpy>=1.24",
|
|
24
24
|
"pandas>=2.0",
|
|
25
|
+
# The notebook syntax tidy3, tidy3a, plot3 and stats3 share (facet
|
|
26
|
+
# formulas, ^ in geom_function, backtick names). Standard library only.
|
|
27
|
+
"tidysyntax>=0.1,<0.2",
|
|
25
28
|
]
|
|
26
29
|
|
|
27
30
|
[project.optional-dependencies]
|