diff --git a/pyocd/core/session.py b/pyocd/core/session.py index 4618be9b6..babac8d59 100644 --- a/pyocd/core/session.py +++ b/pyocd/core/session.py @@ -1,6 +1,6 @@ # pyOCD debugger # Copyright (c) 2018-2020 Arm Limited -# Copyright (c) 2021-2022 Chris Reed +# Copyright (c) 2021-2023 Chris Reed # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,16 +15,20 @@ # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations + from contextlib import contextmanager import logging import logging.config import yaml import os from pathlib import Path +import sys import weakref from inspect import (getfullargspec, signature) from types import SimpleNamespace from typing import (Any, Callable, Generator, Sequence, Union, cast, Dict, List, Mapping, Optional, TYPE_CHECKING) +from typing_extensions import Self from . import exceptions from .options_manager import OptionsManager @@ -38,6 +42,9 @@ from ..gdbserver.gdbserver import GDBServer from ..board.board import Board +# Check whether the eval_str parameter for inspect.signature is available. +HAS_SIGNATURE_EVAL_STR = (sys.version_info[:2] >= (3, 10)) + LOG = logging.getLogger(__name__) ## @brief Set of default config filenames to search for. @@ -95,7 +102,7 @@ class Session(Notifier): _current_session: Optional[weakref.ref] = None @classmethod - def get_current(cls) -> "Session": + def get_current(cls) -> Self: """@brief Return the most recently created Session instance or a default Session. By default this method will return the most recently created Session object that is @@ -111,11 +118,11 @@ def get_current(cls) -> "Session": if session is not None: return session - return Session(None) + return cls(None) def __init__( self, - probe: Optional["DebugProbe"], + probe: Optional[DebugProbe], auto_open: bool = True, options: Optional[Mapping[str, Any]] = None, option_defaults: Optional[Mapping[str, Any]] = None, @@ -159,8 +166,8 @@ def __init__( self._delegate: Optional[Any] = None self._auto_open = auto_open self._options = OptionsManager() - self._gdbservers: Dict[int, "GDBServer"] = {} - self._probeserver: Optional["DebugProbeServer"] = None + self._gdbservers: Dict[int, GDBServer] = {} + self._probeserver: Optional[DebugProbeServer] = None self._context_state = SimpleNamespace() # Set this session on the probe, if we were given a probe. @@ -314,17 +321,17 @@ def is_open(self) -> bool: return self._inited and not self._closed @property - def probe(self) -> Optional["DebugProbe"]: + def probe(self) -> Optional[DebugProbe]: """@brief The @ref pyocd.probe.debug_probe.DebugProbe "DebugProbe" instance.""" return self._probe @property - def board(self) -> Optional["Board"]: + def board(self) -> Optional[Board]: """@brief The @ref pyocd.board.board.Board "Board" object.""" return self._board @property - def target(self) -> Optional["SoCTarget"]: + def target(self) -> Optional[SoCTarget]: """@brief The @ref pyocd.core.target.soc_target "SoCTarget" object representing the SoC. This is the @ref pyocd.core.target.soc_target "SoCTarget" instance owned by the board. @@ -352,7 +359,7 @@ def delegate(self, new_delegate: Any) -> None: self._delegate = new_delegate @property - def user_script_proxy(self) -> "UserScriptDelegateProxy": + def user_script_proxy(self) -> UserScriptDelegateProxy: """@brief The UserScriptDelegateProxy object for a loaded user script.""" # Create a proxy if there isn't already one. This is a fallback in case there isn't a user script, # yet a Python $-command is executed and needs the user script namespace in which to run. @@ -363,21 +370,21 @@ def user_script_proxy(self) -> "UserScriptDelegateProxy": return self._user_script_proxy @property - def user_script_print_proxy(self) -> "PrintProxy": + def user_script_print_proxy(self) -> PrintProxy: return self._user_script_print_proxy @property - def gdbservers(self) -> Dict[int, "GDBServer"]: + def gdbservers(self) -> Dict[int, GDBServer]: """@brief Dictionary of core numbers to @ref pyocd.gdbserver.gdbserver.GDBServer "GDBServer" instances.""" return self._gdbservers @property - def probeserver(self) -> Optional["DebugProbeServer"]: + def probeserver(self) -> Optional[DebugProbeServer]: """@brief A @ref pyocd.probe.tcp_probe_server.DebugProbeServer "DebugProbeServer" instance.""" return self._probeserver @probeserver.setter - def probeserver(self, server: "DebugProbeServer") -> None: + def probeserver(self, server: DebugProbeServer) -> None: """@brief Setter for the `probeserver` property.""" self._probeserver = server @@ -405,7 +412,7 @@ def __enter__(self) -> "Session": raise return self - def __exit__(self, exc_type: type, value: Any, traceback: "TracebackType") -> bool: + def __exit__(self, exc_type: type, value: Any, traceback: TracebackType) -> bool: self.close() return False @@ -642,7 +649,10 @@ def _command_decorator(fn: Callable): classname = names_list[0].capitalize() + "Command" # Examine the command function's signature to extract arguments and their types. - sig = signature(fn) + if HAS_SIGNATURE_EVAL_STR: + sig = signature(fn, eval_str=True) + else: + sig = signature(fn) arg_converters = [] has_var_args = False usage_fields: List[str] = [] @@ -663,19 +673,35 @@ def _command_decorator(fn: Callable): if typ is parm.empty: LOG.error("user command function '%s' is missing type annotation for parameter '%s'", fn.__name__, parm.name) - return fn + return None + + # If we don't have Python 3.10 or later, then we must manually un-stringize the type. + # Using eval() to un-stringize won't work in all cases, but is sufficient for the types + # supported by pyocd's commands. + if not HAS_SIGNATURE_EVAL_STR: + try: + typ = eval(typ, fn.__globals__) + except Exception: + LOG.error("parameter '%s' of user command function '%s' has an unsupported type", + parm.name, fn.__name__) + return None # Otherwise add to param converter list. - if issubclass(typ, str): - arg_converters.append(lambda _, x: x) - elif issubclass(typ, float): - arg_converters.append(lambda _, x: float(x)) - elif issubclass(typ, int): - arg_converters.append(CommandBase._convert_value) - else: + try: + if issubclass(typ, str): + arg_converters.append(lambda _, x: x) + elif issubclass(typ, float): + arg_converters.append(lambda _, x: float(x)) + elif issubclass(typ, int): + arg_converters.append(CommandBase._convert_value) + else: + LOG.error("parameter '%s' of user command function '%s' has an unsupported type", + parm.name, fn.__name__) + return None + except TypeError: LOG.error("parameter '%s' of user command function '%s' has an unsupported type", parm.name, fn.__name__) - return fn + return None usage_fields.append(parm.name.upper()) # parse() method of the new command class.