8000 Upgrade the Colab runtime to IPython 8.39.0 (from 7.34.0), with traitlets by copybara-service[bot] · Pull Request #6088 · googlecolab/colabtools · GitHub
[go: up one dir, main page]

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions google/colab/_completion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Copyright 2026 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Helpers for shaping `complete_reply` payloads.

Kept free of IPython and ipykernel imports so it can be unit tested against an
interpreter that does not provide the IPython 8.x-specific modules.
"""

import re

# The dotted expression immediately preceding the cursor, e.g. the `a.b` in
# `print(a.b.pre`. Used to restore fully-qualified attribute completions.
_DOTTED_PREFIX = re.compile(
r'[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$'
)

# ipykernel's `complete_reply` metadata key for IPython `Completion` entries.
JUPYTER_EXPERIMENTAL_KEY = '_jupyter_types_experimental'


def requalify_attribute_matches(code, reply):
"""Rewrites relative attribute completions back to fully-qualified names.

IPython 8.x returns `Completion` objects: for `s.cap` the
match text is `.capitalize`, replacing `code[cursor_start:cursor_end]` where
`cursor_start` is the offset of the `.`. Pre-8.x returned `s.capitalize` with
`cursor_start` at the start of the whole expression.

Colab's contract with the frontend, and `compute_completion_metadata`'s
`object_inspect` lookups, both assume the fully-qualified form: `.capitalize`
resolves to nothing, so the completion popup silently loses its type and
signature detail. This restores the pre-8.x shape.

Only applies when the reply is unambiguously an attribute completion (every
match begins with `.`), so other matchers (keyword arguments, dict keys,
magics, file paths) are left untouched.

Args:
code: The full code buffer that was completed.
reply: A `do_complete` reply dict. Mutated in place.

Returns:
The same `reply` dict.
"""
matches = reply.get('matches')
cursor_start = reply.get('cursor_start')
if not matches or cursor_start is None:
return reply
if not all(match.startswith('.') for match in matches):
return reply

prefix_match = _DOTTED_PREFIX.search(code[:cursor_start])
if not prefix_match:
return reply
prefix = prefix_match.group()

reply['matches'] = [prefix + match for match in matches]
reply['cursor_start'] = cursor_start - len(prefix)

# Keep ipykernel's parallel `complete_reply` metadata consistent with the
# rewrite; otherwise its offsets disagree with the ones just widened.
experimental = reply.get('metadata', {}).get(JUPYTER_EXPERIMENTAL_KEY)
for entry in experimental or ():
if entry.get('text', '').startswith('.'):
entry['text'] = prefix + entry['text']
entry['start'] = reply['cursor_start']

return reply
9 changes: 7 additions & 2 deletions google/colab/_debugpy_repr.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,23 @@
# Fork of
# debugpy/third_party/pydevd/_pydevd_bundle/pydevd_suspended_frames.py
# to add shape to custom types.
def _get_var_data(self, fmt=None):
def _get_var_data(self, fmt=None, context=None, **safe_repr_custom_attrs):
"""Gets the debug adapter protocol variable representation.

Args:
self: The AbstractVariable
fmt: Optional formatting params from the request.
context: Optional context in which the variable is evaluated (e.g. 'repl',
'hover'). Added, along with `safe_repr_custom_attrs`, in debugpy 1.8.x.
**safe_repr_custom_attrs: Additional keyword args.

Returns:
Dict representing the debug adapter protocol variable.
"""

var_data = _original_get_var_data(self, fmt)
var_data = _original_get_var_data(
self, fmt, context, **safe_repr_custom_attrs
)
shape = get_shape(self.value)
if shape:
presentation_hint = var_data.get('presentationHint', {})
Expand Down
4 changes: 3 additions & 1 deletion google/colab/_import_hooks/_bokeh.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ def _register_hook():
sys.meta_path = [_BokehImportHook()] + sys.meta_path


def _post_execute():
def _post_execute(*args, **kwargs):
# IPython 8.x dropped backcall from EventManager.register; accept args here.
del args, kwargs
global _bokeh_loaded_in_this_cell
_bokeh_loaded_in_this_cell = False
IPython.get_ipython().events.unregister('post_run_cell', _post_execute) # pylint: disable=undefined-variable
Expand Down
35 changes: 30 additions & 5 deletions google/colab/_inspector.py
BD0
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,28 @@
_UNAVAILABLE_MODULE_NAME = '<unknown>'


def _ipython_info_fields():
"""Returns the field names IPython's own inspector guarantees are present.

