cnotebook 1.2.0__py3-none-any.whl → 2.1.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/helpers.py CHANGED
@@ -1,7 +1,17 @@
1
+ import logging
1
2
  import re
2
- from typing import Callable
3
+ from typing import Callable, Literal, Sequence
3
4
  from openeye import oechem, oedepict
4
5
 
6
+ log = logging.getLogger("cnotebook")
7
+
8
+
9
+ # Type alias for highlight style
10
+ HighlightStyle = int | Literal["overlay_default", "overlay_ball_and_stick"]
11
+
12
+ # Type alias for highlight colors
13
+ HighlightColors = oechem.OEColor | oechem.OEColorIter
14
+
5
15
 
6
16
  def escape_html(val):
7
17
  """
@@ -44,26 +54,148 @@ def remove_omega_conformer_id(val):
44
54
 
45
55
  def create_structure_highlighter(
46
56
  query: str | oechem.OESubSearch | oechem.OEMCSSearch | oechem.OEQMol,
47
- color: oechem.OEColor = oechem.OEColor(oechem.OELightBlue),
48
- style: int = oedepict.OEHighlightStyle_Stick
57
+ color: HighlightColors | None = None,
58
+ style: HighlightStyle = "overlay_default"
49
59
  ) -> Callable[[oedepict.OE2DMolDisplay], None]:
50
60
  """
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
61
+ Closure that creates a callback to highlight SMARTS patterns or MCSS results in a molecule.
62
+
63
+ :param query: SMARTS pattern, oechem.OESubSearch, or oechem.OEMCSSearch object.
64
+ :param color: Highlight color(s). Can be a single oechem.OEColor or an oechem.OEColorIter
65
+ (e.g., oechem.OEGetLightColors()). Defaults to oechem.OEGetLightColors().
66
+ :param style: Highlight style. Can be an int (OEHighlightStyle constant) or a string
67
+ ("overlay_default", "overlay_ball_and_stick"). Defaults to "overlay_default".
68
+ :returns: Callback function that highlights structures.
56
69
  """
70
+ # Default color
71
+ if color is None:
72
+ color = oechem.OEGetLightColors()
57
73
 
74
+ # Create the substructure search object
58
75
  if isinstance(query, (str, oechem.OEQMol)):
59
76
  ss = oechem.OESubSearch(query)
60
-
61
- elif not isinstance(query, (oechem.OESubSearch, oechem.OEMCSSearch)):
77
+ elif isinstance(query, (oechem.OESubSearch, oechem.OEMCSSearch)):
78
+ ss = query
79
+ else:
62
80
  raise TypeError(f'Cannot create structure highlighter with object pattern of type {type(query).__name__}')
63
81
 
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)
82
+ # Determine highlighting approach based on style
83
+ use_overlay = isinstance(style, str) and style in ("overlay_default", "overlay_ball_and_stick")
84
+
85
+ # Check if color is compatible with overlay
86
+ if use_overlay and isinstance(color, oechem.OEColor):
87
+ log.warning(
88
+ "Overlay coloring is not compatible with a single oechem.OEColor. Falling back to standard highlighting")
89
+ use_overlay = False
90
+ style = oedepict.OEHighlightStyle_BallAndStick
91
+
92
+ if use_overlay:
93
+ # Overlay highlighting with color iterator
94
+ highlight = oedepict.OEHighlightOverlayByBallAndStick(color)
95
+
96
+ def _overlay_highlighter(disp: oedepict.OE2DMolDisplay):
97
+ oedepict.OEAddHighlightOverlay(disp, highlight, ss.Match(disp.GetMolecule(), True))
98
+
99
+ return _overlay_highlighter
100
+
101
+ else:
102
+ # Traditional highlighting with OEHighlightStyle int
103
+ # For traditional highlighting, we need a single color
104
+ if isinstance(color, oechem.OEColor):
105
+ highlight_color = color
106
+ else:
107
+ # Get first color from iterator for traditional highlighting
108
+ highlight_color = oechem.OELightBlue
109
+ for c in color:
110
+ highlight_color = c
111
+ break
112
+
113
+ def _structure_highlighter(disp: oedepict.OE2DMolDisplay):
114
+ for match in ss.Match(disp.GetMolecule(), True):
115
+ oedepict.OEAddHighlighting(disp, highlight_color, style, match)
116
+
117
+ return _structure_highlighter
118
+
119
+
120
+ def highlight_smarts(
121
+ mol: oechem.OEMolBase,
122
+ smarts: str | Sequence[str],
123
+ color: oechem.OEColor | Sequence[oechem.OEColor] = oechem.OEColor(oechem.OELightBlue),
124
+ style: int | Sequence[int] = oedepict.OEHighlightStyle_Stick,
125
+ opts: oedepict.OE2DMolDisplayOptions | None = None,
126
+ ) -> oedepict.OE2DMolDisplay:
127
+ """Highlight SMARTS patterns in a molecule and return a display object.
128
+
129
+ :param mol: OpenEye molecule to highlight.
130
+ :param smarts: SMARTS pattern or sequence of SMARTS patterns to highlight.
131
+ :param color: Highlight color or sequence of colors. If a single color, it is
132
+ applied to all patterns. If a sequence, must match length of smarts.
133
+ :param style: Highlight style or sequence of styles. If a single style, it is
134
+ applied to all patterns. If a sequence, must match length of smarts.
135
+ :param opts: Display options. If None, default options are used.
136
+ :returns: OE2DMolDisplay object with highlighted substructures.
137
+ :raises ValueError: If color/style sequence length doesn't match smarts length.
138
+
139
+ Example::
140
+
141
+ from openeye import oechem
142
+ import cnotebook
143
+
144
+ mol = oechem.OEGraphMol()
145
+ oechem.OESmilesToMol(mol, "c1ccc2c(c1)nc(n2)N")
146
+
147
+ # Single color/style for all patterns
148
+ disp = cnotebook.highlight_smarts(mol, ["ncn", "c1ccccc1"])
149
+
150
+ # Different colors for each pattern
151
+ disp = cnotebook.highlight_smarts(
152
+ mol,
153
+ ["ncn", "c1ccccc1"],
154
+ color=[oechem.OEColor(oechem.OELightBlue), oechem.OEColor(oechem.OEPink)]
155
+ )
156
+ """
157
+ # Prepare molecule for depiction
158
+ oedepict.OEPrepareDepiction(mol)
159
+
160
+ # Create display options if not provided
161
+ if opts is None:
162
+ opts = oedepict.OE2DMolDisplayOptions()
163
+
164
+ # Create display object
165
+ disp = oedepict.OE2DMolDisplay(mol, opts)
166
+
167
+ # Normalize smarts to a list
168
+ if isinstance(smarts, str):
169
+ smarts = [smarts]
170
+
171
+ n_patterns = len(smarts)
172
+
173
+ # Normalize colors to a list
174
+ if isinstance(color, oechem.OEColor):
175
+ colors = [color] * n_patterns
176
+ else:
177
+ colors = list(color)
178
+ if len(colors) != n_patterns:
179
+ raise ValueError(
180
+ f"Length of color sequence ({len(colors)}) must match "
181
+ f"length of smarts sequence ({n_patterns})"
182
+ )
183
+
184
+ # Normalize styles to a list
185
+ if isinstance(style, int):
186
+ styles = [style] * n_patterns
187
+ else:
188
+ styles = list(style)
189
+ if len(styles) != n_patterns:
190
+ raise ValueError(
191
+ f"Length of style sequence ({len(styles)}) must match "
192
+ f"length of smarts sequence ({n_patterns})"
193
+ )
194
+
195
+ # Highlight all patterns with corresponding color and style
196
+ for pattern, c, s in zip(smarts, colors, styles):
197
+ subs = oechem.OESubSearch(pattern)
198
+ for match in subs.Match(mol, True):
199
+ oedepict.OEAddHighlighting(disp, c, s, match)
68
200
 
69
- return _structure_highlighter
201
+ return disp
cnotebook/ipython_ext.py CHANGED
@@ -1,12 +1,9 @@
1
1
  import logging
2
2
  from openeye import oechem, oedepict
3
3
  from .render import (
4
- CNotebookContext,
5
- pass_cnotebook_context,
6
4
  oemol_to_html,
7
5
  oedisp_to_html,
8
6
  oeimage_to_html,
9
- render_molecule_grid # Re-export for backward compatibility
10
7
  )
11
8
 
12
9
  # Only register iPython formatters if that is present
cnotebook/marimo_ext.py CHANGED
@@ -11,11 +11,22 @@ from openeye import oechem, oedepict
11
11
 
12
12
  # Import oepandas for dtype checking
13
13
  try:
14
+ # noinspection PyUnusedImports
14
15
  import oepandas as oepd
15
16
  oepandas_available = True
16
17
  except ImportError:
17
18
  oepandas_available = False
18
19
 
20
+ # Import oepolars for dtype checking
21
+ try:
22
+ # noinspection PyUnusedImports
23
+ import polars as pl
24
+ # noinspection PyUnusedImports
25
+ import oepolars as oeplr
26
+ oepolars_available = True
27
+ except ImportError:
28
+ oepolars_available = False
29
+
19
30
  from .context import cnotebook_context, get_series_context
20
31
  from .render import (
21
32
  oemol_to_html,
@@ -184,6 +195,48 @@ try:
184
195
  # Do the installation
185
196
  install_marimo_pandas_formatter()
186
197
 
198
+ def marimo_polars_formatter(df: pl.DataFrame):
199
+ """
200
+ Marimo DataFrame formatter for Polars DataFrames with molecule columns.
201
+ """
202
+ format_mapping = {}
203
+
204
+ # Check for MoleculeType / DisplayType (OEPolars specific)
205
+ if oepolars_available:
206
+ for col in df.columns:
207
+ dtype = df.schema[col]
208
+
209
+ if isinstance(dtype, oeplr.MoleculeType):
210
+ series = df.get_column(col)
211
+ metadata = series.chem.metadata if hasattr(series, 'chem') else {}
212
+ ctx = get_series_context(metadata).copy()
213
+ format_mapping[col] = _create_molecule_formatter(ctx)
214
+
215
+ elif isinstance(dtype, oeplr.DisplayType):
216
+ series = df.get_column(col)
217
+ metadata = series.chem.metadata if hasattr(series, 'chem') else {}
218
+ ctx = get_series_context(metadata).copy()
219
+ format_mapping[col] = _create_display_formatter(ctx)
220
+
221
+ # Return a Marimo table with our custom mapping
222
+ # noinspection PyProtectedMember,PyTypeChecker
223
+ return table(df, selection=None, format_mapping=format_mapping, pagination=True)._mime_()
224
+
225
+ def install_marimo_polars_formatter():
226
+ """Install the Polars DataFrame formatter if polars is available."""
227
+ if not oepolars_available:
228
+ return
229
+
230
+ # Check if we've already installed it to avoid duplicates
231
+ for typ, func in OPINIONATED_FORMATTERS.formatters.items():
232
+ if typ is pl.DataFrame and func.__name__ == "marimo_polars_formatter":
233
+ return # Already installed
234
+
235
+ OPINIONATED_FORMATTERS.formatters[pl.DataFrame] = marimo_polars_formatter
236
+
237
+ if oepolars_available:
238
+ install_marimo_polars_formatter()
239
+
187
240
  except (ImportError, AttributeError) as ex:
188
241
  # Marimo not installed or API changed - skip formatter registration
189
242
  log.debug(f'Marimo formatter registration skipped: {ex}')
@@ -203,3 +256,17 @@ def _display_dataframe(self: pd.DataFrame):
203
256
  return "text/html", render_dataframe(df=self, formatters=None, col_space=None)
204
257
 
205
258
  pd.DataFrame._mime_ = _display_dataframe
259
+
260
+ if oepolars_available:
261
+ from .polars_ext import render_polars_dataframe
262
+
263
+ def _display_polars_dataframe(self: pl.DataFrame):
264
+ """
265
+ Fallback MIME hook for Polars DataFrames in non-Marimo contexts.
266
+
267
+ In Marimo, the internal table patch handles DataFrame display.
268
+ This is used for static exports or other tools that check _mime_.
269
+ """
270
+ return "text/html", render_polars_dataframe(df=self, formatters=None, col_space=None)
271
+
272
+ pl.DataFrame._mime_ = _display_polars_dataframe