cnotebook 1.0.1__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 +104 -0
- cnotebook/align.py +390 -0
- cnotebook/context.py +491 -0
- cnotebook/helpers.py +69 -0
- cnotebook/ipython_ext.py +255 -0
- cnotebook/marimo_ext.py +34 -0
- cnotebook/pandas_ext.py +900 -0
- cnotebook/render.py +198 -0
- cnotebook-1.0.1.dist-info/METADATA +275 -0
- cnotebook-1.0.1.dist-info/RECORD +13 -0
- cnotebook-1.0.1.dist-info/WHEEL +5 -0
- cnotebook-1.0.1.dist-info/licenses/LICENSE +21 -0
- cnotebook-1.0.1.dist-info/top_level.txt +1 -0
cnotebook/context.py
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
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
|
+
"""
|
|
34
|
+
Value that can be deferred to the global CNotebook context
|
|
35
|
+
"""
|
|
36
|
+
def __init__(self, name: str, value: T | _Deferred):
|
|
37
|
+
self.name = name
|
|
38
|
+
self._value = value
|
|
39
|
+
self._initial_value = value
|
|
40
|
+
|
|
41
|
+
def reset(self):
|
|
42
|
+
"""
|
|
43
|
+
Reset this deferred value to the initial value (when the object was created)
|
|
44
|
+
"""
|
|
45
|
+
self._value = self._initial_value
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def is_deferred(self) -> bool:
|
|
49
|
+
"""
|
|
50
|
+
Check if the value is deferred to the global context
|
|
51
|
+
:return: True if the value is deferred to the global
|
|
52
|
+
"""
|
|
53
|
+
return self._value is DEFERRED
|
|
54
|
+
|
|
55
|
+
def get(self) -> T:
|
|
56
|
+
"""
|
|
57
|
+
If the value is DEFERRED then we defer to the local context
|
|
58
|
+
:return: Value
|
|
59
|
+
"""
|
|
60
|
+
if self.is_deferred:
|
|
61
|
+
ctx = cnotebook_context.get()
|
|
62
|
+
if not hasattr(ctx, self.name):
|
|
63
|
+
raise AttributeError(f"Global context missing attribute '{self.name}'")
|
|
64
|
+
return getattr(ctx, self.name)
|
|
65
|
+
return self._value
|
|
66
|
+
|
|
67
|
+
def set(self, value: T | _Deferred) -> None:
|
|
68
|
+
"""
|
|
69
|
+
Set a value (we never set the global context)
|
|
70
|
+
:param value: Value to set
|
|
71
|
+
"""
|
|
72
|
+
self._value = value
|
|
73
|
+
|
|
74
|
+
def __str__(self):
|
|
75
|
+
return str(self.get())
|
|
76
|
+
|
|
77
|
+
def __repr__(self):
|
|
78
|
+
return repr(self.get())
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class CNotebookContext:
|
|
82
|
+
"""
|
|
83
|
+
Context in which to render OpenEye objects within IPython
|
|
84
|
+
"""
|
|
85
|
+
# Supported image formats and their MIME types for rendering
|
|
86
|
+
supported_mime_types = {
|
|
87
|
+
'png': 'image/png',
|
|
88
|
+
'svg': 'image/svg+xml'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
*,
|
|
94
|
+
width: float | _Deferred = 0,
|
|
95
|
+
height: float | _Deferred = 0,
|
|
96
|
+
min_width: float | None | _Deferred = 200.0,
|
|
97
|
+
min_height: float | None | _Deferred = 200.0,
|
|
98
|
+
max_width: float | None | _Deferred = None,
|
|
99
|
+
max_height: float | None | _Deferred = None,
|
|
100
|
+
structure_scale: int | _Deferred = oedepict.OEScale_Default * 0.9,
|
|
101
|
+
title_font_scale: float | _Deferred = 1.0,
|
|
102
|
+
image_format: str | _Deferred = "png",
|
|
103
|
+
bond_width_scaling: bool | _Deferred = False,
|
|
104
|
+
callbacks: Iterable[Callable[[oedepict.OE2DMolDisplay], None]] | None | _Deferred = None,
|
|
105
|
+
scope: Literal["local", "global"] = "global",
|
|
106
|
+
title: bool = True
|
|
107
|
+
):
|
|
108
|
+
"""
|
|
109
|
+
Create the render context
|
|
110
|
+
:param width: Image width (default of None means it is determined by the structure scale)
|
|
111
|
+
:param height: Image height (default of None means it is determined by the structure scale)
|
|
112
|
+
:param min_width: Minimum image width (prevents tiny images)
|
|
113
|
+
:param min_height: Minimum image height (prevents tiny images)
|
|
114
|
+
:param structure_scale: Structure scale
|
|
115
|
+
:param title_font_scale: Font scaling (valid is 0.5 to 2.0)
|
|
116
|
+
:param image_format: Image format
|
|
117
|
+
:param bond_width_scaling: Bond width scaling
|
|
118
|
+
"""
|
|
119
|
+
self._width = DeferredValue[float]("width", width)
|
|
120
|
+
self._height = DeferredValue[float]("height", height)
|
|
121
|
+
self._min_height = DeferredValue[float | None]("min_height", min_height)
|
|
122
|
+
self._min_width = DeferredValue[float | None]("min_width", min_width)
|
|
123
|
+
self._max_width = DeferredValue[float | None]("max_width", max_width)
|
|
124
|
+
self._max_height = DeferredValue[float | None]("max_height", max_height)
|
|
125
|
+
self._structure_scale = DeferredValue[int]("structure_scale", structure_scale)
|
|
126
|
+
self._title_font_scale = DeferredValue[float]("title_font_scale", title_font_scale)
|
|
127
|
+
self._image_format = DeferredValue[str]("image_format", image_format)
|
|
128
|
+
self._bond_width_scaling = DeferredValue[bool]("bond_width_scaling", bond_width_scaling)
|
|
129
|
+
self._title = DeferredValue[bool]("title", title)
|
|
130
|
+
self._scope = scope
|
|
131
|
+
|
|
132
|
+
# Set the callbacks (and do some type checking)
|
|
133
|
+
if callbacks is None:
|
|
134
|
+
self._callbacks = DeferredValue[list[Callable[[oedepict.OE2DMolDisplay], None]]](
|
|
135
|
+
"callbacks",
|
|
136
|
+
DEFERRED if scope == "local" else []
|
|
137
|
+
)
|
|
138
|
+
elif isinstance(callbacks, Iterable):
|
|
139
|
+
self._callbacks = DeferredValue[list[Callable[[oedepict.OE2DMolDisplay], None]]](
|
|
140
|
+
"callbacks",
|
|
141
|
+
list(callbacks)
|
|
142
|
+
)
|
|
143
|
+
elif callbacks is DEFERRED:
|
|
144
|
+
self._callbacks = DeferredValue[list[Callable[[oedepict.OE2DMolDisplay], None]]](
|
|
145
|
+
"callbacks",
|
|
146
|
+
DEFERRED
|
|
147
|
+
)
|
|
148
|
+
else:
|
|
149
|
+
raise TypeError(f'Invalid type for display callbacks: {type(callbacks).__name__}')
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def width(self) -> float:
|
|
153
|
+
return self._width.get()
|
|
154
|
+
|
|
155
|
+
@width.setter
|
|
156
|
+
def width(self, value: float) -> None:
|
|
157
|
+
if self.max_width is not None and value > self.max_width:
|
|
158
|
+
log.warning(f'Width exceeds max_width: {value} > {self.max_width}')
|
|
159
|
+
|
|
160
|
+
self._width.set(value)
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def height(self) -> float:
|
|
164
|
+
return self._height.get()
|
|
165
|
+
|
|
166
|
+
@height.setter
|
|
167
|
+
def height(self, value: float) -> None:
|
|
168
|
+
if self.max_height is not None and value > self.max_height:
|
|
169
|
+
log.warning(f'Height exceeds max_height: {value} > {self.max_height}')
|
|
170
|
+
|
|
171
|
+
self._height.set(value)
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def min_width(self) -> float | None:
|
|
175
|
+
return self._min_width.get()
|
|
176
|
+
|
|
177
|
+
@min_width.setter
|
|
178
|
+
def min_width(self, value: float | None) -> None:
|
|
179
|
+
self._min_width.set(value)
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def max_width(self) -> float | None:
|
|
183
|
+
return self._max_width.get()
|
|
184
|
+
|
|
185
|
+
@max_width.setter
|
|
186
|
+
def max_width(self, value: float | None):
|
|
187
|
+
if value is not None and self.width > value:
|
|
188
|
+
log.warning(f'Current width exceeds max_width: {self.width} > {value}')
|
|
189
|
+
|
|
190
|
+
self._max_width.set(value)
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def max_height(self) -> float | None:
|
|
194
|
+
return self._max_height.get()
|
|
195
|
+
|
|
196
|
+
@max_height.setter
|
|
197
|
+
def max_height(self, value: float | None):
|
|
198
|
+
if value is not None and self.height > value:
|
|
199
|
+
log.warning(f'Current height exceeds max_height: {self.height} > {value}')
|
|
200
|
+
|
|
201
|
+
self._max_height.set(value)
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def min_height(self) -> float | None:
|
|
205
|
+
return self._min_height.get()
|
|
206
|
+
|
|
207
|
+
@min_height.setter
|
|
208
|
+
def min_height(self, value: float | None) -> None:
|
|
209
|
+
self._min_height.set(value)
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def structure_scale(self) -> int:
|
|
213
|
+
return self._structure_scale.get()
|
|
214
|
+
|
|
215
|
+
@structure_scale.setter
|
|
216
|
+
def structure_scale(self, value: int) -> None:
|
|
217
|
+
self._structure_scale.set(value)
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def title_font_scale(self) -> float:
|
|
221
|
+
return self._title_font_scale.get()
|
|
222
|
+
|
|
223
|
+
@title_font_scale.setter
|
|
224
|
+
def title_font_scale(self, value: float) -> None:
|
|
225
|
+
self._title_font_scale.set(value)
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def bond_width_scaling(self) -> bool:
|
|
229
|
+
return self._bond_width_scaling.get()
|
|
230
|
+
|
|
231
|
+
@bond_width_scaling.setter
|
|
232
|
+
def bond_width_scaling(self, value: bool) -> None:
|
|
233
|
+
self._bond_width_scaling.set(value)
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def image_format(self) -> str:
|
|
237
|
+
return self._image_format.get()
|
|
238
|
+
|
|
239
|
+
@image_format.setter
|
|
240
|
+
def image_format(self, value: str) -> None:
|
|
241
|
+
self._image_format.set(value)
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def scope(self) -> Literal["global", "local"]:
|
|
245
|
+
return self._scope
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def callbacks(self) -> tuple[Callable[[oedepict.OE2DMolDisplay], None], ...]:
|
|
249
|
+
# noinspection PyTypeChecker
|
|
250
|
+
return tuple(self._callbacks.get())
|
|
251
|
+
|
|
252
|
+
def reset_callbacks(self) -> None:
|
|
253
|
+
self._callbacks.reset()
|
|
254
|
+
|
|
255
|
+
@property
|
|
256
|
+
def title(self) -> bool:
|
|
257
|
+
return self._title.get()
|
|
258
|
+
|
|
259
|
+
@title.setter
|
|
260
|
+
def title(self, value: bool) -> None:
|
|
261
|
+
self._title.set(value)
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def image_mime_type(self) -> str:
|
|
265
|
+
mime_type = self.supported_mime_types.get(self.image_format, None)
|
|
266
|
+
if mime_type is None:
|
|
267
|
+
raise KeyError(f'No MIME type registered for image format {self.image_format}')
|
|
268
|
+
return mime_type
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def display_options(self) -> oedepict.OE2DMolDisplayOptions:
|
|
272
|
+
opts = oedepict.OE2DMolDisplayOptions()
|
|
273
|
+
opts.SetHeight(self.height)
|
|
274
|
+
opts.SetWidth(self.width)
|
|
275
|
+
opts.SetScale(self.structure_scale)
|
|
276
|
+
opts.SetTitleFontScale(self.title_font_scale)
|
|
277
|
+
opts.SetBondWidthScaling(self.bond_width_scaling)
|
|
278
|
+
|
|
279
|
+
if not self.title:
|
|
280
|
+
opts.SetTitleLocation(oedepict.OETitleLocation_Hidden)
|
|
281
|
+
|
|
282
|
+
return opts
|
|
283
|
+
|
|
284
|
+
def add_callback(self, callback: Callable[[oedepict.OE2DMolDisplay], None]):
|
|
285
|
+
"""
|
|
286
|
+
Add a callback that modifies an oedepict.OE2DMolDisplay to the current context
|
|
287
|
+
:param callback: Callback to add
|
|
288
|
+
"""
|
|
289
|
+
if self._callbacks.is_deferred:
|
|
290
|
+
self._callbacks.set([])
|
|
291
|
+
self._callbacks.get().append(callback)
|
|
292
|
+
|
|
293
|
+
def create_molecule_display(
|
|
294
|
+
self,
|
|
295
|
+
mol: oechem.OEMolBase,
|
|
296
|
+
min_height: int | None = None,
|
|
297
|
+
min_width: int | None = None
|
|
298
|
+
) -> oedepict.OE2DMolDisplay:
|
|
299
|
+
"""
|
|
300
|
+
Create a molecule display that enforces minimum image height and width
|
|
301
|
+
:param mol: Molecule
|
|
302
|
+
:param min_height: Minimum image height
|
|
303
|
+
:param min_width: Minimum image width
|
|
304
|
+
:return: Molecule display
|
|
305
|
+
"""
|
|
306
|
+
disp = oedepict.OE2DMolDisplay(mol, self.display_options)
|
|
307
|
+
|
|
308
|
+
# If the image was too small, and we're not enforcing a specific image size
|
|
309
|
+
if ((self.width == 0.0 and self.min_width is not None and disp.GetWidth() < self.min_width) or
|
|
310
|
+
(self.height == 0.0 and self.min_height is not None and disp.GetHeight() < self.min_height)):
|
|
311
|
+
|
|
312
|
+
min_height = min_height or self.min_height
|
|
313
|
+
min_width = min_width or self.min_width
|
|
314
|
+
|
|
315
|
+
# Create a new display context
|
|
316
|
+
new_ctx = self.copy()
|
|
317
|
+
|
|
318
|
+
# If width was not enforced already, then enforce the minimum width
|
|
319
|
+
if self.width == 0.0 and min_width is not None:
|
|
320
|
+
new_ctx.width = min_width if disp.GetWidth() < self.min_width else 0.0
|
|
321
|
+
|
|
322
|
+
# If height was not enforced already, then enforce the minimum height
|
|
323
|
+
if self.height == 0.0 and min_height is not None:
|
|
324
|
+
new_ctx.height = min_height if disp.GetHeight() < self.min_height else 0.0
|
|
325
|
+
|
|
326
|
+
# Create the display object
|
|
327
|
+
disp = oedepict.OE2DMolDisplay(mol, new_ctx.display_options)
|
|
328
|
+
|
|
329
|
+
# We need to scale down the image if it exceeds the max_width or max_height
|
|
330
|
+
if ((self.max_width is not None and disp.GetWidth() > self.max_width) or
|
|
331
|
+
(self.max_height is not None and disp.GetHeight() > self.max_height)):
|
|
332
|
+
|
|
333
|
+
# Create a new display context
|
|
334
|
+
new_ctx = self.copy()
|
|
335
|
+
|
|
336
|
+
# Set whatever parameter exceeded the maximum and let the other scale
|
|
337
|
+
if self.max_width is not None and disp.GetWidth() > self.max_width:
|
|
338
|
+
new_ctx.width = self.max_width
|
|
339
|
+
new_ctx.height = 0
|
|
340
|
+
|
|
341
|
+
elif self.max_height is not None and disp.GetHeight() > self.max_height:
|
|
342
|
+
new_ctx.width = 0
|
|
343
|
+
new_ctx.height = self.max_height
|
|
344
|
+
|
|
345
|
+
new_ctx.structure_scale = oedepict.OEScale_AutoScale
|
|
346
|
+
|
|
347
|
+
# Create the display object
|
|
348
|
+
disp = oedepict.OE2DMolDisplay(mol, new_ctx.display_options)
|
|
349
|
+
|
|
350
|
+
# TODO: Check the display again and see if we've exceeded max width or height again and potentially
|
|
351
|
+
# constrain both width and height
|
|
352
|
+
|
|
353
|
+
return disp
|
|
354
|
+
|
|
355
|
+
def reset(self) -> None:
|
|
356
|
+
"""
|
|
357
|
+
Reset the rendering context to default values
|
|
358
|
+
"""
|
|
359
|
+
self._width.reset()
|
|
360
|
+
self._height.reset()
|
|
361
|
+
self._min_width.reset()
|
|
362
|
+
self._min_height.reset()
|
|
363
|
+
self._max_width.reset()
|
|
364
|
+
self._max_height.reset()
|
|
365
|
+
self._structure_scale.reset()
|
|
366
|
+
self._title_font_scale.reset()
|
|
367
|
+
self._image_format.reset()
|
|
368
|
+
self._bond_width_scaling.reset()
|
|
369
|
+
self._title.reset()
|
|
370
|
+
self._callbacks.reset()
|
|
371
|
+
|
|
372
|
+
def copy(self) -> 'CNotebookContext':
|
|
373
|
+
"""
|
|
374
|
+
Copy this object
|
|
375
|
+
:return: Copy of the object
|
|
376
|
+
"""
|
|
377
|
+
return CNotebookContext(
|
|
378
|
+
width=self.width,
|
|
379
|
+
height=self.height,
|
|
380
|
+
min_width=self.min_width,
|
|
381
|
+
min_height=self.min_height,
|
|
382
|
+
max_width=self.max_width,
|
|
383
|
+
max_height=self.max_height,
|
|
384
|
+
structure_scale=self.structure_scale,
|
|
385
|
+
title_font_scale=self.title_font_scale,
|
|
386
|
+
title=self.title,
|
|
387
|
+
image_format=self.image_format,
|
|
388
|
+
bond_width_scaling=self.bond_width_scaling,
|
|
389
|
+
callbacks=self.callbacks,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
########################################################################################################################
|
|
394
|
+
# !!!!!!!!! Global render context !!!!!!!!!
|
|
395
|
+
########################################################################################################################
|
|
396
|
+
|
|
397
|
+
# Create our global render context
|
|
398
|
+
cnotebook_context: ContextVar[CNotebookContext] = ContextVar("cnotebook_context", default=CNotebookContext())
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
########################################################################################################################
|
|
402
|
+
# Decorator to automatically pass global rendering context
|
|
403
|
+
########################################################################################################################
|
|
404
|
+
|
|
405
|
+
def pass_cnotebook_context(func):
|
|
406
|
+
"""
|
|
407
|
+
Decorator that passes a copy of the current molecule render context
|
|
408
|
+
:param func: Function to decorate
|
|
409
|
+
:return: Decorated function
|
|
410
|
+
"""
|
|
411
|
+
# TODO: Inspect func signature and check that it uses the ctx keyword
|
|
412
|
+
@wraps(func)
|
|
413
|
+
def call_with_render_context(*args, **kwargs):
|
|
414
|
+
|
|
415
|
+
# If we happened to be called with a custom molecule render context
|
|
416
|
+
if "ctx" in kwargs:
|
|
417
|
+
ctx = kwargs.pop("ctx")
|
|
418
|
+
|
|
419
|
+
if ctx is None:
|
|
420
|
+
ctx = cnotebook_context.get().copy()
|
|
421
|
+
|
|
422
|
+
# Other things are not OK
|
|
423
|
+
elif not isinstance(ctx, CNotebookContext):
|
|
424
|
+
raise TypeError("Received object of type type {} for OERenderContext (ctx) when calling {}".format(
|
|
425
|
+
type(ctx).__name__,
|
|
426
|
+
func.__name__
|
|
427
|
+
))
|
|
428
|
+
else:
|
|
429
|
+
ctx = cnotebook_context.get().copy()
|
|
430
|
+
|
|
431
|
+
# Call the function
|
|
432
|
+
return func(*args, **kwargs, ctx=ctx)
|
|
433
|
+
return call_with_render_context
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
########################################################################################################################
|
|
437
|
+
# Local rendering context
|
|
438
|
+
########################################################################################################################
|
|
439
|
+
|
|
440
|
+
def create_local_context(
|
|
441
|
+
width: float = DEFERRED,
|
|
442
|
+
height: float = DEFERRED,
|
|
443
|
+
min_width: float = DEFERRED,
|
|
444
|
+
min_height: float = DEFERRED,
|
|
445
|
+
max_width: float = DEFERRED,
|
|
446
|
+
max_height: float = DEFERRED,
|
|
447
|
+
structure_scale: int = DEFERRED,
|
|
448
|
+
title_font_scale: float = DEFERRED,
|
|
449
|
+
image_format: str = DEFERRED,
|
|
450
|
+
bond_width_scaling: bool = DEFERRED,
|
|
451
|
+
callbacks: Iterable[Callable[[oedepict.OE2DMolDisplay], None]] | None = DEFERRED
|
|
452
|
+
) -> CNotebookContext:
|
|
453
|
+
return CNotebookContext(
|
|
454
|
+
width=width,
|
|
455
|
+
height=height,
|
|
456
|
+
min_width=min_width,
|
|
457
|
+
min_height=min_height,
|
|
458
|
+
max_width=max_width,
|
|
459
|
+
max_height=max_height,
|
|
460
|
+
structure_scale=structure_scale,
|
|
461
|
+
title_font_scale=title_font_scale,
|
|
462
|
+
image_format=image_format,
|
|
463
|
+
bond_width_scaling=bond_width_scaling,
|
|
464
|
+
callbacks=callbacks,
|
|
465
|
+
scope="local"
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def get_series_context(metadata: dict[Any, Any], save: bool = False) -> CNotebookContext:
|
|
470
|
+
"""
|
|
471
|
+
Get the series context, else wrap the global context into a series context. This looks for the key "cnotebook" in
|
|
472
|
+
the metadta.
|
|
473
|
+
:param metadata: Series metadata
|
|
474
|
+
:param save: Whether to save any new metadata object that we create
|
|
475
|
+
:return: Series rendering context
|
|
476
|
+
"""
|
|
477
|
+
ctx = metadata.get("cnotebook", create_local_context())
|
|
478
|
+
|
|
479
|
+
# Make sure context is a valid object
|
|
480
|
+
if not isinstance(ctx, CNotebookContext):
|
|
481
|
+
log.warning(
|
|
482
|
+
"Replacing unexpected object of type %s for metadata key 'cnotebook' with a CNotebookLocalContext",
|
|
483
|
+
type(ctx).__name__
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
ctx = create_local_context()
|
|
487
|
+
|
|
488
|
+
if save:
|
|
489
|
+
metadata["cnotebook"] = ctx
|
|
490
|
+
|
|
491
|
+
return ctx
|
cnotebook/helpers.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Callable
|
|
3
|
+
from openeye import oechem, oedepict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def escape_html(val):
|
|
7
|
+
"""
|
|
8
|
+
Perform the same HTML escaping done by Pandas for displaying in Notebooks
|
|
9
|
+
:param val: Value to escape
|
|
10
|
+
:return: Escaped value (if val was a string)
|
|
11
|
+
"""
|
|
12
|
+
if isinstance(val, str):
|
|
13
|
+
return val.replace("&", r"&").replace("<", r"<").replace(">", r">")
|
|
14
|
+
return val
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def escape_brackets(val):
|
|
18
|
+
"""
|
|
19
|
+
Escapes only HTML brackets
|
|
20
|
+
:param val: Value to escape
|
|
21
|
+
:return: Escaped value (if string)
|
|
22
|
+
"""
|
|
23
|
+
if isinstance(val, str):
|
|
24
|
+
return val.replace("<", r"<").replace(">", r">")
|
|
25
|
+
return val
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Remove conformer identifier from compound ID
|
|
29
|
+
CONFORMER_ID_REGEX = re.compile(r'(.*?)_\d+$')
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def remove_omega_conformer_id(val):
|
|
33
|
+
"""
|
|
34
|
+
Remove the conformer ID from a compound identifier
|
|
35
|
+
:param val: Value
|
|
36
|
+
:return: Processed value
|
|
37
|
+
"""
|
|
38
|
+
if isinstance(val, str):
|
|
39
|
+
m = re.search(CONFORMER_ID_REGEX, val)
|
|
40
|
+
if m is not None:
|
|
41
|
+
return m.group(1)
|
|
42
|
+
return val
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def create_structure_highlighter(
|
|
46
|
+
query: str | oechem.OESubSearch | oechem.OEMCSSearch | oechem.OEQMol,
|
|
47
|
+
color: oechem.OEColor = oechem.OEColor(oechem.OELightBlue),
|
|
48
|
+
style: int = oedepict.OEHighlightStyle_Stick
|
|
49
|
+
) -> Callable[[oedepict.OE2DMolDisplay], None]:
|
|
50
|
+
"""
|
|
51
|
+
Closure that creates a callback to highlight SMARTS patterns or MCSS results in a molecule
|
|
52
|
+
:param query: SMARTS pattern, oechem.OESubSearch, or oechem.OEMCSSearch object
|
|
53
|
+
:param color: Highlight color
|
|
54
|
+
:param style: Highlight style
|
|
55
|
+
:return: Function that highlights structures
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
if isinstance(query, (str, oechem.OEQMol)):
|
|
59
|
+
ss = oechem.OESubSearch(query)
|
|
60
|
+
|
|
61
|
+
elif not isinstance(query, (oechem.OESubSearch, oechem.OEMCSSearch)):
|
|
62
|
+
raise TypeError(f'Cannot create structure highlighter with object pattern of type {type(query).__name__}')
|
|
63
|
+
|
|
64
|
+
# Create the callback as a closure
|
|
65
|
+
def _structure_highlighter(disp: oedepict.OE2DMolDisplay):
|
|
66
|
+
for match in ss.Match(disp.GetMolecule(), True):
|
|
67
|
+
oedepict.OEAddHighlighting(disp, color, style, match)
|
|
68
|
+
|
|
69
|
+
return _structure_highlighter
|