diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 000000000..3d525c1c8 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,38 @@ +# Instructions for Gemini CLI in a `uv` Python project + +This `GEMINI.md` file provides context and instructions for the Gemini CLI when working with this +Python project, which utilizes `uv` for environment and package management. + +## General Instructions + +- **Environment Management:** Prefer using `uv` for all Python environment management tasks. +- **Package Installation:** Always use `uv` to install packages and ensure they are installed within + the project's virtual environment. +- **Running Scripts/Commands:** + - To run Python scripts within the project's virtual environment, use `uv run ...`. + - To run programs directly from a PyPI package (installing it on the fly if necessary), use + `uvx ...` (shortcut for `uv tool run`). +- **New Dependencies:** If a new dependency is required, please state the reason for its inclusion. + +## Python Code Standards + +To ensure Python code adheres to required standards, the following commands **must** be run before +creating or modifying any `.py` files: + +```bash +make check +``` + +To run unit tests use the following command: + +```bash +make test +``` + +To make sure the documentation builds properly, use the following command: + +```bash +make docs-test +``` + +All 3 of the above commands should be run prior to committing code. diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index 92dc6b0d3..5fc484608 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -1,4 +1,4 @@ -"""Module efines the ArgparseCompleter class which provides argparse-based tab completion to cmd2 apps. +"""Module defines the ArgparseCompleter class which provides argparse-based tab completion to cmd2 apps. See the header of argparse_custom.py for instructions on how to use these features. """ @@ -533,7 +533,7 @@ def _format_completions(self, arg_state: _ArgumentState, completions: list[str] if not self._cmd2_app.matches_sorted: # If all orig_value types are numbers, then sort by that value if all_nums: - completion_items.sort(key=lambda c: c.orig_value) # type: ignore[no-any-return] + completion_items.sort(key=lambda c: c.orig_value) # Otherwise sort as strings else: @@ -726,12 +726,12 @@ def _complete_arg( if not arg_choices.is_completer: choices_func = arg_choices.choices_provider if isinstance(choices_func, ChoicesProviderFuncWithTokens): - completion_items = choices_func(*args, **kwargs) # type: ignore[arg-type] + completion_items = choices_func(*args, **kwargs) else: # pragma: no cover # This won't hit because runtime checking doesn't check function argument types and will always # resolve true above. Mypy, however, does see the difference and gives an error that can't be # ignored. Mypy issue #5485 discusses this problem - completion_items = choices_func(*args) # type: ignore[arg-type] + completion_items = choices_func(*args) # else case is already covered above else: completion_items = arg_choices diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index f3e5344b2..4f0e99f60 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -1433,7 +1433,7 @@ def __init__( description=description, # type: ignore[arg-type] epilog=epilog, # type: ignore[arg-type] parents=parents if parents else [], - formatter_class=formatter_class, # type: ignore[arg-type] + formatter_class=formatter_class, prefix_chars=prefix_chars, fromfile_prefix_chars=fromfile_prefix_chars, argument_default=argument_default, @@ -1498,7 +1498,7 @@ def format_help(self) -> str: formatter = self._get_formatter() # usage - formatter.add_usage(self.usage, self._actions, self._mutually_exclusive_groups) # type: ignore[arg-type] + formatter.add_usage(self.usage, self._actions, self._mutually_exclusive_groups) # description formatter.add_text(self.description) diff --git a/cmd2/clipboard.py b/cmd2/clipboard.py index 284d57df5..4f78925cf 100644 --- a/cmd2/clipboard.py +++ b/cmd2/clipboard.py @@ -2,7 +2,7 @@ import typing -import pyperclip # type: ignore[import] +import pyperclip # type: ignore[import-untyped] def get_paste_buffer() -> str: diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 52be358e5..87c9ce1d2 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -137,7 +137,7 @@ # NOTE: When using gnureadline with Python 3.13, start_ipython needs to be imported before any readline-related stuff with contextlib.suppress(ImportError): - from IPython import start_ipython # type: ignore[import] + from IPython import start_ipython from .rl_utils import ( RlType, @@ -163,7 +163,7 @@ if rl_type == RlType.NONE: # pragma: no cover Cmd2Console(sys.stderr).print(rl_warning, style=Cmd2Style.WARNING) else: - from .rl_utils import ( # type: ignore[attr-defined] + from .rl_utils import ( readline, rl_force_redisplay, ) @@ -1068,7 +1068,7 @@ def _unregister_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None: for action in command_parser._actions: if isinstance(action, argparse._SubParsersAction): - action.remove_parser(subcommand_name) # type: ignore[arg-type,attr-defined] + action.remove_parser(subcommand_name) # type: ignore[attr-defined] break @property @@ -3094,11 +3094,11 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState: kwargs['executable'] = shell # For any stream that is a StdSim, we will use a pipe so we can capture its output - proc = subprocess.Popen( # type: ignore[call-overload] # noqa: S602 + proc = subprocess.Popen( # noqa: S602 statement.pipe_to, stdin=subproc_stdin, stdout=subprocess.PIPE if isinstance(self.stdout, utils.StdSim) else self.stdout, # type: ignore[unreachable] - stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr, # type: ignore[unreachable] + stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr, shell=True, **kwargs, ) @@ -3115,7 +3115,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState: subproc_stdin.close() new_stdout.close() raise RedirectionError(f'Pipe process exited with code {proc.returncode} before command could run') - redir_saved_state.redirecting = True # type: ignore[unreachable] + redir_saved_state.redirecting = True cmd_pipe_proc_reader = utils.ProcReader(proc, cast(TextIO, self.stdout), sys.stderr) self.stdout = new_stdout @@ -3351,7 +3351,7 @@ def complete_none(text: str, state: int) -> str | None: # pragma: no cover # n parser.add_argument( 'arg', suppress_tab_hint=True, - choices=choices, # type: ignore[arg-type] + choices=choices, choices_provider=choices_provider, completer=completer, ) @@ -4435,7 +4435,7 @@ def complete_set_value( arg_name, metavar=arg_name, help=settable.description, - choices=settable.choices, # type: ignore[arg-type] + choices=settable.choices, choices_provider=settable.choices_provider, completer=settable.completer, ) @@ -4566,15 +4566,15 @@ def do_shell(self, args: argparse.Namespace) -> None: # still receive the SIGINT since it is in the same process group as us. with self.sigint_protection: # For any stream that is a StdSim, we will use a pipe so we can capture its output - proc = subprocess.Popen( # type: ignore[call-overload] # noqa: S602 + proc = subprocess.Popen( # noqa: S602 expanded_command, stdout=subprocess.PIPE if isinstance(self.stdout, utils.StdSim) else self.stdout, # type: ignore[unreachable] - stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr, # type: ignore[unreachable] + stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr, shell=True, **kwargs, ) - proc_reader = utils.ProcReader(proc, cast(TextIO, self.stdout), sys.stderr) # type: ignore[arg-type] + proc_reader = utils.ProcReader(proc, cast(TextIO, self.stdout), sys.stderr) proc_reader.wait() # Save the return code of the application for use in a pyscript @@ -4656,9 +4656,9 @@ def _set_up_py_shell_env(self, interp: InteractiveConsole) -> _SavedCmd2Env: # Save off the current completer and set a new one in the Python console # Make sure it tab completes from its locals() dictionary cmd2_env.readline_settings.completer = readline.get_completer() - interp.runcode("from rlcompleter import Completer") # type: ignore[arg-type] - interp.runcode("import readline") # type: ignore[arg-type] - interp.runcode("readline.set_completer(Completer(locals()).complete)") # type: ignore[arg-type] + interp.runcode(compile("from rlcompleter import Completer", "", "exec")) + interp.runcode(compile("import readline", "", "exec")) + interp.runcode(compile("readline.set_completer(Completer(locals()).complete)", "", "exec")) # Set up sys module for the Python console self._reset_py_display() @@ -4889,18 +4889,18 @@ def do_ipy(self, _: argparse.Namespace) -> bool | None: # pragma: no cover # Detect whether IPython is installed try: - import traitlets.config.loader as traitlets_loader # type: ignore[import] + import traitlets.config.loader as traitlets_loader # Allow users to install ipython from a cmd2 prompt when needed and still have ipy command work try: _dummy = start_ipython # noqa: F823 except NameError: - from IPython import start_ipython # type: ignore[import] + from IPython import start_ipython - from IPython.terminal.interactiveshell import ( # type: ignore[import] + from IPython.terminal.interactiveshell import ( TerminalInteractiveShell, ) - from IPython.terminal.ipapp import ( # type: ignore[import] + from IPython.terminal.ipapp import ( TerminalIPythonApp, ) except ImportError: diff --git a/cmd2/decorators.py b/cmd2/decorators.py index cae1b399a..de4bc2e50 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -180,7 +180,7 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> bool | None: cmd2_app, statement = _parse_positionals(args) _, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(command_name, statement, preserve_quotes) args_list = _arg_swap(args, statement, parsed_arglist) - return func(*args_list, **kwargs) # type: ignore[call-arg] + return func(*args_list, **kwargs) command_name = func.__name__[len(constants.COMMAND_FUNC_PREFIX) :] cmd_wrapper.__doc__ = func.__doc__ @@ -336,7 +336,7 @@ def cmd_wrapper(*args: Any, **kwargs: dict[str, Any]) -> bool | None: delattr(ns, constants.NS_ATTR_SUBCMD_HANDLER) args_list = _arg_swap(args, statement_arg, *new_args) - return func(*args_list, **kwargs) # type: ignore[call-arg] + return func(*args_list, **kwargs) command_name = func.__name__[len(constants.COMMAND_FUNC_PREFIX) :] diff --git a/cmd2/parsing.py b/cmd2/parsing.py index 8a6acb08f..5dca03d3b 100644 --- a/cmd2/parsing.py +++ b/cmd2/parsing.py @@ -81,7 +81,7 @@ class Macro: @dataclass(frozen=True) -class Statement(str): # type: ignore[override] # noqa: SLOT000 +class Statement(str): # noqa: SLOT000 """String subclass with additional attributes to store the results of parsing. The ``cmd`` module in the standard library passes commands around as a diff --git a/cmd2/rl_utils.py b/cmd2/rl_utils.py index b6ae824c1..29d8b4e5a 100644 --- a/cmd2/rl_utils.py +++ b/cmd2/rl_utils.py @@ -24,11 +24,11 @@ # Prefer statically linked gnureadline if installed due to compatibility issues with libedit try: - import gnureadline as readline # type: ignore[import] + import gnureadline as readline # type: ignore[import-not-found] except ImportError: # Note: If this actually fails, you should install gnureadline on Linux/Mac or pyreadline3 on Windows. with contextlib.suppress(ImportError): - import readline # type: ignore[no-redef] + import readline class RlType(Enum): @@ -279,7 +279,7 @@ def rl_in_search_mode() -> bool: # pragma: no cover readline_state = ctypes.c_int.in_dll(readline_lib, "rl_readline_state").value return bool(in_search_mode & readline_state) if rl_type == RlType.PYREADLINE: - from pyreadline3.modes.emacs import ( # type: ignore[import] + from pyreadline3.modes.emacs import ( # type: ignore[import-not-found] EmacsMode, ) @@ -294,3 +294,8 @@ def rl_in_search_mode() -> bool: # pragma: no cover ) return readline.rl.mode.process_keyevent_queue[-1] in search_funcs return False + + +__all__ = [ + 'readline', +] diff --git a/cmd2/utils.py b/cmd2/utils.py index bf4d14864..685084432 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -697,7 +697,7 @@ def do_echo(self, arglist): for item in func: setattr(item, constants.CMD_ATTR_HELP_CATEGORY, category) elif inspect.ismethod(func): - setattr(func.__func__, constants.CMD_ATTR_HELP_CATEGORY, category) # type: ignore[attr-defined] + setattr(func.__func__, constants.CMD_ATTR_HELP_CATEGORY, category) else: setattr(func, constants.CMD_ATTR_HELP_CATEGORY, category) @@ -716,7 +716,7 @@ def get_defining_class(meth: Callable[..., Any]) -> type[Any] | None: if inspect.ismethod(meth) or ( inspect.isbuiltin(meth) and hasattr(meth, '__self__') and hasattr(meth.__self__, '__class__') ): - for cls in inspect.getmro(meth.__self__.__class__): # type: ignore[attr-defined] + for cls in inspect.getmro(meth.__self__.__class__): if meth.__name__ in cls.__dict__: return cls meth = getattr(meth, '__func__', meth) # fallback to __qualname__ parsing