Skip to content
Open
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/typecheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ jobs:
python-version: ${{ matrix.python-version }}

- name: Check typing
run: uv run mypy .
run: uv run ty check
18 changes: 9 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ check: ## Run code quality tools.
@uv lock --locked
@echo "🚀 Auto-formatting/Linting code and documentation: Running prek"
@uv run prek run -a
@echo "🚀 Static type checking: Running mypy"
@uv run mypy
@echo "🚀 Static type checking: Running ty"
@uv run ty check

.PHONY: format
format: ## Perform ruff formatting
Expand All @@ -31,7 +31,7 @@ lint: ## Perform ruff linting

.PHONY: typecheck
typecheck: ## Perform type checking
@uv run mypy
@uv run ty check

.PHONY: test
test: ## Test the code with pytest.
Expand Down Expand Up @@ -76,7 +76,7 @@ publish: validate-tag build ## Publish a release to PyPI, uses token from ~/.pyp
# Define variables for files/directories to clean
BUILD_DIRS = build dist *.egg-info
DOC_DIRS = build
MYPY_DIRS = .mypy_cache dmypy.json dmypy.sock
TY_DIRS = .ty_cache .red_knot_cache
TEST_DIRS = .cache .pytest_cache htmlcov
TEST_FILES = .coverage coverage.xml

Expand All @@ -90,10 +90,10 @@ clean-docs: ## Clean documentation artifacts
@echo "🚀 Removing documentation artifacts"
@uv run python -c "import shutil; import os; [shutil.rmtree(d, ignore_errors=True) for d in '$(DOC_DIRS)'.split() if os.path.isdir(d)]"

.PHONY: clean-mypy
clean-mypy: ## Clean mypy artifacts
@echo "🚀 Removing mypy artifacts"
@uv run python -c "import shutil; import os; [shutil.rmtree(d, ignore_errors=True) for d in '$(MYPY_DIRS)'.split() if os.path.isdir(d)]"
.PHONY: clean-ty
clean-ty: ## Clean ty artifacts
@echo "🚀 Removing ty artifacts"
@uv run python -c "import shutil; import os; [shutil.rmtree(d, ignore_errors=True) for d in '$(TY_DIRS)'.split() if os.path.isdir(d)]"

.PHONY: clean-pycache
clean-pycache: ## Clean pycache artifacts
Expand All @@ -112,7 +112,7 @@ clean-test: ## Clean test artifacts
@uv run python -c "from pathlib import Path; [Path(f).unlink(missing_ok=True) for f in '$(TEST_FILES)'.split()]"

.PHONY: clean
clean: clean-build clean-docs clean-mypy clean-pycache clean-ruff clean-test ## Clean all artifacts
clean: clean-build clean-docs clean-ty clean-pycache clean-ruff clean-test ## Clean all artifacts
@echo "🚀 Cleaned all artifacts"

