cnotebook 2.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cnotebook/__init__.py +400 -0
- cnotebook/align.py +454 -0
- cnotebook/context.py +523 -0
- cnotebook/grid/__init__.py +55 -0
- cnotebook/grid/grid.py +1649 -0
- cnotebook/helpers.py +201 -0
- cnotebook/ipython_ext.py +56 -0
- cnotebook/marimo_ext.py +272 -0
- cnotebook/pandas_ext.py +1156 -0
- cnotebook/polars_ext.py +1235 -0
- cnotebook/render.py +200 -0
- cnotebook-2.1.0.dist-info/METADATA +336 -0
- cnotebook-2.1.0.dist-info/RECORD +16 -0
- cnotebook-2.1.0.dist-info/WHEEL +5 -0
- cnotebook-2.1.0.dist-info/licenses/LICENSE +21 -0
- cnotebook-2.1.0.dist-info/top_level.txt +1 -0
cnotebook/context.py
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from typing import Callable, Any, Literal, Generic, TypeVar
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
# noinspection PyPackageRequirements
|
|
7
|
+
from contextvars import ContextVar
|
|
8
|
+
from openeye import oechem, oedepict
|
|
9
|
+
|
|
10
|
+
log = logging.getLogger("cnotebook")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _Deferred(Enum):
|
|
14
|
+
"""
|
|
15
|
+
Sentinel to defer to global context
|
|
16
|
+
This uses the approach suggested by Guido van Rossum
|
|
17
|
+
https://github.com/python/typing/issues/236#issuecomment-227180301
|
|
18
|
+
"""
|
|
19
|
+
value = 0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
DEFERRED = _Deferred.value
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
########################################################################################################################
|
|
26
|
+
# Global Rendering Context
|
|
27
|
+
########################################################################################################################
|
|
28
|
+
|
|
29
|
+
T = TypeVar('T')
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DeferredValue(Generic[T]):
|
|
33
|
+
"""A value that can be deferred to the global CNotebook context.
|
|
34
|
+
|
|
35
|
+
When a value is set to ``DEFERRED``, accessing it will look up the
|
|
36
|
+
corresponding attribute from the global context instead.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, name: str, value: T | _Deferred):
|
|
40
|
+
"""Create a deferred value.
|
|
41
|
+
|
|
42
|
+
:param name: Attribute name to look up in global context when deferred.
|
|
43
|
+
:param value: Initial value, or ``DEFERRED`` to use global context.
|
|
44
|
+
"""
|
|
45
|
+
self.name = name
|
|
46
|
+
self._value = value
|
|
47
|
+
self._initial_value = value
|
|
48
|
+
|
|
49
|
+
def reset(self):
|
|
50
|
+
"""
|
|
51
|
+
Reset this deferred value to the initial value (when the object was created)
|
|
52
|
+
"""
|
|
53
|
+
self._value = self._initial_value
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def is_deferred(self) -> bool:
|
|
57
|
+
"""
|
|
58
|
+
Check if the value is deferred to the global context
|
|
59
|
+
:return: True if the value is deferred to the global
|
|
60
|
+
"""
|
|
61
|
+
return self._value is DEFERRED
|
|
62
|
+
|
|
63
|
+
def get(self) -> T:
|
|
64
|
+
"""
|
|
65
|
+
If the value is DEFERRED then we defer to the local context
|
|
66
|
+
:return: Value
|
|
67
|
+
"""
|
|
68
|
+
if self.is_deferred:
|
|
69
|
+
ctx = cnotebook_context.get()
|
|
70
|
+
if not hasattr(ctx, self.name):
|
|
71
|
+
raise AttributeError(f"Global context missing attribute '{self.name}'")
|
|
72
|
+
return getattr(ctx, self.name)
|
|
73
|
+
return self._value
|
|
74
|
+
|
|
75
|
+
def set(self, value: T | _Deferred) -> None:
|
|
76
|
+
"""
|
|
77
|
+
Set a value (we never set the global context)
|
|
78
|
+
:param value: Value to set
|
|
79
|
+
"""
|
|
80
|
+
self._value = value
|
|
81
|
+
|
|
82
|
+
def __str__(self):
|
|
83
|
+
return str(self.get())
|
|
84
|
+
|
|
85
|
+
def __repr__(self):
|
|
86
|
+
return repr(self.get())
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class CNotebookContext:
|
|
90
|
+
"""Context for rendering OpenEye objects in IPython/Jupyter environments.
|
|
91
|
+
|
|
92
|
+
This context controls how molecules and other OpenEye objects are rendered
|
|
93
|
+
as images. It supports deferred values that fall back to a global context.
|
|
94
|
+
|
|
95
|
+
:cvar supported_mime_types: Mapping of image formats to MIME types.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
# Supported image formats and their MIME types for rendering
|
|
99
|
+
supported_mime_types = {
|
|
100
|
+
'png': 'image/png',
|
|
101
|
+
'svg': 'image/svg+xml'
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
*,
|
|
107
|
+
width: float | _Deferred = 0,
|
|
108
|
+
height: float | _Deferred = 0,
|
|
109
|
+
min_width: float | None | _Deferred = 200.0,
|
|
110
|
+
min_height: float | None | _Deferred = 200.0,
|
|
111
|
+
max_width: float | None | _Deferred = None,
|
|
112
|
+
max_height: float | None | _Deferred = None,
|
|
113
|
+
structure_scale: float | _Deferred = oedepict.OEScale_Default * 0.6,
|
|
114
|
+
atom_label_font_scale: float | _Deferred = 1.0,
|
|
115
|
+
title_font_scale: float | _Deferred = 1.0,
|
|
116
|
+
image_format: str | _Deferred = "png",
|
|
117
|
+
bond_width_scaling: bool | _Deferred = False,
|
|
118
|
+
callbacks: Iterable[Callable[[oedepict.OE2DMolDisplay], None]] | None | _Deferred = None,
|
|
119
|
+
scope: Literal["local", "global"] = "global",
|
|
120
|
+
title: bool = True
|
|
121
|
+
):
|
|
122
|
+
"""Create a rendering context.
|
|
123
|
+
|
|
124
|
+
:param width: Image width in pixels. If 0, determined by structure scale.
|
|
125
|
+
:param height: Image height in pixels. If 0, determined by structure scale.
|
|
126
|
+
:param min_width: Minimum image width in pixels (prevents tiny images).
|
|
127
|
+
:param min_height: Minimum image height in pixels (prevents tiny images).
|
|
128
|
+
:param max_width: Maximum image width in pixels, or None for no limit.
|
|
129
|
+
:param max_height: Maximum image height in pixels, or None for no limit.
|
|
130
|
+
:param structure_scale: Scale factor for structure rendering.
|
|
131
|
+
:param atom_label_font_scale: Scale factor for atom labels (0.5 to 2.0).
|
|
132
|
+
:param title_font_scale: Scale factor for title font (0.5 to 2.0).
|
|
133
|
+
:param image_format: Output image format ("png" or "svg").
|
|
134
|
+
:param bond_width_scaling: Whether to scale bond widths with structure scale.
|
|
135
|
+
:param callbacks: List of callables to invoke on OE2DMolDisplay before rendering.
|
|
136
|
+
Each callback receives the display object and can modify it.
|
|
137
|
+
:param scope: Context scope - "local" defers unset values to global context,
|
|
138
|
+
"global" uses defaults directly.
|
|
139
|
+
:param title: Whether to display molecule titles.
|
|
140
|
+
"""
|
|
141
|
+
self._width = DeferredValue[float]("width", width)
|
|
142
|
+
self._height = DeferredValue[float]("height", height)
|
|
143
|
+
self._min_height = DeferredValue[float | None]("min_height", min_height)
|
|
144
|
+
self._min_width = DeferredValue[float | None]("min_width", min_width)
|
|
145
|
+
self._max_width = DeferredValue[float | None]("max_width", max_width)
|
|
146
|
+
self._max_height = DeferredValue[float | None]("max_height", max_height)
|
|
147
|
+
self._structure_scale = DeferredValue[float]("structure_scale", structure_scale)
|
|
148
|
+
self._atom_label_font_scale = DeferredValue[float | None]("atom_label_font_scale", atom_label_font_scale)
|
|
149
|
+
self._title_font_scale = DeferredValue[float]("title_font_scale", title_font_scale)
|
|
150
|
+
self._image_format = DeferredValue[str]("image_format", image_format)
|
|
151
|
+
self._bond_width_scaling = DeferredValue[bool]("bond_width_scaling", bond_width_scaling)
|
|
152
|
+
self._title = DeferredValue[bool]("title", title)
|
|
153
|
+
self._scope = scope
|
|
154
|
+
|
|
155
|
+
# Set the callbacks (and do some type checking)
|
|
156
|
+
if callbacks is None:
|
|
157
|
+
self._callbacks = DeferredValue[list[Callable[[oedepict.OE2DMolDisplay], None]]](
|
|
158
|
+
"callbacks",
|
|
159
|
+
DEFERRED if scope == "local" else []
|
|
160
|
+
)
|
|
161
|
+
elif isinstance(callbacks, Iterable):
|
|
162
|
+
self._callbacks = DeferredValue[list[Callable[[oedepict.OE2DMolDisplay], None]]](
|
|
163
|
+
"callbacks",
|
|
164
|
+
list(callbacks)
|
|
165
|
+
)
|
|
166
|
+
elif callbacks is DEFERRED:
|
|
167
|
+
self._callbacks = DeferredValue[list[Callable[[oedepict.OE2DMolDisplay], None]]](
|
|
168
|
+
"callbacks",
|
|
169
|
+
DEFERRED
|
|
170
|
+
)
|
|
171
|
+
else:
|
|
172
|
+
raise TypeError(f'Invalid type for display callbacks: {type(callbacks).__name__}')
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def width(self) -> float:
|
|
176
|
+
return self._width.get()
|
|
177
|
+
|
|
178
|
+
@width.setter
|
|
179
|
+
def width(self, value: float) -> None:
|
|
180
|
+
if self.max_width is not None and value > self.max_width:
|
|
181
|
+
log.warning(f'Width exceeds max_width: {value} > {self.max_width}')
|
|
182
|
+
|
|
183
|
+
self._width.set(value)
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def height(self) -> float:
|
|
187
|
+
return self._height.get()
|
|
188
|
+
|
|
189
|
+
@height.setter
|
|
190
|
+
def height(self, value: float) -> None:
|
|
191
|
+
if self.max_height is not None and value > self.max_height:
|
|
192
|
+
log.warning(f'Height exceeds max_height: {value} > {self.max_height}')
|
|
193
|
+
|
|
194
|
+
self._height.set(value)
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def min_width(self) -> float | None:
|
|
198
|
+
return self._min_width.get()
|
|
199
|
+
|
|
200
|
+
@min_width.setter
|
|
201
|
+
def min_width(self, value: float | None) -> None:
|
|
202
|
+
self._min_width.set(value)
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def max_width(self) -> float | None:
|
|
206
|
+
return self._max_width.get()
|
|
207
|
+
|
|
208
|
+
@max_width.setter
|
|
209
|
+
def max_width(self, value: float | None):
|
|
210
|
+
if value is not None and self.width > value:
|
|
211
|
+
log.warning(f'Current width exceeds max_width: {self.width} > {value}')
|
|
212
|
+
|
|
213
|
+
self._max_width.set(value)
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def max_height(self) -> float | None:
|
|
217
|
+
return self._max_height.get()
|
|
218
|
+
|
|
219
|
+
@max_height.setter
|
|
220
|
+
def max_height(self, value: float | None):
|
|
221
|
+
if value is not None and self.height > value:
|
|
222
|
+
log.warning(f'Current height exceeds max_height: {self.height} > {value}')
|
|
223
|
+
|
|
224
|
+
self._max_height.set(value)
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def min_height(self) -> float | None:
|
|
228
|
+
return self._min_height.get()
|
|
229
|
+
|
|
230
|
+
@min_height.setter
|
|
231
|
+
def min_height(self, value: float | None) -> None:
|
|
232
|
+
self._min_height.set(value)
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def structure_scale(self) -> float:
|
|
236
|
+
return self._structure_scale.get()
|
|
237
|
+
|
|
238
|
+
@structure_scale.setter
|
|
239
|
+
def structure_scale(self, value: float) -> None:
|
|
240
|
+
self._structure_scale.set(value)
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def atom_label_font_scale(self) -> float:
|
|
244
|
+
return self._atom_label_font_scale.get()
|
|
245
|
+
|
|
246
|
+
@atom_label_font_scale.setter
|
|
247
|
+
def atom_label_font_scale(self, value: float) -> None:
|
|
248
|
+
self._atom_label_font_scale.set(value)
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def title_font_scale(self) -> float:
|
|
252
|
+
return self._title_font_scale.get()
|
|
253
|
+
|
|
254
|
+
@title_font_scale.setter
|
|
255
|
+
def title_font_scale(self, value: float) -> None:
|
|
256
|
+
self._title_font_scale.set(value)
|
|
257
|
+
|
|
258
|
+
@property
|
|
259
|
+
def bond_width_scaling(self) -> bool:
|
|
260
|
+
return self._bond_width_scaling.get()
|
|
261
|
+
|
|
262
|
+
@bond_width_scaling.setter
|
|
263
|
+
def bond_width_scaling(self, value: bool) -> None:
|
|
264
|
+
self._bond_width_scaling.set(value)
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def image_format(self) -> str:
|
|
268
|
+
return self._image_format.get()
|
|
269
|
+
|
|
270
|
+
@image_format.setter
|
|
271
|
+
def image_format(self, value: str) -> None:
|
|
272
|
+
self._image_format.set(value)
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def scope(self) -> Literal["global", "local"]:
|
|
276
|
+
return self._scope
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def callbacks(self) -> tuple[Callable[[oedepict.OE2DMolDisplay], None], ...]:
|
|
280
|
+
# noinspection PyTypeChecker
|
|
281
|
+
return tuple(self._callbacks.get())
|
|
282
|
+
|
|
283
|
+
def reset_callbacks(self) -> None:
|
|
284
|
+
self._callbacks.reset()
|
|
285
|
+
|
|
286
|
+
@property
|
|
287
|
+
def title(self) -> bool:
|
|
288
|
+
return self._title.get()
|
|
289
|
+
|
|
290
|
+
@title.setter
|
|
291
|
+
def title(self, value: bool) -> None:
|
|
292
|
+
self._title.set(value)
|
|
293
|
+
|
|
294
|
+
@property
|
|
295
|
+
def image_mime_type(self) -> str:
|
|
296
|
+
mime_type = self.supported_mime_types.get(self.image_format, None)
|
|
297
|
+
if mime_type is None:
|
|
298
|
+
raise KeyError(f'No MIME type registered for image format {self.image_format}')
|
|
299
|
+
return mime_type
|
|
300
|
+
|
|
301
|
+
@property
|
|
302
|
+
def display_options(self) -> oedepict.OE2DMolDisplayOptions:
|
|
303
|
+
opts = oedepict.OE2DMolDisplayOptions()
|
|
304
|
+
opts.SetHeight(self.height)
|
|
305
|
+
opts.SetWidth(self.width)
|
|
306
|
+
opts.SetScale(self.structure_scale)
|
|
307
|
+
opts.SetTitleFontScale(self.title_font_scale)
|
|
308
|
+
opts.SetBondWidthScaling(self.bond_width_scaling)
|
|
309
|
+
opts.SetAtomLabelFontScale(self.atom_label_font_scale)
|
|
310
|
+
|
|
311
|
+
if not self.title:
|
|
312
|
+
opts.SetTitleLocation(oedepict.OETitleLocation_Hidden)
|
|
313
|
+
|
|
314
|
+
return opts
|
|
315
|
+
|
|
316
|
+
def add_callback(self, callback: Callable[[oedepict.OE2DMolDisplay], None]):
|
|
317
|
+
"""
|
|
318
|
+
Add a callback that modifies an oedepict.OE2DMolDisplay to the current context
|
|
319
|
+
:param callback: Callback to add
|
|
320
|
+
"""
|
|
321
|
+
if self._callbacks.is_deferred:
|
|
322
|
+
self._callbacks.set([])
|
|
323
|
+
self._callbacks.get().append(callback)
|
|
324
|
+
|
|
325
|
+
def create_molecule_display(
|
|
326
|
+
self,
|
|
327
|
+
mol: oechem.OEMolBase,
|
|
328
|
+
min_height: int | None = None,
|
|
329
|
+
min_width: int | None = None
|
|
330
|
+
) -> oedepict.OE2DMolDisplay:
|
|
331
|
+
"""
|
|
332
|
+
Create a molecule display that enforces minimum image height and width
|
|
333
|
+
:param mol: Molecule
|
|
334
|
+
:param min_height: Minimum image height
|
|
335
|
+
:param min_width: Minimum image width
|
|
336
|
+
:return: Molecule display
|
|
337
|
+
"""
|
|
338
|
+
disp = oedepict.OE2DMolDisplay(mol, self.display_options)
|
|
339
|
+
|
|
340
|
+
# If the image was too small, and we're not enforcing a specific image size
|
|
341
|
+
if ((self.width == 0.0 and self.min_width is not None and disp.GetWidth() < self.min_width) or
|
|
342
|
+
(self.height == 0.0 and self.min_height is not None and disp.GetHeight() < self.min_height)):
|
|
343
|
+
|
|
344
|
+
min_height = min_height or self.min_height
|
|
345
|
+
min_width = min_width or self.min_width
|
|
346
|
+
|
|
347
|
+
# Create a new display context
|
|
348
|
+
new_ctx = self.copy()
|
|
349
|
+
|
|
350
|
+
# If width was not enforced already, then enforce the minimum width
|
|
351
|
+
if self.width == 0.0 and min_width is not None:
|
|
352
|
+
new_ctx.width = min_width if disp.GetWidth() < self.min_width else 0.0
|
|
353
|
+
|
|
354
|
+
# If height was not enforced already, then enforce the minimum height
|
|
355
|
+
if self.height == 0.0 and min_height is not None:
|
|
356
|
+
new_ctx.height = min_height if disp.GetHeight() < self.min_height else 0.0
|
|
357
|
+
|
|
358
|
+
# Create the display object
|
|
359
|
+
disp = oedepict.OE2DMolDisplay(mol, new_ctx.display_options)
|
|
360
|
+
|
|
361
|
+
# We need to scale down the image if it exceeds the max_width or max_height
|
|
362
|
+
if ((self.max_width is not None and disp.GetWidth() > self.max_width) or
|
|
363
|
+
(self.max_height is not None and disp.GetHeight() > self.max_height)):
|
|
364
|
+
|
|
365
|
+
# Create a new display context
|
|
366
|
+
new_ctx = self.copy()
|
|
367
|
+
|
|
368
|
+
# Set whatever parameter exceeded the maximum and let the other scale
|
|
369
|
+
if self.max_width is not None and disp.GetWidth() > self.max_width:
|
|
370
|
+
new_ctx.width = self.max_width
|
|
371
|
+
new_ctx.height = 0
|
|
372
|
+
|
|
373
|
+
elif self.max_height is not None and disp.GetHeight() > self.max_height:
|
|
374
|
+
new_ctx.width = 0
|
|
375
|
+
new_ctx.height = self.max_height
|
|
376
|
+
|
|
377
|
+
new_ctx.structure_scale = oedepict.OEScale_AutoScale
|
|
378
|
+
|
|
379
|
+
# Create the display object
|
|
380
|
+
disp = oedepict.OE2DMolDisplay(mol, new_ctx.display_options)
|
|
381
|
+
|
|
382
|
+
# TODO: Check the display again and see if we've exceeded max width or height again and potentially
|
|
383
|
+
# constrain both width and height
|
|
384
|
+
|
|
385
|
+
return disp
|
|
386
|
+
|
|
387
|
+
def reset(self) -> None:
|
|
388
|
+
"""
|
|
389
|
+
Reset the rendering context to default values
|
|
390
|
+
"""
|
|
391
|
+
self._width.reset()
|
|
392
|
+
self._height.reset()
|
|
393
|
+
self._min_width.reset()
|
|
394
|
+
self._min_height.reset()
|
|
395
|
+
self._max_width.reset()
|
|
396
|
+
self._max_height.reset()
|
|
397
|
+
self._structure_scale.reset()
|
|
398
|
+
self._title_font_scale.reset()
|
|
399
|
+
self._image_format.reset()
|
|
400
|
+
self._bond_width_scaling.reset()
|
|
401
|
+
self._title.reset()
|
|
402
|
+
self._callbacks.reset()
|
|
403
|
+
|
|
404
|
+
def copy(self) -> 'CNotebookContext':
|
|
405
|
+
"""
|
|
406
|
+
Copy this object
|
|
407
|
+
:return: Copy of the object
|
|
408
|
+
"""
|
|
409
|
+
return CNotebookContext(
|
|
410
|
+
width=self.width,
|
|
411
|
+
height=self.height,
|
|
412
|
+
min_width=self.min_width,
|
|
413
|
+
min_height=self.min_height,
|
|
414
|
+
max_width=self.max_width,
|
|
415
|
+
max_height=self.max_height,
|
|
416
|
+
structure_scale=self.structure_scale,
|
|
417
|
+
title_font_scale=self.title_font_scale,
|
|
418
|
+
title=self.title,
|
|
419
|
+
image_format=self.image_format,
|
|
420
|
+
bond_width_scaling=self.bond_width_scaling,
|
|
421
|
+
callbacks=self.callbacks,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
########################################################################################################################
|
|
426
|
+
# !!!!!!!!! Global render context !!!!!!!!!
|
|
427
|
+
########################################################################################################################
|
|
428
|
+
|
|
429
|
+
# Create our global render context
|
|
430
|
+
cnotebook_context: ContextVar[CNotebookContext] = ContextVar("cnotebook_context", default=CNotebookContext())
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
########################################################################################################################
|
|
434
|
+
# Decorator to automatically pass global rendering context
|
|
435
|
+
########################################################################################################################
|
|
436
|
+
|
|
437
|
+
def pass_cnotebook_context(func):
|
|
438
|
+
"""
|
|
439
|
+
Decorator that passes a copy of the current molecule render context
|
|
440
|
+
:param func: Function to decorate
|
|
441
|
+
:return: Decorated function
|
|
442
|
+
"""
|
|
443
|
+
# TODO: Inspect func signature and check that it uses the ctx keyword
|
|
444
|
+
@wraps(func)
|
|
445
|
+
def call_with_render_context(*args, **kwargs):
|
|
446
|
+
|
|
447
|
+
# If we happened to be called with a custom molecule render context
|
|
448
|
+
if "ctx" in kwargs:
|
|
449
|
+
ctx = kwargs.pop("ctx")
|
|
450
|
+
|
|
451
|
+
if ctx is None:
|
|
452
|
+
ctx = cnotebook_context.get().copy()
|
|
453
|
+
|
|
454
|
+
# Other things are not OK
|
|
455
|
+
elif not isinstance(ctx, CNotebookContext):
|
|
456
|
+
raise TypeError("Received object of type type {} for OERenderContext (ctx) when calling {}".format(
|
|
457
|
+
type(ctx).__name__,
|
|
458
|
+
func.__name__
|
|
459
|
+
))
|
|
460
|
+
else:
|
|
461
|
+
ctx = cnotebook_context.get().copy()
|
|
462
|
+
|
|
463
|
+
# Call the function
|
|
464
|
+
return func(*args, **kwargs, ctx=ctx)
|
|
465
|
+
return call_with_render_context
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
########################################################################################################################
|
|
469
|
+
# Local rendering context
|
|
470
|
+
########################################################################################################################
|
|
471
|
+
|
|
472
|
+
def create_local_context(
|
|
473
|
+
width: float = DEFERRED,
|
|
474
|
+
height: float = DEFERRED,
|
|
475
|
+
min_width: float = DEFERRED,
|
|
476
|
+
min_height: float = DEFERRED,
|
|
477
|
+
max_width: float = DEFERRED,
|
|
478
|
+
max_height: float = DEFERRED,
|
|
479
|
+
structure_scale: int = DEFERRED,
|
|
480
|
+
title_font_scale: float = DEFERRED,
|
|
481
|
+
image_format: str = DEFERRED,
|
|
482
|
+
bond_width_scaling: bool = DEFERRED,
|
|
483
|
+
callbacks: Iterable[Callable[[oedepict.OE2DMolDisplay], None]] | None = DEFERRED
|
|
484
|
+
) -> CNotebookContext:
|
|
485
|
+
return CNotebookContext(
|
|
486
|
+
width=width,
|
|
487
|
+
height=height,
|
|
488
|
+
min_width=min_width,
|
|
489
|
+
min_height=min_height,
|
|
490
|
+
max_width=max_width,
|
|
491
|
+
max_height=max_height,
|
|
492
|
+
structure_scale=structure_scale,
|
|
493
|
+
title_font_scale=title_font_scale,
|
|
494
|
+
image_format=image_format,
|
|
495
|
+
bond_width_scaling=bond_width_scaling,
|
|
496
|
+
callbacks=callbacks,
|
|
497
|
+
scope="local"
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def get_series_context(metadata: dict[Any, Any], save: bool = False) -> CNotebookContext:
|
|
502
|
+
"""
|
|
503
|
+
Get the series context, else wrap the global context into a series context. This looks for the key "cnotebook" in
|
|
504
|
+
the metadta.
|
|
505
|
+
:param metadata: Series metadata
|
|
506
|
+
:param save: Whether to save any new metadata object that we create
|
|
507
|
+
:return: Series rendering context
|
|
508
|
+
"""
|
|
509
|
+
ctx = metadata.get("cnotebook", create_local_context())
|
|
510
|
+
|
|
511
|
+
# Make sure context is a valid object
|
|
512
|
+
if not isinstance(ctx, CNotebookContext):
|
|
513
|
+
log.warning(
|
|
514
|
+
"Replacing unexpected object of type %s for metadata key 'cnotebook' with a CNotebookLocalContext",
|
|
515
|
+
type(ctx).__name__
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
ctx = create_local_context()
|
|
519
|
+
|
|
520
|
+
if save:
|
|
521
|
+
metadata["cnotebook"] = ctx
|
|
522
|
+
|
|
523
|
+
return ctx
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Interactive molecule grid for Jupyter and Marimo notebooks."""
|
|
2
|
+
|
|
3
|
+
from cnotebook.grid.grid import MolGrid
|
|
4
|
+
from typing import Iterable, List, Optional, Union
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def molgrid(
|
|
8
|
+
mols: Iterable,
|
|
9
|
+
*,
|
|
10
|
+
title_field: Optional[str] = "Title",
|
|
11
|
+
tooltip_fields: Optional[List[str]] = None,
|
|
12
|
+
n_items_per_page: int = 24,
|
|
13
|
+
width: int = 200,
|
|
14
|
+
height: int = 200,
|
|
15
|
+
image_format: str = "svg",
|
|
16
|
+
select: bool = True,
|
|
17
|
+
information: bool = True,
|
|
18
|
+
data: Optional[Union[str, List[str]]] = None,
|
|
19
|
+
search_fields: Optional[List[str]] = None,
|
|
20
|
+
name: Optional[str] = None,
|
|
21
|
+
) -> MolGrid:
|
|
22
|
+
"""Create an interactive molecule grid.
|
|
23
|
+
|
|
24
|
+
:param mols: Iterable of OpenEye molecule objects.
|
|
25
|
+
:param title_field: Molecule field for title (None to hide).
|
|
26
|
+
:param tooltip_fields: List of fields for tooltip.
|
|
27
|
+
:param n_items_per_page: Molecules per page.
|
|
28
|
+
:param width: Image width in pixels (default 200).
|
|
29
|
+
:param height: Image height in pixels (default 200).
|
|
30
|
+
:param image_format: "svg" or "png" (default "svg").
|
|
31
|
+
:param select: Enable selection checkboxes.
|
|
32
|
+
:param information: Enable info button with hover tooltip.
|
|
33
|
+
:param data: Column(s) to display in info tooltip. If None, auto-detects
|
|
34
|
+
simple types (string, int, float) from DataFrame.
|
|
35
|
+
:param search_fields: Fields for text search.
|
|
36
|
+
:param name: Grid identifier.
|
|
37
|
+
:returns: MolGrid instance.
|
|
38
|
+
"""
|
|
39
|
+
return MolGrid(
|
|
40
|
+
mols,
|
|
41
|
+
title_field=title_field,
|
|
42
|
+
tooltip_fields=tooltip_fields,
|
|
43
|
+
n_items_per_page=n_items_per_page,
|
|
44
|
+
width=width,
|
|
45
|
+
height=height,
|
|
46
|
+
image_format=image_format,
|
|
47
|
+
select=select,
|
|
48
|
+
information=information,
|
|
49
|
+
data=data,
|
|
50
|
+
search_fields=search_fields,
|
|
51
|
+
name=name,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
__all__ = ["MolGrid", "molgrid"]
|