Skip to content

Commit ed47107

Browse files
committed
Have builin-lazy magics have a lower priority than registered on.
Otherwise we break people overriding magics closes #15380
1 parent 19f9ae0 commit ed47107

2 files changed

Lines changed: 65 additions & 2 deletions

File tree

‎IPython/core/magic.py‎

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,9 @@ def register_lazy(
564564
self.lazy_magics[name] = fully_qualified_name
565565
kinds = magic_kinds if magic_kind == "line_cell" else (magic_kind,)
566566
for kind in kinds:
567+
existing = self.magics[kind].get(name)
568+
if existing is not None and not isinstance(existing, LazyMagic):
569+
continue
567570
self.magics[kind][name] = LazyMagic(self, fully_qualified_name, kind, name)
568571

569572
def load_lazy(self, magic_name: str) -> None:
@@ -590,7 +593,10 @@ def load_lazy(self, magic_name: str) -> None:
590593
if sep:
591594
from importlib import import_module
592595

593-
self.register(getattr(import_module(module_name), class_name))
596+
self._register(
597+
(getattr(import_module(module_name), class_name),),
598+
lazy_spec=spec,
599+
)
594600
else:
595601
assert self.shell is not None
596602
self.shell.run_line_magic("load_ext", spec)
@@ -645,6 +651,22 @@ def register(self, *magic_objects: type[Magics] | Magics) -> None:
645651
----------
646652
*magic_objects : one or more classes or instances
647653
"""
654+
self._register(magic_objects, lazy_spec=None)
655+
656+
def _register(
657+
self,
658+
magic_objects: tuple[type[Magics] | Magics, ...],
659+
lazy_spec: str | None,
660+
) -> None:
661+
"""Back end of :meth:`register`.
662+
663+
`lazy_spec` is the spec being resolved when this registration is the
664+
result of a lazy load, and None when the caller asked for it
665+
explicitly. A lazily loaded class only fills in the names it is still
666+
the declared provider of: a magic somebody registered for real -- as
667+
IPykernel does with ``%edit`` -- outranks a declaration, whichever
668+
happens to be loaded last.
669+
"""
648670
# Start by validating them to ensure they have all had their magic
649671
# methods registered at the instance level
650672
for m in magic_objects:
@@ -661,7 +683,16 @@ def register(self, *magic_objects: type[Magics] | Magics) -> None:
661683
# table of callables
662684
self.registry[m.__class__.__name__] = m
663685
for mtype in magic_kinds:
664-
self.magics[mtype].update(m.magics[mtype])
686+
table = self.magics[mtype]
687+
for magic_name, func in m.magics[mtype].items():
688+
if lazy_spec is not None:
689+
existing = table.get(magic_name)
690+
if existing is not None and not (
691+
isinstance(existing, LazyMagic)
692+
and existing.spec == lazy_spec
693+
):
694+
continue
695+
table[magic_name] = func
665696

666697
def register_function(
667698
self,

‎tests/test_magic.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1946,6 +1946,38 @@ def test_lazy_magics():
19461946
ip.run_line_magic("lazy_line", "")
19471947

19481948

1949+
def test_registered_magic_beats_lazy_one():
1950+
"""A magic registered for real outranks one that is only declared.
1951+
1952+
IPykernel overrides core magics such as ``%edit`` by registering its own
1953+
class at shell init; loading the class that lazily declares them (for any
1954+
of the other magics it provides) must not take the override back out.
1955+
See https://github.com/ipython/ipython/issues/15380.
1956+
"""
1957+
1958+
@magics_class
1959+
class OverridingMagics(Magics):
1960+
@line_magic
1961+
def edit(self, line):
1962+
print("Overridden Edit")
1963+
1964+
mm = ip.magics_manager
1965+
original = mm.magics["line"]["edit"]
1966+
try:
1967+
mm.register(OverridingMagics)
1968+
# `%load` comes from the same class as the core `%edit`.
1969+
mm.load_lazy("load")
1970+
assert mm.find("line", "edit").__self__.__class__ is OverridingMagics
1971+
# Neither does re-declaring the core one shadow it.
1972+
mm.register_lazy("edit", "IPython.core.magics.code:CodeMagics", "line")
1973+
assert mm.find("line", "edit").__self__.__class__ is OverridingMagics
1974+
mm.load_all_lazy_magics()
1975+
assert mm.find("line", "edit").__self__.__class__ is OverridingMagics
1976+
finally:
1977+
mm.magics["line"]["edit"] = original
1978+
mm.registry.pop("OverridingMagics", None)
1979+
1980+
19491981
TEST_MODULE = """
19501982
print('Loaded my_tmp')
19511983
if __name__ == "__main__":

0 commit comments

Comments
 (0)