Skip to content

Commit 4ee88bc

Browse files
committed
Lazy imports of graph object hierarchy for Python 3.7+
This involved splitting validators/graph object classes back into separate files
1 parent 4ef310f commit 4ee88bc

File tree

10,107 files changed

+622112
-599495
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

10,107 files changed

+622112
-599495
lines changed

Diff for: packages/python/plotly/_plotly_utils/importers.py

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import importlib
2+
3+
4+
def relative_import(parent_name, rel_modules=(), rel_classes=()):
5+
"""
6+
Helper function to import submodules lazily in Pythong 3.7+
7+
8+
Parameters
9+
----------
10+
rel_modules: list of str
11+
list of submodules to import, of the form .submodule
12+
rel_classes: list of str
13+
list of submodule classes/variables to import, of the form ._submodule.Foo
14+
15+
Returns
16+
-------
17+
tuple
18+
Tuple that should be assigned to __all__, __getattr__ in the caller
19+
"""
20+
module_names = {rel_module.split(".")[-1]: rel_module for rel_module in rel_modules}
21+
class_names = {rel_path.split(".")[-1]: rel_path for rel_path in rel_classes}
22+
23+
def __getattr__(import_name):
24+
# In Python 3.7+, lazy import submodules
25+
26+
# Check for submodule
27+
if import_name in module_names:
28+
# print(parent_name, import_name)
29+
rel_import = module_names[import_name]
30+
return importlib.import_module(rel_import, parent_name)
31+
32+
# Check for submodule class
33+
if import_name in class_names:
34+
# print(parent_name, import_name)
35+
rel_path_parts = class_names[import_name].split(".")
36+
rel_module = ".".join(rel_path_parts[:-1])
37+
class_name = import_name
38+
class_module = importlib.import_module(rel_module, parent_name)
39+
return getattr(class_module, class_name)
40+
41+
raise AttributeError(
42+
"module {__name__!r} has no attribute {name!r}".format(
43+
name=import_name, __name__=parent_name
44+
)
45+
)
46+
47+
__all__ = list(module_names) + list(class_names)
48+
49+
return __all__, __getattr__

Diff for: packages/python/plotly/codegen/__init__.py

+87-45
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import os
23
import os.path as opath
34
import shutil
45
import subprocess
@@ -17,6 +18,7 @@
1718
FrameNode,
1819
write_init_py,
1920
ElementDefaultsNode,
21+
build_from_imports_py,
2022
)
2123
from codegen.validators import (
2224
write_validator_py,
@@ -190,14 +192,6 @@ def perform_codegen():
190192
# -------------------
191193
for node in all_compound_nodes:
192194
write_datatype_py(outdir, node)
193-
alls.setdefault(node.path_parts, [])
194-
alls[node.path_parts].extend(
195-
c.name_datatype_class for c in node.child_compound_datatypes
196-
)
197-
if node.parent_path_parts == ():
198-
# Add top-level classes to alls
199-
alls.setdefault((), [])
200-
alls[node.parent_path_parts].append(node.name_datatype_class)
201195

202196
# ### Deprecated ###
203197
# These are deprecated legacy datatypes like graph_objs.Marker
@@ -219,54 +213,86 @@ def perform_codegen():
219213
layout_array_nodes,
220214
)
221215

216+
# Write validator __init__.py files
217+
# ---------------------------------
218+
# ### Write __init__.py files for each validator package ###
219+
validator_rel_class_imports = {}
220+
for node in all_datatype_nodes:
221+
if node.is_mapped:
222+
continue
223+
key = node.parent_path_parts
224+
validator_rel_class_imports.setdefault(key, []).append(
225+
f"._{node.name_property}.{node.name_validator_class}"
226+
)
227+
228+
# Add Data validator
229+
root_validator_pairs = validator_rel_class_imports[()]
230+
root_validator_pairs.append("._data.DataValidator")
231+
232+
# Output validator __init__.py files
233+
validators_pkg = opath.join(outdir, "validators")
234+
for path_parts, rel_classes in validator_rel_class_imports.items():
235+
write_init_py(validators_pkg, path_parts, [], rel_classes)
236+
222237
# Write datatype __init__.py files
223238
# --------------------------------
224-
# ### Build mapping from parent package to datatype class ###
225-
path_to_datatype_import_info = {}
239+
datatype_rel_class_imports = {}
240+
datatype_rel_module_imports = {}
241+
226242
for node in all_compound_nodes:
227243
key = node.parent_path_parts
228244

