cnotebook 1.0.1__py3-none-any.whl → 1.2.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 CHANGED
@@ -1,16 +1,14 @@
1
1
  import logging
2
2
  from .pandas_ext import render_dataframe
3
3
  from .context import cnotebook_context
4
- from .ipython_ext import (
5
- register_ipython_formatters as _register_ipython_formatters,
6
- render_molecule_grid
7
- )
4
+ from .ipython_ext import register_ipython_formatters as _register_ipython_formatters
5
+ from .render import render_molecule_grid
8
6
 
9
7
  # Only import formatter registration from the Pandas module, otherwise have users import functionality from there
10
8
  # to avoid confusion
11
9
  from cnotebook.pandas_ext import register_pandas_formatters as _register_pandas_formatters
12
10
 
13
- __version__ = '1.0.1'
11
+ __version__ = '1.2.0'
14
12
 
15
13
  ###########################################
16
14
 
cnotebook/ipython_ext.py CHANGED
@@ -1,16 +1,13 @@
1
1
  import logging
2
- from typing import Literal
3
- from math import floor, ceil
4
2
  from openeye import oechem, oedepict
5
- from collections.abc import Iterable, Sequence
6
3
  from .render import (
7
4
  CNotebookContext,
8
5
  pass_cnotebook_context,
9
6
  oemol_to_html,
10
7
  oedisp_to_html,
11
- oeimage_to_html
8
+ oeimage_to_html,
9
+ render_molecule_grid # Re-export for backward compatibility
12
10
  )
13
- from .helpers import create_structure_highlighter
14
11
 
15
12
  # Only register iPython formatters if that is present
16
13
  try:
@@ -23,199 +20,6 @@ except ModuleNotFoundError:
23
20
  log = logging.getLogger("cnotebook")
24
21
 
25
22
 
