diff --git a/meny/casehandlers.py b/meny/casehandlers.py index a7c8250..539d7d5 100644 --- a/meny/casehandlers.py +++ b/meny/casehandlers.py @@ -1,15 +1,14 @@ -from abc import abstractclassmethod -from typing import Any, Dict, Iterable, List, Optional +from abc import abstractclassmethod, abstractmethod +from typing import Any, Sequence, List import meny from meny.exceptions import MenuError from ast import literal_eval from inspect import getfullargspec, unwrap from types import FunctionType from meny.infos import _error_info_case -from meny.exceptions import MenuError -def _handle_args(func: FunctionType, args: Iterable[str]) -> List: +def _handle_args(func: FunctionType, args: Sequence[str]) -> List: """ Handles list of strings that are the arguments using ast.literal_eval. @@ -24,7 +23,9 @@ def _handle_args(func: FunctionType, args: Iterable[str]) -> List: if len(args) > len(params): raise MenuError(f"Got too many arguments, should be {len(params)}, but got {len(args)}") - typed_arglist = [None] * len(args) + typed_arglist: List[str | None] = [None] * len(args) + i = 0 + arg = None try: for i, (param, arg) in enumerate(zip(params, args)): if argsspec.annotations.get(param, None) == str: @@ -32,20 +33,16 @@ def _handle_args(func: FunctionType, args: Iterable[str]) -> List: else: typed_arglist[i] = literal_eval(arg) except (ValueError, SyntaxError) as e: - raise MenuError( - f"Got arguments: {args}\n" f"But could not evaluate argument at position {i}:\n\t {arg}" - ) from e + raise MenuError(f"Got arguments: {args}\n" f"But could not evaluate argument at position {i}:\n\t {arg}") from e return typed_arglist def _handle_casefunc(casefunc: FunctionType, args: List[str], menu: meny.Menu) -> Any: - program_args = menu.case_args.get(casefunc, ()) - program_kwargs = menu.case_kwargs.get(casefunc, {}) + program_args = (menu.case_args or {}).get(casefunc, ()) + program_kwargs = (menu.case_kwargs or {}).get(casefunc, {}) if program_args or program_kwargs: # If programmatic arguments if args: - raise MenuError( - "This function takes arguments progammatically" " and should not be given any arguments" - ) + raise MenuError("This function takes arguments progammatically" " and should not be given any arguments") return casefunc(*program_args, **program_kwargs) elif args: # Raises TypeError if wrong number of arguments @@ -67,7 +64,8 @@ def __call__(cls, menu: meny.Menu, casefunc: FunctionType, args: List[str]) -> N finally: cls.afterCallReturn(menu, casefunc, args) - @abstractclassmethod + @classmethod + @abstractmethod def onCall(cls, menu: meny.Menu, casefunc: FunctionType, args: List[str]) -> None: """ Responsibility: @@ -75,7 +73,8 @@ def onCall(cls, menu: meny.Menu, casefunc: FunctionType, args: List[str]) -> Non Do whatever else to enforce handler behavior (related to unittests) """ - @abstractclassmethod + @classmethod + @abstractmethod def afterCallReturn(cls, menu: meny.Menu, casefunc: FunctionType, args: List[str]) -> None: """ Responsibility: diff --git a/meny/cli.py b/meny/cli.py index 8aa8296..ef3a993 100644 --- a/meny/cli.py +++ b/meny/cli.py @@ -11,7 +11,6 @@ import string import re import json -import subprocess import shutil import platform import signal @@ -31,6 +30,8 @@ def load_module_from_path(path: Path): sys.path.append(str(Path(path).parent)) loader = importlib.machinery.SourceFileLoader(f"__meny_module_{path.stem}", str(path)) spec = importlib.util.spec_from_loader(loader.name, loader) + if spec is None: + raise ImportError(f"Could not load {path}") module = importlib.util.module_from_spec(spec) loader.exec_module(module) sys.path.pop() @@ -58,14 +59,14 @@ def menu_from_python_code(filepath: Path, repeat: bool): class MenyTemplate(string.Template): default_arg = r"[\w ]*" delimiter = "@" - pattern = fr""" + pattern = rf""" @(?: (?P@) | # Escape sequence of two delimiters (?P\w+) | # delimiter and a Python identifier {{(?P\w+=?{default_arg})}} | # delimiter and a braced identifier (?P) # Other ill-formed delimiter exprs ) - """ + """ # type: ignore def get_casefunc(command: str, executable: str): @@ -95,7 +96,6 @@ def get_casefunc(command: str, executable: str): args = ", ".join(arg_components) # Remove default argument from command string - template = MenyTemplate(re.sub(fr"@{{(\w+)={MenyTemplate.default_arg}}}", r"@{\1}", command)) if executable is not None: executable = f"'{executable}'" @@ -158,13 +158,17 @@ def cli(): sys.exit(1) try: - signal.signal(signal.SIGINT, lambda *args, **kwargs: None) + signal.signal(signal.SIGINT, lambda *__args__, **__kwargs__: None) if filepath.suffix == ".json": if platform.system() == "Windows": executable = shutil.which("powershell") else: executable = shutil.which("bash") - executable = Path(executable).as_posix() # Need this or will crash in windows due to backslash stuff + + if not executable: + executable = "sh" + + executable = Path(executable).as_posix() # Need this or will crash in windows due to backslash stuff returnDict = menu_from_json(filepath, args.repeat, args.executable or executable) else: returnDict = menu_from_python_code(filepath, args.repeat) diff --git a/meny/menu.py b/meny/menu.py index f590741..d622fba 100644 --- a/meny/menu.py +++ b/meny/menu.py @@ -2,17 +2,19 @@ Contains the command line interface (CLI) class, along its factory function: menu() """ + +from importlib.util import find_spec from time import sleep from types import FunctionType, ModuleType -from typing import Any, Callable, Dict, Iterable, List, Optional, Union +from typing import Any, Callable, Dict, Iterable, List, Optional, Union, Sequence from meny import config as cng from meny import strings from meny.funcmap import construct_funcmap from meny.utils import ( _assert_supported, - _extract_and_preprocess_functions, - _get_module_cases, + extract_and_preprocess_functions, + get_module_cases, input_splitter, clear_screen, ) @@ -21,7 +23,7 @@ import os -def raise_interrupt(*args, **kwargs) -> None: +def raise_interrupt(*__args__, **__kwargs__) -> None: """ Raises keyboard interrupt """ @@ -130,12 +132,8 @@ def __init__( if frontend == "auto": self._frontend = _menu_simple - try: - import curses - + if find_spec("curses"): self._frontend = _menu_curses - except ImportError: - pass elif frontend == "fancy": self._frontend = _menu_curses @@ -236,13 +234,12 @@ def run(self) -> Dict: Menu._depth -= 1 if Menu._depth == 0: Menu._return_mode = None - self._case_handler = None return Menu._return or {} def build_menu( - cases: Union[Iterable[FunctionType], Dict[str, FunctionType], ModuleType], + cases: Union[Sequence[FunctionType], Dict[str, FunctionType], ModuleType], title: Optional[str] = None, *, case_args: Optional[Dict[FunctionType, tuple]] = None, @@ -260,9 +257,9 @@ def build_menu( Returns Menu object """ if isinstance(cases, ModuleType): - cases_to_send = _get_module_cases(cases) + cases_to_send = get_module_cases(cases) elif isinstance(cases, dict): - cases_to_send = _extract_and_preprocess_functions(cases) + cases_to_send = extract_and_preprocess_functions(cases) # If this menu is the first menu initialized, and is given the locally # defined functions, then must filter the functions that are defined # in __main__ @@ -271,13 +268,13 @@ def build_menu( if moduleName == "__main__": cases_to_send = [case for case in cases_to_send if case.__module__ == "__main__"] - elif isinstance(cases, Iterable): + elif isinstance(cases, Sequence): # Looks kinda stupid, but it reuses the code, which is nice - cases_to_send = _extract_and_preprocess_functions({case.__name__: case for case in cases}) + cases_to_send = extract_and_preprocess_functions({case.__name__: case for case in cases}) else: raise TypeError(f"Invalid type for cases, got: {type(cases)}") - cases_to_send: Iterable[FunctionType] + cases_to_send: Sequence[FunctionType] cases_to_send = filter(lambda case: cng._CASE_IGNORE not in vars(case), cases_to_send) diff --git a/meny/utils.py b/meny/utils.py index 2bf137f..a83101d 100644 --- a/meny/utils.py +++ b/meny/utils.py @@ -14,7 +14,7 @@ RE_ANSI = re.compile(r"\x1b\[[;\d]*[A-Za-z]") # Taken from tqdm source code, matches escape codes -RE_INPUT = re.compile("[\w.-]+|\[.*?\]|\{.*?\}|\(.*?\)|\".*?\"|'.*?'") +RE_INPUT = re.compile(r"[\w.-]+|\[.*?\]|\{.*?\}|\(.*?\)|\".*?\"|'.*?'") def _assert_supported(arg: Any, paramname: str, supported: Container): @@ -27,7 +27,7 @@ def _assert_supported(arg: Any, paramname: str, supported: Container): AssertionError: Got unsupported argument for parameter "animal". Available options are: ('dog', 'rabbit') """ assert arg in supported, ( - f'Got unsupported argument "' + 'Got unsupported argument "' + strings.YELLOW + str(arg) + strings.END @@ -74,7 +74,7 @@ def clear_screen() -> None: os.system(_CLEAR_COMMAND) -def _extract_and_preprocess_functions(dict_: Dict[str, FunctionType]) -> List[FunctionType]: +def extract_and_preprocess_functions(dict_: Dict[str, FunctionType]) -> List[FunctionType]: """ Parameters ------------- @@ -105,7 +105,7 @@ def input_splitter(argstring: str) -> List[str]: return RE_INPUT.findall(argstring) -def _get_module_cases(module: ModuleType) -> List[FunctionType]: +def get_module_cases(module: ModuleType) -> List[FunctionType]: """Get all functions defined in module""" def inModule(f): moduleOfF = getmodule(f) diff --git a/tests/test.py b/tests/test.py index defbd61..3a4d40c 100644 --- a/tests/test.py +++ b/tests/test.py @@ -29,7 +29,7 @@ def func2(): def func3(): pass - cases = meny.utils._extract_and_preprocess_functions(locals()) + cases = meny.utils.extract_and_preprocess_functions(locals()) self.assertListEqual(cases, [linear, quadratic, cubic, func1, func2, func3]) def test_input_splitter(self):