summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authororson <orson.network@gmail.com>2020-02-21 20:09:10 -0500
committerMario Corchero <mcorcherojim@bloomberg.net>2021-07-16 11:10:33 +0200
commite78c3c76d2f0aaf6180c786605857a6ebc0cd3f3 (patch)
tree10cb2f592e0f424d6c581b3b67e5afc7b11e6fde
parent6b035517571e63b6a63a493740c5506ec1e5da44 (diff)
downloaddateutil-git-e78c3c76d2f0aaf6180c786605857a6ebc0cd3f3.tar.gz
Lazy-load submodules in Python 3.7+
This uses PEP 562 to implement lazy loading of submodules in dateutil (GH-771).
-rw-r--r--changelog.d/1007.feature.rst6
-rw-r--r--dateutil/__init__.py16
-rw-r--r--dateutil/test/test_imports.py64
3 files changed, 86 insertions, 0 deletions
diff --git a/changelog.d/1007.feature.rst b/changelog.d/1007.feature.rst
new file mode 100644
index 0000000..33a7f55
--- /dev/null
+++ b/changelog.d/1007.feature.rst
@@ -0,0 +1,6 @@
+Made all ``dateutil`` submodules lazily imported using `PEP 562
+<https://www.python.org/dev/peps/pep-0562/>`_. On Python 3.7+, things like
+``import dateutil; dateutil.tz.gettz("America/New_York")`` will now work
+without explicitly importing ``dateutil.tz``, with the import occurring behind
+the scenes on first use. The old behavior remains on Python 3.6 and earlier.
+Fixed by Orson Adams. (gh issue #771, gh pr #1007)
diff --git a/dateutil/__init__.py b/dateutil/__init__.py
index 0defb82..a2c19c0 100644
--- a/dateutil/__init__.py
+++ b/dateutil/__init__.py
@@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
+import sys
+
try:
from ._version import version as __version__
except ImportError:
@@ -6,3 +8,17 @@ except ImportError:
__all__ = ['easter', 'parser', 'relativedelta', 'rrule', 'tz',
'utils', 'zoneinfo']
+
+def __getattr__(name):
+ import importlib
+
+ if name in __all__:
+ return importlib.import_module("." + name, __name__)
+ raise AttributeError(
+ "module {!r} has not attribute {!r}".format(__name__, name)
+ )
+
+
+def __dir__():
+ # __dir__ should include all the lazy-importable modules as well.
+ return [x for x in globals() if x not in sys.modules] + __all__
diff --git a/dateutil/test/test_imports.py b/dateutil/test/test_imports.py
index 60b8600..7d0749e 100644
--- a/dateutil/test/test_imports.py
+++ b/dateutil/test/test_imports.py
@@ -1,5 +1,69 @@
import sys
+import unittest
import pytest
+import six
+
+MODULE_TYPE = type(sys)
+
+
+# Tests live in datetutil/test which cause a RuntimeWarning for Python2 builds.
+# But since we expect lazy imports tests to fail for Python < 3.7 we'll ignore those
+# warnings with this filter.
+
+if six.PY2:
+ filter_import_warning = pytest.mark.filterwarnings("ignore::RuntimeWarning")
+else:
+
+ def filter_import_warning(f):
+ return f
+
+
+@pytest.fixture(scope="function")
+def clean_import():
+ """Create a somewhat clean import base for lazy import tests"""
+ du_modules = {
+ mod_name: mod
+ for mod_name, mod in sys.modules.items()
+ if mod_name.startswith("dateutil")
+ }
+
+ other_modules = {
+ mod_name for mod_name in sys.modules if mod_name not in du_modules
+ }
+
+ for mod_name in du_modules:
+ del sys.modules[mod_name]
+
+ yield
+
+ # Delete anything that wasn't in the origin sys.modules list
+ for mod_name in list(sys.modules):
+ if mod_name not in other_modules:
+ del sys.modules[mod_name]
+
+ # Restore original modules
+ for mod_name, mod in du_modules.items():
+ sys.modules[mod_name] = mod
+
+
+@filter_import_warning
+@pytest.mark.parametrize(
+ "module",
+ ["easter", "parser", "relativedelta", "rrule", "tz", "utils", "zoneinfo"],
+)
+def test_lazy_import(clean_import, module):
+ """Test that dateutil.[submodule] works for py version > 3.7"""
+
+ import dateutil, importlib
+
+ if sys.version_info < (3, 7):
+ pytest.xfail("Lazy loading does not work for Python < 3.7")
+
+ mod_obj = getattr(dateutil, module, None)
+ assert isinstance(mod_obj, MODULE_TYPE)
+
+ mod_imported = importlib.import_module("dateutil.%s" % module)
+ assert mod_obj is mod_imported
HOST_IS_WINDOWS = sys.platform.startswith('win')