Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lazy imports (from django) #339

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions libvcs/utils/module_loading.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import os
import sys
from importlib import import_module
from importlib.util import find_spec as importlib_find


def cached_import(module_path, class_name):
"""
Credit: https://github.com/django/django/blob/4.0.4/django/utils/module_loading.py
"""
modules = sys.modules
if module_path not in modules or (
# Module is not fully initialized.
getattr(modules[module_path], "__spec__", None) is not None
and getattr(modules[module_path].__spec__, "_initializing", False) is True
):
import_module(module_path)
return getattr(modules[module_path], class_name)


def import_string(dotted_path):
"""
Import a dotted module path and return the attribute/class designated by the
last name in the path. Raise ImportError if the import failed.

Credit: https://github.com/django/django/blob/4.0.4/django/utils/module_loading.py
"""
try:
module_path, class_name = dotted_path.rsplit(".", 1)
except ValueError as err:
raise ImportError("%s doesn't look like a module path" % dotted_path) from err

try:
return cached_import(module_path, class_name)
except AttributeError as err:
raise ImportError(
'Module "%s" does not define a "%s" attribute/class'
% (module_path, class_name)
) from err


def module_has_submodule(package, module_name):
"""See if 'module' is in 'package'.

Credit: https://github.com/django/django/blob/4.0.4/django/utils/module_loading.py
"""
try:
package_name = package.__name__
package_path = package.__path__
except AttributeError:
# package isn't a package.
return False

full_module_name = package_name + "." + module_name
try:
return importlib_find(full_module_name, package_path) is not None
except ModuleNotFoundError:
# When module_name is an invalid dotted path, Python raises
# ModuleNotFoundError.
return False


def module_dir(module):
"""
Find the name of the directory that contains a module, if possible.

Raise ValueError otherwise, e.g. for namespace packages that are split
over several directories.

Credit: https://github.com/django/django/blob/4.0.4/django/utils/module_loading.py
"""
# Convert to list because __path__ may not support indexing.
paths = list(getattr(module, "__path__", []))
if len(paths) == 1:
return paths[0]
else:
filename = getattr(module, "__file__", None)
if filename is not None:
return os.path.dirname(filename)
raise ValueError("Cannot determine directory containing %s" % module)
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ line_length = 88
[tool:pytest]
addopts = --tb=short --no-header --showlocals --doctest-modules
doctest_optionflags = ELLIPSIS NORMALIZE_WHITESPACE
norecursedirs = tests/utils/test_module
Empty file added tests/utils/__init__.py
Empty file.
5 changes: 5 additions & 0 deletions tests/utils/test_module/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class SiteMock:
_registry = {}


site = SiteMock()
Empty file.
11 changes: 11 additions & 0 deletions tests/utils/test_module/another_bad_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from . import site

content = "Another Bad Module"

site._registry.update(
{
"foo": "bar",
}
)

raise Exception("Some random exception.")
9 changes: 9 additions & 0 deletions tests/utils/test_module/another_good_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from . import site

content = "Another Good Module"

site._registry.update(
{
"lorem": "ipsum",
}
)
3 changes: 3 additions & 0 deletions tests/utils/test_module/bad_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import a_package_name_that_does_not_exist # NOQA

content = "Bad Module"
Empty file.
1 change: 1 addition & 0 deletions tests/utils/test_module/child_module/grandchild_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
content = "Grandchild Module"
1 change: 1 addition & 0 deletions tests/utils/test_module/good_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
content = "Good Module"
Empty file.
146 changes: 146 additions & 0 deletions tests/utils/test_module_loading.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""
Credit: https://github.com/django/django/blob/4.0.4/tests/utils_tests/test_module_loading.py

From April 25th, 2021. Changes:

- pytest compatibility, use monkeypatch.syspath_prepend
- Removed django-specific material
""" # noqa: E501
import os
import sys
import unittest
from importlib import import_module

import pytest

from libvcs.utils.module_loading import import_string, module_has_submodule

PY310 = sys.version_info >= (3, 10)


class DefaultLoader(unittest.TestCase):
def test_loader(self):
"Normal module existence can be tested"
test_module = import_module("tests.utils.test_module")
test_no_submodule = import_module("tests.utils.test_no_submodule")

# An importable child
self.assertTrue(module_has_submodule(test_module, "good_module"))
mod = import_module("tests.utils.test_module.good_module")
self.assertEqual(mod.content, "Good Module")

# A child that exists, but will generate an import error if loaded
self.assertTrue(module_has_submodule(test_module, "bad_module"))
with self.assertRaises(ImportError):
import_module("tests.utils.test_module.bad_module")

# A child that doesn't exist
self.assertFalse(module_has_submodule(test_module, "no_such_module"))
with self.assertRaises(ImportError):
import_module("tests.utils.test_module.no_such_module")

# A child that doesn't exist, but is the name of a package on the path
self.assertFalse(module_has_submodule(test_module, "django"))
with self.assertRaises(ImportError):
import_module("tests.utils.test_module.django")

# Don't be confused by caching of import misses
import types # NOQA: causes attempted import of tests.utils.types

self.assertFalse(module_has_submodule(sys.modules["tests.utils"], "types"))

# A module which doesn't have a __path__ (so no submodules)
self.assertFalse(module_has_submodule(test_no_submodule, "anything"))
with self.assertRaises(ImportError):
import_module("tests.utils.test_no_submodule.anything")

def test_has_sumbodule_with_dotted_path(self):
"""Nested module existence can be tested."""
test_module = import_module("tests.utils.test_module")
# A grandchild that exists.
self.assertIs(
module_has_submodule(test_module, "child_module.grandchild_module"), True
)
# A grandchild that doesn't exist.
self.assertIs(
module_has_submodule(test_module, "child_module.no_such_module"), False
)
# A grandchild whose parent doesn't exist.
self.assertIs(
module_has_submodule(test_module, "no_such_module.grandchild_module"), False
)
# A grandchild whose parent is not a package.
self.assertIs(
module_has_submodule(test_module, "good_module.no_such_module"), False
)


class EggLoader:
def setUp(self):
self.egg_dir = "%s/eggs" % os.path.dirname(__file__)

def tearDown(self):
sys.path_importer_cache.clear()

sys.modules.pop("egg_module.sub1.sub2.bad_module", None)
sys.modules.pop("egg_module.sub1.sub2.good_module", None)
sys.modules.pop("egg_module.sub1.sub2", None)
sys.modules.pop("egg_module.sub1", None)
sys.modules.pop("egg_module.bad_module", None)
sys.modules.pop("egg_module.good_module", None)
sys.modules.pop("egg_module", None)

def test_shallow_loader(self, monkeypatch: pytest.MonkeyPatch):
"Module existence can be tested inside eggs"
egg_name = "%s/test_egg.egg" % self.egg_dir
monkeypatch.syspath_prepend(egg_name)
egg_module = import_module("egg_module")

# An importable child
self.assertTrue(module_has_submodule(egg_module, "good_module"))
mod = import_module("egg_module.good_module")
self.assertEqual(mod.content, "Good Module")

# A child that exists, but will generate an import error if loaded
self.assertTrue(module_has_submodule(egg_module, "bad_module"))
with self.assertRaises(ImportError):
import_module("egg_module.bad_module")

# A child that doesn't exist
self.assertFalse(module_has_submodule(egg_module, "no_such_module"))
with self.assertRaises(ImportError):
import_module("egg_module.no_such_module")

def test_deep_loader(self, monkeypatch: pytest.MonkeyPatch):
"Modules deep inside an egg can still be tested for existence"
egg_name = "%s/test_egg.egg" % self.egg_dir
monkeypatch.syspath_prepend(egg_name)
egg_module = import_module("egg_module.sub1.sub2")

# An importable child
self.assertTrue(module_has_submodule(egg_module, "good_module"))
mod = import_module("egg_module.sub1.sub2.good_module")
self.assertEqual(mod.content, "Deep Good Module")

# A child that exists, but will generate an import error if loaded
self.assertTrue(module_has_submodule(egg_module, "bad_module"))
with self.assertRaises(ImportError):
import_module("egg_module.sub1.sub2.bad_module")

# A child that doesn't exist
self.assertFalse(module_has_submodule(egg_module, "no_such_module"))
with self.assertRaises(ImportError):
import_module("egg_module.sub1.sub2.no_such_module")


class ModuleImportTests:
def test_import_string(self):
cls = import_string("libvcs.utils.module_loading.import_string")
self.assertEqual(cls, import_string)

# Test exceptions raised
with self.assertRaises(ImportError):
import_string("no_dots_in_path")
msg = 'Module "tests.utils" does not define a "unexistent" attribute'
with self.assertRaisesMessage(ImportError, msg):
import_string("tests.utils.unexistent")
1 change: 1 addition & 0 deletions tests/utils/test_no_submodule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Used to test for modules which don't have submodules.