26
- @pass_cnotebook_context
27
- def render_molecule_grid(
28
- mols: Sequence[oechem.OEMolBase],
29
- nrows: int | None = None,
30
- ncols: int | None = None,
31
- max_width: int = 1280,
32
- max_columns: int = 100,
33
- min_width: int | None = None,
34
- min_height: int | None = None,
35
- align: oechem.OEMolBase | Literal["first"] | None = None,
36
- smarts: str | Iterable[str] | oechem.OESubSearch | Iterable[oechem.OESubSearch] | None = None,
37
- color: oechem.OEColor = oechem.OEColor(oechem.OELightBlue),
38
- style: int = oedepict.OEHighlightStyle_Stick,
39
- scale: float = 1.0,
40
- *,
41
- ctx: CNotebookContext
42
- ) -> oedepict.OEImage:
43
- """
44
- Convenience function to render a molecule grid
45
- :param ctx: Current OpenEye rendering context
46
- :param mols: Iterable of OpenEye molecules
47
- :param nrows: Number of rows to display in the grid
48
- :param ncols: Number of columns to display in the grid
49
- :param max_width: Maximum width of the image
50
- :param max_columns: Maximum number of columns (if ncols is being automatically calculated)
51
- :param min_width: Minimum image width (prevents tiny images)
52
- :param min_height: Minimum image height (prevents tiny images)
53
- :param align: Alignment to the first molecule, or to a reference molecule
54
- :param smarts: SMARTS substructure highlighting
55
- :param color: SMARTS highlighting color
56
- :param style: SMARTS highlighting style
57
- :param scale: Image scale in the 2D grid
58
- :return: Image of the molecule grid
59
- """
60
- # Re-scale the images
61
- ctx.display_options.SetScale(ctx.display_options.GetScale() * scale)
62
-
63
- # ---------------------------------------------------------------
64
- # Input validation
65
- # ---------------------------------------------------------------
66
-
67
- # Handle single molecules
68
- if isinstance(mols, oechem.OEMolBase):
69
- mols = [mols]
70
-
71
- # Make a copy of the molecules (so we do not modify them)
72
- # We can use OEGraphMol here because we don't care about conformers
73
- # Filter out None values first
74
- mols = [oechem.OEGraphMol(mol) for mol in mols if mol is not None]
75
-
76
- if len(mols) == 0:
77
- log.warning("No molecules or display objects to render into a grid")
78
- # Return a minimal 1x1 image instead of 0x0 to avoid OpenEye bug
79
- return oedepict.OEImage(1, 1)
80
-
81
- # Get the subset of molecules that will actually be displayed
82
- valid = []
83
-
84
- for idx, mol in enumerate(mols):
85
- if mol is not None:
86
- if isinstance(mol, oechem.OEMolBase):
87
- if mol.IsValid():
88
- valid.append(mol)
89
- else:
90
- log.warning(f'Molecule at index {idx} is not valid')
91
-
92
- else:
93
- log.warning(f'Object at index is not a molecule but type {type(mol).__name__}')
94
-
95
- if len(valid) == 0:
96
- log.warning("No valid molecules or display objects to render into a grid")
97
- # Return a minimal 1x1 image instead of 0x0 to avoid OpenEye bug
98
- return oedepict.OEImage(1, 1)
99
-
100
- # ---------------------------------------------------------------
101
- # For highlighting SMARTS
102
- # ---------------------------------------------------------------
103
-
104
- highlighers = None
105
-
106
- if smarts is not None:
107
- highlighers = []
108
- # Case: Single pattern
109
- if isinstance(smarts, (str, oechem.OESubSearch)):
110
- highlighers.append(
111
- create_structure_highlighter(
112
- smarts,
113
- color=color,
114
- style=style
115
- )
116
- )
117
-
118
- else:
119
- for pattern in smarts:
120
- highlighers.append(
121
- create_structure_highlighter(
122
- pattern,
123
- color=color,
124
- style=style
125
- )
126
- )
127
-
128
- # ---------------------------------------------------------------
129
- # For substructure alignment
130
- # ---------------------------------------------------------------
131
-
132
- align_mcss = None
133
-
134
- if align is not None:
135
-
136
- # If we are doing an alignment
137
- if isinstance(align, bool) and align:
138
- align_ref = mols[0]
139
-
140
- elif isinstance(align, oechem.OEMolBase):
141
- align_ref = align
142
-
143
- else:
144
- raise TypeError(f'Cannot initialize MCSS alignment reference from type {type(align).__name__}')
145
-
146
- # Set up the MCSS
147
- align_mcss = oechem.OEMCSSearch(oechem.OEMCSType_Approximate)
148
- align_mcss.Init(align_ref, oechem.OEExprOpts_DefaultAtoms, oechem.OEExprOpts_DefaultBonds)
149
-
150
- # ---------------------------------------------------------------
151
- # Create the display objects for each molecule
152
- # ---------------------------------------------------------------
153
-
154
- displays = []
155
- max_disp_width = float('-inf')
156
- max_disp_height = float('-inf')
157
-
158
- for mol in valid:
159
- if align_mcss is not None:
160
- oedepict.OEPrepareAlignedDepiction(mol, align_mcss)
161
- else:
162
- oedepict.OEPrepareDepiction(mol)
163
-
164
- # Create the display object
165
- disp = ctx.create_molecule_display(mol, min_width=min_width, min_height=min_height)
166
- # disp = oedepict.OE2DMolDisplay(mol, ctx.display_options)
167
-
168
- # Highlight SMARTS patterns
169
- if highlighers is not None:
170
- for highlight in highlighers:
171
- highlight(disp)
172
-
173
- displays.append(disp)
174
-
175
- if disp.GetWidth() > max_disp_width:
176
- max_disp_width = disp.GetWidth()
177
-
178
- if disp.GetHeight() > max_disp_height:
179
- max_disp_height = disp.GetHeight()
180
-
181
- # ---------------------------------------------------------------
182
- # Figure out the geometry of the full image
183
- # ---------------------------------------------------------------
184
- # Case: We have one molecule
185
- if len(displays) == 1:
186
- ncols = 1
187
- nrows = 1
188
-
189
- # Case: We are computing based on max_width
190
- elif ncols is None and nrows is None:
191
-
192
- # Number of columns we can fit into max_width
193
- ncols = min(floor(max_width / max_disp_width), max_columns, len(displays))
194
- nrows = ceil(len(displays) / ncols)
195
-
196
- elif nrows is not None:
197
- ncols = ceil(len(displays) / nrows)
198
-
199
- elif ncols is not None:
200
- nrows = ceil(len(displays) / ncols)
201
-
202
- else:
203
- raise RuntimeError("Cannot determine number of rows and columns in molecule grid")
204
-
205
- # Image width and height
206
- width = max_disp_width * ncols
207
- height = max_disp_height * nrows
208
-
209
- image = oedepict.OEImage(width, height)
210
- grid = oedepict.OEImageGrid(image, nrows, ncols)
211
-
212
- # Render the molecules
213
- for disp, cell in zip(displays, grid.GetCells()):
214
- oedepict.OERenderMolecule(cell, disp)
215
-
216
- return image
217
-
218
-
219
23
  ########################################################################################################################
220
24
  # Register iPython formatters
221
25
  ########################################################################################################################
cnotebook/marimo_ext.py CHANGED
@@ -1,12 +1,42 @@
1
+ """
2
+ Marimo integration for CNotebook.
3
+
4
+ This module provides MIME handlers for OpenEye objects and patches Marimo's
5
+ internal table implementation to support molecule rendering with callbacks
6
+ (highlighting, alignment, etc.) in Marimo's built-in DataFrame table component.
7
+ """
8
+ import logging
1
9
  import pandas as pd
2
10
  from openeye import oechem, oedepict