`oinspect.Inspector._make_info_unformatted` and `_append_info_field` subscript
the info dict directly (`info['isclass']`, `field = info[key]`), so any field
IPython knows about must exist on the dict `ColabInspector.info` returns, even
when Colab has no value for it.

Returns:
A tuple of field names.
"""
# IPython 8.x moved the list into an `InfoDict` TypedDict and made the
# module-level `info_fields` alias emit a DeprecationWarning.
info_dict = getattr(oinspect, 'InfoDict', None)
if info_dict is not None:
return tuple(info_dict.__annotations__)
return tuple(oinspect.info_fields)


_IPYTHON_INFO_FIELDS = _ipython_info_fields()


def getdoc(obj):
"""Custom wrapper for inspect.getdoc.

Expand Down Expand Up @@ -528,18 +550,19 @@ def info(self, obj, formatter=None, info=None, detail_level=0):
# we're safe ignoring them.

obj_type = type(obj)
out = {
out = dict.fromkeys(_IPYTHON_INFO_FIELDS)
out.update({
'name': oname,
'found': True,
'is_class': inspect.isclass(obj),
'isclass': inspect.isclass(obj),
'string_form': None,
# Fill in empty values.
'docstring': None,
'file': None,
'isalias': False,
'ismagic': info.ismagic if info else False,
'namespace': info.namespace if info else '',
}
})
if detail_level >= self.str_detail_level:
out['string_form'] = _safe_repr(obj)

Expand Down Expand Up @@ -571,7 +594,7 @@ def info(self, obj, formatter=None, info=None, detail_level=0):
source = _getsource(obj.__class__)
if source is not None:
out['source'] = source
if 'source' not in out:
if out.get('source') is None:
formatter = formatter or (lambda x: x)
docstring = formatter(getdoc(obj) or '<no docstring>')
if docstring:
Expand Down Expand Up @@ -626,7 +649,9 @@ def info(self, obj, formatter=None, info=None, detail_level=0):

out['argspec'] = _getargspec_dict(obj)

return oinspect.object_info(**out)
# We return `out` directly: as of IPython 8.39 `oinspect.object_info(**out)`
# keeps only its explicit name/found/isclass/isalias/ismagic params.
return out


def _iscallable(obj):
Expand Down
20 changes: 19 additions & 1 deletion google/colab/_kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
# limitations under the License.
"""Colab-specific kernel customizations."""

import inspect
import time

from google.colab import _completion
from google.colab import _shell
from google.colab import _shell_customizations
from ipykernel import ipkernel
Expand All @@ -28,6 +30,16 @@ class Kernel(ipkernel.IPythonKernel):
def _shell_class_default(self):
return _shell.Shell

def __init__(self, **kwargs):
super().__init__(**kwargs)
# ipykernel constructs the shell with an explicit
# `compiler_class=XCachingCompiler` argument, which takes precedence over
# any default the shell class declares for that trait. Swap the compiler in
# after the shell exists so that Colab's traceback frame headers keep
# naming the cell file. See CellPathCompiler for why that matters.
self.shell.compiler_class = _shell.CellPathCompiler
self.shell.compile = self.shell.compiler_class()

def do_inspect(self, code, cursor_pos, detail_level=0, *args, **kwargs):
start_time = time.perf_counter_ns()
name = tokenutil.token_at_cursor(code, cursor_pos)
Expand Down Expand Up @@ -58,7 +70,7 @@ def do_inspect(self, code, cursor_pos, detail_level=0, *args, **kwargs):

return reply_content

