Skip to content

Commit b02516c

Browse files
petercorkeclaude
authored andcommitted
feat(base): add environment() and move isnotebook out of the matplotlib block
isnotebook() lived inside graphics.py's matplotlib try-block, so it vanished when matplotlib was missing, and it reported False for Google Colab and JupyterLite. RTB, MVTB and bdsim each grew their own copy as a result. Add environment(), returning "terminal", "ipython", "jupyter", "colab" or "jupyterlite", in a new matplotlib-free module, and make isnotebook() a thin wrapper that is True for jupyter, colab and jupyterlite. smb.isnotebook is still exported. Tests use a faked get_ipython/sys.modules/sys.platform. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
1 parent 824a459 commit b02516c

4 files changed

Lines changed: 144 additions & 21 deletions

File tree

‎spatialmath/base/__init__.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from spatialmath.base.vectors import * # lgtm [py/polluting-import]
1111
from spatialmath.base.symbolic import * # lgtm [py/polluting-import]
1212
from spatialmath.base.numeric import * # lgtm [py/polluting-import]
13+
from spatialmath.base._environment import * # lgtm [py/polluting-import]
1314

1415
import importlib
1516

@@ -46,7 +47,6 @@
4647
"plotvol2": "graphics",
4748
"plotvol3": "graphics",
4849
"expand_dims": "graphics",
49-
"isnotebook": "graphics",
5050
}
5151

5252

@@ -394,6 +394,7 @@ def __getattr__(name):
394394
"axes_logic",
395395
"set_window_title",
396396
"expand_dims",
397+
"environment",
397398
"isnotebook",
398399
# spatial.base.numeric
399400
"numjac",

‎spatialmath/base/_environment.py‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Part of Spatial Math Toolbox for Python
2+
# Copyright (c) 2000 Peter Corke
3+
# MIT Licence, see details in top-level file: LICENCE
4+
5+
"""
6+
Detect the interactive environment that Python is running in.
7+
8+
This module deliberately does not import Matplotlib, so it is cheap to import
9+
and works when Matplotlib is not installed.
10+
"""
11+
12+
import sys
13+
from typing import Literal
14+
15+
__all__ = ["environment", "isnotebook"]
16+
17+
Environment = Literal["terminal", "ipython", "jupyter", "colab", "jupyterlite"]
18+
19+
20+
def environment() -> Environment:
21+
"""
22+
Determine the environment that Python is running in
23+
24+
:return: the environment
25+
:rtype: str
26+
27+
The result is one of:
28+
29+
======================== ====================================================
30+
``"terminal"`` standard Python interpreter, or script
31+
``"ipython"`` IPython running in a terminal
32+
``"jupyter"`` Jupyter kernel, eg. JupyterLab, Notebook, VS Code or
33+
qtconsole
34+
``"colab"`` Google Colab
35+
``"jupyterlite"`` JupyterLite, running in the browser (Pyodide)
36+
======================== ====================================================
37+
38+
.. note:: The different Jupyter front ends (JupyterLab, classic Notebook,
39+
VS Code, Jupyter Desktop) cannot be reliably told apart from within the
40+
kernel, so all are reported as ``"jupyter"``. How figures will be
41+
rendered depends on the Matplotlib backend, not on the front end.
42+
43+
:seealso: :func:`isnotebook`
44+
"""
45+
# Colab and JupyterLite both also provide an IPython kernel, so test for
46+
# them first
47+
if "google.colab" in sys.modules:
48+
return "colab"
49+
if sys.platform == "emscripten":
50+
return "jupyterlite"
51+
52+
try:
53+
shell = get_ipython().__class__.__name__ # type: ignore[name-defined]
54+
except NameError:
55+
return "terminal" # probably standard Python interpreter
56+
57+
if shell == "ZMQInteractiveShell":
58+
return "jupyter" # Jupyter notebook or qtconsole
59+
elif shell == "TerminalInteractiveShell":
60+
return "ipython"
61+
else:
62+
return "terminal" # other type (?)
63+
64+
65+
def isnotebook() -> bool:
66+
"""
67+
Determine if code is being run from a Jupyter notebook
68+
69+
:return: True if running in a Jupyter kernel, Google Colab or JupyterLite
70+
:rtype: bool
71+
72+
:seealso: :func:`environment`
73+
74+
:references:
75+
76+
- https://stackoverflow.com/questions/15411967/how-can-i-check-if-code-is-executed-in-the-ipython-notebook/39662359#39662359
77+
"""
78+
return environment() in ("jupyter", "colab", "jupyterlite")