3
- from .context import cnotebook_context
4
- from .render import oemol_to_html, oedisp_to_html, oeimage_to_html
11
+
12
+ # Import oepandas for dtype checking
13
+ try:
14
+ import oepandas as oepd
15
+ oepandas_available = True
16
+ except ImportError:
17
+ oepandas_available = False
18
+
19
+ from .context import cnotebook_context, get_series_context
20
+ from .render import (
21
+ oemol_to_html,
22
+ oedisp_to_html,
23
+ oeimage_to_html,
24
+ oemol_to_disp,
25
+ render_empty_molecule,
26
+ render_invalid_molecule
27
+ )
5
28
  from .pandas_ext import render_dataframe
6
29
 
30
+ log = logging.getLogger("cnotebook")
31
+
32
+
33
+ ########################################################################################################################
34
+ # MIME handlers for individual OpenEye objects
35
+ ########################################################################################################################
36
+
7
37
  def _display_mol(self: oechem.OEMolBase):
8
38
  ctx = cnotebook_context.get().copy()
9
- ctx.image_format = "png"
39
+ # Allow user's image_format preference (SVG or PNG)
10
40
  return "text/html", oemol_to_html(self, ctx=ctx)
11
41
 
12
42
  oechem.OEMolBase._mime_ = _display_mol
@@ -14,21 +44,162 @@ oechem.OEMolBase._mime_ = _display_mol
14
44
 
15
45
  def _display_display(self: oedepict.OE2DMolDisplay):
16
46
  ctx = cnotebook_context.get().copy()
17
- ctx.image_format = "png"
47
+ # Allow user's image_format preference (SVG or PNG)
18
48
  return "text/html", oedisp_to_html(self, ctx=ctx)
19
49
 
20
- oedepict.OE2DMolDisplay.__mime__ = _display_display
50
+ oedepict.OE2DMolDisplay._mime_ = _display_display
21
51
 
22
52
 
23
53
  def _display_image(self: oedepict.OEImage):
24
54
  ctx = cnotebook_context.get().copy()
25
- ctx.image_format = "png"
55
+ # Allow user's image_format preference (SVG or PNG)
26
56
  return "text/html", oeimage_to_html(self, ctx=ctx)
27
57
 
28
- oedepict.OEImage.__mime__ = _display_image
58
+ oedepict.OEImage._mime_ = _display_image
29
59
 
30
- def display_dataframe(self: pd.DataFrame):
31
- ctx = cnotebook_context.get().copy()
32
- return render_dataframe(df=self, formatters=None, col_space=None)
33
60
 