def complete_request(self, stream, ident, parent):
async def complete_request(self, stream, ident, parent):
"""Colab-specific complete_request handler.

Overrides the default to allow providing additional metadata in the
Expand All @@ -75,6 +87,12 @@ def complete_request(self, stream, ident, parent):
cursor_pos = content['cursor_pos']

matches = self.do_complete(code, cursor_pos)
# `do_complete` is sync in ipykernel today, but the base handler awaits
# it if it is awaitable. Mirror that so a future async implementation
# keeps working.
if inspect.isawaitable(matches):
matches = await matches
matches = _completion.requalify_attribute_matches(code, matches)
if (
parent.get('metadata', {})
.get('colab_options', {})
Expand Down
67 changes: 37 additions & 30 deletions google/colab/_shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,18 @@
import datetime
import os
import sys
import traceback

from google.colab import _history
from google.colab import _inspector
from google.colab import _pip
from google.colab import _shell_customizations
from google.colab import _system_commands
from ipykernel import compiler
from ipykernel import jsonutil
from ipykernel import zmqshell
from IPython.core import events
from IPython.core import interactiveshell
from IPython.core import oinspect
from IPython.utils import PyColorize
from ipython_genutils import py3compat

# Python doesn't expose a name in builtins for a getset descriptor attached to a
# python class implemented in C, eg an entry in this array:
Expand All @@ -46,13 +44,22 @@ def _show_pip_warning():
return os.environ.get('SKIP_COLAB_PIP_WARNING', '0') == '0'


class CellPathCompiler(compiler.XCachingCompiler):
"""Caching compiler that keeps cell filenames in traceback frame headers.

Subclasses ipykernel's compiler rather than IPython's so that cells keep
being compiled to `/tmp/ipykernel_<pid>/<hash>.py`; that filename is what
carries the hash the frame header needs to expose.
"""

def format_code_name(self, name):
del name # Unused; opts out of IPython 8.x's `Cell In[N]` label.
return None


class Shell(zmqshell.ZMQInteractiveShell):
"""Shell with additional Colab-specific features."""

def init_events(self):
self.events = events.EventManager(self, events.available_events)
self.events.register('pre_execute', self._clear_warning_registry)

def init_inspector(self):
"""Initialize colab's custom inspector."""
self.inspector = _inspector.ColabInspector(
Expand Down Expand Up @@ -127,8 +134,8 @@ def _showtraceback(self, etype, evalue, stb):

exc_content = {
'traceback': stb,
'ename': py3compat.unicode_type(etype.__name__),
'evalue': py3compat.safe_unicode(evalue),
'ename': str(etype.__name__),
'evalue': str(evalue),
}

if error_details:
Expand Down Expand Up @@ -186,32 +193,32 @@ def _getattr_property(obj, attrname):
return getattr(obj, attrname)

def object_inspect(self, oname, detail_level=0):
info = self._ofind(oname)
"""Returns information about the named object.

if info['found']:
Differs from the base implementation only in degrading a raising inspection
into a not-found result rather than propagating out of the kernel handler.

Args:
oname: name of the object to inspect.
detail_level: 0 or 1; 1 means "include more detail".

Returns:
A dict with information about the named object, as built by
`_inspector.ColabInspector.info`.
"""
with self.builtin_trap:
info = self._object_find(oname)
if not info.found:
return oinspect.object_info(name=oname, found=False)
try:
info = self._object_find(oname)
# We need to avoid arbitrary python objects remaining in info (and
# potentially being serialized below); `obj` itself needs to be
# removed, but retained for use below, and `parent` isn't used at all.
obj = info.pop('obj', '')
info.pop('parent', '')
result = self.inspector.info(
obj, oname, info=info, detail_level=detail_level
return self.inspector.info(
info.obj, oname, info=info, detail_level=detail_level
)
except Exception as e: # pylint: disable=broad-except
except Exception: # pylint: disable=broad-except
self.kernel.log.info(
'Exception caught during object inspection: '
'{!r}\nTraceback:\n{}'.format(
e, ''.join(traceback.format_tb(sys.exc_info()[2]))
)
'Exception caught during object inspection', exc_info=True
)
result = oinspect.InfoDict()
else:
result = super(Shell, self).object_inspect(
oname, detail_level=detail_level
)
return result
return oinspect.object_info(name=oname, found=False)

def run_cell_magic(self, magic_name, line, cell):
# We diverge from Jupyter behavior here: we want to allow cell magics with a
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
# Note: these dependency versions should be kept in-sync with the versions
# specified in the docker container requirements files.
'google-auth==2.49.0',
'ipykernel==6.17.1',
'ipykernel==6.21.3',
'ipyparallel==8.8.0',
'ipython==7.34.0',
'ipython==8.39.0',
'pandas==2.2.3',
'jupyter-server==2.20.0',
'portpicker==1.5.2',
Expand Down
Loading
0