‎spatialmath/base/graphics.py‎

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1822,26 +1822,6 @@ def set_window_title(fig: plt.Figure, title: str) -> bool:
18221822
return True
18231823
return False
18241824

1825-
def isnotebook() -> bool:
1826-
"""
1827-
Determine if code is being run from a Jupyter notebook
1828-
1829-
:references:
1830-
1831-
- https://stackoverflow.com/questions/15411967/how-can-i-check-if-code-
1832-
is-executed-in-the-ipython-notebook/39662359#39662359
1833-
"""
1834-
try:
1835-
shell = get_ipython().__class__.__name__
1836-
if shell == "ZMQInteractiveShell":
1837-
return True # Jupyter notebook or qtconsole
1838-
elif shell == "TerminalInteractiveShell":
1839-
return False # Terminal running IPython
1840-
else:
1841-
return False # Other type (?)
1842-
except NameError:
1843-
return False # Probably standard Python interpreter
1844-
18451825
if __name__ == "__main__":
18461826
import pathlib
18471827

‎tests/base/test_environment.py‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import builtins
2+
import sys
3+
4+
import pytest
5+
6+
from spatialmath.base import environment, isnotebook
7+
8+
# These tests are mocked: a fake get_ipython() and sys.modules / sys.platform
9+
# stand in for the real front ends, which cannot be run under CI.
10+
11+
12+
def fake_shell(name):
13+
shell = type(name, (), {})()
14+
return lambda: shell
15+
16+
17+
@pytest.fixture
18+
def plain(monkeypatch):
19+
"""Standard interpreter: no IPython, not Colab, not Pyodide"""
20+
monkeypatch.delattr(builtins, "get_ipython", raising=False)
21+
monkeypatch.delitem(sys.modules, "google.colab", raising=False)
22+
monkeypatch.setattr(sys, "platform", "linux")
23+
return monkeypatch
24+
25+
26+
def test_terminal(plain):
27+
assert environment() == "terminal"
28+
assert not isnotebook()
29+
30+
31+
def test_ipython_terminal(plain):
32+
plain.setattr(
33+
builtins, "get_ipython", fake_shell("TerminalInteractiveShell"), raising=False
34+
)
35+
assert environment() == "ipython"
36+
assert not isnotebook()
37+
38+
39+
def test_jupyter(plain):
40+
plain.setattr(
41+
builtins, "get_ipython", fake_shell("ZMQInteractiveShell"), raising=False
42+
)
43+
assert environment() == "jupyter"
44+
assert isnotebook()
45+
46+
47+
def test_unknown_shell(plain):
48+
plain.setattr(builtins, "get_ipython", fake_shell("SomethingElse"), raising=False)
49+
assert environment() == "terminal"
50+
assert not isnotebook()
51+
52+
53+
def test_colab(plain):
54+
# Colab's shell is not a ZMQInteractiveShell, so detect it by the module
55+
plain.setattr(builtins, "get_ipython", fake_shell("Shell"), raising=False)
56+
plain.setitem(sys.modules, "google.colab", object())
57+
assert environment() == "colab"
58+
assert isnotebook()
59+
60+
61+
def test_jupyterlite(plain):
62+
plain.setattr(sys, "platform", "emscripten")
63+
assert environment() == "jupyterlite"
64+
assert isnotebook()

0 commit comments

Comments
 (0)