245+
# class import
246+
datatype_rel_class_imports.setdefault(key, []).append(
247+
f"._{node.name_undercase}.{node.name_datatype_class}"
248+
)
249+
229250
# submodule import
230251
if node.child_compound_datatypes:
231-
232-
path_to_datatype_import_info.setdefault(key, []).append(
233-
(f"plotly.graph_objs{node.parent_dotpath_str}", node.name_undercase)
252+
datatype_rel_module_imports.setdefault(key, []).append(
253+
f".{node.name_undercase}"
234254
)
235-
alls[node.parent_path_parts].append(node.name_undercase)
236255

237256
# ### Write plotly/graph_objs/graph_objs.py ###
238257
# This if for backward compatibility. It just imports everything from
239258
# graph_objs/__init__.py
240259
write_graph_objs_graph_objs(outdir)
241260

242261
# ### Add Figure and FigureWidget ###
243-
root_datatype_imports = path_to_datatype_import_info[()]
244-
root_datatype_imports.append(("._figure", "Figure"))
245-
alls[()].append("Figure")
262+
root_datatype_imports = datatype_rel_class_imports[()]
263+
root_datatype_imports.append("._figure.Figure")
246264

247265
# ### Add deprecations ###
248-
root_datatype_imports.append(("._deprecations", DEPRECATED_DATATYPES.keys()))
249-
alls[()].extend(DEPRECATED_DATATYPES.keys())
250-
251-
# Sort alls
252-
for k, v in alls.items():
253-
alls[k] = list(sorted(v))
266+
for dep_clas in DEPRECATED_DATATYPES:
267+
root_datatype_imports.append(f"._deprecations.{dep_clas}")
254268

269+
validate_import = "from .._validate import validate\n"
255270
optional_figure_widget_import = f"""
256-
__all__ = {alls[()]}
257-
try:
258-
import ipywidgets
259-
from distutils.version import LooseVersion
260-
if LooseVersion(ipywidgets.__version__) >= LooseVersion('7.0.0'):
261-
from ._figurewidget import FigureWidget
262-
__all__.append("FigureWidget")
263-
del LooseVersion
264-
del ipywidgets
265-
except ImportError:
266-
pass
271+
if sys.version_info < (3, 7):
272+
try:
273+
import ipywidgets
274+
from distutils.version import LooseVersion
275+
if LooseVersion(ipywidgets.__version__) >= LooseVersion('7.0.0'):
276+
from ..graph_objs._figurewidget import FigureWidget
277+
del LooseVersion
278+
del ipywidgets
279+
except ImportError:
280+
pass
281+
else:
282+
orig_getattr = __getattr__
283+
def __getattr__(import_name):
284+
if import_name == "FigureWidget":
285+
try:
286+
import ipywidgets
287+
from distutils.version import LooseVersion
288+
if LooseVersion(ipywidgets.__version__) >= LooseVersion('7.0.0'):
289+
from ..graph_objs._figurewidget import FigureWidget
290+
return FigureWidget
291+
except ImportError:
292+
pass
293+
294+
return orig_getattr(import_name)
267295
"""
268-
root_datatype_imports.append(optional_figure_widget_import)
269-
270296
# ### __all__ ###
271297
for path_parts, class_names in alls.items():
272298
if path_parts and class_names:
@@ -276,18 +302,34 @@ def perform_codegen():
276302

277303
# ### Output datatype __init__.py files ###
278304
graph_objs_pkg = opath.join(outdir, "graph_objs")
279-
for path_parts, import_pairs in path_to_datatype_import_info.items():
280-
write_init_py(graph_objs_pkg, path_parts, import_pairs)
305+
for path_parts in datatype_rel_class_imports:
306+
rel_classes = datatype_rel_class_imports[path_parts]
307+
rel_modules = datatype_rel_module_imports.get(path_parts, [])
308+
if path_parts == ():
309+
init_extra = validate_import + optional_figure_widget_import
310+
else:
311+
init_extra = ""
312+
write_init_py(graph_objs_pkg, path_parts, rel_modules, rel_classes, init_extra)
281313

282314
# ### Output graph_objects.py alias
283-
graph_objects_path = opath.join(outdir, "graph_objects.py")
315+
graph_objects_rel_classes = [
316+
"..graph_objs." + rel_path.split(".")[-1]
317+
for rel_path in datatype_rel_class_imports[()]
318+
]
319+
graph_objects_rel_modules = [
320+
"..graph_objs." + rel_module.split(".")[-1]
321+
for rel_module in datatype_rel_module_imports[()]
322+
]
323+
324+
graph_objects_init_source = build_from_imports_py(
325+
graph_objects_rel_modules,
326+
graph_objects_rel_classes,
327+
init_extra=validate_import + optional_figure_widget_import,
328+
)
329+
graph_objects_path = opath.join(outdir, "graph_objects", "__init__.py")
330+
os.makedirs(opath.join(outdir, "graph_objects"), exist_ok=True)
284331
with open(graph_objects_path, "wt") as f:
285-
f.write(
286-
f"""\
287-
from __future__ import absolute_import
288-
from plotly.graph_objs import *
289-
__all__ = {alls[()]}"""
290-
)
332+
f.write(graph_objects_init_source)
291333

292334
# ### Run black code formatter on output directories ###
293335
subprocess.call(["black", "--target-version=py27", validators_pkgdir])

Diff for: packages/python/plotly/codegen/datatypes.py

+41-49
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,22 @@ def _subplot_re_match(self, prop):
153153
"""
154154
)
155155

156-
# ### Property definitions ###
157156
child_datatype_nodes = node.child_datatypes
158-
159157
subtype_nodes = child_datatype_nodes
158+
valid_props_list = sorted(
159+
[node.name_property for node in subtype_nodes + literal_nodes]
160+
)
161+
buffer.write(
162+
f"""
163+
# class properties
164+
# --------------------
165+
_parent_path_str = '{node.parent_path_str}'
166+
_path_str = '{node.path_str}'
167+
_valid_props = {{"{'", "'.join(valid_props_list)}"}}
168+
"""
169+
)
170+
171+
# ### Property definitions ###
160172
for subtype_node in subtype_nodes:
161173
if subtype_node.is_array_element:
162174
prop_type = (
@@ -243,15 +255,9 @@ def {literal_node.name_property}(self):
243255
)
244256

245257
# ### Private properties descriptions ###
258+
valid_props = {node.name_property for node in subtype_nodes}
246259
buffer.write(
247260
f"""
248-
249-
# property parent name
250-
# --------------------
251-
@property
252-
def _parent_path_str(self):
253-
return '{node.parent_path_str}'
254-
255261
# Self properties description
256262
# ---------------------------
257263
@property
@@ -309,6 +315,23 @@ def __init__(self"""
309315
f"""
310316
super({datatype_class}, self).__init__('{node.name_property}')
311317
318+
if '_parent' in kwargs:
319+
self._parent = kwargs['_parent']
320+
return
321+
"""
322+
)
323+
324+
if datatype_class == "Layout":
325+
buffer.write(
326+
f"""
327+
# Override _valid_props for instance so that instance can mutate set
328+
# to support subplot properties (e.g. xaxis2)
329+
self._valid_props = {{"{'", "'.join(valid_props_list)}"}}
330+
"""
331+
)
332+
333+
buffer.write(
334+
f"""
312335
# Validate arg
313336
# ------------
314337
if arg is None:
@@ -326,23 +349,8 @@ def __init__(self"""
326349
# Handle skip_invalid
327350
# -------------------
328351
self._skip_invalid = kwargs.pop('skip_invalid', False)
329-
330-
# Import validators
331-
# -----------------
332-
from plotly.validators{node.parent_dotpath_str} import (
333-
{undercase} as v_{undercase})
334-
335-
# Initialize validators
336-
# ---------------------"""
352+
"""
337353
)
338-
for subtype_node in subtype_nodes:
339-
if not subtype_node.is_mapped:
340-
sub_name = subtype_node.name_property
341-
sub_validator = subtype_node.name_validator_class
342-
buffer.write(
343-
f"""
344-
self._validators['{sub_name}'] = v_{undercase}.{sub_validator}()"""
345-
)
346354

347355
buffer.write(
348356
f"""
@@ -352,27 +360,13 @@ def __init__(self"""
352360
)
353361
for subtype_node in subtype_nodes:
354362
name_prop = subtype_node.name_property
355-
if name_prop == "template" or subtype_node.is_mapped:
356-
# Special handling for layout.template to avoid infinite
357-
# recursion. Only initialize layout.template object if non-None
358-
# value specified.
359-
#
360-
# Same special handling for mapped nodes (e.g. layout.titlefont)
361-
# to keep them for overriding mapped property with None
362-
buffer.write(
363-
f"""
363+
buffer.write(
364+
f"""
364365
_v = arg.pop('{name_prop}', None)
365366
_v = {name_prop} if {name_prop} is not None else _v
366367
if _v is not None:
367368
self['{name_prop}'] = _v"""
368-
)
369-
else:
370-
buffer.write(
371-
f"""
372-
_v = arg.pop('{name_prop}', None)
373-
self['{name_prop}'] = {name_prop} \
374-
if {name_prop} is not None else _v"""
375-
)
369+
)
376370

377371
# ### Literals ###
378372
if literal_nodes:
@@ -381,18 +375,14 @@ def __init__(self"""
381375
382376
# Read-only literals
383377
# ------------------
384-
from _plotly_utils.basevalidators import LiteralValidator"""
378+
"""
385379
)
386380
for literal_node in literal_nodes:
387381
lit_name = literal_node.name_property
388-
lit_parent = literal_node.parent_path_str
389382
lit_val = repr(literal_node.node_data)
390383
buffer.write(
391384
f"""
392385
self._props['{lit_name}'] = {lit_val}
393-
self._validators['{lit_name}'] =\
394-
LiteralValidator(plotly_name='{lit_name}',\
395-
parent_name='{lit_parent}', val={lit_val})
396386
arg.pop('{lit_name}', None)"""
397387
)
398388

@@ -609,13 +599,15 @@ def write_datatype_py(outdir, node):
609599

610600
# Build file path
611601
# ---------------
612-
filepath = opath.join(outdir, "graph_objs", *node.parent_path_parts, "__init__.py")
602+
# filepath = opath.join(outdir, "graph_objs", *node.parent_path_parts, "__init__.py")
603+
filepath = opath.join(
604+
outdir, "graph_objs", *node.parent_path_parts, "_" + node.name_undercase + ".py"
605+
)
613606

614607
# Generate source code
615608
# --------------------
616609
datatype_source = build_datatype_py(node)
617610

618611
# Write file
619612
# ----------
620-
621613
write_source_py(datatype_source, filepath, leading_newlines=2)

0 commit comments

Comments
 (0)