from fastcore.test import *
from basedpl import Array
from IPython.utils.capture import capture_output
import numpy as npaplnb core
aplnb adds %apl and %%apl magics to IPython. A basedpl.Session owns the APL workspace, native values, errors and interruption. aplnb renders the captured output and loads the language bar.
The line magic returns a native array or function. The cell magic displays APL session output, including implicit expression results. Both use the same session.
Rendering output
AplOut
def AplOut(
*args, **kwargs
):APL output text, displayed in the SAX2 font where HTML is available.
AplOut preserves APL spacing. Its HTML representation escapes the output before putting it in a <pre> element.
out = AplOut('1 < 2')
assert '<' in out._repr_html_()
out1 < 2
The magics
APLMagic accepts an existing basedpl.Session. Without one, it starts a session on first use. The language bar and SAX2 stylesheet are loaded once for each magic instance.
APLMagic
def APLMagic(
session:NoneType=None
):IPython APL magics sharing a lazily started bAsedPL session.
APLMagic.apl
def apl(
line, cell:NoneType=None
):Evaluate a line as a native value or display a cell as an APL session.
Cell output comes from Session.run, which includes implicit expression results. Line output comes from Session.eval, which captures explicit output and returns native values. A trailing ; suppresses all cell display without skipping execution. Output produced before an error is displayed before the error is raised.
Tab completes user and system names in APL input. Completion reads the existing session without starting or evaluating it.
APLMagic.complete
def complete(
context
):Complete visible APL names in line and cell magics.
create_magic
def create_magic(
shell:NoneType=None, session:NoneType=None
):Register line and cell magics with shell, optionally sharing a bAsedPL session.
The cell magic displays each expression. Assignments remain silent.
%%apl
m←2 3⍴⍳6
m×1010 20 30 40 50 60
The line magic returns a native basedpl.Array. Use .np or .py for conversion. Comments are parsed by bAsedPL, including ⍝ inside quoted strings.
z = %apl m ⍝ a matrix
test_is(type(z), Array)
np.testing.assert_array_equal(z.np, [[1, 2, 3], [4, 5, 6]])
test_eq(magic.apl("'a⍝b' ⍝ comment").py, 'a⍝b')
z[[1., 2., 3.], [4., 5., 6.]]
Python values can be bound through the same session. Python integers stay exact.
magic.session(x=np.arange(1, 4))
value = %apl +/x
test_eq(value.py, 6)
test_is(type(value.py), int)
valueArray(6)
bAsedPL’s display commands also work in a cell.
%%apl
]Display (1 2)'ab'┌→───────────┐ │ ┌→──┐ ┌→─┐ │ │ │1 2│ │ab│ │ │ └~──┘ └──┘ │ └∊───────────┘
A suppressed cell still updates the workspace.
%%apl
quiet←7;Names and help
Use ]help name for help and ]help name -source for source, in either magic. Leading definition comments supply help. Inspection leaves the function uncalled.
%%apl
double←{⍝ Double the argument
⎕←'called' ⋄ ⍵×2}with capture_output() as cap: magic.apl(']help double')
assert 'Double the argument' in cap.outputs[0].data['text/markdown']
with capture_output() as cap: magic.apl('', ']help double -source')
assert "⎕←'called'" in cap.outputs[0].data['text/markdown']
test_eq(len(cap.outputs), 1)Completion follows the magic’s workspace, including names defined after registration. Quoted text and comments stay literal.
from IPython.core.completer import provisionalcompletershell = get_ipython()
for text, expected in [('%apl dou', 'double'), ('v = %apl dou', 'double'), ('%%apl\n1+•sr', '•src')]:
with provisionalcompleter(): matches = list(shell.Completer.completions(text, len(text)))
assert expected in [m.text for m in matches]
for text in ["%%apl\n'dou", '%%apl\n⍝ dou', 'ordinary_python']:
with provisionalcompleter(): matches = list(shell.Completer.completions(text, len(text)))
assert not any(m.type == 'APL name' for m in matches)Errors and session state
Errors retain source locations and leave completed assignments in the workspace. Incomplete input raises a syntax error without replacing the session.
with capture_output() as cap: test_fail(lambda: magic.apl('', '⎕←42 ⋄ 1÷0'), contains='DOMAIN ERROR')
assert '42' in cap.outputs[0].data['text/html']
test_fail(lambda: magic.apl('', '{⍵+1'), contains='SYNTAX ERROR')
test_eq(magic.apl('quiet').py, 9)Set magic.session.timeout to a per-evaluation deadline in seconds. Ctrl-C interrupts the evaluation. Output captured before cooperative cancellation is displayed. The Rust worker thread retains the session after cancellation. Native-library calls and individual BigInt operations can delay cancellation; the thread is never forcibly killed.
Installation
%load_ext aplnb registers the magics in the current kernel. aplnb_install adds the extension to the default IPython profile. Registration does not start an APL worker.
load_ipython_extension
def load_ipython_extension(
ipython
):Register the APL magics when IPython loads this extension.
Symbol input
The browser input method uses bAsedPL’s symbol names and aliases. It is active in %%apl cells, %apl lines and Monaco editors configured for APL. Matching prefers exact names, then prefixes, then abbreviations. A unique match is accepted with Tab or a delimiter. Enter accepts it before the notebook’s usual newline or execution action. Ambiguous matches remain unchanged and appear in a clickable list.
Strings, comments and pasted text are not expanded. Cursor movement and Escape cancel automatic expansion. The clickable language bar can still insert literal glyphs.
Hold left Alt/Option to type glyphs using bAsedPL’s keyboard: Alt-h ←, Alt-minus ×, Alt-equals ÷, Alt-Shift-a ⍶. These chords insert literal glyphs, including inside APL strings and comments. Right Option keeps its native keyboard behavior. The glyph map is shared with the bAsedPL REPL.
The JavaScript assertions live in tests/input.js. Pytest runs them through fastcdp’s QUnit runner using bAsedPL’s actual symbol catalogue. Editor integration checks exercise CodeMirror, Monaco and textarea. Install the dev extra and Chrome for Testing (fastcdp-setup --install stable), then run pytest -q. QUnit and the editor fixtures load from CDNs. Node is not required.
Cleanup
magic.session.close()