Skip to content

Commit

Permalink
Simplify implementation of lazy modules and add tests for it
Browse files Browse the repository at this point in the history
  • Loading branch information
ccordoba12 committed Jun 13, 2021
1 parent 3292155 commit 509a79a
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 82 deletions.
116 changes: 34 additions & 82 deletions spyder_kernels/utils/lazymodules.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,108 +10,60 @@
Delayed modules classes.
They are useful to not import big modules until it's really necessary.
"""

Notes:
from spyder_kernels.utils.misc import is_module_installed

* When accessing second level objects (e.g. numpy.ma.MaskedArray), you
need to add them to the fake class that is returned in place of the
missing module.
"""

# =============================================================================
# Class to use for missing objects
# Auxiliary classes
# =============================================================================
class FakeObject(object):
"""Fake class used in replacement of missing objects"""
pass


# =============================================================================
# Numpy
# =============================================================================
class _DelayedNumpy(object):
"""Import Numpy only when one of its attributes is accessed."""

def __getattribute__(self, name):
try:
import numpy
except Exception:
FakeNumpy = FakeObject
FakeNumpy.MaskedArray = FakeObject
return FakeNumpy

return getattr(numpy, name)

numpy = _DelayedNumpy()


# =============================================================================
# Pandas
# =============================================================================
class _DelayedPandas(object):
"""Import Pandas only when one of its attributes is accessed."""

def __getattribute__(self, name):
try:
import pandas
except Exception:
return FakeObject

return getattr(pandas, name)

pandas = _DelayedPandas()
class _LazyModuleLoader(object):
"""Lazy module loader class."""

def __init__(self, modname, second_level_attrs=None):
"""
Lazy module loader class.
# =============================================================================
# Pillow
# =============================================================================
class _DelayedPIL(object):
"""Import Pillow only when one of its attributes is accessed."""
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

def __getattribute__(self, name):
try:
import PIL.Image
except Exception:
FakePIL = FakeObject
FakePIL.Image = FakeObject
return FakePIL
# Set required second level attributes
if second_level_attrs is not None:
for attr in second_level_attrs:
setattr(self.__spy_mod__, attr, FakeObject)

return getattr(PIL, name)
def __getattr__(self, name):
if is_module_installed(self.__spy_modname__):
self.__spy_mod__ = __import__(self.__spy_modname__)
else:
return self.__spy_mod__

PIL = _DelayedPIL()
return getattr(self.__spy_mod__, name)


# =============================================================================
# BeautifulSoup
# Lazy modules
# =============================================================================
class _DelayedBs4(object):
"""Import bs4 only when one of its attributes is accessed."""

def __getattribute__(self, name):
try:
import bs4
except Exception:
FakeBs4 = FakeObject
FakeBs4.NavigableString = FakeObject
return FakeBs4
numpy = _LazyModuleLoader('numpy', ['MaskedArray'])

return getattr(bs4, name)

bs4 = _DelayedBs4()


# =============================================================================
# Scipy
# =============================================================================
class _DelayedScipy(object):
"""Import Scipy only when one of its attributes is accessed."""
pandas = _LazyModuleLoader('pandas')

def __getattribute__(self, name):
try:
import scipy.io
except Exception:
return FakeObject
PIL = _LazyModuleLoader('PIL', ['Image'])

return getattr(scipy, name)
bs4 = _LazyModuleLoader('bs4', ['NavigableString'])

scipy = _DelayedScipy()
scipy = _LazyModuleLoader('scipy.io')
40 changes: 40 additions & 0 deletions spyder_kernels/utils/tests/test_lazymodules.py
Original file line number Diff line number Diff line change
@@ -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 _LazyModuleLoader, FakeObject


def test_non_existent_module():
"""Test that we retun FakeObject's for non-existing modules."""
mod = _LazyModuleLoader('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 = _LazyModuleLoader('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__

0 comments on commit 509a79a

Please sign in to comment.