From 7cb767224df4cae372f2cc4bde653ccab54fcd05 Mon Sep 17 00:00:00 2001
From: Joseph Weston <joseph.weston08@gmail.com>
Date: Tue, 28 Feb 2017 19:05:23 +0100
Subject: [PATCH] fix discretizer packaging and optional testing

Previously the testing/importing 'continuum' would fail if sympy was not
installed. Now we do the following:

    * add sympy as an optional dependency in 'extras_require'
    * force pytest to ignore tests in packages that have uninstalled
      dependencies by defining a hook in 'conftest.py'
    * use the 'class as a module' hack when importing 'continuum'.
      When sympy is not installed the continuum module will be replaced
      with an instance of 'ExtensionUnavailable' that will raise a runtime
      error on attribute access.
    * no warning is raised if sympy is not installed (it is an
      optional dependency).
---
 conftest.py                 | 42 +++++++++++++++++++++++++++++++++++++
 kwant/__init__.py           | 11 +---------
 kwant/_common.py            | 24 +++++++++++++++++++++
 kwant/continuum/__init__.py | 14 ++++++++++---
 setup.py                    |  6 +++++-
 5 files changed, 83 insertions(+), 14 deletions(-)
 create mode 100644 conftest.py

diff --git a/conftest.py b/conftest.py
new file mode 100644
index 00000000..00882f37
--- /dev/null
+++ b/conftest.py
@@ -0,0 +1,42 @@
+# Copyright 2011-2017 Kwant authors.
+#
+# This file is part of Kwant.  It is subject to the license terms in the file
+# LICENSE.rst found in the top-level directory of this distribution and at
+# http://kwant-project.org/license.  A list of Kwant authors can be found in
+# the file AUTHORS.rst at the top-level directory of this distribution and at
+# http://kwant-project.org/authors.
+"""Pytest plugin to ignore packages that have uninstalled dependencies.
+
+This ignores packages on test collection, which is required when the
+tests reside in a package that itself requires the dependency to be
+installed.
+"""
+
+import importlib
+
+
+# map from subpackage to sequence of dependency module names
+subpackage_dependencies = {
+    'kwant/continuum': ['sympy']
+}
+
+
+# map from subpackage to sequence of dependency modules that are not installed
+dependencies_not_installed = {}
+for package, dependencies in subpackage_dependencies.items():
+    not_installed = []
+    for dep in dependencies:
+        try:
+            importlib.import_module(dep)
+        except ImportError:
+            not_installed.append(dep)
+    if len(not_installed) != 0:
+        dependencies_not_installed[package] = not_installed
+
+
+def pytest_ignore_collect(path, config):
+    for subpackage, not_installed in dependencies_not_installed.items():
+        if subpackage in path.strpath:
+            print('ignoring {} because the following dependencies are not '
+                  'installed: {}'.format(subpackage, ', '.join(not_installed)))
+            return True
diff --git a/kwant/__init__.py b/kwant/__init__.py
index b00f7f54..442a6ddf 100644
--- a/kwant/__init__.py
+++ b/kwant/__init__.py
@@ -31,7 +31,7 @@ __all__.extend(['KwantDeprecationWarning', 'UserCodeError'])
 from ._common import version as __version__
 
 for module in ['system', 'builder', 'lattice', 'solvers', 'digest', 'rmt',
-               'operator', 'kpm', 'wraparound']:
+               'operator', 'kpm', 'wraparound', 'continuum']:
     exec('from . import {0}'.format(module))
     __all__.append(module)
 
@@ -63,12 +63,3 @@ def test(verbose=True):
                      "-s"] + (['-v'] if verbose else []))
 
 test.__test__ = False
-
-
-# Exposing discretizer
-try:
-    from .continuum import discretize
-    __all__.extend(['discretize'])
-except ImportError:
-    warnings.warn('Discretizer module not available. Is sympy installed?',
-                  RuntimeWarning)
diff --git a/kwant/_common.py b/kwant/_common.py
index e7f716eb..0836c6cc 100644
--- a/kwant/_common.py
+++ b/kwant/_common.py
@@ -107,6 +107,30 @@ class UserCodeError(Exception):
     pass
 
 
+class ExtensionUnavailable:
+    """Class that replaces unavailable extension modules in the Kwant namespace.
+
+    Some extensions for Kwant (e.g. 'kwant.continuum') require additional
+    dependencies that are not required for core functionality. When the
+    additional dependencies are not installed an instance of this class will
+    be inserted into Kwant's root namespace to simulate the presence of the
+    extension and inform users that they need to install additional
+    dependencies.
+
+    See https://mail.python.org/pipermail/python-ideas/2012-May/014969.html
+    for more details.
+    """
+
+    def __init__(self, name, dependencies):
+        self.name = name
+        self.dependencies = ', '.join(dependencies)
+
+    def __getattr__(self, _):
+        msg = ("'{}' is not available because one or more of the following "
+               "dependencies are not installed: {}")
+        raise RuntimeError(msg.format(self.name, self.dependencies))
+
+
 def ensure_isinstance(obj, typ, msg=None):
     if isinstance(obj, typ):
         return
diff --git a/kwant/continuum/__init__.py b/kwant/continuum/__init__.py
index b90fd964..d7ef983d 100644
--- a/kwant/continuum/__init__.py
+++ b/kwant/continuum/__init__.py
@@ -6,10 +6,18 @@
 # the file AUTHORS.rst at the top-level directory of this distribution and at
 # http://kwant-project.org/authors.
 
-from .discretizer import discretize, discretize_symbolic, build_discretized
-from ._common import momentum_operators, position_operators
-from ._common import sympify, lambdify, make_commutative
+import sys
 
+from .._common import ExtensionUnavailable
+
+try:
+    from .discretizer import discretize, discretize_symbolic, build_discretized
+    from ._common import momentum_operators, position_operators
+    from ._common import sympify, lambdify, make_commutative
+except ImportError:
+    sys.modules[__name__] = ExtensionUnavailable(__name__, ('sympy',))
+
+del sys, ExtensionUnavailable
 
 __all__ = ['discretize', 'discretize_symbolic', 'build_discretized',
            'momentum_operators', 'position_operators', 'sympify',
diff --git a/setup.py b/setup.py
index d67c15fe..74c299f8 100755
--- a/setup.py
+++ b/setup.py
@@ -646,7 +646,11 @@ def main():
                     'test': test},
           ext_modules=exts,
           install_requires=['numpy > 1.6.1', 'scipy >= 0.11.0', 'tinyarray'],
-          extras_require={'plotting': 'matplotlib >= 1.2'},
+          extras_require={
+              'plotting': 'matplotlib >= 1.2',
+              # Ubuntu 16.04 is the oldest supported distro with python3-sympy
+              'continuum': 'sympy >= 0.7.6',
+          },
           classifiers=[c.strip() for c in classifiers.split('\n')])
 
 if __name__ == '__main__':
-- 
GitLab