diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b39513a..e2bbccc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and - {pull}`704` adds the `--explain` flag to show why tasks would be executed. Closes {issue}`466`. - {pull}`706` disables syntax highlighting for platform version information in session header. - {pull}`707` drops support for Python 3.9 as it has reached end of life. +- {pull}`708` updates mypy and fixes type issues. ## 0.5.5 - 2025-07-25 diff --git a/pyproject.toml b/pyproject.toml index e88c6dee..7f19dd16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ test = [ "coiled>=1.42.0", "cloudpickle>=3.0.0", ] -typing = ["mypy>=1.9.0,<1.11", "nbqa>=1.8.5"] +typing = ["mypy>=1.11.0", "nbqa>=1.8.5"] [project.urls] Changelog = "https://pytask-dev.readthedocs.io/en/stable/changes.html" diff --git a/src/_pytask/capture.py b/src/_pytask/capture.py index 8c430ad9..0e2a47fd 100644 --- a/src/_pytask/capture.py +++ b/src/_pytask/capture.py @@ -39,6 +39,7 @@ from typing import Generic from typing import NamedTuple from typing import TextIO +from typing import cast from typing import final import click @@ -125,8 +126,10 @@ def name(self) -> str: @property def mode(self) -> str: - # TextIOWrapper doesn't expose a mode, but at least some of our tests check it. - return self.buffer.mode.replace("b", "") + # TextIOWrapper doesn't expose a mode, but at least some of our + # tests check it. + assert hasattr(self.buffer, "mode") + return cast("str", self.buffer.mode.replace("b", "")) class CaptureIO(io.TextIOWrapper): @@ -153,6 +156,7 @@ class DontReadFromInput(TextIO): @property def encoding(self) -> str: + assert sys.__stdin__ is not None return sys.__stdin__.encoding def read(self, size: int = -1) -> str: # noqa: ARG002 @@ -539,7 +543,7 @@ def snap(self) -> bytes: res = self.tmpfile.buffer.read() self.tmpfile.seek(0) self.tmpfile.truncate() - return res + return res # type: ignore[return-value] def writeorg(self, data: bytes) -> None: """Write to original file descriptor.""" diff --git a/src/_pytask/collect_command.py b/src/_pytask/collect_command.py index 6b7b8f6a..183de4ac 100644 --- a/src/_pytask/collect_command.py +++ b/src/_pytask/collect_command.py @@ -124,11 +124,15 @@ def _find_common_ancestor_of_all_nodes( for task in tasks: all_paths.append(task.path) if show_nodes: - all_paths.extend( - x.path for x in tree_leaves(task.depends_on) if isinstance(x, PPathNode) + all_paths.extend( # type: ignore[var-annotated] + x.path + for x in tree_leaves(task.depends_on) # type: ignore[arg-type] + if isinstance(x, PPathNode) ) - all_paths.extend( - x.path for x in tree_leaves(task.produces) if isinstance(x, PPathNode) + all_paths.extend( # type: ignore[var-annotated] + x.path + for x in tree_leaves(task.produces) # type: ignore[arg-type] + if isinstance(x, PPathNode) ) return find_common_ancestor(*all_paths, *paths) @@ -196,7 +200,7 @@ def _print_collected_tasks( ) if show_nodes: - deps = list(tree_leaves(task.depends_on)) + deps: list[Any] = list(tree_leaves(task.depends_on)) # type: ignore[arg-type] for node in sorted( deps, key=( @@ -208,7 +212,7 @@ def _print_collected_tasks( text = format_node_name(node, (common_ancestor,)) task_branch.add(Text.assemble(FILE_ICON, "")) - products = list(tree_leaves(task.produces)) + products: list[Any] = list(tree_leaves(task.produces)) # type: ignore[arg-type] for node in sorted( products, key=lambda x: x.path.as_posix() diff --git a/src/_pytask/dag.py b/src/_pytask/dag.py index d8c8b325..bc0f9c62 100644 --- a/src/_pytask/dag.py +++ b/src/_pytask/dag.py @@ -88,8 +88,8 @@ def _add_product( for task in tasks: dag.add_node(task.signature, task=task) - tree_map(lambda x: _add_dependency(dag, task, x), task.depends_on) - tree_map(lambda x: _add_product(dag, task, x), task.produces) + tree_map(lambda x: _add_dependency(dag, task, x), task.depends_on) # type: ignore[arg-type] + tree_map(lambda x: _add_product(dag, task, x), task.produces) # type: ignore[arg-type] # If a node is a PythonNode wrapped in another PythonNode, it is a product from # another task that is a dependency in the current task. Thus, draw an edge @@ -98,7 +98,7 @@ def _add_product( lambda x: dag.add_edge(x.value.signature, x.signature) if isinstance(x, PythonNode) and isinstance(x.value, PythonNode) else None, - task.depends_on, + task.depends_on, # type: ignore[arg-type] ) return dag diff --git a/src/_pytask/dag_command.py b/src/_pytask/dag_command.py index e90079e4..b099c045 100644 --- a/src/_pytask/dag_command.py +++ b/src/_pytask/dag_command.py @@ -215,7 +215,7 @@ def _shorten_node_labels(dag: nx.DiGraph, paths: list[Path]) -> nx.DiGraph: """Shorten the node labels in the graph for a better experience.""" node_names = dag.nodes short_names = reduce_names_of_multiple_nodes(node_names, dag, paths) - short_names = [i.plain if isinstance(i, Text) else i for i in short_names] # type: ignore[attr-defined] + short_names = [i.plain if isinstance(i, Text) else i for i in short_names] old_to_new = dict(zip(node_names, short_names, strict=False)) return nx.relabel_nodes(dag, old_to_new) diff --git a/src/_pytask/debugging.py b/src/_pytask/debugging.py index 0ac3d62c..29df7539 100644 --- a/src/_pytask/debugging.py +++ b/src/_pytask/debugging.py @@ -11,7 +11,9 @@ import click +from _pytask.capture import CaptureManager from _pytask.console import console +from _pytask.live import LiveManager from _pytask.node_protocols import PTask from _pytask.outcomes import Exit from _pytask.pluginmanager import hookimpl @@ -24,8 +26,6 @@ from pluggy import PluginManager - from _pytask.capture import CaptureManager - from _pytask.live import LiveManager from _pytask.session import Session @@ -289,6 +289,7 @@ def _init_pdb( if header is not None: console.rule(header, characters=">", style="default") else: + assert isinstance(capman, CaptureManager) capturing = cls._is_capturing(capman) if capturing: console.rule( @@ -299,6 +300,8 @@ def _init_pdb( else: console.rule(f"PDB {method}", characters=">", style="default") + assert isinstance(capman, CaptureManager) + assert isinstance(live_manager, LiveManager) return cls._import_pdb_cls(capman, live_manager)(**kwargs) @classmethod @@ -392,7 +395,7 @@ def wrap_function_for_tracing(session: Session, task: PTask) -> None: # 3.7.4) runcall's first param is `func`, which means we'd get an exception if one # of the kwargs to task_function was called `func`. @functools.wraps(task_function) - def wrapper(*args: Any, **kwargs: Any) -> None: + def wrapper(*args: Any, **kwargs: Any) -> Any: capman = session.config["pm"].get_plugin("capturemanager") live_manager = session.config["pm"].get_plugin("live_manager") diff --git a/src/_pytask/execute.py b/src/_pytask/execute.py index 3622562d..edbd0347 100644 --- a/src/_pytask/execute.py +++ b/src/_pytask/execute.py @@ -301,7 +301,11 @@ def pytask_execute_task_teardown(session: Session, task: PTask) -> None: return collect_provisional_products(session, task) - missing_nodes = [node for node in tree_leaves(task.produces) if not node.state()] + missing_nodes: list[Any] = [ # type: ignore[var-annotated] + node + for node in tree_leaves(task.produces) # type: ignore[arg-type] + if not node.state() + ] if missing_nodes: paths = session.config["paths"] files = [format_node_name(i, paths).plain for i in missing_nodes] diff --git a/src/_pytask/git.py b/src/_pytask/git.py index fe0fa2be..580cfa52 100644 --- a/src/_pytask/git.py +++ b/src/_pytask/git.py @@ -16,8 +16,10 @@ def is_git_installed() -> bool: def cmd_output(*cmd: str, **kwargs: Any) -> tuple[int, str, str]: """Execute a command and capture the output.""" r = subprocess.run(cmd, capture_output=True, check=False, **kwargs) - stdout = r.stdout.decode() if r.stdout is not None else None - stderr = r.stderr.decode() if r.stderr is not None else None + stdout = r.stdout.decode() if r.stdout is not None else "" + stderr = r.stderr.decode() if r.stderr is not None else "" + assert isinstance(stdout, str) + assert isinstance(stderr, str) return r.returncode, stdout, stderr diff --git a/src/_pytask/provisional.py b/src/_pytask/provisional.py index 2e582bc2..85c355c7 100644 --- a/src/_pytask/provisional.py +++ b/src/_pytask/provisional.py @@ -40,7 +40,8 @@ def pytask_execute_task_setup(session: Session, task: PTask) -> None: """ task.depends_on = tree_map_with_path( # type: ignore[assignment] - lambda p, x: collect_provisional_nodes(session, task, x, p), task.depends_on + lambda p, x: collect_provisional_nodes(session, task, x, p), + task.depends_on, # type: ignore[arg-type] ) if task.signature in TASKS_WITH_PROVISIONAL_NODES: recreate_dag(session, task) diff --git a/src/_pytask/provisional_utils.py b/src/_pytask/provisional_utils.py index 9a09b096..84755fef 100644 --- a/src/_pytask/provisional_utils.py +++ b/src/_pytask/provisional_utils.py @@ -65,7 +65,7 @@ def collect_provisional_nodes( task_name=task_name, ), ), - provisional_nodes, + provisional_nodes, # type: ignore[arg-type] ) @@ -100,7 +100,8 @@ def collect_provisional_products(session: Session, task: PTask) -> None: # Replace provisional nodes with their actually resolved nodes. task.produces = tree_map_with_path( # type: ignore[assignment] - lambda p, x: collect_provisional_nodes(session, task, x, p), task.produces + lambda p, x: collect_provisional_nodes(session, task, x, p), + task.produces, # type: ignore[arg-type] ) if task.signature in TASKS_WITH_PROVISIONAL_NODES: diff --git a/src/_pytask/task_utils.py b/src/_pytask/task_utils.py index b10176a3..9dbfb049 100644 --- a/src/_pytask/task_utils.py +++ b/src/_pytask/task_utils.py @@ -208,7 +208,7 @@ def _parse_after( for func in after: if not hasattr(func, "pytask_meta"): func = task()(func) # noqa: PLW2901 - new_after.append(func.pytask_meta._id) + new_after.append(func.pytask_meta._id) # type: ignore[attr-defined] return new_after msg = ( "'after' should be an expression string, a task, or a list of tasks. Got "