Skip to content

Commit 5a21fe6

Browse files
authored
Merge pull request #148 from krassowski/fix/r-magics
Enhanced argument parsing in rpy2 magics
2 parents c294c2c + 0b766fa commit 5a21fe6

File tree

8 files changed

+180
-13
lines changed

8 files changed

+180
-13
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
## `@krassowski/jupyterlab-lsp 0.7.0-rc.0`
1010

1111
- features
12+
1213
- reduced space taken up by the statusbar indicator
1314
- implemented statusbar popover with connections statuses
1415
- generates types for server data responses from JSON schema (
@@ -32,6 +33,12 @@
3233
- bash LSP now also covers `%%bash` magic cell in addition to `%%sh` (
3334
[#144](https://github.com/krassowski/jupyterlab-lsp/pull/144)
3435
)
36+
- rpy2 magics received enhanced support for argument parsing
37+
in both parent Python document (re-written overrides) and
38+
exctracted R documents (improved foreign code extractor) (
39+
[#148](https://github.com/krassowski/jupyterlab-lsp/pull/148)
40+
)
41+
3542
- bugfixes
3643
- diagnostics in foreign documents are now correctly updated (
3744
[133fd3d](https://github.com/krassowski/jupyterlab-lsp/pull/129/commits/133fd3d71401c7e5affc0a8637ee157de65bef62)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { IVirtualDocumentBlock, VirtualDocument } from '../virtual/document';
2+
import { expect } from 'chai';
3+
import { foreign_code_extractors } from './defaults';
4+
import { CodeEditor } from '@jupyterlab/codeeditor';
5+
6+
function wrap_in_python_lines(line: string) {
7+
return 'print("some code before")\n' + line + '\n' + 'print("and after")\n';
8+
}
9+
10+
describe('Default extractors', () => {
11+
let document: VirtualDocument;
12+
13+
function extract(code: string) {
14+
return document.extract_foreign_code(code, null, {
15+
line: 0,
16+
column: 0
17+
});
18+
}
19+
20+
function get_the_only_virtual(
21+
foreign_document_map: Map<CodeEditor.IRange, IVirtualDocumentBlock>
22+
) {
23+
expect(foreign_document_map.size).to.equal(1);
24+
25+
let { virtual_document } = foreign_document_map.get(
26+
foreign_document_map.keys().next().value
27+
);
28+
29+
return virtual_document;
30+
}
31+
32+
beforeEach(() => {
33+
document = new VirtualDocument(
34+
'python',
35+
'test.ipynb',
36+
{},
37+
foreign_code_extractors,
38+
false,
39+
'py',
40+
false
41+
);
42+
});
43+
44+
afterEach(() => {
45+
document.clear();
46+
});
47+
48+
describe('%R line magic', () => {
49+
it('extracts simple commands', () => {
50+
let code = wrap_in_python_lines('%R ggplot()');
51+
let { cell_code_kept, foreign_document_map } = extract(code);
52+
53+
// should not be removed, but left for the static analysis (using magic overrides)
54+
expect(cell_code_kept).to.equal(code);
55+
let r_document = get_the_only_virtual(foreign_document_map);
56+
expect(r_document.language).to.equal('r');
57+
expect(r_document.value).to.equal('ggplot()\n');
58+
});
59+
60+
it('parses input (into a dummy data frame)', () => {
61+
let code = wrap_in_python_lines('%R -i df ggplot(df)');
62+
let { foreign_document_map } = extract(code);
63+
64+
let r_document = get_the_only_virtual(foreign_document_map);
65+
expect(r_document.language).to.equal('r');
66+
expect(r_document.value).to.equal('df <- data.frame()\nggplot(df)\n');
67+
});
68+
69+
it('parses multiple inputs (into dummy data frames)', () => {
70+
let code = wrap_in_python_lines('%R -i df -i x ggplot(df)');
71+
let r_document = get_the_only_virtual(extract(code).foreign_document_map);
72+
expect(r_document.value).to.equal(
73+
'df <- data.frame()\n' + 'x <- data.frame()\n' + 'ggplot(df)\n'
74+
);
75+
});
76+
77+
it('parses inputs ignoring other arguments', () => {
78+
let code = wrap_in_python_lines('%R -i df --width 300 -o x ggplot(df)');
79+
let r_document = get_the_only_virtual(extract(code).foreign_document_map);
80+
expect(r_document.value).to.equal(
81+
'df <- data.frame()\n' + 'ggplot(df)\n'
82+
);
83+
});
84+
});
85+
86+
describe('%%R cell magic', () => {
87+
it('extracts simple commands', () => {
88+
let code = '%%R\nggplot()';
89+
let { cell_code_kept, foreign_document_map } = extract(code);
90+
91+
expect(cell_code_kept).to.equal(code);
92+
let r_document = get_the_only_virtual(foreign_document_map);
93+
expect(r_document.language).to.equal('r');
94+
expect(r_document.value).to.equal('ggplot()\n');
95+
});
96+
});
97+
98+
it('parses input (into a dummy data frame)', () => {
99+
let code = '%%R -i df\nggplot(df)';
100+
let { foreign_document_map } = extract(code);
101+
102+
let r_document = get_the_only_virtual(foreign_document_map);
103+
expect(r_document.language).to.equal('r');
104+
expect(r_document.value).to.equal('df <- data.frame()\nggplot(df)\n');
105+
});
106+
});

packages/jupyterlab-lsp/src/extractors/defaults.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
import { IForeignCodeExtractorsRegistry } from './types';
22
import { RegExpForeignCodeExtractor } from './regexp';
3+
import {
4+
extract_r_args,
5+
rpy2_args_pattern,
6+
RPY2_MAX_ARGS
7+
} from '../magics/rpy2';
8+
9+
function rpy2_replacer(match: string, ...args: string[]) {
10+
let r = extract_r_args(args, -3);
11+
// define dummy input variables using empty data frames
12+
let inputs = r.inputs.map(i => i + ' <- data.frame()').join('\n');
13+
if (inputs !== '') {
14+
inputs += '\n';
15+
}
16+
return `${inputs}${r.rest}`;
17+
}
318

419
// TODO: make the regex code extractors configurable
520
export let foreign_code_extractors: IForeignCodeExtractorsRegistry = {
@@ -10,15 +25,15 @@ export let foreign_code_extractors: IForeignCodeExtractorsRegistry = {
1025
//
1126
new RegExpForeignCodeExtractor({
1227
language: 'r',
13-
pattern: '^%%R( .*?)?\n([^]*)',
14-
extract_to_foreign: '$2',
28+
pattern: '^%%R' + rpy2_args_pattern(RPY2_MAX_ARGS) + '\n([^]*)',
29+
extract_to_foreign: rpy2_replacer,
1530
is_standalone: false,
1631
file_extension: 'R'
1732
}),
1833
new RegExpForeignCodeExtractor({
1934
language: 'r',
20-
pattern: '(^|\n)%R (.*)\n?',
21-
extract_to_foreign: '$2',
35+
pattern: '(?:^|\n)%R' + rpy2_args_pattern(RPY2_MAX_ARGS) + ' (.*)\n?',
36+
extract_to_foreign: rpy2_replacer,
2237
is_standalone: false,
2338
file_extension: 'R'
2439
}),

packages/jupyterlab-lsp/src/extractors/regexp.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { IExtractedCode, IForeignCodeExtractor } from './types';
22
import { position_at_offset } from '../positioning';
3+
import { replacer } from '../magics/overrides';
34

45
export class RegExpForeignCodeExtractor implements IForeignCodeExtractor {
56
options: RegExpForeignCodeExtractor.IOptions;
@@ -39,6 +40,7 @@ export class RegExpForeignCodeExtractor implements IForeignCodeExtractor {
3940
let matched_string = match[0];
4041
let foreign_code_fragment = matched_string.replace(
4142
this.expression,
43+
// @ts-ignore
4244
this.options.extract_to_foreign
4345
);
4446

@@ -110,7 +112,7 @@ namespace RegExpForeignCodeExtractor {
110112
* for the use in virtual document of the foreign language.
111113
* For the R example this should be '$3'
112114
*/
113-
extract_to_foreign: string;
115+
extract_to_foreign: string | replacer;
114116
/**
115117
* String boolean if everything (true, default) or nothing (false) should be kept in the host document.
116118
*

packages/jupyterlab-lsp/src/magics/defaults.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { IOverridesRegistry } from './overrides';
22
import {
33
parse_r_args,
4+
rpy2_args_pattern,
5+
RPY2_MAX_ARGS,
46
rpy2_reverse_pattern,
57
rpy2_reverse_replacement
68
} from './rpy2';
@@ -46,7 +48,7 @@ export const language_specific_overrides: IOverridesRegistry = {
4648
},
4749
{
4850
// support up to 10 arguments
49-
pattern: '%R' + '(?: -(\\S+) (\\S+))?'.repeat(10) + '(.*)(\n)?',
51+
pattern: '%R' + rpy2_args_pattern(RPY2_MAX_ARGS) + '(.*)(\n)?',
5052
replacement: (match, ...args) => {
5153
let r = parse_r_args(args, -4);
5254
return `${r.outputs}rpy2.ipython.rmagic.RMagics.R("${r.content}", "${r.others}"${r.inputs})`;
@@ -81,7 +83,7 @@ export const language_specific_overrides: IOverridesRegistry = {
8183
// rpy2 extension R magic - this should likely go as an example to the user documentation, rather than being a default
8284
// only handles simple one input - one output case
8385
{
84-
pattern: '^%%R' + '(?: -(\\S) (\\S+))?'.repeat(10) + '(\n)?([\\s\\S]*)',
86+
pattern: '^%%R' + rpy2_args_pattern(RPY2_MAX_ARGS) + '(\n)?([\\s\\S]*)',
8587
replacement: (match, ...args) => {
8688
let r = parse_r_args(args, -3);
8789
return `${r.outputs}rpy2.ipython.rmagic.RMagics.R("""${r.content}""", "${r.others}"${r.inputs})`;

packages/jupyterlab-lsp/src/magics/rpy2.spec.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ describe('rpy2 IPython overrides', () => {
3232
let line_magics_map = new LineMagicsMap(
3333
language_specific_overrides['python'].line_magics
3434
);
35-
it('inputs and outputs', () => {
35+
it('works with the short form arguments, inputs and outputs', () => {
3636
let line = '%R -i x';
3737
let override = line_magics_map.override_for(line);
3838
expect(override).to.equal('rpy2.ipython.rmagic.RMagics.R("", "", x)');
@@ -75,5 +75,22 @@ describe('rpy2 IPython overrides', () => {
7575
reverse = line_magics_map.reverse.override_for(override);
7676
expect(reverse).to.equal(line);
7777
});
78+
79+
it('works with the long form arguments', () => {
80+
let line = '%R --input x';
81+
let override = line_magics_map.override_for(line);
82+
expect(override).to.equal('rpy2.ipython.rmagic.RMagics.R("", "", x)');
83+
let reverse = line_magics_map.reverse.override_for(override);
84+
// TODO: make this preserve the long form
85+
expect(reverse).to.equal('%R -i x');
86+
87+
line = '%R --width 800 --height 400 command()';
88+
override = line_magics_map.override_for(line);
89+
expect(override).to.equal(
90+
'rpy2.ipython.rmagic.RMagics.R(" command()", "--width 800 --height 400")'
91+
);
92+
reverse = line_magics_map.reverse.override_for(override);
93+
expect(reverse).to.equal(line);
94+
});
7895
});
7996
});

packages/jupyterlab-lsp/src/magics/rpy2.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
export function parse_r_args(args: string[], content_position: number) {
1+
export const RPY2_MAX_ARGS = 10;
2+
3+
export function extract_r_args(args: string[], content_position: number) {
24
let inputs = [];
35
let outputs = [];
46
let others = [];
@@ -7,15 +9,27 @@ export function parse_r_args(args: string[], content_position: number) {
79
let variable = args[i + 1];
810
if (typeof arg === 'undefined') {
911
break;
10-
} else if (arg === 'i') {
12+
} else if (arg === 'i' || arg === '-input') {
1113
inputs.push(variable);
12-
} else if (arg === 'o') {
14+
} else if (arg === 'o' || arg === '-output') {
1315
outputs.push(variable);
1416
} else {
1517
others.push('-' + arg + ' ' + variable);
1618
}
1719
}
18-
let rest = args.slice(content_position, content_position + 1);
20+
return {
21+
inputs: inputs,
22+
outputs: outputs,
23+
rest: args.slice(content_position, content_position + 1),
24+
others: others
25+
};
26+
}
27+
28+
export function parse_r_args(args: string[], content_position: number) {
29+
let { inputs, outputs, rest, others } = extract_r_args(
30+
args,
31+
content_position
32+
);
1933
let input_variables = inputs.join(', ');
2034
if (input_variables) {
2135
input_variables = ', ' + input_variables;
@@ -81,3 +95,7 @@ export function rpy2_reverse_replacement(match: string, ...args: string[]) {
8195
contents: contents
8296
};
8397
}
98+
99+
export function rpy2_args_pattern(max_n: number) {
100+
return '(?: -(\\S+) (\\S+))?'.repeat(max_n);
101+
}

packages/jupyterlab-lsp/src/virtual/document.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ interface IVirtualLine {
2929
editor: CodeMirror.Editor;
3030
}
3131

32-
interface IVirtualDocumentBlock {
32+
export interface IVirtualDocumentBlock {
3333
/**
3434
* Line corresponding to the block in the entire foreign document
3535
*/

0 commit comments

Comments
 (0)