plot-io 0.12.0__py3-none-win_arm64.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.
- plot_io/__init__.py +73 -0
- plot_io/_methods.py +1691 -0
- plot_io/_native/__init__.py +199 -0
- plot_io/_native/__main__.py +197 -0
- plot_io/_native/_api.py +457 -0
- plot_io/_native/bin/windows-arm64.dll +0 -0
- plot_io/_native/kernels.py +335 -0
- plot_io/_native/manifest.json +49 -0
- plot_io/axis.py +1347 -0
- plot_io/boxes.py +1156 -0
- plot_io/canvas.py +389 -0
- plot_io/color/__init__.py +3 -0
- plot_io/color/data.py +1646 -0
- plot_io/color/maps.py +922 -0
- plot_io/color/values.py +295 -0
- plot_io/color/variants.py +1246 -0
- plot_io/colorbar.py +1426 -0
- plot_io/columns.py +614 -0
- plot_io/decorations.py +920 -0
- plot_io/defaults.py +809 -0
- plot_io/density.py +113 -0
- plot_io/ext.py +68 -0
- plot_io/facets.py +240 -0
- plot_io/figure.py +788 -0
- plot_io/formatters.py +97 -0
- plot_io/genome/__init__.py +17 -0
- plot_io/genome/adapters.py +356 -0
- plot_io/genome/combine.py +123 -0
- plot_io/genome/contacts.py +508 -0
- plot_io/genome/intervals.py +750 -0
- plot_io/genome/lanes.py +191 -0
- plot_io/genome/layout.py +184 -0
- plot_io/genome/links.py +364 -0
- plot_io/genome/pack.py +27 -0
- plot_io/genome/readers.py +145 -0
- plot_io/genome/regions.py +228 -0
- plot_io/genome/scale.py +349 -0
- plot_io/genome/series.py +344 -0
- plot_io/genome/signal.py +621 -0
- plot_io/genome/sites.py +782 -0
- plot_io/genome/sources.py +459 -0
- plot_io/genome/testing.py +161 -0
- plot_io/geometry.py +289 -0
- plot_io/groups.py +308 -0
- plot_io/guides.py +503 -0
- plot_io/helpers/__init__.py +17 -0
- plot_io/helpers/clustermap.py +202 -0
- plot_io/helpers/dendrogram.py +172 -0
- plot_io/helpers/grids.py +304 -0
- plot_io/helpers/qq.py +334 -0
- plot_io/helpers/raincloud.py +249 -0
- plot_io/helpers/tree.py +214 -0
- plot_io/helpers/volcano.py +505 -0
- plot_io/hints.py +125 -0
- plot_io/kernels.py +740 -0
- plot_io/labels.py +566 -0
- plot_io/layers/__init__.py +100 -0
- plot_io/layers/base.py +630 -0
- plot_io/layers/context.py +87 -0
- plot_io/layers/core.py +446 -0
- plot_io/layers/geoms.py +1362 -0
- plot_io/layers/grammar.py +586 -0
- plot_io/layers/paint.py +614 -0
- plot_io/layers/pipeline.py +170 -0
- plot_io/layers/positions.py +431 -0
- plot_io/layers/stats.py +1088 -0
- plot_io/layout.py +1381 -0
- plot_io/legend.py +605 -0
- plot_io/levels.py +509 -0
- plot_io/limits.py +124 -0
- plot_io/lines.py +42 -0
- plot_io/mapped.py +522 -0
- plot_io/markers.py +110 -0
- plot_io/markup.py +128 -0
- plot_io/numbers.py +354 -0
- plot_io/occupancy.py +295 -0
- plot_io/plot.py +597 -0
- plot_io/presets/__init__.py +51 -0
- plot_io/presets/_util.py +153 -0
- plot_io/presets/area.py +145 -0
- plot_io/presets/bar.py +261 -0
- plot_io/presets/box.py +400 -0
- plot_io/presets/catpoints.py +177 -0
- plot_io/presets/contour.py +353 -0
- plot_io/presets/curves.py +30 -0
- plot_io/presets/dots.py +355 -0
- plot_io/presets/ecdf.py +97 -0
- plot_io/presets/errorbar.py +106 -0
- plot_io/presets/errors.py +296 -0
- plot_io/presets/fill.py +74 -0
- plot_io/presets/heatmap.py +322 -0
- plot_io/presets/hexbin.py +198 -0
- plot_io/presets/hist.py +391 -0
- plot_io/presets/hist2d.py +87 -0
- plot_io/presets/kde.py +162 -0
- plot_io/presets/kde2d.py +147 -0
- plot_io/presets/line.py +143 -0
- plot_io/presets/pie.py +502 -0
- plot_io/presets/regression.py +115 -0
- plot_io/presets/scatter.py +194 -0
- plot_io/presets/stem.py +100 -0
- plot_io/presets/violin.py +323 -0
- plot_io/py.typed +0 -0
- plot_io/registry.py +82 -0
- plot_io/render.py +480 -0
- plot_io/rows.py +491 -0
- plot_io/scales.py +103 -0
- plot_io/seams.py +138 -0
- plot_io/settings.py +807 -0
- plot_io/shapes.py +66 -0
- plot_io/sharing.py +232 -0
- plot_io/sizemaps.py +97 -0
- plot_io/spec.py +240 -0
- plot_io/states.py +216 -0
- plot_io/styles/__init__.py +307 -0
- plot_io/styles/library.py +397 -0
- plot_io/tables.py +138 -0
- plot_io/text.py +412 -0
- plot_io/ticks.py +400 -0
- plot_io/times.py +287 -0
- plot_io/titles.py +225 -0
- plot_io/transform.py +114 -0
- plot_io/types.py +13604 -0
- plot_io/units.py +768 -0
- plot_io/validators.py +1029 -0
- plot_io/values.py +190 -0
- plot_io-0.12.0.dist-info/METADATA +215 -0
- plot_io-0.12.0.dist-info/RECORD +130 -0
- plot_io-0.12.0.dist-info/WHEEL +4 -0
- plot_io-0.12.0.dist-info/licenses/LICENSE +28 -0
plot_io/color/values.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Color values: the ``color`` validator, named color scales, references to the plot's colors
|
|
2
|
+
(``"C0"``) and operations on colors (alpha, saturation, luminance).
|
|
3
|
+
|
|
4
|
+
Named color scales: ``"[variant] name [level]"``: ``"blue"``, ``"blue 0.45"``,
|
|
5
|
+
``"pastel blue"``, ``"pastel blue 45%"``.
|
|
6
|
+
|
|
7
|
+
Each name has eleven shades: levels 0.05 and 0.1 to 0.9 and 0.95 (Tailwind CSS 4's 50,
|
|
8
|
+
100 to 900 and 950, in OKLCh); level 0 is white and level 1 black. A level between two
|
|
9
|
+
shades is interpolated in Oklab; the color is then gamut mapped into sRGB as CSS Color 4
|
|
10
|
+
specifies (vec_io's ``oklab()``). A bare name is its level 0.5. These names take
|
|
11
|
+
precedence over CSS's (``"red"`` is Red 0.5, not ``#ff0000``; ``"brown"`` is Brown 0.5,
|
|
12
|
+
not ``#a52a2a``); other CSS names (``"navy"``) are vec_io's.
|
|
13
|
+
|
|
14
|
+
Variants (:data:`VARIANTS`): ``base`` (Tailwind's), ``vivid``, ``pastel``, ``forest``,
|
|
15
|
+
``dim``, ``arctic``, ``desert``, ``dusty``, ``neon``. A name without a variant word takes
|
|
16
|
+
the ``color_variant`` in effect where it is drawn (:class:`Named`, :func:`variant_of`).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import colorsys
|
|
20
|
+
import functools
|
|
21
|
+
import re
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from vec_io.paint import CMYK, as_rgba, oklch_to_oklab, parse_color, srgb_to_oklab, to_rgba
|
|
25
|
+
|
|
26
|
+
from .. import hints
|
|
27
|
+
from ..units import Fixed
|
|
28
|
+
from ..validators import Color, typed
|
|
29
|
+
from .variants import TABLES, VARIANTS
|
|
30
|
+
|
|
31
|
+
__all__ = ["SCALES", "VARIANTS", "VARIANT_DOC", "Named", "color", "split_colors", "scale_color", "scale_names",
|
|
32
|
+
"resolve_color", "variant_of", "check_variant", "with_alpha", "desaturate",
|
|
33
|
+
"luminance", "ink_for", "nearest_named"]
|
|
34
|
+
|
|
35
|
+
#: The shades of each scale of the base variant (levels 0.05, 0.1–0.9, 0.95: Tailwind's 50,
|
|
36
|
+
#: 100–900, 950) as OKLCh: lightness in %, chroma, hue in degrees.
|
|
37
|
+
SCALES = {**TABLES["base"], "black": ((0, 0, 0),) * 11, "white": ((100, 0, 0),) * 11}
|
|
38
|
+
_TABLES = {v: {**TABLES[v], "black": SCALES["black"], "white": SCALES["white"]}
|
|
39
|
+
for v in VARIANTS}
|
|
40
|
+
_ALIASES = {"grey": "gray"}
|
|
41
|
+
_RE = re.compile(r"^\s*(?:([a-z]+)\s+)?([a-z]+)(?:\s+(\d*\.?\d+(?:e[-+]?\d+)?)(%?))?\s*$", re.I)
|
|
42
|
+
#: Stop positions: white, the eleven shades, black.
|
|
43
|
+
_STOPS = (0.0, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95, 1.0)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def scale_names() -> list[str]:
|
|
47
|
+
return sorted(SCALES)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
#: The doc of the ``color_variant`` settings.
|
|
51
|
+
VARIANT_DOC = ("Variant of the named colors written without one (`\"red\"`, color maps from names, "
|
|
52
|
+
"the style's colors): `base`, `vivid`, `pastel`, "
|
|
53
|
+
"`forest`, `dim`, `arctic`, `desert`, `dusty`, `neon`.")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@typed("Literal[" + ", ".join(map(repr, VARIANTS)) + "]")
|
|
57
|
+
def check_variant(v: Any) -> str:
|
|
58
|
+
"""A variant word (``"pastel"``); ``ValueError`` naming the nine otherwise."""
|
|
59
|
+
if isinstance(v, str) and v.strip().lower() in VARIANTS:
|
|
60
|
+
return v.strip().lower()
|
|
61
|
+
raise ValueError(f"unknown color variant {v!r} (variants: {', '.join(VARIANTS)})")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _oklab(name: str, i: int, variant: str = "base") -> tuple[float, float, float]:
|
|
65
|
+
"""Oklab of stop ``i`` of a scale (:data:`_STOPS`): 0 white, 1–11 the shades, 12
|
|
66
|
+
black."""
|
|
67
|
+
if i == 0:
|
|
68
|
+
return (1.0, 0.0, 0.0)
|
|
69
|
+
if i == 12:
|
|
70
|
+
return (0.0, 0.0, 0.0)
|
|
71
|
+
L, C, H = _TABLES[variant][name][i - 1]
|
|
72
|
+
return oklch_to_oklab(L / 100, C, H)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@functools.lru_cache(maxsize=4096)
|
|
76
|
+
def _hex(name: str, level: float, variant: str = "base") -> str:
|
|
77
|
+
if 0.1 <= level <= 0.9: # between shades 100 and 900
|
|
78
|
+
pos = level * 10
|
|
79
|
+
i = min(int(pos), 9)
|
|
80
|
+
t = pos - i
|
|
81
|
+
i += 1
|
|
82
|
+
else: # white, 50, 100 or 900, 950, black
|
|
83
|
+
pos = (level if level < 0.1 else level - 0.9) * 20
|
|
84
|
+
i = min(int(pos), 1)
|
|
85
|
+
t = pos - i
|
|
86
|
+
if level > 0.9:
|
|
87
|
+
i += 10
|
|
88
|
+
a, b = _oklab(name, i, variant), _oklab(name, i + 1, variant)
|
|
89
|
+
if t < 1e-9 or t > 1 - 1e-9:
|
|
90
|
+
t = round(t)
|
|
91
|
+
lab = [x + (y - x) * t for x, y in zip(a, b)]
|
|
92
|
+
color = parse_color("oklab({!r} {!r} {!r})".format(*lab))
|
|
93
|
+
if color is None: # not reached: valid oklab()
|
|
94
|
+
raise ValueError(f"no color for {name} {level}")
|
|
95
|
+
return "#" + "".join(f"{round(v * 255):02x}" for v in as_rgba(color)[:3])
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def rgba_of(c: Any) -> tuple[float, ...] | None:
|
|
99
|
+
"""``(r, g, b, a)`` of a color (a print color's RGB approximation); ``None`` for none."""
|
|
100
|
+
r = to_rgba(c)
|
|
101
|
+
return None if r is None else as_rgba(r)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def nearest_named(c: Any, level: float | None = None, variant: str = "base") -> Any:
|
|
105
|
+
"""The named color closest to ``c`` (in Oklab) in ``variant``: the nearest of the
|
|
106
|
+
shades 100 to 900 (levels 0.1–0.9), or with ``level``, the scale of the nearest shade
|
|
107
|
+
at that level. Colors vec_io cannot read (``None``, CMYK) are returned as given; alpha
|
|
108
|
+
is kept."""
|
|
109
|
+
if c is None or isinstance(c, CMYK):
|
|
110
|
+
return c
|
|
111
|
+
rgba = rgba_of(c)
|
|
112
|
+
if rgba is None:
|
|
113
|
+
return c
|
|
114
|
+
near = _nearest(tuple(round(v, 6) for v in rgba[:3]), level, variant)
|
|
115
|
+
return near if len(rgba) < 4 or rgba[3] >= 1 else with_alpha(near, rgba[3])
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@functools.lru_cache(maxsize=4096)
|
|
119
|
+
def _nearest(rgb: tuple[float, ...], level: float | None, variant: str) -> str:
|
|
120
|
+
lab = srgb_to_oklab(*rgb)
|
|
121
|
+
best = (float("inf"), "", 0)
|
|
122
|
+
for name in SCALES:
|
|
123
|
+
for i in range(1, 10):
|
|
124
|
+
d = sum((x - y) ** 2 for x, y in zip(lab, _oklab(name, i + 1, variant)))
|
|
125
|
+
if d < best[0]:
|
|
126
|
+
best = (d, name, i)
|
|
127
|
+
_, name, i = best
|
|
128
|
+
return _hex(name, i / 10 if level is None else level, variant)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class Named(Fixed):
|
|
132
|
+
"""A named color as checked: ``source`` the text given, ``value`` its hex in its own
|
|
133
|
+
variant (``variant``, or base when it has none); a name without a variant word takes
|
|
134
|
+
the variant in effect where it is drawn (:meth:`in_variant`)."""
|
|
135
|
+
|
|
136
|
+
__slots__ = ("name", "level", "variant")
|
|
137
|
+
|
|
138
|
+
def __init__(self, source: str, name: str, level: float, variant: str | None) -> None:
|
|
139
|
+
super().__init__(source, _hex(name, level, variant or "base"))
|
|
140
|
+
self.name, self.level, self.variant = name, level, variant
|
|
141
|
+
|
|
142
|
+
def in_variant(self, variant: str) -> str:
|
|
143
|
+
"""The hex in ``variant``, unless the name has its own."""
|
|
144
|
+
if self.variant is not None or variant == "base":
|
|
145
|
+
return self.value
|
|
146
|
+
return _hex(self.name, self.level, variant)
|
|
147
|
+
|
|
148
|
+
def in_context(self, obj: Any) -> str:
|
|
149
|
+
return self.value if self.variant is not None else self.in_variant(variant_of(obj))
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def varies(self) -> bool:
|
|
153
|
+
return self.variant is None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def parse_named(text: Any) -> tuple[str, float, str | None] | None:
|
|
157
|
+
"""``(scale, level, variant or None)`` of a named color (``"pastel blue 0.3"``);
|
|
158
|
+
``None`` when ``text`` is not one. A level outside 0–1 or an unknown variant word
|
|
159
|
+
before a scale name raises ``ValueError``."""
|
|
160
|
+
if not isinstance(text, str):
|
|
161
|
+
return None
|
|
162
|
+
m = _RE.match(text)
|
|
163
|
+
if m is None:
|
|
164
|
+
return None
|
|
165
|
+
name = m.group(2).lower()
|
|
166
|
+
name = _ALIASES.get(name, name)
|
|
167
|
+
if name not in SCALES:
|
|
168
|
+
return None
|
|
169
|
+
variant = m.group(1)
|
|
170
|
+
if variant is not None:
|
|
171
|
+
variant = variant.lower()
|
|
172
|
+
if variant not in VARIANTS:
|
|
173
|
+
raise ValueError(f"{text!r}: unknown color variant {m.group(1)!r} (variants: "
|
|
174
|
+
f"{', '.join(VARIANTS)})")
|
|
175
|
+
level = 0.5 if m.group(3) is None else float(m.group(3)) / (100 if m.group(4) else 1)
|
|
176
|
+
if not 0 <= level <= 1:
|
|
177
|
+
raise ValueError(f"{text!r}: the level of a named color is in 0–1, got {level:g}")
|
|
178
|
+
return name, level, variant
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def scale_color(text: Any, variant: str = "base") -> str | None:
|
|
182
|
+
"""``"#rrggbb"`` of a named color (``"name"``, ``"name level"``, level 0–1 or a
|
|
183
|
+
percentage, a variant word first or ``variant``); ``None`` when ``text`` is not a
|
|
184
|
+
named color. A level outside 0–1 or an unknown variant word raises ``ValueError``."""
|
|
185
|
+
p = parse_named(text)
|
|
186
|
+
if p is None:
|
|
187
|
+
return None
|
|
188
|
+
return _hex(p[0], p[1], p[2] or variant)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def variant_of(obj: Any) -> str:
|
|
192
|
+
"""The ``color_variant`` in effect for ``obj``: its own setting, else its nearest
|
|
193
|
+
ancestor's that has the setting; ``"base"`` when none has it."""
|
|
194
|
+
o = obj
|
|
195
|
+
while o is not None:
|
|
196
|
+
settings = getattr(o, "SETTINGS", None)
|
|
197
|
+
if settings is not None and "color_variant" in settings:
|
|
198
|
+
return o.resolve("color_variant")
|
|
199
|
+
parent = getattr(o, "_parent", None)
|
|
200
|
+
o = parent() if parent is not None else None
|
|
201
|
+
return "base"
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _color(v: Any) -> Any:
|
|
205
|
+
"""A color vec_io accepts, a named color (``"blue"``, ``"pastel blue 0.45"``) or a
|
|
206
|
+
reference to the plot's colors ``"C0"``, ``"C1"``, …"""
|
|
207
|
+
if isinstance(v, Fixed): # already checked
|
|
208
|
+
return v
|
|
209
|
+
if isinstance(v, str) and len(v) >= 2 and v[0] in "Cc" and v[1:].isascii() and v[1:].isdigit():
|
|
210
|
+
return "C" + v[1:]
|
|
211
|
+
named = parse_named(v)
|
|
212
|
+
if named is not None:
|
|
213
|
+
return Named(v.strip(), *named)
|
|
214
|
+
try:
|
|
215
|
+
rgba = to_rgba(v)
|
|
216
|
+
except (TypeError, ValueError):
|
|
217
|
+
rgba = None
|
|
218
|
+
if rgba is None and not (isinstance(v, str) and v.strip().lower() in ("none", "transparent")):
|
|
219
|
+
raise ValueError(f"invalid color {v!r}{hints.color(v)}")
|
|
220
|
+
return v
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
#: The validator of colors (``color(v)`` checks ``v``).
|
|
224
|
+
color = Color(_color)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def resolve_color(c: Any, cycle: tuple[Any, ...]) -> Any:
|
|
228
|
+
"""A color with ``"C<n>"`` replaced by the plot's color ``n`` (of ``cycle``)."""
|
|
229
|
+
if isinstance(c, str) and len(c) >= 2 and c[0] == "C" and c[1:].isascii() and c[1:].isdigit():
|
|
230
|
+
return cycle[int(c[1:]) % len(cycle)]
|
|
231
|
+
if isinstance(c, str) and c.strip().lower() in ("none", "transparent"):
|
|
232
|
+
return None
|
|
233
|
+
return c
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def with_alpha(c: Any, opacity: float) -> Any:
|
|
237
|
+
"""``c`` with its alpha multiplied by ``opacity`` (as an RGBA tuple)."""
|
|
238
|
+
if c is None:
|
|
239
|
+
return None
|
|
240
|
+
if opacity >= 1:
|
|
241
|
+
return c
|
|
242
|
+
if isinstance(c, CMYK):
|
|
243
|
+
return c.with_alpha(c.alpha * opacity)
|
|
244
|
+
rgba = rgba_of(c)
|
|
245
|
+
if rgba is None:
|
|
246
|
+
return None
|
|
247
|
+
a = rgba[3] if len(rgba) > 3 else 1.0
|
|
248
|
+
return (rgba[0], rgba[1], rgba[2], a * opacity)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def desaturate(c: Any, k: float) -> Any:
|
|
252
|
+
"""``c`` with its HLS saturation multiplied by ``k`` (as an RGBA tuple)."""
|
|
253
|
+
rgba = rgba_of(c)
|
|
254
|
+
if rgba is None:
|
|
255
|
+
return None
|
|
256
|
+
h, light, s = colorsys.rgb_to_hls(rgba[0], rgba[1], rgba[2])
|
|
257
|
+
r, g, b = colorsys.hls_to_rgb(h, light, s * k)
|
|
258
|
+
return (r, g, b, rgba[3] if len(rgba) > 3 else 1.0)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def luminance(c: Any) -> float:
|
|
262
|
+
"""Luma of ``c`` (0–1): Rec. 709 weights of its sRGB components (not linearized),
|
|
263
|
+
composited over white."""
|
|
264
|
+
if c is None:
|
|
265
|
+
return 1.0
|
|
266
|
+
rgba = rgba_of(c)
|
|
267
|
+
if rgba is None:
|
|
268
|
+
return 1.0
|
|
269
|
+
r, g, b = (float(x) for x in rgba[:3])
|
|
270
|
+
a = float(rgba[3]) if len(rgba) > 3 else 1.0
|
|
271
|
+
lum = 0.2126 * r + 0.7152 * g + 0.0722 * b
|
|
272
|
+
return a * lum + (1.0 - a) # over a white background
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def ink_for(fill: Any) -> str:
|
|
276
|
+
"""Black or white, whichever contrasts more with ``fill``."""
|
|
277
|
+
return "black" if luminance(fill) > 0.45 else "white"
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def split_colors(text: str) -> list[str] | None:
|
|
281
|
+
"""The colors of a comma-separated list (``"violet 0.4, sky, rgb(1 2 3)"``; commas
|
|
282
|
+
inside parentheses do not split), or ``None`` when ``text`` has no list comma."""
|
|
283
|
+
parts, depth, start = [], 0, 0
|
|
284
|
+
for i, ch in enumerate(text):
|
|
285
|
+
if ch == "(":
|
|
286
|
+
depth += 1
|
|
287
|
+
elif ch == ")":
|
|
288
|
+
depth -= 1
|
|
289
|
+
elif ch == "," and depth == 0:
|
|
290
|
+
parts.append(text[start:i])
|
|
291
|
+
start = i + 1
|
|
292
|
+
if not parts:
|
|
293
|
+
return None
|
|
294
|
+
parts.append(text[start:])
|
|
295
|
+
return [p.strip() for p in parts]
|