Skip to content

Commit b42c1e3

Browse files
authored
Merge pull request #346 from krassowski/fix-ipython-overrides
Fix IPython overrides
2 parents e97324d + ff7b74d commit b42c1e3

File tree

5 files changed

+157
-12
lines changed

5 files changed

+157
-12
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
## CHANGELOG
22

3+
### `@krassowski/jupyterlab-lsp 2.0.3` (unreleased)
4+
5+
- bug fixes
6+
7+
- improve code overrides for IPython line magics ([#346])
8+
- implement missing code overrides for IPython's pinfo (`?`) and pinfo2 (`??`) syntactic sugar ([#346])
9+
10+
[#346]: https://github.com/krassowski/jupyterlab-lsp/issues/346
11+
312
### `@krassowski/jupyterlab-lsp 2.0.2` (2020-09-07)
413

514
- bug fixes

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ describe('rpy2 IPython overrides', () => {
7676
expect(reverse).to.equal(line);
7777
});
7878

79+
it('does not substitute magic-like constructs', () => {
80+
let line = 'print("%R -i x")';
81+
let override = line_magics.override_for(line);
82+
expect(override).to.equal(null);
83+
});
84+
7985
it('works with the long form arguments', () => {
8086
let line = '%R --input x';
8187
let override = line_magics.override_for(line);

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,18 @@ import {
66
rpy2_reverse_pattern,
77
rpy2_reverse_replacement
88
} from './rpy2';
9+
import { LINE_MAGIC_PREFIX } from '../ipython/overrides';
910

1011
export let overrides: IScopedCodeOverride[] = [
1112
{
1213
// support up to 10 arguments
13-
pattern: '%R' + rpy2_args_pattern(RPY2_MAX_ARGS) + '(.*)(\n)?',
14-
replacement: (match, ...args) => {
14+
pattern:
15+
LINE_MAGIC_PREFIX + '%R' + rpy2_args_pattern(RPY2_MAX_ARGS) + '(.*)(\n)?',
16+
replacement: (match, prefix, ...args) => {
1517
let r = parse_r_args(args, -4);
16-
return `${r.outputs}rpy2.ipython.rmagic.RMagics.R("${r.content}", "${r.others}"${r.inputs})`;
18+
// note: only supports assignment or -o/--output, not both
19+
// TODO assignment like in x = %R 1 should be distinguished from -o
20+
return `${prefix}${r.outputs}rpy2.ipython.rmagic.RMagics.R("${r.content}", "${r.others}"${r.inputs})`;
1721
},
1822
scope: 'line',
1923
reverse: {

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

Lines changed: 68 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,62 @@ 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+
});
169+
});
170+
171+
describe('IPython help line magics', () => {
172+
let line_magics_map = new ReversibleOverridesMap(
173+
overrides.filter(override => override.scope == 'line')
174+
);
175+
176+
it('overrides pinfo', () => {
177+
let override = line_magics_map.override_for('?int');
178+
expect(override).to.equal("get_ipython().run_line_magic('pinfo', 'int')");
179+
180+
let reverse = line_magics_map.reverse.override_for(override);
181+
expect(reverse).to.equal('?int');
182+
183+
override = line_magics_map.override_for('int?');
184+
expect(override).to.equal(
185+
"get_ipython().run_line_magic('pinfo', 'int')"
186+
);
187+
188+
reverse = line_magics_map.reverse.override_for(override);
189+
expect(reverse).to.equal('int?');
190+
});
191+
192+
it('overrides pinfo2', () => {
193+
let override = line_magics_map.override_for('??int');
194+
expect(override).to.equal(
195+
"get_ipython().run_line_magic('pinfo2', 'int')"
196+
);
197+
198+
let reverse = line_magics_map.reverse.override_for(override);
199+
expect(reverse).to.equal('??int');
200+
201+
override = line_magics_map.override_for('int??');
202+
expect(override).to.equal(
203+
"get_ipython().run_line_magic('pinfo2', 'int')"
204+
);
205+
206+
reverse = line_magics_map.reverse.override_for(override);
207+
expect(reverse).to.equal('int??');
208+
});
142209
});
143210
});

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

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,40 @@ 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+
* Look behind could be used to avoid capturing the group,
41+
* but at the time of writing support is only at 77%.
42+
*/
43+
export const LINE_MAGIC_PREFIX = '^(\\s*|\\s*\\S+\\s*=\\s*)';
44+
1945
export let overrides: IScopedCodeOverride[] = [
2046
/**
2147
* Line magics
2248
*/
2349
// filter out IPython line magics and shell assignments:
2450
// 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")", "")`.
3151
{
32-
pattern: '(^|\\s|=)!(\\S+)(.*)(\n)?',
52+
pattern: LINE_MAGIC_PREFIX + '!([^=\\s]+)(.*)(\n)?',
3353
replacement: '$1get_ipython().getoutput("$2$3")$4',
3454
scope: 'line',
3555
reverse: {
@@ -39,7 +59,46 @@ export let overrides: IScopedCodeOverride[] = [
3959
}
4060
},
4161
{
42-
pattern: '(^|\\s|=)%(\\S+)(.*)(\n)?',
62+
// note: assignments of pinfo/pinfo2 are not supported by IPython
63+
pattern: '^(\\s*)' + '(\\?{1,2})(\\S+)(\n)?',
64+
replacement: (match, prefix, marks, name, line_break) => {
65+
const cmd = marks == '?' ? 'pinfo' : 'pinfo2';
66+
line_break = line_break || '';
67+
// trick: use single quotes to distinguish
68+
return `${prefix}get_ipython().run_line_magic(\'${cmd}\', \'${name}\')${line_break}`;
69+
},
70+
scope: 'line',
71+
reverse: {
72+
pattern:
73+
"get_ipython\\(\\).run_line_magic\\('(pinfo2?)', '(.*?)'\\)(\n)?",
74+
replacement: (match, cmd, name) => {
75+
const marks = cmd == 'pinfo' ? '?' : '??';
76+
return `${marks}${name}`;
77+
},
78+
scope: 'line'
79+
}
80+
},
81+
{
82+
pattern: '^(\\s*)' + '([^\\?\\s]+)(\\?{1,2})(\n)?',
83+
replacement: (match, prefix, name, marks, line_break) => {
84+
const cmd = marks == '?' ? 'pinfo' : 'pinfo2';
85+
line_break = line_break || '';
86+
// trick: use two spaces to distinguish pinfo using suffix (int?) from the one using prefix (?int)
87+
return `${prefix}get_ipython().run_line_magic(\'${cmd}\', \'${name}\')${line_break}`;
88+
},
89+
scope: 'line',
90+
reverse: {
91+
pattern:
92+
"get_ipython\\(\\).run_line_magic\\('(pinfo2?)', '(.*?)'\\)(\n)?",
93+
replacement: (match, cmd, name) => {
94+
const marks = cmd == 'pinfo' ? '?' : '??';
95+
return `${name}${marks}`;
96+
},
97+
scope: 'line'
98+
}
99+
},
100+
{
101+
pattern: LINE_MAGIC_PREFIX + '%(\\S+)(.*)(\n)?',
43102
replacement: (match, prefix, name, args, line_break) => {
44103
args = empty_or_escaped(args);
45104
line_break = line_break || '';

0 commit comments

Comments
 (0)