Skip to content

Commit 000d775

Browse files
authored
Merge pull request #825 from plotly/issue749-r-func-list
R: Exclude nested and private functions from generated NAMESPACE
2 parents 9774281 + 4a85802 commit 000d775

File tree

3 files changed

+143
-30
lines changed

3 files changed

+143
-30
lines changed

Diff for: dash/development/_r_components_generation.py

+57-29
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,31 @@ def generate_exports(
622622
package_suggests,
623623
**kwargs
624624
):
625+
export_string = make_namespace_exports(components, prefix)
626+
627+
# Look for wildcards in the metadata
628+
has_wildcards = False
629+
for component_data in metadata.values():
630+
if any(key.endswith('-*') for key in component_data['props']):
631+
has_wildcards = True
632+
break
633+
634+
# now, bundle up the package information and create all the requisite
635+
# elements of an R package, so that the end result is installable either
636+
# locally or directly from GitHub
637+
generate_rpkg(
638+
pkg_data,
639+
rpkg_data,
640+
project_shortname,
641+
export_string,
642+
package_depends,
643+
package_imports,
644+
package_suggests,
645+
has_wildcards,
646+
)
647+
648+
649+
def make_namespace_exports(components, prefix):
625650
export_string = ""
626651
for component in components:
627652
if (
@@ -639,46 +664,49 @@ def generate_exports(
639664
omitlist = ["utils.R", "internal.R"] + [
640665
"{}{}.R".format(prefix, component) for component in components
641666
]
642-
stripped_line = ""
643667
fnlist = []
644668

645669
for script in os.listdir("R"):
646670
if script.endswith(".R") and script not in omitlist:
647671
rfilelist += [os.path.join("R", script)]
648672

649-
# in R, either = or <- may be used to create and assign objects
650-
definitions = ["<-function", "=function"]
651-
652673
for rfile in rfilelist:
653674
with open(rfile, "r") as script:
654-
for line in script:
655-
stripped_line = line.replace(" ", "").replace("\n", "")
656-
if any(fndef in stripped_line for fndef in definitions):
657-
fnlist += set([re.split("<-|=", stripped_line)[0]])
675+
s = script.read()
676+
677+
# remove comments
678+
s = re.sub('#.*$', '', s, flags=re.M)
679+
680+
# put the whole file on one line
681+
s = s.replace("\n", " ").replace("\r", " ")
682+
683+
# empty out strings, in case of unmatched block terminators
684+
s = re.sub(r"'([^'\\]|\\'|\\[^'])*'", "''", s)
685+
s = re.sub(r'"([^"\\]|\\"|\\[^"])*"', '""', s)
686+
687+
# empty out block terminators () and {}
688+
# so we don't catch nested functions, or functions as arguments
689+
# repeat until it stops changing, in case of multiply nested blocks
690+
prev_len = len(s) + 1
691+
while len(s) < prev_len:
692+
prev_len = len(s)
693+
s = re.sub(r"\(([^()]|\(\))*\)", "()", s)
694+
s = re.sub(r"\{([^{}]|\{\})*\}", "{}", s)
695+
696+
# now, in whatever is left, look for functions
697+
matches = re.findall(
698+
# in R, either = or <- may be used to create and assign objects
699+
r"([^A-Za-z0-9._]|^)([A-Za-z0-9._]+)\s*(=|<-)\s*function", s
700+
)
701+
for match in matches:
702+
fn = match[1]
703+
# Allow users to mark functions as private by prefixing with .
704+
if fn[0] != "." and fn not in fnlist:
705+
fnlist.append(fn)
658706

659707
export_string += "\n".join("export({})".format(function)
660708
for function in fnlist)
661-
662-
# Look for wildcards in the metadata
663-
has_wildcards = False
664-
for component_data in metadata.values():
665-
if any(key.endswith('-*') for key in component_data['props']):
666-
has_wildcards = True
667-
break
668-
669-
# now, bundle up the package information and create all the requisite
670-
# elements of an R package, so that the end result is installable either
671-
# locally or directly from GitHub
672-
generate_rpkg(
673-
pkg_data,
674-
rpkg_data,
675-
project_shortname,
676-
export_string,
677-
package_depends,
678-
package_imports,
679-
package_suggests,
680-
has_wildcards,
681-
)
709+
return export_string
682710

683711

684712
def get_r_prop_types(type_object):

Diff for: dash/development/component_generator.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def generate_components(
8989
)
9090
sys.exit(1)
9191

92-
metadata = safe_json_loads(out.decode())
92+
metadata = safe_json_loads(out.decode("utf-8"))
9393

9494
generator_methods = [generate_class_file]
9595

Diff for: tests/unit/development/test_r_component_gen.py

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import os
2+
import shutil
3+
import re
4+
from textwrap import dedent
5+
6+
import pytest
7+
8+
from dash.development._r_components_generation import (
9+
make_namespace_exports
10+
)
11+
12+
13+
@pytest.fixture
14+
def make_r_dir():
15+
os.makedirs("R")
16+
17+
yield
18+
19+
shutil.rmtree("R")
20+
21+
22+
def test_r_exports(make_r_dir):
23+
extra_file = dedent("""
24+
# normal function syntax
25+
my_func <- function(a, b) {
26+
c <- a + b
27+
nested_func <- function() { stop("no!") }
28+
another_to_exclude = function(d) { d * d }
29+
another_to_exclude(c)
30+
}
31+
32+
# indented (no reason but we should allow) and using = instead of <-
33+
# also braces in comments enclosing it {
34+
my_func2 = function() {
35+
s <- "unmatched closing brace }"
36+
ignore_please <- function() { 1 }
37+
}
38+
# }
39+
40+
# real example from dash-table that should exclude FUN
41+
df_to_list <- function(df) {
42+
if(!(is.data.frame(df)))
43+
stop("!")
44+
setNames(lapply(split(df, seq(nrow(df))),
45+
FUN = function (x) {
46+
as.list(x)
47+
}), NULL)
48+
}
49+
50+
# single-line compressed
51+
util<-function(x){x+1}
52+
53+
# prefix with . to tell us to ignore
54+
.secret <- function() { stop("You can't see me") }
55+
56+
# . in the middle is OK though
57+
not.secret <- function() { 42 }
58+
""")
59+
60+
components = ["Component1", "Component2"]
61+
prefix = 'pre'
62+
63+
expected_exports = [prefix + c for c in components] + [
64+
"my_func",
65+
"my_func2",
66+
"df_to_list",
67+
"util",
68+
"not.secret"
69+
]
70+
71+
mock_component_file = dedent("""
72+
nope <- function() { stop("we don't look in component files") }
73+
""")
74+
75+
with open(os.path.join("R", "preComponent1.R"), "w") as f:
76+
f.write(mock_component_file)
77+
78+
with open(os.path.join("R", "extras.R"), "w") as f:
79+
f.write(extra_file)
80+
81+
exports = make_namespace_exports(components, prefix)
82+
print(exports)
83+
matches = re.findall(r"export\(([^()]+)\)", exports.replace('\n', ' '))
84+
85+
assert matches == expected_exports

0 commit comments

Comments
 (0)