Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 7 additions & 3 deletions src/_pytask/capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand Down Expand Up @@ -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."""
Expand Down
16 changes: 10 additions & 6 deletions src/_pytask/collect_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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=(
Expand All @@ -208,7 +212,7 @@ def _print_collected_tasks(
text = format_node_name(node, (common_ancestor,))
task_branch.add(Text.assemble(FILE_ICON, "<Dependency ", text, ">"))

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()
Expand Down
6 changes: 3 additions & 3 deletions src/_pytask/dag.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/_pytask/dag_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
9 changes: 6 additions & 3 deletions src/_pytask/debugging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,8 +26,6 @@

from pluggy import PluginManager

from _pytask.capture import CaptureManager
from _pytask.live import LiveManager
from _pytask.session import Session


Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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")

Expand Down
6 changes: 5 additions & 1 deletion src/_pytask/execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
6 changes: 4 additions & 2 deletions src/_pytask/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
3 changes: 2 additions & 1 deletion src/_pytask/provisional.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions src/_pytask/provisional_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def collect_provisional_nodes(
task_name=task_name,
),
),
provisional_nodes,
provisional_nodes, # type: ignore[arg-type]
)


Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion src/_pytask/task_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "
Expand Down