Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/lint_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ classifiers = [
"Intended Audience :: Developers",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]

dependencies = [
Expand Down Expand Up @@ -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
Expand Down
30 changes: 20 additions & 10 deletions src/genie_python/genie.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions src/genie_python/genie_api_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'.
Expand Down
12 changes: 6 additions & 6 deletions src/genie_python/scanning_instrument_pylint_plugin.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand All @@ -22,16 +22,16 @@ 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,
parent=node.parent,
end_lineno=0,
end_col_offset=0,
)
arguments = astroid.Arguments(
arguments = Arguments(
vararg=None,
kwarg=None,
parent=new_func,
Expand All @@ -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)
8 changes: 5 additions & 3 deletions tests/test_genie.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."
)

Expand All @@ -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(
Expand Down
8 changes: 4 additions & 4 deletions tests/test_script_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:\\",
Expand Down
Loading