Refactor mod_import to use importlib

Switch from the deprecated imp to importlib. Also add tests and
clean up logic flow. This should be quite a bit faster than the
old implementation as well.
This commit is contained in:
Greg Taylor 2019-09-15 18:21:33 -07:00
parent e395ea9371
commit da48fa2e52
2 changed files with 60 additions and 34 deletions

View file

@ -5,6 +5,8 @@ TODO: Not nearly all utilities are covered yet.
""" """
import os.path
import mock import mock
from django.test import TestCase from django.test import TestCase
from datetime import datetime from datetime import datetime
@ -203,3 +205,27 @@ class TestDateTimeFormat(TestCase):
self.assertEqual(utils.datetime_format(dtobj), "19:54") self.assertEqual(utils.datetime_format(dtobj), "19:54")
dtobj = datetime(2019, 8, 28, 21, 32) dtobj = datetime(2019, 8, 28, 21, 32)
self.assertEqual(utils.datetime_format(dtobj), "21:32:00") self.assertEqual(utils.datetime_format(dtobj), "21:32:00")
class TestImportFunctions(TestCase):
def _t_dir_file(self, filename):
testdir = os.path.dirname(os.path.abspath(__file__))
return os.path.join(testdir, filename)
def test_mod_import(self):
loaded_mod = utils.mod_import('evennia.utils.ansi')
self.assertIsNotNone(loaded_mod)
def test_mod_import_invalid(self):
loaded_mod = utils.mod_import('evennia.utils.invalid_module')
self.assertIsNone(loaded_mod)
def test_mod_import_from_path(self):
test_path = self._t_dir_file('test_eveditor.py')
loaded_mod = utils.mod_import_from_path(test_path)
self.assertIsNotNone(loaded_mod)
def test_mod_import_from_path_invalid(self):
test_path = self._t_dir_file('invalid_filename.py')
loaded_mod = utils.mod_import_from_path(test_path)
self.assertIsNone(loaded_mod)

View file

@ -9,7 +9,6 @@ be of use when designing your own game.
import os import os
import gc import gc
import sys import sys
import imp
import types import types
import math import math
import re import re
@ -17,6 +16,7 @@ import textwrap
import random import random
import inspect import inspect
import traceback import traceback
import importlib.machinery
from twisted.internet.task import deferLater from twisted.internet.task import deferLater
from twisted.internet.defer import returnValue # noqa - used as import target from twisted.internet.defer import returnValue # noqa - used as import target
from os.path import join as osjoin from os.path import join as osjoin
@ -1166,6 +1166,30 @@ def has_parent(basepath, obj):
return False return False
def mod_import_from_path(path):
"""
Load a Python module at the specified path.
Args:
path (str): An absolute path to a Python module to load.
Returns:
(module or None): An imported module if the path was a valid
Python module. Returns `None` if the import failed.
"""
if not os.path.isabs(path):
path = os.path.abspath(path)
dirpath, filename = path.rsplit(os.path.sep, 1)
modname = filename.rstrip('.py')
try:
return importlib.machinery.SourceFileLoader(modname, path).load_module()
except OSError:
logger.log_trace(f"Could not find module '{modname}' ({modname}.py) at path '{dirpath}'")
return None
def mod_import(module): def mod_import(module):
""" """
A generic Python module loader. A generic Python module loader.
@ -1173,52 +1197,28 @@ def mod_import(module):
Args: Args:
module (str, module): This can be either a Python path module (str, module): This can be either a Python path
(dot-notation like `evennia.objects.models`), an absolute path (dot-notation like `evennia.objects.models`), an absolute path
(e.g. `/home/eve/evennia/evennia/objects.models.py`) or an (e.g. `/home/eve/evennia/evennia/objects/models.py`) or an
already imported module object (e.g. `models`) already imported module object (e.g. `models`)
Returns: Returns:
module (module or None): An imported module. If the input argument was (module or None): An imported module. If the input argument was
already a module, this is returned as-is, otherwise the path is already a module, this is returned as-is, otherwise the path is
parsed and imported. Returns `None` and logs error if import failed. parsed and imported. Returns `None` and logs error if import failed.
""" """
if not module: if not module:
return None return None
if isinstance(module, types.ModuleType): if isinstance(module, types.ModuleType):
# if this is already a module, we are done # if this is already a module, we are done
mod = module return module
else:
# first try to import as a python path
try:
mod = __import__(module, fromlist=["None"])
except ImportError as ex:
# check just where the ImportError happened (it could have been
# an erroneous import inside the module as well). This is the
# trivial way to do it ...
if not str(ex).startswith("No module named "):
raise
# error in this module. Try absolute path import instead if module.endswith('.py') and os.path.exists(module):
return mod_import_from_path(module)
if not os.path.isabs(module):
module = os.path.abspath(module)
path, filename = module.rsplit(os.path.sep, 1)
modname = re.sub(r"\.py$", "", filename)
try: try:
result = imp.find_module(modname, [path]) return import_module(module)
except ImportError: except ImportError:
logger.log_trace("Could not find module '%s' (%s.py) at path '%s'" % (modname, modname, path))
return None return None
try:
mod = imp.load_module(modname, *result)
except ImportError:
logger.log_trace("Could not find or import module %s at path '%s'" % (modname, path))
mod = None
# we have to close the file handle manually
result[0].close()
return mod
def all_from_module(module): def all_from_module(module):