Skip to content

Commit 3678aa9

Browse files
authored
Extract chart studio functionality, optimized imports (#1476)
## Overview This PR is an important step towards the [version 4 goal](#1420) of removing all of the chart studio (i.e. cloud-service related) functionality from plotly.py, and putting it in a separate optional package. ## chart studio extraction For the time being, I've done this by creating a new top-level `chart_studio` package next to the top-level `plotly` package. I've moved all of the cloud-related functionality to the `chart_studio` package, following the same structure as in the current plotly package. For example, the `plotly.plotly` module was moved to `chart_studio.plotly`. This PR takes advantage of the `_future_plotly_` system introduced in #1474 to make this refactor backward compatible. - By default all of the old entry points are still usable and they are aliased to the `chart_studio` package. - If the `extract_chart_studio` future flag is set, then deprecation warnings are raised whenever the `chart_studio` modules/functions are used from their legacy locations under the `plotly` package. - If the `remove_deprecations` future flag is set then the chart studio functions are fully removed from the plotly package and are accessible only under `chart_studio`. When `remove_deprecations` is set, `plotly` has no dependency on the `chart_studio` package. ## Usage To remove the chart_studio functionality from the main `plotly` module, use the ```python from _plotly_future_ import remove_deprecations ``` This will further speed up imports, and will allow for testing code to make sure it will be compatible with the package structure of plotly.py version 4. ## Import optimization This PR also makes a relatively minor change to the code generation logic for `graph_objs` and `validator` that yields an import time reduction of ~10-20% . Rather that creating a single file for each datatype and validator class, all of the classes in a `graph_obj` or `validator` module are specified directly in the `__init__.py` file. This reduces the number of files significantly, which seems to yield a modest but consistent speedup while being 100% backward compatible.
1 parent 5f27aec commit 3678aa9

File tree

8,169 files changed

+496021
-484974
lines changed

Some content is hidden

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

8,169 files changed

+496021
-484974
lines changed

Diff for: _plotly_future_/__init__.py

+49
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import warnings
2+
import functools
3+
14
_future_flags = set()
25

36

@@ -6,3 +9,49 @@ def _assert_plotly_not_imported():
69
if 'plotly' in sys.modules:
710
raise ImportError("""\
811
The _plotly_future_ module must be imported before the plotly module""")
12+
13+
14+
warnings.filterwarnings(
15+
'default',
16+
'.*?is deprecated, please use chart_studio*',
17+
DeprecationWarning
18+
)
19+
20+
21+
def _chart_studio_warning(submodule):
22+
if 'extract_chart_studio' in _future_flags:
23+
warnings.warn(
24+
'The plotly.{submodule} module is deprecated, '
25+
'please use chart_studio.{submodule} instead'
26+
.format(submodule=submodule),
27+
DeprecationWarning,
28+
stacklevel=2)
29+
30+
31+
def _chart_studio_deprecation(fn):
32+
33+
fn_name = fn.__name__
34+
fn_module = fn.__module__
35+
plotly_name = '.'.join(
36+
['plotly'] + fn_module.split('.')[1:] + [fn_name])
37+
chart_studio_name = '.'.join(
38+
['chart_studio'] + fn_module.split('.')[1:] + [fn_name])
39+
40+
msg = """\
41+
{plotly_name} is deprecated, please use {chart_studio_name}\
42+
""".format(plotly_name=plotly_name, chart_studio_name=chart_studio_name)
43+
44+
@functools.wraps(fn)
45+
def wrapper(*args, **kwargs):
46+
if 'extract_chart_studio' in _future_flags:
47+
warnings.warn(
48+
msg,
49+
DeprecationWarning,
50+
stacklevel=2)
51+
52+
return fn(*args, **kwargs)
53+
54+
return wrapper
55+
56+
57+
__all__ = ['_future_flags', '_chart_studio_warning']

Diff for: _plotly_future_/extract_chart_studio.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from __future__ import absolute_import
2+
from _plotly_future_ import _future_flags, _assert_plotly_not_imported
3+
4+
_assert_plotly_not_imported()
5+
_future_flags.add('extract_chart_studio')

Diff for: _plotly_future_/remove_deprecations.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from __future__ import absolute_import
2+
from _plotly_future_ import _future_flags, _assert_plotly_not_imported
3+
4+
_assert_plotly_not_imported()
5+
_future_flags.add('remove_deprecations')

Diff for: _plotly_future_/v4.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
from __future__ import absolute_import
2-
from _plotly_future_ import renderer_defaults, template_defaults
2+
from _plotly_future_ import (
3+
renderer_defaults, template_defaults, extract_chart_studio,
4+
remove_deprecations)

Diff for: _plotly_utils/basevalidators.py

+15-23
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,18 @@
1+
from __future__ import absolute_import
2+
13
import base64
24
import numbers
35
import textwrap
46
import uuid
57
from importlib import import_module
68
import copy
7-
89
import io
910
from copy import deepcopy
10-
1111
import re
12-
13-
# Optional imports
14-
# ----------------
1512
import sys
1613
from six import string_types
1714

18-
np = None
19-
pd = None
20-
21-
try:
22-
np = import_module('numpy')
23-
24-
try:
25-
pd = import_module('pandas')
26-
except ImportError:
27-
pass
28-
29-
except ImportError:
30-
pass
15+
from _plotly_utils.optional_imports import get_module
3116

3217

3318
# back-port of fullmatch from Py3.4+
@@ -50,6 +35,8 @@ def to_scalar_or_list(v):
5035
# Python native scalar type ('float' in the example above).
5136
# We explicitly check if is has the 'item' method, which conventionally
5237
# converts these types to native scalars.
38+
np = get_module('numpy')
39+
pd = get_module('pandas')
5340
if np and np.isscalar(v) and hasattr(v, 'item'):
5441
return v.item()
5542
if isinstance(v, (list, tuple)):
@@ -86,7 +73,8 @@ def copy_to_readonly_numpy_array(v, kind=None, force_numeric=False):
8673
np.ndarray
8774
Numpy array with the 'WRITEABLE' flag set to False
8875
"""
89-
76+
np = get_module('numpy')
77+
pd = get_module('pandas')
9078
assert np is not None
9179

9280
# ### Process kind ###
@@ -175,7 +163,9 @@ def is_numpy_convertable(v):
175163
def is_homogeneous_array(v):
176164
"""
177165
Return whether a value is considered to be a homogeneous array
178-
"""
166+
"""
167+
np = get_module('numpy')
168+
pd = get_module('pandas')
179169
if ((np and isinstance(v, np.ndarray) or
180170
(pd and isinstance(v, (pd.Series, pd.Index))))):
181171
return True
@@ -616,7 +606,7 @@ def description(self):
616606
as a plotly.grid_objs.Column object""".format(plotly_name=self.plotly_name))
617607

618608
def validate_coerce(self, v):
619-
from plotly.grid_objs import Column
609+
from chart_studio.grid_objs import Column
620610
if v is None:
621611
# Pass None through
622612
pass
@@ -704,7 +694,7 @@ def validate_coerce(self, v):
704694
# Pass None through
705695
pass
706696
elif self.array_ok and is_homogeneous_array(v):
707-
697+
np = get_module('numpy')
708698
try:
709699
v_array = copy_to_readonly_numpy_array(v, force_numeric=True)
710700
except (ValueError, TypeError, OverflowError):
@@ -825,7 +815,7 @@ def validate_coerce(self, v):
825815
# Pass None through
826816
pass
827817
elif self.array_ok and is_homogeneous_array(v):
828-
818+
np = get_module('numpy')
829819
v_array = copy_to_readonly_numpy_array(v,
830820
kind=('i', 'u'),
831821
force_numeric=True)
@@ -964,6 +954,8 @@ def validate_coerce(self, v):
964954
self.raise_invalid_elements(invalid_els)
965955

966956
if is_homogeneous_array(v):
957+
np = get_module('numpy')
958+
967959
# If not strict, let numpy cast elements to strings
968960
v = copy_to_readonly_numpy_array(v, kind='U')
969961

Diff for: _plotly_utils/exceptions.py

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
class PlotlyError(Exception):
2+
pass
3+
4+
5+
class PlotlyEmptyDataError(PlotlyError):
6+
pass
7+
8+
9+
class PlotlyGraphObjectError(PlotlyError):
10+
def __init__(self, message='', path=(), notes=()):
11+
"""
12+
General graph object error for validation failures.
13+
14+
:param (str|unicode) message: The error message.
15+
:param (iterable) path: A path pointing to the error.
16+
:param notes: Add additional notes, but keep default exception message.
17+
18+
"""
19+
self.message = message
20+
self.plain_message = message # for backwards compat
21+
self.path = list(path)
22+
self.notes = notes
23+
super(PlotlyGraphObjectError, self).__init__(message)
24+
25+
def __str__(self):
26+
"""This is called by Python to present the error message."""
27+
format_dict = {
28+
'message': self.message,
29+
'path': '[' + ']['.join(repr(k) for k in self.path) + ']',
30+
'notes': '\n'.join(self.notes)
31+
}
32+
return ('{message}\n\nPath To Error: {path}\n\n{notes}'
33+
.format(**format_dict))
34+
35+
36+
class PlotlyDictKeyError(PlotlyGraphObjectError):
37+
def __init__(self, obj, path, notes=()):
38+
"""See PlotlyGraphObjectError.__init__ for param docs."""
39+
format_dict = {'attribute': path[-1], 'object_name': obj._name}
40+
message = ("'{attribute}' is not allowed in '{object_name}'"
41+
.format(**format_dict))
42+
notes = [obj.help(return_help=True)] + list(notes)
43+
super(PlotlyDictKeyError, self).__init__(
44+
message=message, path=path, notes=notes
45+
)
46+
47+
48+
class PlotlyDictValueError(PlotlyGraphObjectError):
49+
def __init__(self, obj, path, notes=()):
50+
"""See PlotlyGraphObjectError.__init__ for param docs."""
51+
format_dict = {'attribute': path[-1], 'object_name': obj._name}
52+
message = ("'{attribute}' has invalid value inside '{object_name}'"
53+
.format(**format_dict))
54+
notes = [obj.help(path[-1], return_help=True)] + list(notes)
55+
super(PlotlyDictValueError, self).__init__(
56+
message=message, notes=notes, path=path
57+
)
58+
59+
60+
class PlotlyListEntryError(PlotlyGraphObjectError):
61+
def __init__(self, obj, path, notes=()):
62+
"""See PlotlyGraphObjectError.__init__ for param docs."""
63+
format_dict = {'index': path[-1], 'object_name': obj._name}
64+
message = ("Invalid entry found in '{object_name}' at index, '{index}'"
65+
.format(**format_dict))
66+
notes = [obj.help(return_help=True)] + list(notes)
67+
super(PlotlyListEntryError, self).__init__(
68+
message=message, path=path, notes=notes
69+
)
70+
71+
72+
class PlotlyDataTypeError(PlotlyGraphObjectError):
73+
def __init__(self, obj, path, notes=()):
74+
"""See PlotlyGraphObjectError.__init__ for param docs."""
75+
format_dict = {'index': path[-1], 'object_name': obj._name}
76+
message = ("Invalid entry found in '{object_name}' at index, '{index}'"
77+
.format(**format_dict))
78+
note = "It's invalid because it doesn't contain a valid 'type' value."
79+
notes = [note] + list(notes)
80+
super(PlotlyDataTypeError, self).__init__(
81+
message=message, path=path, notes=notes
82+
)

Diff for: _plotly_utils/files.py

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import os
2+
3+
PLOTLY_DIR = os.environ.get("PLOTLY_DIR",
4+
os.path.join(os.path.expanduser("~"), ".plotly"))
5+
TEST_FILE = os.path.join(PLOTLY_DIR, ".permission_test")
6+
7+
8+
def _permissions():
9+
try:
10+
if not os.path.exists(PLOTLY_DIR):
11+
try:
12+
os.mkdir(PLOTLY_DIR)
13+
except Exception:
14+
# in case of race
15+
if not os.path.isdir(PLOTLY_DIR):
16+
raise
17+
with open(TEST_FILE, 'w') as f:
18+
f.write('testing\n')
19+
try:
20+
os.remove(TEST_FILE)
21+
except Exception:
22+
pass
23+
return True
24+
except Exception: # Do not trap KeyboardInterrupt.
25+
return False
26+
27+
28+
_file_permissions = None
29+
30+
31+
def ensure_writable_plotly_dir():
32+
# Cache permissions status
33+
global _file_permissions
34+
if _file_permissions is None:
35+
_file_permissions = _permissions()
36+
return _file_permissions

Diff for: _plotly_utils/optional_imports.py

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""
2+
Stand-alone module to provide information about whether optional deps exist.
3+
4+
"""
5+
from __future__ import absolute_import
6+
7+
from importlib import import_module
8+
import logging
9+
10+
logger = logging.getLogger(__name__)
11+
_not_importable = set()
12+
13+
14+
def get_module(name):
15+
"""
16+
Return module or None. Absolute import is required.
17+
18+
:param (str) name: Dot-separated module path. E.g., 'scipy.stats'.
19+
:raise: (ImportError) Only when exc_msg is defined.
20+
:return: (module|None) If import succeeds, the module will be returned.
21+
22+
"""
23+
if name not in _not_importable:
24+
try:
25+
return import_module(name)
26+
except ImportError:
27+
_not_importable.add(name)
28+
except Exception as e:
29+
_not_importable.add(name)
30+
msg = "Error importing optional module {}".format(name)
31+
logger.exception(msg)

0 commit comments

Comments
 (0)