diff --git a/spyder_kernels/console/kernel.py b/spyder_kernels/console/kernel.py index 7b748cf4..1c42e032 100644 --- a/spyder_kernels/console/kernel.py +++ b/spyder_kernels/console/kernel.py @@ -26,8 +26,10 @@ from spyder_kernels.py3compat import TEXT_TYPES, to_text_string from spyder_kernels.comms.frontendcomm import FrontendComm from spyder_kernels.py3compat import PY3, input -from spyder_kernels.utils.misc import ( +from spyder_kernels.utils.iofuncs import iofunctions +from spyder_kernels.utils.mpl import ( MPL_BACKENDS_FROM_SPYDER, MPL_BACKENDS_TO_SPYDER, INLINE_FIGURE_FORMATS) +from spyder_kernels.utils.nsview import get_remote_data, make_remote_view # Excluded variables from the Variable Explorer (i.e. they are not @@ -157,7 +159,6 @@ def get_namespace_view(self): * 'numpy_type' is its Numpy type (if any) computed with `get_numpy_type_string`. """ - from spyder_kernels.utils.nsview import make_remote_view settings = self.namespace_view_settings if settings: @@ -172,8 +173,6 @@ def get_var_properties(self): Get some properties of the variables in the current namespace """ - from spyder_kernels.utils.nsview import get_remote_data - settings = self.namespace_view_settings if settings: ns = self._get_current_namespace() @@ -234,11 +233,9 @@ def load_data(self, filename, ext, overwrite=False): In the other hand, with 'overwrite=False', a new variable will be created with a sufix starting with 000 i.e 'var000' (default behavior). """ - from spyder_kernels.utils.iofuncs import iofunctions from spyder_kernels.utils.misc import fix_reference_name glbs = self._mglobals() - load_func = iofunctions.load_funcs[ext] data, error_message = load_func(filename) @@ -261,9 +258,6 @@ def load_data(self, filename, ext, overwrite=False): def save_namespace(self, filename): """Save namespace into filename""" - from spyder_kernels.utils.nsview import get_remote_data - from spyder_kernels.utils.iofuncs import iofunctions - ns = self._get_current_namespace() settings = self.namespace_view_settings data = get_remote_data(ns, settings, mode='picklable', diff --git a/spyder_kernels/console/start.py b/spyder_kernels/console/start.py index c35b195b..2c5e7f40 100644 --- a/spyder_kernels/console/start.py +++ b/spyder_kernels/console/start.py @@ -18,8 +18,9 @@ import site # Local imports -from spyder_kernels.utils.misc import ( - MPL_BACKENDS_FROM_SPYDER, INLINE_FIGURE_FORMATS, is_module_installed) +from spyder_kernels.utils.misc import is_module_installed +from spyder_kernels.utils.mpl import ( + MPL_BACKENDS_FROM_SPYDER, INLINE_FIGURE_FORMATS) PY2 = sys.version[0] == '2' diff --git a/spyder_kernels/utils/iofuncs.py b/spyder_kernels/utils/iofuncs.py index 8fca883a..7565a17b 100644 --- a/spyder_kernels/utils/iofuncs.py +++ b/spyder_kernels/utils/iofuncs.py @@ -30,18 +30,10 @@ import copy import glob -# Third party imports -# - If pandas fails to import here (for any reason), Spyder -# will crash at startup (e.g. see Issue 2300) -# - This also prevents Spyder to start IPython kernels -# (see Issue 2456) -try: - import pandas as pd -except: - pd = None #analysis:ignore - # Local imports from spyder_kernels.py3compat import getcwd, pickle, PY2, to_text_string +from spyder_kernels.utils.lazymodules import ( + FakeObject, numpy as np, pandas as pd, PIL, scipy as sp) class MatlabStruct(dict): @@ -110,8 +102,6 @@ def get_matlab_value(val): From the oct2py project, see https://pythonhosted.org/oct2py/conversions.html """ - import numpy as np - # Extract each item of a list. if isinstance(val, list): return [get_matlab_value(v) for v in val] @@ -156,113 +146,102 @@ def get_matlab_value(val): return val -try: - import numpy as np +def load_matlab(filename): + if sp.io is FakeObject: + return None, '' + try: - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - import scipy.io as spio - except AttributeError: - # Python 2.5: warnings.catch_warnings was introduced in Python 2.6 - import scipy.io as spio # analysis:ignore - except: - spio = None - - if spio is None: - load_matlab = None - save_matlab = None - else: - def load_matlab(filename): - try: - out = spio.loadmat(filename, struct_as_record=True) - data = dict() - for (key, value) in out.items(): - data[key] = get_matlab_value(value) - return data, None - except Exception as error: - return None, str(error) - - def save_matlab(data, filename): - try: - spio.savemat(filename, data, oned_as='row') - except Exception as error: - return str(error) -except: - load_matlab = None - save_matlab = None + out = sp.io.loadmat(filename, struct_as_record=True) + data = dict() + for (key, value) in out.items(): + data[key] = get_matlab_value(value) + return data, None + except Exception as error: + return None, str(error) -try: - import numpy as np # analysis:ignore +def save_matlab(data, filename): + if sp.io is FakeObject: + return - def load_array(filename): - try: - name = osp.splitext(osp.basename(filename))[0] - data = np.load(filename) - if isinstance(data, np.lib.npyio.NpzFile): - return dict(data), None - elif hasattr(data, 'keys'): - return data, None - else: - return {name: data}, None - except Exception as error: - return None, str(error) - - def __save_array(data, basename, index): - """Save numpy array""" - fname = basename + '_%04d.npy' % index - np.save(fname, data) - return fname -except: - load_array = None - - -try: - from spyder.pil_patch import Image - - if sys.byteorder == 'little': - _ENDIAN = '<' - else: - _ENDIAN = '>' - DTYPES = { - "1": ('|b1', None), - "L": ('|u1', None), - "I": ('%si4' % _ENDIAN, None), - "F": ('%sf4' % _ENDIAN, None), - "I;16": ('|u2', None), - "I;16S": ('%si2' % _ENDIAN, None), - "P": ('|u1', None), - "RGB": ('|u1', 3), - "RGBX": ('|u1', 4), - "RGBA": ('|u1', 4), - "CMYK": ('|u1', 4), - "YCbCr": ('|u1', 4), - } - def __image_to_array(filename): - img = Image.open(filename) - try: - dtype, extra = DTYPES[img.mode] - except KeyError: - raise RuntimeError("%s mode is not supported" % img.mode) - shape = (img.size[1], img.size[0]) - if extra is not None: - shape += (extra,) - return np.array(img.getdata(), dtype=np.dtype(dtype)).reshape(shape) + try: + sp.io.savemat(filename, data, oned_as='row') + except Exception as error: + return str(error) - def load_image(filename): - try: - name = osp.splitext(osp.basename(filename))[0] - return {name: __image_to_array(filename)}, None - except Exception as error: - return None, str(error) -except: - load_image = None + +def load_array(filename): + if np.load is FakeObject: + return None, '' + + try: + name = osp.splitext(osp.basename(filename))[0] + data = np.load(filename) + if isinstance(data, np.lib.npyio.NpzFile): + return dict(data), None + elif hasattr(data, 'keys'): + return data, None + else: + return {name: data}, None + except Exception as error: + return None, str(error) + + +def __save_array(data, basename, index): + """Save numpy array""" + fname = basename + '_%04d.npy' % index + np.save(fname, data) + return fname + + +if sys.byteorder == 'little': + _ENDIAN = '<' +else: + _ENDIAN = '>' + +DTYPES = { + "1": ('|b1', None), + "L": ('|u1', None), + "I": ('%si4' % _ENDIAN, None), + "F": ('%sf4' % _ENDIAN, None), + "I;16": ('|u2', None), + "I;16S": ('%si2' % _ENDIAN, None), + "P": ('|u1', None), + "RGB": ('|u1', 3), + "RGBX": ('|u1', 4), + "RGBA": ('|u1', 4), + "CMYK": ('|u1', 4), + "YCbCr": ('|u1', 4), +} + + +def __image_to_array(filename): + img = PIL.Image.open(filename) + try: + dtype, extra = DTYPES[img.mode] + except KeyError: + raise RuntimeError("%s mode is not supported" % img.mode) + shape = (img.size[1], img.size[0]) + if extra is not None: + shape += (extra,) + return np.array(img.getdata(), dtype=np.dtype(dtype)).reshape(shape) + + +def load_image(filename): + if PIL.Image is FakeObject or np.array is FakeObject: + return None, '' + + try: + name = osp.splitext(osp.basename(filename))[0] + return {name: __image_to_array(filename)}, None + except Exception as error: + return None, str(error) def load_pickle(filename): """Load a pickle file as a dictionary""" try: - if pd: + if pd.read_pickle is not FakeObject: return pd.read_pickle(filename), None else: with open(filename, 'rb') as fid: @@ -315,13 +294,13 @@ def save_dictionary(data, filename): raise RuntimeError('No supported objects to save') saved_arrays = {} - if load_array is not None: + if np.ndarray is not FakeObject: # Saving numpy arrays with np.save arr_fname = osp.splitext(filename)[0] for name in list(data.keys()): try: - if isinstance(data[name], - np.ndarray) and data[name].size > 0: + if (isinstance(data[name], np.ndarray) and + data[name].size > 0): # Save arrays at data root fname = __save_array(data[name], arr_fname, len(saved_arrays)) @@ -335,8 +314,8 @@ def save_dictionary(data, filename): iterator = iter(list(data[name].items())) to_remove = [] for index, value in iterator: - if isinstance(value, - np.ndarray) and value.size > 0: + if (isinstance(value, np.ndarray) and + value.size > 0): fname = __save_array(value, arr_fname, len(saved_arrays)) saved_arrays[(name, index)] = ( @@ -407,12 +386,12 @@ def load_dictionary(filename): with open(pickle_filename, 'rb') as fdesc: data = pickle.loads(fdesc.read()) saved_arrays = {} - if load_array is not None: + if np.load is not FakeObject: # Loading numpy arrays saved with np.save try: saved_arrays = data.pop('__saved_arrays__') for (name, index), fname in list(saved_arrays.items()): - arr = np.load( osp.join(tmp_folder, fname) ) + arr = np.load(osp.join(tmp_folder, fname)) if index is None: data[name] = arr elif isinstance(data[name], dict): diff --git a/spyder_kernels/utils/lazymodules.py b/spyder_kernels/utils/lazymodules.py new file mode 100644 index 00000000..d65847b4 --- /dev/null +++ b/spyder_kernels/utils/lazymodules.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (c) 2009- Spyder Kernels Contributors +# +# Licensed under the terms of the MIT License +# (see spyder_kernels/__init__.py for details) +# ----------------------------------------------------------------------------- + +""" +Lazy modules. + +They are useful to not import big modules until it's really necessary. +""" + +from spyder_kernels.utils.misc import is_module_installed + + +# ============================================================================= +# Auxiliary classes +# ============================================================================= +class FakeObject(object): + """Fake class used in replacement of missing objects""" + pass + + +class LazyModule(object): + """Lazy module loader class.""" + + def __init__(self, modname, second_level_attrs=None): + """ + Lazy module loader class. + + Parameters + ---------- + modname: str + Module name to lazy load. + second_level_attrs: list (optional) + List of second level attributes to add to the FakeObject + that stands for the module in case it's not found. + """ + self.__spy_modname__ = modname + self.__spy_mod__ = FakeObject + + # Set required second level attributes + if second_level_attrs is not None: + for attr in second_level_attrs: + setattr(self.__spy_mod__, attr, FakeObject) + + def __getattr__(self, name): + if is_module_installed(self.__spy_modname__): + self.__spy_mod__ = __import__(self.__spy_modname__) + else: + return self.__spy_mod__ + + return getattr(self.__spy_mod__, name) + + +# ============================================================================= +# Lazy modules +# ============================================================================= +numpy = LazyModule('numpy', ['MaskedArray']) + +pandas = LazyModule('pandas') + +PIL = LazyModule('PIL.Image', ['Image']) + +bs4 = LazyModule('bs4', ['NavigableString']) + +scipy = LazyModule('scipy.io') diff --git a/spyder_kernels/utils/misc.py b/spyder_kernels/utils/misc.py index 8a088081..4bc4e992 100644 --- a/spyder_kernels/utils/misc.py +++ b/spyder_kernels/utils/misc.py @@ -11,25 +11,6 @@ import re -# Mapping of inline figure formats -INLINE_FIGURE_FORMATS = { - '0': 'png', - '1': 'svg' -} - -# Mapping of matlotlib backends options to Spyder -MPL_BACKENDS_TO_SPYDER = { - 'module://ipykernel.pylab.backend_inline': 0, - 'Qt5Agg': 2, - 'Qt4Agg': 3, - 'MacOSX': 4, - 'GTK3Agg': 5, - 'GTKAgg': 6, - 'WX': 7, - 'TkAgg': 8 -} - - def is_module_installed(module_name): """ Simpler version of spyder.utils.programs.is_module_installed. @@ -47,33 +28,6 @@ def is_module_installed(module_name): return False -def automatic_backend(): - """Get Matplolib automatic backend option.""" - if is_module_installed('PyQt5'): - auto_backend = 'qt5' - elif is_module_installed('PyQt4'): - auto_backend = 'qt4' - elif is_module_installed('_tkinter'): - auto_backend = 'tk' - else: - auto_backend = 'inline' - return auto_backend - - -# Mapping of Spyder options to backends -MPL_BACKENDS_FROM_SPYDER = { - '0': 'inline', - '1': automatic_backend(), - '2': 'qt5', - '3': 'qt4', - '4': 'osx', - '5': 'gtk3', - '6': 'gtk', - '7': 'wx', - '8': 'tk' -} - - def fix_reference_name(name, blacklist=None): """Return a syntax-valid Python reference name from an arbitrary name""" name = "".join(re.split(r'[^0-9a-zA-Z_]', name)) diff --git a/spyder_kernels/utils/mpl.py b/spyder_kernels/utils/mpl.py new file mode 100644 index 00000000..26538de3 --- /dev/null +++ b/spyder_kernels/utils/mpl.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (c) 2009- Spyder Kernels Contributors +# +# Licensed under the terms of the MIT License +# (see spyder_kernels/__init__.py for details) +# ----------------------------------------------------------------------------- + +"""Matplotlib utilities.""" + +from spyder_kernels.utils.misc import is_module_installed + + +# Mapping of inline figure formats +INLINE_FIGURE_FORMATS = { + '0': 'png', + '1': 'svg' +} + + +# Mapping of matlotlib backends options to Spyder +MPL_BACKENDS_TO_SPYDER = { + 'module://ipykernel.pylab.backend_inline': 0, + 'Qt5Agg': 2, + 'Qt4Agg': 3, + 'MacOSX': 4, + 'GTK3Agg': 5, + 'GTKAgg': 6, + 'WX': 7, + 'TkAgg': 8 +} + + +def automatic_backend(): + """Get Matplolib automatic backend option.""" + if is_module_installed('PyQt5'): + auto_backend = 'qt5' + elif is_module_installed('PyQt4'): + auto_backend = 'qt4' + elif is_module_installed('_tkinter'): + auto_backend = 'tk' + else: + auto_backend = 'inline' + return auto_backend + + +# Mapping of Spyder options to backends +MPL_BACKENDS_FROM_SPYDER = { + '0': 'inline', + '1': automatic_backend(), + '2': 'qt5', + '3': 'qt4', + '4': 'osx', + '5': 'gtk3', + '6': 'gtk', + '7': 'wx', + '8': 'tk' +} diff --git a/spyder_kernels/utils/nsview.py b/spyder_kernels/utils/nsview.py index 469e4183..671789de 100644 --- a/spyder_kernels/utils/nsview.py +++ b/spyder_kernels/utils/nsview.py @@ -7,7 +7,7 @@ # ----------------------------------------------------------------------------- """ -Utilities +Utilities to build a namespace view. """ from __future__ import print_function @@ -22,44 +22,25 @@ is_type_text_string, is_binary_string, PY2, to_binary_string, iteritems) +from spyder_kernels.utils.lazymodules import ( + bs4, FakeObject, numpy as np, pandas as pd, PIL) #============================================================================== -# FakeObject +# Numpy support #============================================================================== -class FakeObject(object): - """Fake class used in replacement of missing modules""" - pass - - -#============================================================================== -# Numpy arrays and numeric types support -#============================================================================== -try: - from numpy import (ndarray, array, matrix, recarray, integer, - int64, int32, int16, int8, uint64, uint32, uint16, uint8, - float64, float32, float16, complex64, complex128, bool_) - from numpy.ma import MaskedArray - from numpy import savetxt as np_savetxt - from numpy import get_printoptions, set_printoptions -except: - ndarray = array = matrix = recarray = MaskedArray = np_savetxt = \ - int64 = int32 = int16 = int8 = uint64 = uint32 = uint16 = uint8 = \ - float64 = float32 = float16 = complex64 = complex128 = bool_ = FakeObject - - -NUMERIC_NUMPY_TYPES = (int64, int32, int16, int8, uint64, uint32, uint16, - uint8, float64, float32, float16, complex64, complex128, - bool_) +def get_numeric_numpy_types(): + return (np.int64, np.int32, np.int16, np.int8, np.uint64, np.uint32, + np.uint16, np.uint8, np.float64, np.float32, np.float16, + np.complex64, np.complex128, np.bool_) def get_numpy_dtype(obj): """Return NumPy data type associated to obj Return None if NumPy is not available or if obj is not a NumPy array or scalar""" - if ndarray is not FakeObject: + if np.ndarray is not FakeObject: # NumPy is available - import numpy as np if isinstance(obj, np.generic) or isinstance(obj, np.ndarray): # Numpy scalars all inherit from np.generic. # Numpy arrays all inherit from np.ndarray. @@ -84,35 +65,6 @@ def get_numpy_type_string(value): return 'Array' -#============================================================================== -# Pandas support -#============================================================================== -try: - from pandas import DataFrame, Index, Series -except: - DataFrame = Index = Series = FakeObject - - -#============================================================================== -# PIL Images support -#============================================================================== -try: - from spyder import pil_patch - Image = pil_patch.Image.Image -except: - Image = FakeObject # analysis:ignore - - -#============================================================================== -# BeautifulSoup support (see Issue 2448) -#============================================================================== -try: - import bs4 - NavigableString = bs4.element.NavigableString -except: - NavigableString = FakeObject # analysis:ignore - - #============================================================================== # Misc. #============================================================================== @@ -133,7 +85,8 @@ def try_to_eval(value): def get_size(item): """Return shape/size/len of an item of arbitrary type""" try: - if hasattr(item, 'shape') and isinstance(item.shape, (tuple, integer)): + if (hasattr(item, 'shape') and + isinstance(item.shape, (tuple, np.integer))): try: if item.shape: return item.shape @@ -145,7 +98,8 @@ def get_size(item): # get the shape of these objects. # Fixes spyder-ide/spyder-kernels#217 return (-1, -1) - elif hasattr(item, 'size') and isinstance(item.size, (tuple, integer)): + elif (hasattr(item, 'size') and + isinstance(item.size, (tuple, np.integer))): try: return item.size except RecursionError: @@ -176,7 +130,6 @@ def get_object_attrs(obj): #============================================================================== import datetime - try: from dateutil.parser import parse as dateparse except: @@ -239,7 +192,7 @@ def is_editable_type(value): ] if (get_type_string(value) not in supported_types and - not isinstance(value, Index)): + not isinstance(value, pd.Index)): np_dtype = get_numpy_dtype(value) if np_dtype is None or not hasattr(value, 'size'): return False @@ -350,33 +303,34 @@ def value_to_display(value, minmax=False, level=0): """Convert value for display purpose""" # To save current Numpy printoptions np_printoptions = FakeObject + numeric_numpy_types = get_numeric_numpy_types() try: - if ndarray is not FakeObject: + if np.ndarray is not FakeObject: # Save printoptions - np_printoptions = get_printoptions() + np_printoptions = np.get_printoptions() # Set max number of elements to show for Numpy arrays # in our display - set_printoptions(threshold=10) - if isinstance(value, recarray): + np.set_printoptions(threshold=10) + if isinstance(value, np.recarray): if level == 0: fields = value.names display = 'Field names: ' + ', '.join(fields) else: display = 'Recarray' - elif isinstance(value, MaskedArray): + elif isinstance(value, np.ma.MaskedArray): display = 'Masked array' - elif isinstance(value, ndarray): + elif isinstance(value, np.ndarray): if level == 0: if minmax: try: display = 'Min: %r\nMax: %r' % (value.min(), value.max()) except (TypeError, ValueError): - if value.dtype.type in NUMERIC_NUMPY_TYPES: + if value.dtype.type in numeric_numpy_types: display = str(value) else: display = default_display(value) - elif value.dtype.type in NUMERIC_NUMPY_TYPES: + elif value.dtype.type in numeric_numpy_types: display = str(value) else: display = default_display(value) @@ -384,12 +338,12 @@ def value_to_display(value, minmax=False, level=0): display = 'Numpy array' elif any([type(value) == t for t in [list, set, tuple, dict]]): display = collections_display(value, level+1) - elif isinstance(value, Image): + elif isinstance(value, PIL.Image.Image): if level == 0: display = '%s Mode: %s' % (address(value), value.mode) else: display = 'Image' - elif isinstance(value, DataFrame): + elif isinstance(value, pd.DataFrame): if level == 0: cols = value.columns if PY2 and len(cols) > 0: @@ -408,12 +362,12 @@ def value_to_display(value, minmax=False, level=0): display = 'Column names: ' + ', '.join(list(cols)) else: display = 'Dataframe' - elif isinstance(value, NavigableString): + elif isinstance(value, bs4.element.NavigableString): # Fixes Issue 2448 display = to_text_string(value) if level > 0: display = u"'" + display + u"'" - elif isinstance(value, Index): + elif isinstance(value, pd.Index): if level == 0: try: display = value._summary() @@ -449,14 +403,14 @@ def value_to_display(value, minmax=False, level=0): display = str(value) elif (isinstance(value, NUMERIC_TYPES) or isinstance(value, bool) or - isinstance(value, NUMERIC_NUMPY_TYPES)): + isinstance(value, numeric_numpy_types)): display = repr(value) else: if level == 0: display = default_display(value) else: display = default_display(value, with_module=False) - except: + except Exception: display = default_display(value) # Truncate display at 70 chars to avoid freezing Spyder @@ -470,7 +424,7 @@ def value_to_display(value, minmax=False, level=0): # Restore Numpy printoptions if np_printoptions is not FakeObject: - set_printoptions(**np_printoptions) + np.set_printoptions(**np_printoptions) return display @@ -530,19 +484,19 @@ def display_to_value(value, default_value, ignore_errors=True): def get_type_string(item): """Return type string of an object.""" # Numpy objects (don't change the order!) - if isinstance(item, MaskedArray): + if isinstance(item, np.ma.MaskedArray): return "MaskedArray" - if isinstance(item, matrix): + if isinstance(item, np.matrix): return "Matrix" - if isinstance(item, ndarray): + if isinstance(item, np.ndarray): return "NDArray" # Pandas objects - if isinstance(item, DataFrame): + if isinstance(item, pd.DataFrame): return "DataFrame" - if isinstance(item, Index): + if isinstance(item, pd.Index): return type(item).__name__ - if isinstance(item, Series): + if isinstance(item, pd.Series): return "Series" found = re.findall(r"<(?:type|class) '(\S*)'>", @@ -558,14 +512,15 @@ def get_type_string(item): def is_known_type(item): """Return True if object has a known type""" # Unfortunately, the masked array case is specific - return isinstance(item, MaskedArray) or get_type_string(item) != 'Unknown' + return (isinstance(item, np.ma.MaskedArray) or + get_type_string(item) != 'Unknown') def get_human_readable_type(item): """Return human-readable type string of an item""" - if isinstance(item, (ndarray, MaskedArray)): + if isinstance(item, (np.ndarray, np.ma.MaskedArray)): return u'Array of ' + item.dtype.name - elif isinstance(item, Image): + elif isinstance(item, PIL.Image.Image): return "Image" else: text = get_type_string(item) @@ -671,7 +626,7 @@ def get_supported_types(): pass picklable_types = editable_types[:] try: - from spyder.pil_patch import Image + from PIL import Image editable_types.append(Image.Image) except: pass diff --git a/spyder_kernels/utils/tests/test_lazymodules.py b/spyder_kernels/utils/tests/test_lazymodules.py new file mode 100644 index 00000000..e1caec9c --- /dev/null +++ b/spyder_kernels/utils/tests/test_lazymodules.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (c) 2009- Spyder Kernels Contributors +# +# Licensed under the terms of the MIT License +# (see spyder_kernels/__init__.py for details) +# ----------------------------------------------------------------------------- + +import pytest + +from spyder_kernels.utils.lazymodules import LazyModule, FakeObject + + +def test_non_existent_module(): + """Test that we retun FakeObject's for non-existing modules.""" + mod = LazyModule('no_module', second_level_attrs=['a']) + + # First level attributes must return FakeObject + assert mod.foo is FakeObject + + # Second level attributes in second_level_attrs should return + # FakeObject too. + assert mod.foo.a is FakeObject + + # Other second level attributes should raise an error. + with pytest.raises(AttributeError): + mod.foo.b + + +def test_existing_modules(): + """Test that lazy modules work for existing modules.""" + np = LazyModule('numpy') + import numpy + + # Both the lazy and actual modules should return the same. + assert np.ndarray == numpy.ndarray + + # The lazy module should have these extra attributes + assert np.__spy_mod__ + assert np.__spy_modname__