Skip to content

Commit 202ae93

Browse files
emerybergerclaude
andauthored
Guard get_ipython() being Optional (fixes mypy on master) (#1088)
The linters workflow has been failing on master since IPython 9.16 started annotating get_ipython() as returning Optional[InteractiveShell] (history_manager is optional too). mypy 2.3 reports four union-attr errors in scalene_tracing.py and scalene_magics.py; the existing "type: ignore[no-untyped-call, unused-ignore]" on those lines does not cover that error code, and says so. No commit caused this -- master's last linters run (2026-07-05) passed and nothing has landed since, so it broke when CI's unpinned "pip install mypy ruff ..." picked up the newer versions. Guard both levels. _handle_jupyter_cell() returns False without a live shell, falling through to the ordinary filename rules, which is the behavior it already had when the cell regex didn't match. run_code() prints a plain message and returns; neither value can actually be None there, since a magic only runs inside a live shell. The ignore comments stay put: on older IPython, get_ipython() is untyped, so no-untyped-call still applies, and unused-ignore keeps the comment valid once it doesn't. Verified in a venv pinned to CI's exact versions (mypy 2.3.0, IPython 9.16.0): 4 errors before, clean after, ruff clean. Also re-checked against the older pair (mypy 1.19.0, IPython 9.9.0) so the fix holds in both directions. Behavior spot-checked for all three shapes -- live shell (recovers the cell and returns True), get_ipython() None, and history_manager None. Full pytest suite: 442 passed, 13 skipped. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent c2e9e17 commit 202ae93

2 files changed

Lines changed: 24 additions & 4 deletions

File tree

scalene/scalene_magics.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,20 @@ def run_code(self, args: ScaleneArguments, code: str) -> None:
4040
# Create a file to hold the supplied code.
4141
# We encode the cell number in the string for later recovery.
4242
# The length of the history buffer lets us find the most recent string (this one).
43-
filename = f"_ipython-input-{len(IPython.get_ipython().history_manager.input_hist_raw)-1}-profile" # type: ignore[no-untyped-call,unused-ignore]
43+
#
44+
# IPython >= 9.16 annotates get_ipython() as returning
45+
# Optional[InteractiveShell], and history_manager is itself
46+
# optional. Neither can be None in practice here — a magic only
47+
# runs inside a live shell — but we can't index the history
48+
# without it, so say so plainly instead of raising AttributeError.
49+
shell = IPython.get_ipython() # type: ignore[no-untyped-call,unused-ignore]
50+
history = shell.history_manager if shell is not None else None
51+
if history is None:
52+
print(
53+
"Scalene: no IPython history is available, so this cell cannot be profiled."
54+
)
55+
return
56+
filename = f"_ipython-input-{len(history.input_hist_raw)-1}-profile"
4457
with open(filename, "w+") as tmpfile:
4558
tmpfile.write(code)
4659
args.memory = (

scalene/scalene_tracing.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,16 @@ def _handle_jupyter_cell(self, filename: Filename) -> bool:
154154
import IPython
155155

156156
if result := re.match(r"_ipython-input-([0-9]+)-.*", filename):
157-
cell_contents = IPython.get_ipython().history_manager.input_hist_raw[ # type: ignore[no-untyped-call,unused-ignore]
158-
int(result[1])
159-
]
157+
shell = IPython.get_ipython() # type: ignore[no-untyped-call,unused-ignore]
158+
# IPython >= 9.16 annotates get_ipython() as returning
159+
# Optional[InteractiveShell], and history_manager is itself
160+
# optional (absent when history is disabled). Without a live
161+
# shell there is no cell to recover, so fall through to the
162+
# ordinary filename rules rather than crashing.
163+
history = shell.history_manager if shell is not None else None
164+
if history is None:
165+
return False
166+
cell_contents = history.input_hist_raw[int(result[1])]
160167
with open(filename, "w+") as f:
161168
f.write(cell_contents)
162169
return True

0 commit comments

Comments
 (0)