Skip to content

Commit 9d1b9bd

Browse files
committed
Fix IPython shell and line magic overrides #343
1 parent 9c56a02 commit 9d1b9bd

File tree

2 files changed

+53
-9
lines changed

2 files changed

+53
-9
lines changed

packages/jupyterlab-lsp/src/transclusions/ipython/overrides.spec.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,16 +104,30 @@ describe('Default IPython overrides', () => {
104104
expect(reverse).to.equal(LINE_MAGIC_WITH_SPACE);
105105
});
106106

107-
it('overrides x =%ls', () => {
107+
it('overrides x =%ls and x = %ls', () => {
108108
// this is a corner-case as described in
109109
// https://github.com/krassowski/jupyterlab-lsp/issues/281#issuecomment-645286076
110110
let override = line_magics_map.override_for('x =%ls');
111111
expect(override).to.equal('x =get_ipython().run_line_magic("ls", "")');
112+
113+
override = line_magics_map.override_for('x = %ls');
114+
expect(override).to.equal('x = get_ipython().run_line_magic("ls", "")');
112115
});
113116

114117
it('does not override line-magic-like constructs', () => {
115118
let override = line_magics_map.override_for('list("%")');
116119
expect(override).to.equal(null);
120+
121+
override = line_magics_map.override_for('list(" %test")');
122+
expect(override).to.equal(null);
123+
});
124+
125+
it('does not override modulo operators', () => {
126+
let override = line_magics_map.override_for('3 % 2');
127+
expect(override).to.equal(null);
128+
129+
override = line_magics_map.override_for('3%2');
130+
expect(override).to.equal(null);
117131
});
118132

119133
it('escapes arguments', () => {
@@ -135,9 +149,22 @@ describe('Default IPython overrides', () => {
135149
expect(reverse).to.equal('!ls -o');
136150
});
137151

152+
it('overrides shell commands assignments: x =!ls and x = !ls', () => {
153+
let override = line_magics_map.override_for('x =!ls');
154+
expect(override).to.equal('x =get_ipython().getoutput("ls")');
155+
156+
override = line_magics_map.override_for('x = !ls');
157+
expect(override).to.equal('x = get_ipython().getoutput("ls")');
158+
});
159+
138160
it('does not override shell-like constructs', () => {
139161
let override = line_magics_map.override_for('"!ls"');
140162
expect(override).to.equal(null);
141163
});
164+
165+
it('does not override != comparisons', () => {
166+
let override = line_magics_map.override_for('1 != None');
167+
expect(override).to.equal(null);
168+
})
142169
});
143170
});

packages/jupyterlab-lsp/src/transclusions/ipython/overrides.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,37 @@ function empty_or_escaped(x: string) {
1616
}
1717
}
1818

19+
/**
20+
* Line magics do not have to start with the new line, for example:
21+
* x = !ls
22+
* x = %ls
23+
* x =%ls
24+
* are all valid.
25+
*
26+
* The percent may also appear in strings, e.g. ls('%').
27+
*
28+
* IPython allows magics on start of a line or in assignments (but only there!), thus:
29+
* x = (!ls)
30+
* is invalid syntax!
31+
*
32+
* Therefore we can require that the match starts with either:
33+
* - zero or more whitespaces right after the beginning of a line, or
34+
* - variable then equals (with optional whitespaces)
35+
*
36+
* This will not always work: e.g.:
37+
* x['a = !ls'] = !ls
38+
* is perfectly valid IPython, but regular expressions cannot help here.
39+
*/
40+
export const LINE_MAGIC_PREFIX = '^(\\s*|\\s*\\S+\\s*=\\s*)'
41+
1942
export let overrides: IScopedCodeOverride[] = [
2043
/**
2144
* Line magics
2245
*/
2346
// filter out IPython line magics and shell assignments:
2447
// keep the content, keep magic/command name and new line at the end
25-
26-
// note magics do not have to start with the new line, for example x = !ls or x = %ls are both valid.
27-
// x =%ls is also valid. However, percent may also appear in strings, e.g. ls('%').
28-
// Hence: (^|\\s|=) for shell commands (!) and line magics (%); see issue #281.
29-
// This does not solve all issues, for example `list(" %ls")` still leads to:
30-
// `list(" get_ipython().run_line_magic("ls")", "")`.
3148
{
32-
pattern: '(^|\\s|=)!(\\S+)(.*)(\n)?',
49+
pattern: LINE_MAGIC_PREFIX + '!([^=\\s]+)(.*)(\n)?',
3350
replacement: '$1get_ipython().getoutput("$2$3")$4',
3451
scope: 'line',
3552
reverse: {
@@ -39,7 +56,7 @@ export let overrides: IScopedCodeOverride[] = [
3956
}
4057
},
4158
{
42-
pattern: '(^|\\s|=)%(\\S+)(.*)(\n)?',
59+
pattern: LINE_MAGIC_PREFIX + '%(\\S+)(.*)(\n)?',
4360
replacement: (match, prefix, name, args, line_break) => {
4461
args = empty_or_escaped(args);
4562
line_break = line_break || '';

0 commit comments

Comments
 (0)