.PHONY: help
Expand Down
11 changes: 7 additions & 4 deletions cmd2/annotated.py
Original file line number Diff line number Diff line change
Expand Up @@ -708,10 +708,10 @@ def __init__(self, *args: Any, container_factory: Callable[[list[Any]], Any] | N

def __call__(
self,
_parser: argparse.ArgumentParser,
parser: argparse.ArgumentParser, # noqa: ARG002
namespace: argparse.Namespace,
values: Any,
_option_string: str | None = None,
option_string: str | None = None, # noqa: ARG002
) -> None:
result = values
if self._container_factory is not None and isinstance(values, list):
Expand Down Expand Up @@ -879,7 +879,7 @@ def _resolve_union(
raise TypeError(f"Union type {type_names} is ambiguous for auto-resolution.")

parts = [_resolve_base_type(member, allow_unknown_entry=allow_unknown_entry) for member in non_none]
# Every part is an Enum (guarded above), so each has a converter; the None-filter keeps mypy happy.
# Every part is an Enum (guarded above), so each has a converter; the None-filter keeps the type checker happy.
converters = [part.converter for part in parts if part.converter is not None]
choices = _dedupe_choices(choice for part in parts for choice in (part.choices or []))

Expand Down Expand Up @@ -2189,7 +2189,7 @@ def _find_argument_block(hint: Any) -> type[ArgumentBlock] | None:
return None


def _init_field_names(dc_type: type) -> list[str]:
def _init_field_names(dc_type: Any) -> list[str]:
"""Names of a dataclass's ``init`` fields in definition order (the flat argument names of a block)."""
return [f.name for f in fields(dc_type) if f.init]

Expand Down Expand Up @@ -3113,6 +3113,9 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> bool | None:
except SystemExit as exc:
raise Cmd2ArgparseError from exc

if ns is None:
raise ValueError("ns is None")

setattr(ns, constants.NS_ATTR_STATEMENT, statement)
handler = getattr(ns, constants.NS_ATTR_SUBCOMMAND_FUNC, None)
if base_command and handler is not None:
Expand Down
13 changes: 7 additions & 6 deletions cmd2/argparse_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -747,8 +747,8 @@ def __init__(
super().__init__(
prog=prog,
usage=usage,
description=description, # type: ignore[arg-type]
epilog=epilog, # type: ignore[arg-type]
description=description, # type: ignore[arg-type, ty:invalid-argument-type]
epilog=epilog, # type: ignore[arg-type, ty:invalid-argument-type]
parents=parents,
formatter_class=formatter_class,
prefix_chars=prefix_chars,
Expand All @@ -772,15 +772,15 @@ def __init__(
self.description: HelpContent | None # type: ignore[assignment]
self.epilog: HelpContent | None # type: ignore[assignment]

def print_usage(self, file: IO[str] | None = None) -> None: # type:ignore[override]
def print_usage(self, file: IO[str] | None = None) -> None: # type: ignore[override, ty:invalid-method-override]
"""Override to ensure the formatter is aware of the target file."""
if file is None:
file = self._thread_locals.current_output_file

with self.output_to(file):
super().print_usage(file)

def print_help(self, file: IO[str] | None = None) -> None: # type:ignore[override]
def print_help(self, file: IO[str] | None = None) -> None: # type: ignore[override, ty:invalid-method-override]
"""Override to ensure the formatter is aware of the target file."""
if file is None:
file = self._thread_locals.current_output_file
Expand Down Expand Up @@ -831,7 +831,7 @@ def _build_subparsers_prog_prefix(self, positionals: list[argparse.Action]) -> s
temp_parser = Cmd2ArgumentParser(
prog=self.prog,
usage=None,
formatter_class=self.formatter_class,
formatter_class=cast(type[Cmd2HelpFormatter], self.formatter_class),
add_help=False,
)

Expand Down Expand Up @@ -1037,7 +1037,8 @@ def error(self, message: str) -> NoReturn:

def _get_formatter(self, *_args: Any, **_kwargs: Any) -> Cmd2HelpFormatter:
"""Override with customizations for Cmd2HelpFormatter."""
return self.formatter_class(prog=self.prog, file=self._thread_locals.current_output_file)
formatter_class = cast(type[Cmd2HelpFormatter], self.formatter_class)
return formatter_class(prog=self.prog, file=self._thread_locals.current_output_file)

def format_help(self, *args: Any, **kwargs: Any) -> str:
"""Override to add a newline."""
Expand Down
34 changes: 17 additions & 17 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -849,7 +849,7 @@ def _autoload_commands(self) -> None:
all_commandset_defs = CommandSet.__subclasses__()
existing_commandset_types = [type(command_set) for command_set in self._installed_command_sets]

def load_commandset_by_type(commandset_types: list[type[CommandSet[Any]]]) -> None:
def load_commandset_by_type(commandset_types: Sequence[type[CommandSet[Any]]]) -> None:
for cmdset_type in commandset_types:
# check if the type has sub-classes. We will only auto-load leaf class types.
subclasses = cmdset_type.__subclasses__()
Expand Down Expand Up @@ -2547,11 +2547,11 @@ def _perform_completion(
completer.complete, tokens=raw_tokens[1:] if spec.preserve_quotes else tokens[1:], cmd_set=cmd_set
)
else:
completer_func = self.completedefault # type: ignore[assignment]
completer_func = self.completedefault # type: ignore[assignment, ty:invalid-assignment]

# Not a recognized macro or command
else:
completer_func = self.completedefault # type: ignore[assignment]
completer_func = self.completedefault # type: ignore[assignment, ty:invalid-assignment]

# Otherwise we are completing the command token or performing custom completion
else:
Expand Down Expand Up @@ -2968,7 +2968,7 @@ def onecmd_plus_hooks(
with self.sigint_protection:
if py_bridge_call:
# Start saving command's stdout at this point
self.stdout.pause_storage = False # type: ignore[attr-defined]
self.stdout.pause_storage = False # type: ignore[attr-defined, ty:invalid-assignment]

redir_saved_state = self._redirect_output(statement)

Expand Down Expand Up @@ -3007,7 +3007,7 @@ def onecmd_plus_hooks(

if py_bridge_call:
# Stop saving command's stdout before command finalization hooks run
self.stdout.pause_storage = True # type: ignore[attr-defined]
self.stdout.pause_storage = True # type: ignore[attr-defined, ty:invalid-assignment]
except (SkipPostcommandHooks, EmptyStatement):
# Don't do anything, but do allow command finalization hooks to run
pass
Expand Down Expand Up @@ -3512,7 +3512,7 @@ def _read_raw_input(
self.active_session = self.main_session

# We're not at a terminal, so we're likely reading from a file or a pipe.
prompt_obj = prompt() if callable(prompt) else prompt
prompt_obj = prompt if isinstance(prompt, (ANSI, str)) else prompt()
prompt_str = prompt_obj.value if isinstance(prompt_obj, ANSI) else prompt_obj

# If this is an interactive pipe, then display the prompt first
Expand Down Expand Up @@ -3800,7 +3800,7 @@ def _build_alias_parser() -> Cmd2ArgumentParser:
"An alias is a command that enables replacement of a word by another string.",
)
alias_parser = argparse_utils.DEFAULT_ARGUMENT_PARSER(description=alias_description)
alias_parser.epilog = TextGroup(
alias_parser.epilog = TextGroup( # type: ignore[assignment, ty:invalid-assignment]
"See Also",
"macro",
)
Expand Down Expand Up @@ -3832,7 +3832,7 @@ def _build_alias_create_parser(cls) -> Cmd2ArgumentParser:
"for the actual command the alias resolves to."
),
)
alias_create_parser.epilog = TextGroup("Notes", alias_create_notes)
alias_create_parser.epilog = TextGroup("Notes", alias_create_notes) # type: ignore[assignment, ty:invalid-assignment]

# Add arguments
alias_create_parser.add_argument("name", help="name of this alias")
Expand Down Expand Up @@ -4014,7 +4014,7 @@ def _build_macro_parser() -> Cmd2ArgumentParser:
"A macro is similar to an alias, but it can contain argument placeholders.",
)
macro_parser = argparse_utils.DEFAULT_ARGUMENT_PARSER(description=macro_description)
macro_parser.epilog = TextGroup(
macro_parser.epilog = TextGroup( # type: ignore[assignment, ty:invalid-assignment]
"See Also",
"alias",
)
Expand Down Expand Up @@ -4077,7 +4077,7 @@ def _build_macro_create_parser(cls) -> Cmd2ArgumentParser:
"This default behavior changes if custom completion for macro arguments has been implemented."
),
)
macro_create_parser.epilog = TextGroup("Notes", macro_create_notes)
macro_create_parser.epilog = TextGroup("Notes", macro_create_notes) # type: ignore[assignment, ty:invalid-assignment]

# Add arguments
macro_create_parser.add_argument("name", help="name of this macro")
Expand Down Expand Up @@ -4572,7 +4572,7 @@ def do_shortcuts(self, _: argparse.Namespace) -> None:
@staticmethod
def _build__eof_parser() -> Cmd2ArgumentParser:
_eof_parser = argparse_utils.DEFAULT_ARGUMENT_PARSER(description="Called when Ctrl-D is pressed.")
_eof_parser.epilog = TextGroup(
_eof_parser.epilog = TextGroup( # type: ignore[assignment, ty:invalid-assignment]
"Note",
"This command is for internal use and is not intended to be called from the command line.",
)
Expand Down Expand Up @@ -5032,7 +5032,7 @@ def py_quit() -> None:
# Check if we are running Python code
if py_code_to_run:
try: # noqa: SIM105
interp.runcode(py_code_to_run) # type: ignore[arg-type]
interp.runcode(py_code_to_run) # type: ignore[arg-type, ty:invalid-argument-type]
except BaseException: # noqa: BLE001, S110
# We don't care about any exception that happened in the Python code
pass
Expand Down Expand Up @@ -5418,11 +5418,11 @@ def _initialize_history(self, hist_file: str) -> None:
try:
import lzma as decompress_lib

decompress_exceptions: tuple[type[Exception]] = (decompress_lib.LZMAError,)
decompress_exceptions: tuple[type[Exception], ...] = (decompress_lib.LZMAError,)
except ModuleNotFoundError: # pragma: no cover
import bz2 as decompress_lib # type: ignore[no-redef]

decompress_exceptions: tuple[type[Exception]] = (OSError, ValueError) # type: ignore[no-redef]
decompress_exceptions: tuple[type[Exception], ...] = (OSError, ValueError) # type: ignore[no-redef]

try:
history_json = decompress_lib.decompress(compressed_bytes).decode(encoding="utf-8")
Expand Down Expand Up @@ -5471,7 +5471,7 @@ def _persist_history(self) -> None:
def _build_edit_parser(cls) -> Cmd2ArgumentParser:
edit_description = "Run a text editor and optionally open a file with it."
edit_parser = argparse_utils.DEFAULT_ARGUMENT_PARSER(description=edit_description)
edit_parser.epilog = TextGroup(
edit_parser.epilog = TextGroup( # type: ignore[assignment, ty:invalid-assignment]
"Note",
Text.assemble(
"To set a new editor, run: ",
Expand Down Expand Up @@ -5593,7 +5593,7 @@ def _build__relative_run_script_parser(cls) -> Cmd2ArgumentParser:
_relative_run_script_parser = cls._build_base_run_script_parser()

# Append to existing description
_relative_run_script_parser.description = Group(
_relative_run_script_parser.description = Group( # type: ignore[assignment, ty:invalid-assignment]
cast(Group, _relative_run_script_parser.description),
"\n",
(
Expand All @@ -5602,7 +5602,7 @@ def _build__relative_run_script_parser(cls) -> Cmd2ArgumentParser:
),
)

_relative_run_script_parser.epilog = TextGroup(
_relative_run_script_parser.epilog = TextGroup( # type: ignore[assignment, ty:invalid-assignment]
"Note",
"This command is intended to be used from within a text script.",
)
Expand Down
6 changes: 3 additions & 3 deletions cmd2/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ def arg_decorator(func: ArgparseCommandFunc[CmdOrSetT]) -> RawCommandFunc[CmdOrS
:return: Function that takes raw input and converts to an argparse Namespace to passed to the wrapped function.
"""

@functools.wraps(func)
@functools.wraps(func) # type: ignore[arg-type, ty:invalid-argument-type]
def cmd_wrapper(*args: Any, **kwargs: Any) -> bool | None:
"""Command function wrapper which translates command line into argparse Namespace and call actual command function.

Expand Down Expand Up @@ -345,9 +345,9 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> bool | None:
parsing_results: tuple[argparse.Namespace] | tuple[argparse.Namespace, list[str]]
with arg_parser.output_to(cmd_app.stdout):
if with_unknown_args:
parsing_results = arg_parser.parse_known_args(command_arg_list, initial_namespace)
parsing_results = arg_parser.parse_known_args(command_arg_list, initial_namespace) # type: ignore[assignment, ty:invalid-assignment]
else:
parsing_results = (arg_parser.parse_args(command_arg_list, initial_namespace),)
parsing_results = (arg_parser.parse_args(command_arg_list, initial_namespace),) # type: ignore[assignment, ty:invalid-assignment]
except SystemExit as exc:
raise Cmd2ArgparseError from exc

Expand Down
2 changes: 1 addition & 1 deletion cmd2/pt_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ def __init__(
self._cmd_app = cmd_app
self.custom_settings = custom_settings

def get_completions(self, document: Document, _complete_event: object) -> Iterable[Completion]:
def get_completions(self, document: Document, complete_event: object) -> Iterable[Completion]: # noqa: ARG002
"""Get completions for the current input."""
# Find the beginning of the current word based on delimiters
line = document.text
Expand Down
2 changes: 1 addition & 1 deletion cmd2/rich_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def __repr__(self) -> str:


# Controls when ANSI style sequences are allowed in output
ALLOW_STYLE = AllowStyle.TERMINAL
ALLOW_STYLE: AllowStyle = AllowStyle.TERMINAL


class Cmd2HelpFormatter(RichHelpFormatter):
Expand Down
30 changes: 2 additions & 28 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ dev = [
"codecov>=2.1",
"ipython>=8.23",
"mkdocstrings[python]>=1",
"mypy>=1.13",
"prek>=0.3.5",
"pytest>=8.1.1",
"pytest-cov>=5",
"pytest-mock>=3.14.1",
"ruff>=0.14.10",
"ty>=0.0.73",
"uv-publish>=1.3",
"zensical>=0.0.17",
]
Expand All @@ -63,33 +63,7 @@ test = [
"pytest-cov>=5",
"pytest-mock>=3.14.1",
]
validate = ["mypy>=1.13", "ruff>=0.14.10", "types-setuptools>=80.8.0"]

[tool.mypy]
disallow_incomplete_defs = true
disallow_untyped_calls = true
disallow_untyped_defs = true
exclude = [
"^.git/",
"^.venv/",
"^build/", # .build directory
"^docs/", # docs directory
"^dist/",
"^examples/", # examples directory
"^noxfile\\.py$", # nox config file
"setup\\.py$", # any files named setup.py
"^site/",
"^tests/", # tests directory
]
files = ['.']
show_column_numbers = true
show_error_codes = true
show_error_context = true
strict = true
warn_redundant_casts = true
warn_return_any = true
warn_unreachable = true
warn_unused_ignores = false
validate = ["ruff>=0.14.10", "ty>=0.0.73", "types-setuptools>=80.8.0"]

[tool.pytest.ini_options]
testpaths = ["tests"]
Expand Down
3 changes: 2 additions & 1 deletion ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@ exclude = [
".git-rewrite",
".hg",
".ipynb_checkpoints",
".mypy_cache",
".nox",
".pants.d",
".pyenv",
".pytest_cache",
".pytype",
".red_knot_cache",
".ruff_cache",
".svn",
".tox",
".ty_cache",
".venv",
".vscode",
"__pypackages__",
Expand Down
Loading
Loading