diff --git a/.github/workflows/lint_and_test.yml b/.github/workflows/lint_and_test.yml index a24cf3c2..b96c2735 100644 --- a/.github/workflows/lint_and_test.yml +++ b/.github/workflows/lint_and_test.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: os: ["ubuntu-latest", "windows-latest"] - version: ['3.11', '3.12'] + version: ['3.11', '3.12', '3.13'] fail-fast: false steps: - uses: actions/checkout@v5 diff --git a/pyproject.toml b/pyproject.toml index c5a58d1a..376db723 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ classifiers = [ "Intended Audience :: Developers", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] dependencies = [ @@ -56,7 +57,7 @@ dependencies = [ # - It depends on a couple of heavyweight libs (py4j and tornado) that aren't necessary otherwise plot = [ # When updating, check plotting works in GUI. Must keep pinned to a specific, tested version. - "matplotlib==3.10.1", + "matplotlib==3.10.7", # Python <-> Java communication, to spawn matplotlib plots in GUI "py4j", # Tornado webserver used by custom backend diff --git a/src/genie_python/genie.py b/src/genie_python/genie.py index 13c81ef1..98d7a869 100644 --- a/src/genie_python/genie.py +++ b/src/genie_python/genie.py @@ -118,7 +118,7 @@ class _GetbeamlineparsReturn(TypedDict): print("\ngenie_python version " + VERSION) MIN_SUPPORTED_PYTHON_VERSION = (3, 11, 0) -MAX_SUPPORTED_PYTHON_VERSION = (3, 12, 999) +MAX_SUPPORTED_PYTHON_VERSION = (3, 13, 999) if not (MIN_SUPPORTED_PYTHON_VERSION <= sys.version_info[0:3] <= MAX_SUPPORTED_PYTHON_VERSION): message = ( @@ -1562,16 +1562,26 @@ def __load_module(name: str, directory: str) -> types.ModuleType: raise ValueError(f"Cannot find spec for module {name} in {directory}") module = importlib.util.module_from_spec(spec) - if module.__file__ is None: - raise ValueError(f"Module {name} has no __file__ attribute") + err_msg = ( + f"Cannot load script '{name}' as its name clashes with a standard python module " + f"or with a module accessible elsewhere on the python path.\n" + f"The conflicting module was '{module}'.\n" + f"If this is a user script, rename the user script to avoid the clash." + ) + + try: + module_file = module.__file__ + except AttributeError: + raise ValueError(err_msg) from None + + if module_file is None: + raise ValueError(err_msg) + + module_location = str(module_file) + + if os.path.normpath(os.path.dirname(module_location)) != os.path.normpath(directory): + raise ValueError(err_msg) - if os.path.normpath(os.path.dirname(module.__file__)) != os.path.normpath(directory): - raise ValueError( - f"Cannot load script '{name}' as its name clashes with a standard python module " - f"or with a module accessible elsewhere on the python path.\n" - f"The conflicting module was at '{module.__file__}'.\n" - f"If this is a user script, rename the user script to avoid the clash." - ) sys.modules[name] = module loader = spec.loader if loader is None: diff --git a/src/genie_python/genie_api_setup.py b/src/genie_python/genie_api_setup.py index 3dcfb97c..9ec57ba5 100644 --- a/src/genie_python/genie_api_setup.py +++ b/src/genie_python/genie_api_setup.py @@ -284,8 +284,8 @@ def __init__(self, original_function: Callable) -> None: def just_path_on_load(self, text: str, act_tok: Any) -> list[str]: """ Returns completions for load on a path if load_script in path otherwise returns as before. - Will replace .metadata\.plugins\org.eclipse.pde.core\.bundle_pool\plugins\ - org.python.pydev_5.9.2.201708151115\pysrc\_pydev_bundle\pydev_ipython_console_011.py + Will replace .metadata\\.plugins\\org.eclipse.pde.core\\.bundle_pool\\plugins\\ + org.python.pydev_5.9.2.201708151115\\pysrc\\_pydev_bundle\\pydev_ipython_console_011.py This functions will filter out the pydev completions if the line contains load_script. It know which are the pydev ones because they are marked with the type '11'. diff --git a/src/genie_python/scanning_instrument_pylint_plugin.py b/src/genie_python/scanning_instrument_pylint_plugin.py index 0e8d9a81..85300b37 100644 --- a/src/genie_python/scanning_instrument_pylint_plugin.py +++ b/src/genie_python/scanning_instrument_pylint_plugin.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING, Any -import astroid from astroid import MANAGER +from astroid.nodes import Arguments, ClassDef, FunctionDef, Module if TYPE_CHECKING: from pylint.lint import PyLinter @@ -11,7 +11,7 @@ def register(linter: "PyLinter") -> None: """Register the plugin.""" -def transform(node: astroid.ClassDef, *args: Any, **kwargs: Any) -> None: +def transform(node: ClassDef, *args: Any, **kwargs: Any) -> None: """Add ScanningInstrument methods to the declaring module. If the given class is derived from ScanningInstrument, @@ -22,8 +22,8 @@ def transform(node: astroid.ClassDef, *args: Any, **kwargs: Any) -> None: if node.basenames and "ScanningInstrument" in node.basenames: public_methods = filter(lambda method: not method.name.startswith("__"), node.methods()) for public_method in public_methods: - if isinstance(node.parent, astroid.Module): - new_func = astroid.FunctionDef( + if isinstance(node.parent, Module): + new_func = FunctionDef( name=public_method.name, lineno=0, col_offset=0, @@ -31,7 +31,7 @@ def transform(node: astroid.ClassDef, *args: Any, **kwargs: Any) -> None: end_lineno=0, end_col_offset=0, ) - arguments = astroid.Arguments( + arguments = Arguments( vararg=None, kwarg=None, parent=new_func, @@ -50,4 +50,4 @@ def transform(node: astroid.ClassDef, *args: Any, **kwargs: Any) -> None: node.parent.locals[public_method.name] = [new_func] -MANAGER.register_transform(astroid.ClassDef, transform) +MANAGER.register_transform(ClassDef, transform) diff --git a/tests/test_genie.py b/tests/test_genie.py index 54b58b80..dba955fc 100644 --- a/tests/test_genie.py +++ b/tests/test_genie.py @@ -34,9 +34,9 @@ ) invalid_module_msg = ( - f"Cannot load script test as its name clashes with a standard python module " + f"Cannot load script 'test' as its name clashes with a standard python module " f"or with a module accessible elsewhere on the python path.\nThe conflicting " - f"module was at '{test.__file__}'.\nIf this is a user script, rename the " + f"module was '{test}'.\nIf this is a user script, rename the " f"user script to avoid the clash." ) @@ -60,9 +60,11 @@ def test_GIVEN_script_uses_module_name_WHEN_load_script_THEN_error(self): script = os.path.join(os.path.abspath(os.path.dirname(__file__)), "test_scripts", "test.py") # Act - with self.assertRaises(ValueError, msg=invalid_module_msg): + with self.assertRaises(ValueError) as ex: genie.load_script(script) + self.assertEqual(str(ex.exception), invalid_module_msg) + def test_GIVEN_valid_script_WHEN_load_script_THEN_can_call_script(self): # Arrange script = os.path.join( diff --git a/tests/test_script_checker.py b/tests/test_script_checker.py index def1db33..85b7f755 100644 --- a/tests/test_script_checker.py +++ b/tests/test_script_checker.py @@ -24,7 +24,7 @@ import tempfile import unittest -import astroid +from astroid.nodes import Module from genie_python.genie_epics_api import API from genie_python.genie_script_checker import ScriptChecker @@ -405,17 +405,17 @@ def test_GIVEN_builtin_module_THEN_it_can_safely_be_cached(self): def test_GIVEN_site_packages_module_THEN_it_can_safely_be_cached(self): import numpy - mod = astroid.Module(numpy.__name__, numpy.__file__) + mod = Module(numpy.__name__, numpy.__file__) self.assertTrue(self.checker._can_cache_module("numpy", mod)) def test_GIVEN_user_script_THEN_it_should_not_be_cached(self): name = "my_user_script" - mod = astroid.Module(name, os.path.join("C:\\", "scripts", "my_user_script.py")) + mod = Module(name, os.path.join("C:\\", "scripts", "my_user_script.py")) self.assertFalse(self.checker._can_cache_module(name, mod)) def test_GIVEN_instrument_script_THEN_it_should_not_be_cached(self): name = "my_inst_script" - mod = astroid.Module( + mod = Module( name, os.path.join( "C:\\",