34
- pd.DataFrame.__mime__ = display_dataframe
61
+ ########################################################################################################################
62
+ # Formatter factories for mo.ui.table format_mapping
63
+ ########################################################################################################################
64
+
65
+ def _create_molecule_formatter(ctx):
66
+ """
67
+ Create a formatter closure that renders molecules with callbacks applied.
68
+
69
+ :param ctx: CNotebookContext with callbacks (e.g., highlighting)
70
+ :return: Formatter function for use in mo.ui.table format_mapping
71
+ """
72
+ def formatter(mol):
73
+ if mol is None:
74
+ return ""
75
+
76
+ if not isinstance(mol, oechem.OEMolBase):
77
+ return str(mol)
78
+
79
+ # Handle invalid molecules
80
+ if not mol.IsValid():
81
+ return render_invalid_molecule(ctx=ctx)
82
+
83
+ # Handle empty molecules
84
+ if mol.NumAtoms() == 0:
85
+ return render_empty_molecule(ctx=ctx)
86
+
87
+ # Create display object
88
+ disp = oemol_to_disp(mol, ctx=ctx)
89
+
90
+ # Apply callbacks (highlighting, etc.)
91
+ if ctx.callbacks:
92
+ for callback in ctx.callbacks:
93
+ callback(disp)
94
+
95
+ # Return display object
96
+ return disp
97
+
98
+ return formatter
99
+
100
+
101
+ def _create_display_formatter(ctx):
102
+ """
103
+ Create a formatter closure that renders OE2DMolDisplay objects.
104
+
105
+ :param ctx: CNotebookContext for rendering options
106
+ :return: Formatter function for use in mo.ui.table format_mapping
107
+ """
108
+ def formatter(disp):
109
+ if disp is None:
110
+ return ""
111
+
112
+ if not isinstance(disp, oedepict.OE2DMolDisplay):
113
+ return str(disp)
114
+
115
+ if not disp.IsValid():
116
+ return str(disp)
117
+
118
+ # Copy the display to avoid modifying the original
119
+ disp_copy = oedepict.OE2DMolDisplay(disp)
120
+
121
+ # Apply callbacks if any
122
+ if ctx.callbacks:
123
+ for callback in ctx.callbacks:
124
+ callback(disp_copy)
125
+
126
+ return disp_copy
127
+
128
+ return formatter
129
+
130
+
131
+ ########################################################################################################################
132
+ # Marimo DataFrame formatter registration
133
+ #
134
+ # This registers a custom formatter with Marimo's OPINIONATED_FORMATTERS registry
135
+ # to handle DataFrames containing molecule columns. The formatter:
136
+ # - Detects MoleculeDtype and DisplayDtype columns
137
+ # - Creates format_mapping entries that apply callbacks (highlighting, alignment, etc.)
138
+ # - Returns OE2DMolDisplay objects which Marimo renders via their _mime_() method
139
+ ########################################################################################################################
140
+
141
+ try:
142
+ import marimo as mo
143
+ # noinspection PyProtectedMember,PyUnusedImports
144
+ from marimo._output.formatting import OPINIONATED_FORMATTERS
145
+ # noinspection PyProtectedMember,PyUnusedImports
146
+ from marimo._plugins.ui._impl.table import table
147
+
148
+
149
+ # 1. Define the custom formatting logic
150
+ def marimo_pandas_formatter(df: pd.DataFrame):
151
+ """
152
+ Monkey patch the Marimo DataFrame formatter
153
+ """
154
+ format_mapping = {}
155
+
156
+ # Check for MoleculeDtype / DisplayDtype (OEPandas specific)
157
+ if oepandas_available:
158
+ for col in df.columns:
159
+ dtype = df[col].dtype
160
+
161
+ if isinstance(dtype, oepd.MoleculeDtype):
162
+ arr = df[col].array
163
+ ctx = get_series_context(arr.metadata).copy()
164
+ format_mapping[col] = _create_molecule_formatter(ctx)
165
+
166
+ elif isinstance(dtype, oepd.DisplayDtype):
167
+ arr = df[col].array
168
+ ctx = get_series_context(arr.metadata).copy()
169
+ format_mapping[col] = _create_display_formatter(ctx)
170
+
171
+ # Return a Marimo table with our custom mapping
172
+ # noinspection PyProtectedMember,PyTypeChecker
173
+ return table(df, selection=None, format_mapping=format_mapping, pagination=True)._mime_()
174
+
175
+ # 2. Inject into the Registry
176
+ def install_marimo_pandas_formatter():
177
+ # Check if we've already installed it to avoid duplicates
178
+ for typ, func in OPINIONATED_FORMATTERS.formatters.items():
179
+ if typ is pd.DataFrame and func.__name__ == "marimo_pandas_formatter":
180
+ return # Already installed
181
+
182
+ OPINIONATED_FORMATTERS.formatters[pd.DataFrame] = marimo_pandas_formatter
183
+
184
+ # Do the installation
185
+ install_marimo_pandas_formatter()
186
+
187
+ except (ImportError, AttributeError) as ex:
188
+ # Marimo not installed or API changed - skip formatter registration
189
+ log.debug(f'Marimo formatter registration skipped: {ex}')
190
+
191
+
192
+ ########################################################################################################################
193
+ # Fallback DataFrame MIME handler for non-Marimo contexts
194
+ ########################################################################################################################
195
+
196
+ def _display_dataframe(self: pd.DataFrame):
197
+ """
198
+ Fallback MIME hook for Pandas DataFrames in non-Marimo contexts.
199
+
200
+ In Marimo, the internal table patch handles DataFrame display.
201
+ This is used for static exports or other tools that check _mime_.
202
+ """
203
+ return "text/html", render_dataframe(df=self, formatters=None, col_space=None)
204
+
205
+ pd.DataFrame._mime_ = _display_dataframe
cnotebook/pandas_ext.py CHANGED
@@ -126,9 +126,17 @@ def render_dataframe(
126
126
  # Render columns with MoleculeDtype
127
127
  molecule_columns = set()
128
128
 
129
+ # Capture metadata from ORIGINAL DataFrame BEFORE copying
130
+ # (df.copy() may not preserve array metadata)
131
+ original_metadata_by_col = {}
132
+
129
133
  for col in df.columns:
130
134
  if isinstance(df.dtypes[col], oepd.MoleculeDtype):
131
135
  molecule_columns.add(col)
136
+ # Get metadata from the original array before any copying
137
+ arr = df[col].array
138
+ if hasattr(arr, 'metadata') and arr.metadata:
139
+ original_metadata_by_col[col] = arr.metadata.copy()
132
140
 
133
141
  # We need to copy both the DataFrame and the molecules, because we modify them in-place to render them
134
142
  df = df.copy()
@@ -137,7 +145,11 @@ def render_dataframe(
137
145
  # Direct assignment to help IDE understand this is a MoleculeArray
138
146
  arr = df[col].array
139
147
  assert isinstance(arr, oepd.MoleculeArray)
140
- df[col] = pd.Series(arr.deepcopy(), index=df[col].index, dtype=oepd.MoleculeDtype())
148
+ # Use preserved metadata from original DataFrame (not the copy which may have lost it)
149
+ original_metadata = original_metadata_by_col.get(col, {})
150
+ new_arr = arr.deepcopy()
151
+ new_arr.metadata.update(original_metadata)
152
+ df[col] = pd.Series(new_arr, index=df[col].index, dtype=oepd.MoleculeDtype())
141
153
 
142
154
  # ---------------------------------------------------
143
155
  # Molecule columns
@@ -522,7 +534,7 @@ class SeriesRecalculateDepictionCoordinatesAccessor:
522
534
 
523
535
 
524
536
  @register_dataframe_accessor("reset_depictions")
525
- class SeriesResetDepictionsAccessor:
537
+ class DataFrameResetDepictionsAccessor:
526
538
  def __init__(self, pandas_obj: pd.DataFrame):
527
539
  self._obj = pandas_obj
528
540
 
cnotebook/render.py CHANGED
@@ -2,6 +2,9 @@ import logging
2
2
  import base64
3
3
  from .context import CNotebookContext, pass_cnotebook_context
4
4
  from openeye import oechem, oedepict
5
+ from typing import Literal
6
+ from math import floor, ceil
7
+ from collections.abc import Iterable, Sequence
5
8
 
6
9
  log = logging.getLogger("cnotebook")
7
10
 
@@ -196,3 +199,197 @@ def oeimage_to_html(image: oedepict.OEImage, *, ctx: CNotebookContext) -> str:
196
199
  image_bytes=image_bytes,
197
200
  wrap_svg=ctx.structure_scale != oedepict.OEScale_AutoScale
198
201
  )
202
+
203
+
204
+ @pass_cnotebook_context
205
+ def render_molecule_grid(
206
+ mols: Sequence[oechem.OEMolBase],
207
+ nrows: int | None = None,
208
+ ncols: int | None = None,
209
+ max_width: int = 1280,
210
+ max_columns: int = 100,
211
+ min_width: int | None = None,
212
+ min_height: int | None = None,
213
+ align: oechem.OEMolBase | Literal["first"] | None = None,
214
+ smarts: str | Iterable[str] | oechem.OESubSearch | Iterable[oechem.OESubSearch] | None = None,
215
+ color: oechem.OEColor = oechem.OEColor(oechem.OELightBlue),
216
+ style: int = oedepict.OEHighlightStyle_Stick,
217
+ scale: float = 1.0,
218
+ *,
219
+ ctx: CNotebookContext
220
+ ) -> oedepict.OEImage:
221
+ """
222
+ Convenience function to render a molecule grid
223
+ :param ctx: Current OpenEye rendering context
224
+ :param mols: Iterable of OpenEye molecules
225
+ :param nrows: Number of rows to display in the grid
226
+ :param ncols: Number of columns to display in the grid
227
+ :param max_width: Maximum width of the image
228
+ :param max_columns: Maximum number of columns (if ncols is being automatically calculated)
229
+ :param min_width: Minimum image width (prevents tiny images)
230
+ :param min_height: Minimum image height (prevents tiny images)
231
+ :param align: Alignment to the first molecule, or to a reference molecule
232
+ :param smarts: SMARTS substructure highlighting
233
+ :param color: SMARTS highlighting color
234
+ :param style: SMARTS highlighting style
235
+ :param scale: Image scale in the 2D grid
236
+ :return: Image of the molecule grid
237
+ """
238
+ from .helpers import create_structure_highlighter
239
+
240
+ # Re-scale the images
241
+ ctx.display_options.SetScale(ctx.display_options.GetScale() * scale)
242
+
243
+ # ---------------------------------------------------------------
244
+ # Input validation
245
+ # ---------------------------------------------------------------
246
+
247
+ # Handle single molecules
248
+ if isinstance(mols, oechem.OEMolBase):
249
+ mols = [mols]
250
+
251
+ # Make a copy of the molecules (so we do not modify them)
252
+ # We can use OEGraphMol here because we don't care about conformers
253
+ # Filter out None values first
254
+ mols = [oechem.OEGraphMol(mol) for mol in mols if mol is not None]
255
+
256
+ if len(mols) == 0:
257
+ log.warning("No molecules or display objects to render into a grid")
258
+ # Return a minimal 1x1 image instead of 0x0 to avoid OpenEye bug
259
+ return oedepict.OEImage(1, 1)
260
+
261
+ # Get the subset of molecules that will actually be displayed
262
+ valid = []
263
+
264
+ for idx, mol in enumerate(mols):
265
+ if mol is not None:
266
+ if isinstance(mol, oechem.OEMolBase):
267
+ if mol.IsValid():
268
+ valid.append(mol)
269
+ else:
270
+ log.warning(f'Molecule at index {idx} is not valid')
271
+
272
+ else:
273
+ log.warning(f'Object at index is not a molecule but type {type(mol).__name__}')
274
+
275
+ if len(valid) == 0:
276
+ log.warning("No valid molecules or display objects to render into a grid")
277
+ # Return a minimal 1x1 image instead of 0x0 to avoid OpenEye bug
278
+ return oedepict.OEImage(1, 1)
279
+
280
+ # ---------------------------------------------------------------
281
+ # For highlighting SMARTS
282
+ # ---------------------------------------------------------------
283
+
284
+ highlighers = None
285
+
286
+ if smarts is not None:
287
+ highlighers = []
288
+ # Case: Single pattern
289
+ if isinstance(smarts, (str, oechem.OESubSearch)):
290
+ highlighers.append(
291
+ create_structure_highlighter(
292
+ smarts,
293
+ color=color,
294
+ style=style
295
+ )
296
+ )
297
+
298
+ else:
299
+ for pattern in smarts:
300
+ highlighers.append(
301
+ create_structure_highlighter(
302
+ pattern,
303
+ color=color,
304
+ style=style
305
+ )
306
+ )
307
+
308
+ # ---------------------------------------------------------------
309
+ # For substructure alignment
310
+ # ---------------------------------------------------------------
311
+
312
+ align_mcss = None
313
+
314
+ if align is not None:
315
+
316
+ # If we are doing an alignment
317
+ if isinstance(align, bool) and align:
318
+ align_ref = mols[0]
319
+
320
+ elif isinstance(align, oechem.OEMolBase):
321
+ align_ref = align
322
+
323
+ else:
324
+ raise TypeError(f'Cannot initialize MCSS alignment reference from type {type(align).__name__}')
325
+
326
+ # Set up the MCSS
327
+ align_mcss = oechem.OEMCSSearch(oechem.OEMCSType_Approximate)
328
+ align_mcss.Init(align_ref, oechem.OEExprOpts_DefaultAtoms, oechem.OEExprOpts_DefaultBonds)
329
+
330
+ # ---------------------------------------------------------------
331
+ # Create the display objects for each molecule
332
+ # ---------------------------------------------------------------
333
+
334
+ displays = []
335
+ max_disp_width = float('-inf')
336
+ max_disp_height = float('-inf')
337
+
338
+ for mol in valid:
339
+ if align_mcss is not None:
340
+ oedepict.OEPrepareAlignedDepiction(mol, align_mcss)
341
+ else:
342
+ oedepict.OEPrepareDepiction(mol)
343
+
344
+ # Create the display object
345
+ disp = ctx.create_molecule_display(mol, min_width=min_width, min_height=min_height)
346
+
347
+ # Highlight SMARTS patterns
348
+ if highlighers is not None:
349
+ for highlight in highlighers:
350
+ highlight(disp)
351
+
352
+ displays.append(disp)
353
+
354
+ if disp.GetWidth() > max_disp_width:
355
+ max_disp_width = disp.GetWidth()
356
+
357
+ if disp.GetHeight() > max_disp_height:
358
+ max_disp_height = disp.GetHeight()
359
+
360
+ # ---------------------------------------------------------------
361
+ # Figure out the geometry of the full image
362
+ # ---------------------------------------------------------------
363
+ # Case: We have one molecule
364
+ if len(displays) == 1:
365
+ ncols = 1
366
+ nrows = 1
367
+
368
+ # Case: We are computing based on max_width
369
+ elif ncols is None and nrows is None:
370
+
371
+ # Number of columns we can fit into max_width
372
+ ncols = min(floor(max_width / max_disp_width), max_columns, len(displays))
373
+ nrows = ceil(len(displays) / ncols)
374
+
375
+ elif nrows is not None:
376
+ ncols = ceil(len(displays) / nrows)
377
+
378
+ elif ncols is not None:
379
+ nrows = ceil(len(displays) / ncols)
380
+
381
+ else:
382
+ raise RuntimeError("Cannot determine number of rows and columns in molecule grid")
383
+
384
+ # Image width and height
385
+ width = max_disp_width * ncols
386
+ height = max_disp_height * nrows
387
+
388
+ image = oedepict.OEImage(width, height)
389
+ grid = oedepict.OEImageGrid(image, nrows, ncols)
390
+
391
+ # Render the molecules
392
+ for disp, cell in zip(displays, grid.GetCells()):
393
+ oedepict.OERenderMolecule(cell, disp)
394
+
395
+ return image
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cnotebook
3
- Version: 1.0.1
3
+ Version: 1.2.0
4
4
  Summary: Chemistry visualization in Jupyter Notebooks with the OpenEye Toolkits
5
5
  Author-email: Scott Arne Johnson <scott.arne.johnson@gmail.com>
6
6
  License-Expression: MIT
@@ -23,12 +23,12 @@ Classifier: Programming Language :: Python :: 3 :: Only
23
23
  Classifier: Operating System :: OS Independent
24
24
  Classifier: Framework :: Jupyter
25
25
  Classifier: Framework :: IPython
26
- Requires-Python: >=3.10
26
+ Requires-Python: >=3.11
27
27
  Description-Content-Type: text/markdown
28
28
  License-File: LICENSE
29
- Requires-Dist: pandas<3.0.0,>=2.0.0
29
+ Requires-Dist: pandas>=2.2.0
30
30
  Requires-Dist: oepandas>=2.0.0
31
- Requires-Dist: openeye-toolkits
31
+ Requires-Dist: openeye-toolkits>=2025.2.1
32
32
  Provides-Extra: dev
33
33
  Requires-Dist: invoke; extra == "dev"
34
34
  Requires-Dist: build; extra == "dev"
@@ -38,7 +38,12 @@ Requires-Dist: pytest; extra == "test"
38
38
  Requires-Dist: pytest-cov; extra == "test"
39
39
  Dynamic: license-file
40
40
 
41
- # CNotebook - Chemistry Visualization for Jupyter & Marimo
41
+ # CNotebook
42
+
43
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
44
+ [![OpenEye Toolkits](https://img.shields.io/badge/OpenEye-2025.2.1+-green.svg)](https://www.eyesopen.com/toolkits)
45
+ [![Pandas 2.2+](https://img.shields.io/badge/pandas-2.2+-orange.svg)](https://pandas.pydata.org/)
46
+
42
47
 
43
48
  **Author:** Scott Arne Johnson ([scott.arne.johnson@gmail.com](mailto:scott.arne.johnson@gmail.com))
44
49
 
@@ -70,33 +75,33 @@ mol
70
75
 
71
76
  That's it! CNotebook automatically registers formatters so that OpenEye molecule objects display as chemical structures instead of cryptic text representations.
72
77
 
73
- ## 📚 Features
78
+ ## Features
74
79
 
75
- ### Automatic Rendering
80
+ ### Automatic Rendering
76
81
  - **Zero Configuration**: Just import and go - molecules automatically render as structures
77
82
  - **Multiple Formats**: Supports both Jupyter Notebooks and Marimo environments
78
83
  - **Smart Detection**: Automatically detects your notebook environment
79
84
 
80
- ### 🧪 Molecule Support
85
+ ### Molecule Support
81
86
  - **OEMol Objects**: Direct rendering of `oechem.OEMolBase` derived objects
82
87
  - **OE2DMolDisplay**: Advanced rendering with custom depiction options
83
88
  - **Pandas Integration**: Seamless rendering in DataFrames with oepandas
84
89
 
85
- ### 🎨 Visualization Options
90
+ ### Visualization Options
86
91
  - **Multiple Formats**: PNG (default) or SVG output
87
92
  - **Customizable Sizing**: Configurable width, height, and scaling
88
93
  - **Grid Layouts**: Multi-molecule grid displays
89
94
  - **Substructure Highlighting**: SMARTS pattern highlighting
90
95
  - **Molecular Alignment**: Align molecules to reference structures
91
96
 
92
- ### 📊 Pandas Integration
97
+ ### Pandas Integration
93
98
  - **DataFrame Rendering**: Automatic molecule column detection and rendering
94
99
  - **Column Highlighting**: Highlight different patterns per row
95
100
  - **Alignment Tools**: Align molecular depictions in DataFrames
96
101
  - **Fingerprint Similarity**: Visual similarity coloring
97
102
  - **Property Calculation**: Chemistry-aware DataFrame operations
98
103
 
99
- ## 🛠️ Environment Support
104
+ ## Environment Support
100
105
 
101
106
  ### Jupyter Notebooks
102
107
  CNotebook automatically integrates with Jupyter when imported:
@@ -126,7 +131,7 @@ oechem.OESmilesToMol(mol, "c1ccccc1")
126
131
  mol # Automatically renders as PNG for Marimo compatibility
127
132
  ```
128
133
 
129
- ## 📖 Advanced Usage
134
+ ## Advanced Usage
130
135
 
131
136
  ### Rendering Configuration
132
137
 
@@ -204,7 +209,7 @@ df.fingerprint_similarity("Molecule", reference_mol, inplace=True)
204
209
  df # Shows similarity coloring and Tanimoto coefficients
205
210
  ```
206
211
 
207
- ## 📁 Demo Notebooks
212
+ ## Demo Notebooks
208
213
 
209
214
  Explore comprehensive examples in the `demos/` directory:
210
215
 
@@ -228,7 +233,7 @@ Explore comprehensive examples in the `demos/` directory:
228
233
  - High-quality crisp molecular graphics
229
234
  - SVG compatibility considerations
230
235
 
231
- ## 🔧 Configuration Options
236
+ ## Configuration Options
232
237
 
233
238
  ### Global Context Settings
234
239
 
@@ -254,7 +259,7 @@ ctx.scale = 1.0 # Scaling factor
254
259
  - **Marimo**: Automatically uses PNG format for compatibility
255
260
  - **Console**: Falls back to string representations
256
261
 
257
- ## 🤝 Contributing
262
+ ## Contributing
258
263
 
259
264
  We welcome contributions! Please ensure your code:
260
265
  - Follows existing code style and conventions
@@ -262,11 +267,11 @@ We welcome contributions! Please ensure your code:
262
267
  - Works with both Jupyter and Marimo environments
263
268
  - Maintains compatibility with OpenEye Toolkits
264
269
 
265
- ## 📄 License
270
+ ## License
266
271
 
267
272
  This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
268
273
 
269
- ## 🐛 Issues & Support
274
+ ## Issues & Support
270
275
 
271
276
  For bug reports, feature requests, or general support, please open an issue on GitHub or contact the author at [scott.arne.johnson@gmail.com](mailto:scott.arne.johnson@gmail.com).
272
277
 
@@ -0,0 +1,13 @@
1
+ cnotebook/__init__.py,sha256=31LjoTU4gu19fgH9g3WGG08Tqopq69mSizz-AUu5WQ8,2702
2
+ cnotebook/align.py,sha256=u22GEHJc8QzCb9If2mjuapKPOTBEB2iu6jtUfgmm1UQ,14086
3
+ cnotebook/context.py,sha256=MXOIIZ7PkWZ8Wi11L00ldAqsaNCBmJRc76bETelIq9w,17493
4
+ cnotebook/helpers.py,sha256=r5-CIcmomd8vbd6ILGU6uW3sAjWZPcng82cikZ0ZDZ8,2193
5
+ cnotebook/ipython_ext.py,sha256=mu9eMQiYFEQeT8pS_xh-8Qx6N4dyn6_9u0YwlEudQXo,1989
6
+ cnotebook/marimo_ext.py,sha256=SPvej9LsRIFz8ljujMjppkm92BWTR8xgX9SqFmkY6VA,7169
7
+ cnotebook/pandas_ext.py,sha256=enGXKWDHOv6TK-MbA2H_KsP2IrAEViftyDvCKIjkTX8,35411
8
+ cnotebook/render.py,sha256=S7N7hPF61tppSAo7WN0kF89pWVJWKVMiDczy7RkOltA,13025
9
+ cnotebook-1.2.0.dist-info/licenses/LICENSE,sha256=HbIgeZz-pWGC7BEnYFCQ-jfD1m_BfiosF9qjgWw64GU,1080
10
+ cnotebook-1.2.0.dist-info/METADATA,sha256=i1vWVNcomhxVXtOAnsVk2YykR57T3EEeCvJvg91qst8,8789
11
+ cnotebook-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
+ cnotebook-1.2.0.dist-info/top_level.txt,sha256=jzkieTjQwdNKfMwnoElvDDtNPkeLMjbvWbsbkSsboo8,10
13
+ cnotebook-1.2.0.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- cnotebook/__init__.py,sha256=cdQtNy_bYo3ezhhLrSfQidCaRRAGVfqeIJA6XCh2wV0,2695
2
- cnotebook/align.py,sha256=u22GEHJc8QzCb9If2mjuapKPOTBEB2iu6jtUfgmm1UQ,14086
3
- cnotebook/context.py,sha256=MXOIIZ7PkWZ8Wi11L00ldAqsaNCBmJRc76bETelIq9w,17493
4
- cnotebook/helpers.py,sha256=r5-CIcmomd8vbd6ILGU6uW3sAjWZPcng82cikZ0ZDZ8,2193
5
- cnotebook/ipython_ext.py,sha256=T6kggGkT7YtDfW7XL3kM_wIhac9jVBQaeDgSrjlQp1g,8869
6
- cnotebook/marimo_ext.py,sha256=b7TJU_Wj-UvfeCkvFWKzn28dPR4olbsd4auWG3UpzQ8,1055
7
- cnotebook/pandas_ext.py,sha256=9ED28VrxOzdCELIpUpHminbUrccsNcqDFRMLNc26wVA,34795
8
- cnotebook/render.py,sha256=N5W8-QQGD3d149K6E15MkUvnYtIyFeJ5o4Gphf8Bcuo,6141
9
- cnotebook-1.0.1.dist-info/licenses/LICENSE,sha256=HbIgeZz-pWGC7BEnYFCQ-jfD1m_BfiosF9qjgWw64GU,1080
10
- cnotebook-1.0.1.dist-info/METADATA,sha256=MvrUjSCF8Up_dJOF1dQ1pwlo_LZv0Ft9MStmFDPXQV4,8575
11
- cnotebook-1.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
- cnotebook-1.0.1.dist-info/top_level.txt,sha256=jzkieTjQwdNKfMwnoElvDDtNPkeLMjbvWbsbkSsboo8,10
13
- cnotebook-1.0.1.dist-info/RECORD,,