Skip to content

Commit d1556fd

Browse files
petercorkeclaude
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 41e0a4d commit d1556fd

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

@@ -45,7 +46,6 @@
4546
"plotvol2": "graphics",
4647
"plotvol3": "graphics",
4748
"expand_dims": "graphics",
48-
"isnotebook": "graphics",
4949
}
5050

5151

@@ -392,6 +392,7 @@ def __getattr__(name):
392392
"plot_cuboid",
393393
"axes_logic",
394394
"expand_dims",
395+
"environment",
395396
"isnotebook",
396397
# spatial.base.numeric
397398
"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
@@ -1783,26 +1783,6 @@ def expand_dims(dim: ArrayLike = None, nd: int = 2) -> NDArray:
17831783
else:
17841784
raise ValueError("nd is 2 or 3")
17851785

1786-
def isnotebook() -> bool:
1787-
"""
1788-
Determine if code is being run from a Jupyter notebook
1789-
1790-
:references:
1791-
1792-
- https://stackoverflow.com/questions/15411967/how-can-i-check-if-code-
1793-
is-executed-in-the-ipython-notebook/39662359#39662359
1794-
"""
1795-
try:
1796-
shell = get_ipython().__class__.__name__
1797-
if shell == "ZMQInteractiveShell":
1798-
return True # Jupyter notebook or qtconsole
1799-
elif shell == "TerminalInteractiveShell":
1800-
return False # Terminal running IPython
1801-
else:
1802-
return False # Other type (?)
1803-
except NameError:
1804-
return False # Probably standard Python interpreter
1805-
18061786
if __name__ == "__main__":
18071787
import pathlib
18081788

‎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)