From 77ebefba088af4e6ff0fd4061439bba18ebb7816 Mon Sep 17 00:00:00 2001 From: Douglas Raillard Date: Thu, 5 Oct 2023 12:10:41 +0100 Subject: [PATCH] wa: Remove dependency on "imp" module Python 3.12 removed the "imp" module, so use importlib instead. --- setup.py | 1 + wa/framework/plugin.py | 13 ++------- wa/utils/misc.py | 61 ++++++++++++++++++++++++++++++------------ 3 files changed, 47 insertions(+), 28 deletions(-) diff --git a/setup.py b/setup.py index 36c697c6..3b92599b 100755 --- a/setup.py +++ b/setup.py @@ -79,6 +79,7 @@ params = dict( license='Apache v2', maintainer='ARM Architecture & Technology Device Lab', maintainer_email='workload-automation@arm.com', + python_requires='>= 3.7', setup_requires=[ 'numpy<=1.16.4; python_version<"3"', 'numpy; python_version>="3"', diff --git a/wa/framework/plugin.py b/wa/framework/plugin.py index 1c7bd522..a59ded5e 100644 --- a/wa/framework/plugin.py +++ b/wa/framework/plugin.py @@ -18,8 +18,6 @@ import os import sys import inspect -import imp -import string import logging from collections import OrderedDict, defaultdict from itertools import chain @@ -32,16 +30,10 @@ from wa.framework.exception import (NotFoundError, PluginLoaderError, TargetErro ValidationError, ConfigError, HostError) from wa.utils import log from wa.utils.misc import (ensure_directory_exists as _d, walk_modules, load_class, - merge_dicts_simple, get_article) + merge_dicts_simple, get_article, import_path) from wa.utils.types import identifier -if sys.version_info[0] == 3: - MODNAME_TRANS = str.maketrans(':/\\.', '____') -else: - MODNAME_TRANS = string.maketrans(':/\\.', '____') - - class AttributeCollection(object): """ Accumulator for plugin attribute objects (such as Parameters or Artifacts). @@ -645,8 +637,7 @@ class PluginLoader(object): def _discover_from_file(self, filepath): try: - modname = os.path.splitext(filepath[1:])[0].translate(MODNAME_TRANS) - module = imp.load_source(modname, filepath) + module = import_path(filepath) self._discover_in_module(module) except (SystemExit, ImportError) as e: if self.keep_going: diff --git a/wa/utils/misc.py b/wa/utils/misc.py index 810665e3..c14fb769 100644 --- a/wa/utils/misc.py +++ b/wa/utils/misc.py @@ -21,10 +21,12 @@ Miscellaneous functions that don't fit anywhere else. import errno import hashlib -import imp +import importlib +import inspect import logging import math import os +import pathlib import random import re import shutil @@ -308,32 +310,57 @@ class LoadSyntaxError(Exception): RAND_MOD_NAME_LEN = 30 -def load_struct_from_python(filepath=None, text=None): +def import_path(filepath, module_name=None): + """ + Programmatically import the given Python source file under the name + ``module_name``. If ``module_name`` is not provided, a stable name based on + ``filepath`` will be created. Note that this module name cannot be relied + on, so don't make write import statements assuming this will be stable in + the future. + """ + if not module_name: + path = pathlib.Path(filepath).resolve() + id_ = to_identifier(str(path)) + module_name = f'wa._user_import.{id_}' + + try: + return sys.modules[module_name] + except KeyError: + spec = importlib.util.spec_from_file_location(module_name, filepath) + module = importlib.util.module_from_spec(spec) + try: + sys.modules[module_name] = module + spec.loader.exec_module(module) + except BaseException: + sys.modules.pop(module_name, None) + raise + else: + # We could return the "module" object, but that would not take into + # account any manipulation the module did on sys.modules when + # executing. To be consistent with the import statement, re-lookup + # the module name. + return sys.modules[module_name] + + +def load_struct_from_python(filepath): """Parses a config structure from a .py file. The structure should be composed of basic Python types (strings, ints, lists, dicts, etc.).""" - if not (filepath or text) or (filepath and text): - raise ValueError('Exactly one of filepath or text must be specified.') + try: - if filepath: - modname = to_identifier(filepath) - mod = imp.load_source(modname, filepath) - else: - modname = get_random_string(RAND_MOD_NAME_LEN) - while modname in sys.modules: # highly unlikely, but... - modname = get_random_string(RAND_MOD_NAME_LEN) - mod = imp.new_module(modname) - exec(text, mod.__dict__) # pylint: disable=exec-used - return dict((k, v) - for k, v in mod.__dict__.items() - if not k.startswith('_')) + mod = import_path(filepath) except SyntaxError as e: raise LoadSyntaxError(e.message, filepath, e.lineno) + else: + return { + k: v + for k, v in inspect.getmembers(mod) + if not k.startswith('_') + } def load_struct_from_yaml(filepath=None, text=None): """Parses a config structure from a .yaml file. The structure should be composed of basic Python types (strings, ints, lists, dicts, etc.).""" - # Import here to avoid circular imports # pylint: disable=wrong-import-position,cyclic-import, import-outside-toplevel from wa.utils.serializer import yaml