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 ADDED
@@ -0,0 +1,400 @@
1
+ """
2
+ CNotebook - Ergonomic chemistry visualization in notebooks.
3
+
4
+ Auto-detects available backends (Pandas/Polars) and environments (Jupyter/Marimo).
5
+ Only requires openeye-toolkits; all other dependencies are optional.
6
+ """
7
+ import logging
8
+
9
+ # Required imports (openeye-toolkits)
10
+ from openeye import oechem, oedepict
11
+
12
+ # Core functionality that doesn't depend on backends
13
+ from .context import cnotebook_context, CNotebookContext
14
+ from .helpers import highlight_smarts
15
+
16
+ __version__ = '2.1.0'
17
+
18
+ # Configure logging first
19
+ log = logging.getLogger("cnotebook")
20
+
21
+
22
+ class LevelSpecificFormatter(logging.Formatter):
23
+ """A logging formatter that uses level-specific formats.
24
+
25
+ Uses a simple format for INFO and above, and includes the level name
26
+ for DEBUG messages to help distinguish debug output.
27
+
28
+ :cvar NORMAL_FORMAT: Format string for INFO and above.
29
+ :cvar DEBUG_FORMAT: Format string for DEBUG level.
30
+ """
31
+
32
+ NORMAL_FORMAT = "%(message)s"
33
+ DEBUG_FORMAT = "%(levelname)s: %(message)s"
34
+
35
+ def __init__(self):
36
+ """Create the formatter with the normal format as default."""
37
+ super().__init__(fmt=self.NORMAL_FORMAT, datefmt=None, style='%')
38
+
39
+ def format(self, record: logging.LogRecord) -> str:
40
+ if record.levelno == logging.DEBUG:
41
+ self._style._fmt = self.DEBUG_FORMAT
42
+ else:
43
+ self._style._fmt = self.NORMAL_FORMAT
44
+ return logging.Formatter.format(self, record)
45
+
46
+
47
+ # Configure handler
48
+ _handler = logging.StreamHandler()
49
+ _handler.setLevel(logging.DEBUG)
50
+ _handler.setFormatter(LevelSpecificFormatter())
51
+ log.addHandler(_handler)
52
+ log.setLevel(logging.INFO)
53
+
54
+
55
+ def enable_debugging():
56
+ """Convenience function for enabling the debug log."""
57
+ log.setLevel(logging.DEBUG)
58
+
59
+
60
+ ########################################################################################################################
61
+ # Environment Information
62
+ ########################################################################################################################
63
+
64
+ class CNotebookEnvInfo:
65
+ """Environment information for CNotebook.
66
+
67
+ This class provides read-only access to detected backend and environment
68
+ availability. A singleton instance is created at module load time and
69
+ can be retrieved via :func:`get_env`.
70
+
71
+ All properties are read-only to ensure consistency throughout the
72
+ application lifecycle. Availability is determined by checking if the
73
+ version string is non-empty.
74
+ """
75
+
76
+ def __init__(
77
+ self,
78
+ pandas_version: str,
79
+ polars_version: str,
80
+ ipython_version: str,
81
+ marimo_version: str,
82
+ molgrid_available: bool,
83
+ is_jupyter_notebook: bool,
84
+ is_marimo_notebook: bool,
85
+ ):
86
+ """Create environment info (typically called once at module load).
87
+
88
+ :param pandas_version: Detected Pandas version string, or empty if unavailable.
89
+ :param polars_version: Detected Polars version string, or empty if unavailable.
90
+ :param ipython_version: Detected IPython version string, or empty if unavailable.
91
+ :param marimo_version: Detected Marimo version string, or empty if unavailable.
92
+ :param molgrid_available: Whether MolGrid widget dependencies are available.
93
+ :param is_jupyter_notebook: Whether running in a Jupyter notebook environment.
94
+ :param is_marimo_notebook: Whether running in a Marimo notebook environment.
95
+ """
96
+ self._pandas_version = pandas_version
97
+ self._polars_version = polars_version
98
+ self._ipython_version = ipython_version
99
+ self._marimo_version = marimo_version
100
+ self._molgrid_available = molgrid_available
101
+ self._is_jupyter_notebook = is_jupyter_notebook
102
+ self._is_marimo_notebook = is_marimo_notebook
103
+
104
+ @property
105
+ def pandas_available(self) -> bool:
106
+ """Whether Pandas and OEPandas are available."""
107
+ return bool(self._pandas_version)
108
+
109
+ @property
110
+ def pandas_version(self) -> str:
111
+ """Pandas version string, or empty string if not available."""
112
+ return self._pandas_version
113
+
114
+ @property
115
+ def polars_available(self) -> bool:
116
+ """Whether Polars and OEPolars are available."""
117
+ return bool(self._polars_version)
118
+
119
+ @property
120
+ def polars_version(self) -> str:
121
+ """Polars version string, or empty string if not available."""
122
+ return self._polars_version
123
+
124
+ @property
125
+ def ipython_available(self) -> bool:
126
+ """Whether IPython is available and active."""
127
+ return bool(self._ipython_version)
128
+
129
+ @property
130
+ def ipython_version(self) -> str:
131
+ """IPython version string, or empty string if not available."""
132
+ return self._ipython_version
133
+
134
+ @property
135
+ def marimo_available(self) -> bool:
136
+ """Whether Marimo is available and running in notebook mode."""
137
+ return bool(self._marimo_version)
138
+
139
+ @property
140
+ def marimo_version(self) -> str:
141
+ """Marimo version string, or empty string if not available."""
142
+ return self._marimo_version
143
+
144
+ @property
145
+ def molgrid_available(self) -> bool:
146
+ """Whether MolGrid is available (requires anywidget)."""
147
+ return self._molgrid_available
148
+
149
+ @property
150
+ def is_jupyter_notebook(self) -> bool:
151
+ """Whether running in a Jupyter notebook environment."""
152
+ return self._is_jupyter_notebook
153
+
154
+ @property
155
+ def is_marimo_notebook(self) -> bool:
156
+ """Whether running in a Marimo notebook environment."""
157
+ return self._is_marimo_notebook
158
+
159
+ def __repr__(self) -> str:
160
+ return (
161
+ f"CNotebookEnvInfo("
162
+ f"pandas={self.pandas_available} ({self._pandas_version}), "
163
+ f"polars={self.polars_available} ({self._polars_version}), "
164
+ f"ipython={self.ipython_available} ({self._ipython_version}), "
165
+ f"marimo={self.marimo_available} ({self._marimo_version}), "
166
+ f"molgrid={self._molgrid_available}, "
167
+ f"jupyter={self._is_jupyter_notebook}, "
168
+ f"marimo_nb={self._is_marimo_notebook})"
169
+ )
170
+
171
+
172
+ def _detect_environment() -> CNotebookEnvInfo:
173
+ """Detect available backends and environments.
174
+
175
+ :returns: CNotebookEnvInfo instance with detection results.
176
+ """
177
+ pandas_version = ""
178
+ polars_version = ""
179
+ ipython_version = ""
180
+ marimo_version = ""
181
+ molgrid_available = False
182
+ is_jupyter = False
183
+ is_marimo = False
184
+
185
+ # Detect molgrid (requires anywidget)
186
+ try:
187
+ from cnotebook.grid import molgrid, MolGrid
188
+ molgrid_available = True
189
+ except ImportError:
190
+ pass
191
+
192
+ # Detect pandas/oepandas
193
+ try:
194
+ import pandas as pd
195
+ import oepandas as oepd
196
+ pandas_version = pd.__version__
197
+ except ImportError:
198
+ pass
199
+
200
+ # Detect polars/oepolars
201
+ try:
202
+ import polars as pl
203
+ import oepolars as oeplr
204
+ polars_version = pl.__version__
205
+ except ImportError:
206
+ pass
207
+
208
+ # Detect iPython
209
+ try:
210
+ import IPython
211
+ # noinspection PyProtectedMember
212
+ from IPython import get_ipython
213
+ ipy = get_ipython()
214
+ if ipy is not None:
215
+ ipython_version = IPython.__version__
216
+ # Check if running in Jupyter notebook
217
+ is_jupyter = ipy.__class__.__name__ == 'ZMQInteractiveShell'
218
+ except (ImportError, Exception):
219
+ pass
220
+
221
+ # Detect Marimo
222
+ try:
223
+ import marimo as mo
224
+ if mo.running_in_notebook():
225
+ marimo_version = mo.__version__
226
+ is_marimo = True
227
+ except (ImportError, Exception):
228
+ # Marimo raises exception if not running in notebook context
229
+ pass
230
+
231
+ return CNotebookEnvInfo(
232
+ pandas_version=pandas_version,
233
+ polars_version=polars_version,
234
+ ipython_version=ipython_version,
235
+ marimo_version=marimo_version,
236
+ molgrid_available=molgrid_available,
237
+ is_jupyter_notebook=is_jupyter,
238
+ is_marimo_notebook=is_marimo,
239
+ )
240
+
241
+
242
+ # Initialize environment detection at module load (singleton instance)
243
+ _env_info: CNotebookEnvInfo = _detect_environment()
244
+
245
+
246
+ def get_env() -> CNotebookEnvInfo:
247
+ """Get environment information for CNotebook.
248
+
249
+ Returns a singleton instance containing information about available
250
+ backends and environments. The environment is detected once at module
251
+ load time and the same object is returned on subsequent calls.
252
+
253
+ :returns: CNotebookEnvInfo instance with read-only properties.
254
+
255
+ Example::
256
+
257
+ env = cnotebook.get_env()
258
+ if env.pandas_available:
259
+ print(f"Pandas {env.pandas_version} is available")
260
+ """
261
+ return _env_info
262
+
263
+
264
+ ########################################################################################################################
265
+ # Register Formatters Based on Availability
266
+ ########################################################################################################################
267
+
268
+ # Import and register pandas formatters if available
269
+ if _env_info.pandas_available:
270
+ try:
271
+ from .pandas_ext import render_dataframe, register_pandas_formatters
272
+
273
+ if _env_info.ipython_available:
274
+ from .ipython_ext import register_ipython_formatters
275
+ register_ipython_formatters()
276
+ register_pandas_formatters()
277
+ log.debug("[cnotebook] Registered Pandas formatters for iPython")
278
+ except Exception as e:
279
+ log.warning(f"[cnotebook] Failed to import/register Pandas extension: {e}")
280
+
281
+ # Import and register polars formatters if available
282
+ if _env_info.polars_available:
283
+ try:
284
+ from .polars_ext import render_polars_dataframe, register_polars_formatters
285
+
286
+ if _env_info.ipython_available:
287
+ register_polars_formatters()
288
+ log.debug("[cnotebook] Registered Polars formatters for iPython")
289
+ except Exception as e:
290
+ log.warning(f"[cnotebook] Failed to import/register Polars extension: {e}")
291
+
292
+ # Import marimo extension if available
293
+ if _env_info.marimo_available:
294
+ try:
295
+ from . import marimo_ext
296
+ log.debug("[cnotebook] Imported Marimo extension")
297
+ except Exception as e:
298
+ log.warning(f"[cnotebook] Failed to import Marimo extension: {e}")
299
+
300
+ # Export molgrid at top level if available
301
+ if _env_info.molgrid_available:
302
+ from .grid import molgrid, MolGrid
303
+
304
+
305
+ ########################################################################################################################
306
+ # Unified Display Function
307
+ ########################################################################################################################
308
+
309
+ def display(obj, ctx: CNotebookContext | None = None):
310
+ """Display an OpenEye molecule, display object, or DataFrame in the current notebook environment.
311
+
312
+ This function provides a unified way to display chemistry objects in both Jupyter
313
+ and Marimo notebooks. It automatically detects the environment and uses the
314
+ appropriate display mechanism.
315
+
316
+ :param obj: Object to display. Can be:
317
+ - ``oechem.OEMolBase`` - OpenEye molecule
318
+ - ``oedepict.OE2DMolDisplay`` - OpenEye display object
319
+ - ``pandas.DataFrame`` - Pandas DataFrame (if pandas available)
320
+ - ``polars.DataFrame`` - Polars DataFrame (if polars available)
321
+ :param ctx: Optional rendering context. Only applied to molecules and display
322
+ objects, not DataFrames. If None, uses the global context.
323
+ :returns: A displayable object appropriate for the current environment.
324
+ :raises TypeError: If the object type is not supported.
325
+
326
+ Example::
327
+
328
+ import cnotebook
329
+ from openeye import oechem
330
+
331
+ mol = oechem.OEGraphMol()
332
+ oechem.OESmilesToMol(mol, "c1ccccc1")
333
+
334
+ # Display with default context
335
+ cnotebook.display(mol)
336
+
337
+ # Display with custom context
338
+ ctx = cnotebook.cnotebook_context.get().copy()
339
+ ctx.width = 300
340
+ ctx.height = 300
341
+ cnotebook.display(mol, ctx=ctx)
342
+ """
343
+ from .render import oemol_to_html, oedisp_to_html
344
+
345
+ # Get environment info
346
+ env = get_env()
347
+
348
+ # Determine the context to use
349
+ if ctx is None:
350
+ render_ctx = cnotebook_context.get()
351
+ else:
352
+ render_ctx = ctx
353
+
354
+ # Handle OpenEye molecules
355
+ if isinstance(obj, oechem.OEMolBase):
356
+ html = oemol_to_html(obj, ctx=render_ctx)
357
+ return _display_html(html, env)
358
+
359
+ # Handle OpenEye display objects
360
+ if isinstance(obj, oedepict.OE2DMolDisplay):
361
+ html = oedisp_to_html(obj, ctx=render_ctx)
362
+ return _display_html(html, env)
363
+
364
+ # Handle Pandas DataFrame (if available)
365
+ if env.pandas_available:
366
+ import pandas as pd
367
+ if isinstance(obj, pd.DataFrame):
368
+ # noinspection PyTypeChecker
369
+ html = render_dataframe(obj, ctx=render_ctx)
370
+ return _display_html(html, env)
371
+
372
+ # Handle Polars DataFrame (if available)
373
+ if env.polars_available:
374
+ import polars as pl
375
+ if isinstance(obj, pl.DataFrame):
376
+ html = render_polars_dataframe(obj, ctx=render_ctx)
377
+ return _display_html(html, env)
378
+
379
+ raise TypeError(f"Cannot display object of type {type(obj).__name__}")
380
+
381
+
382
+ def _display_html(html: str, env: CNotebookEnvInfo):
383
+ """Display HTML content in the appropriate notebook environment.
384
+
385
+ :param html: HTML string to display.
386
+ :param env: Environment info.
387
+ :returns: Displayable object for the current environment.
388
+ """
389
+ # Marimo environment
390
+ if env.is_marimo_notebook:
391
+ import marimo as mo
392
+ return mo.Html(html)
393
+
394
+ # Jupyter/IPython environment
395
+ if env.ipython_available:
396
+ from IPython.display import HTML, display as ipy_display
397
+ return ipy_display(HTML(html))
398
+
399
+ # Fallback: just return the HTML string
400
+ return html