diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fbbdbc99..e9ac686e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,7 +30,7 @@ jobs: python-version: ['3.8', '3.9', '3.10', '3.11'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: mamba-org/setup-micromamba@v1 with: environment-name: gha-testing diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 7d30ad9a..326220fa 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -7,7 +7,7 @@ jobs: name: Build and publish Python 🐍 distributions 📦 to PyPI runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python 3.8 uses: actions/setup-python@v4 diff --git a/.github/workflows/update-plugin-list.yml b/.github/workflows/update-plugin-list.yml index 1e36084f..890b382d 100644 --- a/.github/workflows/update-plugin-list.yml +++ b/.github/workflows/update-plugin-list.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/docs/source/changes.md b/docs/source/changes.md index 2a8eba68..d96f40e6 100644 --- a/docs/source/changes.md +++ b/docs/source/changes.md @@ -30,6 +30,7 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and - {pull}`410` allows to pass functions to `PythonNode(hash=...)`. - {pull}`412` adds protocols for tasks. - {pull}`413` removes scripts to generate `.svg`s. +- {pull}`414` allow more ruff rules. ## 0.3.2 - 2023-06-07 diff --git a/docs/source/conf.py b/docs/source/conf.py index 4573656b..ed04c41a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -7,8 +7,10 @@ from __future__ import annotations from importlib.metadata import version +from typing import TYPE_CHECKING -import sphinx +if TYPE_CHECKING: + import sphinx # -- Project information --------------------------------------------------------------- diff --git a/environment.yml b/environment.yml index e0444649..528ad279 100644 --- a/environment.yml +++ b/environment.yml @@ -15,7 +15,7 @@ dependencies: - click - click-default-group - networkx >=2.4 - - pluggy + - pluggy >=1.0.0 - optree >=0.9 - rich - sqlalchemy >=1.4.36 diff --git a/pyproject.toml b/pyproject.toml index b1f287ac..bbdff1f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,8 +33,8 @@ target-version = "py38" select = ["ALL"] fix = true extend-ignore = [ + "I", # ignore isort "TRY", # ignore tryceratops. - "TCH", # ignore non-guarded type imports. # Numpy docstyle "D107", "D203", @@ -46,15 +46,10 @@ extend-ignore = [ "D416", "D417", # Others. - "D404", # Do not start module docstring with "This". - "RET504", # unnecessary variable assignment before return. "S101", # raise errors for asserts. - "B905", # strict parameter for zip that was implemented in py310. - "I", # ignore isort "ANN101", # type annotating self "ANN102", # type annotating cls "FBT", # flake8-boolean-trap - "EM", # flake8-errmsg "ANN401", # flake8-annotate typing.Any "PD", # pandas-vet "COM812", # trailing comma missing, but black takes care of that @@ -62,9 +57,6 @@ extend-ignore = [ "SLF001", # access private members. "S603", "S607", - # Temporary - "TD002", - "TD003", ] @@ -73,7 +65,7 @@ extend-ignore = [ "src/_pytask/hookspecs.py" = ["ARG001"] "src/_pytask/outcomes.py" = ["N818"] "tests/test_capture.py" = ["T201", "PT011"] -"tests/*" = ["D", "ANN", "PLR2004"] +"tests/*" = ["D", "ANN", "PLR2004", "S101"] "scripts/*" = ["D", "INP001"] "docs/source/conf.py" = ["D401", "INP001"] diff --git a/scripts/update_plugin_list.py b/scripts/update_plugin_list.py index 3562056f..4acc6c72 100644 --- a/scripts/update_plugin_list.py +++ b/scripts/update_plugin_list.py @@ -81,8 +81,7 @@ def _escape_rst(text: str) -> str: .replace(">", "\\>") .replace("`", "\\`") ) - text = re.sub(r"_\b", "", text) - return text + return re.sub(r"_\b", "", text) def _iter_plugins() -> Generator[dict[str, str], None, None]: # noqa: C901 diff --git a/setup.cfg b/setup.cfg index 1a39bbfb..79b22790 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,7 +36,7 @@ install_requires = networkx>=2.4 optree>=0.9 packaging - pluggy + pluggy>=1.0.0 rich sqlalchemy>=1.4.36 tomli>=1.0.0 diff --git a/src/_pytask/_inspect.py b/src/_pytask/_inspect.py index 84413c1f..70f86325 100644 --- a/src/_pytask/_inspect.py +++ b/src/_pytask/_inspect.py @@ -99,13 +99,15 @@ def get_annotations( # noqa: C901, PLR0912, PLR0915 obj_locals = None unwrap = obj else: - raise TypeError(f"{obj!r} is not a module, class, or callable.") + msg = f"{obj!r} is not a module, class, or callable." + raise TypeError(msg) if ann is None: return {} if not isinstance(ann, dict): - raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None") + msg = f"{obj!r}.__annotations__ is neither a dict nor None" + raise ValueError(msg) if not ann: return {} @@ -131,10 +133,9 @@ def get_annotations( # noqa: C901, PLR0912, PLR0915 locals = obj_locals # noqa: A001 eval_func = eval - return_value = { + return { key: value if not isinstance(value, str) else eval_func(value, globals, locals) for key, value in ann.items() } - return return_value diff --git a/src/_pytask/capture.py b/src/_pytask/capture.py index cc8886c7..b99bf97f 100644 --- a/src/_pytask/capture.py +++ b/src/_pytask/capture.py @@ -39,12 +39,15 @@ from typing import Generic from typing import Iterator from typing import TextIO +from typing import TYPE_CHECKING import click from _pytask.click import EnumChoice from _pytask.config import hookimpl from _pytask.enums import ShowCapture -from _pytask.node_protocols import PTask + +if TYPE_CHECKING: + from _pytask.node_protocols import PTask class _CaptureMethod(enum.Enum): @@ -148,9 +151,10 @@ class DontReadFromInput: encoding = None def read(self, *_args: Any) -> None: - raise OSError( + msg = ( "pytask: reading from stdin while output is captured! Consider using `-s`." ) + raise OSError(msg) readline = read readlines = read @@ -160,10 +164,12 @@ def __iter__(self) -> DontReadFromInput: return self def fileno(self) -> int: - raise UnsupportedOperation("redirected stdin is pseudofile, has no fileno()") + msg = "redirected stdin is pseudofile, has no fileno()" + raise UnsupportedOperation(msg) def flush(self) -> None: - raise UnsupportedOperation("redirected stdin is pseudofile, has no flush()") + msg = "redirected stdin is pseudofile, has no flush()" + raise UnsupportedOperation(msg) def isatty(self) -> bool: return False @@ -175,22 +181,27 @@ def readable(self) -> bool: return False def seek(self, offset: int) -> int: # noqa: ARG002 - raise UnsupportedOperation("Redirected stdin is pseudofile, has no seek(int).") + msg = "Redirected stdin is pseudofile, has no seek(int)." + raise UnsupportedOperation(msg) def seekable(self) -> bool: return False def tell(self) -> int: - raise UnsupportedOperation("Redirected stdin is pseudofile, has no tell().") + msg = "Redirected stdin is pseudofile, has no tell()." + raise UnsupportedOperation(msg) def truncate(self, size: int) -> None: # noqa: ARG002 - raise UnsupportedOperation("Cannot truncate stdin.") + msg = "Cannot truncate stdin." + raise UnsupportedOperation(msg) def write(self, *args: Any) -> None: # noqa: ARG002 - raise UnsupportedOperation("Cannot write to stdin.") + msg = "Cannot write to stdin." + raise UnsupportedOperation(msg) def writelines(self, *args: Any) -> None: # noqa: ARG002 - raise UnsupportedOperation("Cannot write to stdin.") + msg = "Cannot write to stdin." + raise UnsupportedOperation(msg) def writable(self) -> bool: return False @@ -611,7 +622,8 @@ def resume_capturing(self) -> None: def stop_capturing(self) -> None: """Stop capturing and reset capturing streams.""" if self._state == "stopped": - raise ValueError("was already stopped") + msg = "was already stopped" + raise ValueError(msg) self._state = "stopped" if self.out: self.out.done() @@ -647,7 +659,8 @@ def _get_multicapture(method: _CaptureMethod) -> MultiCapture[str]: return MultiCapture( in_=None, out=SysCapture(1, tee=True), err=SysCapture(2, tee=True) ) - raise ValueError(f"unknown capturing method: {method!r}") + msg = f"unknown capturing method: {method!r}" + raise ValueError(msg) # Own implementation of the CaptureManager. diff --git a/src/_pytask/clean.py b/src/_pytask/clean.py index 41e128f8..d9a69dc3 100644 --- a/src/_pytask/clean.py +++ b/src/_pytask/clean.py @@ -6,7 +6,6 @@ import shutil import sys from pathlib import Path -from types import TracebackType from typing import Any from typing import Generator from typing import Iterable @@ -36,6 +35,7 @@ if TYPE_CHECKING: + from types import TracebackType from typing import NoReturn @@ -242,7 +242,7 @@ def _find_all_unknown_paths( _RecursivePathNode.from_path(path, known_paths, exclude) for path in session.config["paths"] ] - unknown_paths = list( + return list( itertools.chain.from_iterable( [ _find_all_unkown_paths_per_recursive_node(node, include_directories) @@ -250,7 +250,6 @@ def _find_all_unknown_paths( ] ) ) - return unknown_paths @define(repr=False) diff --git a/src/_pytask/cli.py b/src/_pytask/cli.py index 4b6dd39e..2fcef289 100644 --- a/src/_pytask/cli.py +++ b/src/_pytask/cli.py @@ -3,14 +3,17 @@ import sys from typing import Any +from typing import TYPE_CHECKING import click -import pluggy from _pytask.click import ColoredGroup from _pytask.config import hookimpl from _pytask.pluginmanager import get_plugin_manager from packaging.version import parse as parse_version +if TYPE_CHECKING: + import pluggy + _CONTEXT_SETTINGS: dict[str, Any] = { "help_option_names": ("-h", "--help"), diff --git a/src/_pytask/click.py b/src/_pytask/click.py index 6268f790..0e175338 100644 --- a/src/_pytask/click.py +++ b/src/_pytask/click.py @@ -1,4 +1,4 @@ -"""This module contains code related to click.""" +"""Contains code related to click.""" from __future__ import annotations import enum diff --git a/src/_pytask/collect.py b/src/_pytask/collect.py index 1e5f1eba..0f48ec28 100644 --- a/src/_pytask/collect.py +++ b/src/_pytask/collect.py @@ -10,6 +10,7 @@ from typing import Any from typing import Generator from typing import Iterable +from typing import TYPE_CHECKING from _pytask.collect_utils import parse_dependencies_from_task_function from _pytask.collect_utils import parse_products_from_task_function @@ -20,7 +21,6 @@ from _pytask.console import format_task_name from _pytask.exceptions import CollectionError from _pytask.mark_utils import has_mark -from _pytask.models import NodeInfo from _pytask.node_protocols import Node from _pytask.node_protocols import PTask from _pytask.nodes import PathNode @@ -31,12 +31,15 @@ from _pytask.path import find_case_sensitive_path from _pytask.path import import_path from _pytask.report import CollectionReport -from _pytask.session import Session from _pytask.shared import find_duplicates from _pytask.shared import reduce_node_name from _pytask.traceback import render_exc_info from rich.text import Text +if TYPE_CHECKING: + from _pytask.session import Session + from _pytask.models import NodeInfo + @hookimpl def pytask_collect(session: Session) -> bool: @@ -81,8 +84,7 @@ def _collect_from_paths(session: Session) -> None: @hookimpl def pytask_ignore_collect(path: Path, config: dict[str, Any]) -> bool: """Ignore a path during the collection.""" - is_ignored = any(path.match(pattern) for pattern in config["ignore"]) - return is_ignored + return any(path.match(pattern) for pattern in config["ignore"]) @hookimpl diff --git a/src/_pytask/collect_command.py b/src/_pytask/collect_command.py index 1dfac4b8..73428ef7 100644 --- a/src/_pytask/collect_command.py +++ b/src/_pytask/collect_command.py @@ -1,9 +1,8 @@ -"""This module contains the implementation of ``pytask collect``.""" +"""Contains the implementation of ``pytask collect``.""" from __future__ import annotations import sys from collections import defaultdict -from pathlib import Path from typing import Any from typing import TYPE_CHECKING @@ -35,6 +34,7 @@ if TYPE_CHECKING: + from pathlib import Path from typing import NoReturn @@ -134,9 +134,7 @@ def _find_common_ancestor_of_all_nodes( x.path for x in tree_leaves(task.produces) if isinstance(x, PPathNode) ) - common_ancestor = find_common_ancestor(*all_paths, *paths) - - return common_ancestor + return find_common_ancestor(*all_paths, *paths) def _organize_tasks(tasks: list[PTaskWithPath]) -> dict[Path, list[PTaskWithPath]]: diff --git a/src/_pytask/collect_utils.py b/src/_pytask/collect_utils.py index d10549dc..ee26af25 100644 --- a/src/_pytask/collect_utils.py +++ b/src/_pytask/collect_utils.py @@ -1,4 +1,4 @@ -"""This module provides utility functions for :mod:`_pytask.collect`.""" +"""Contains utility functions for :mod:`_pytask.collect`.""" from __future__ import annotations import functools @@ -79,13 +79,12 @@ def parse_nodes( arg_name = parser.__name__ objects = _extract_nodes_from_function_markers(obj, parser) nodes = _convert_objects_to_node_dictionary(objects, arg_name) - nodes = tree_map( + return tree_map( lambda x: _collect_decorator_node( session, path, name, NodeInfo(arg_name, (), x) ), nodes, ) - return nodes def _extract_nodes_from_function_markers( @@ -109,8 +108,7 @@ def _convert_objects_to_node_dictionary(objects: Any, when: str) -> dict[Any, An """Convert objects to node dictionary.""" list_of_dicts = [_convert_to_dict(x) for x in objects] _check_that_names_are_not_used_multiple_times(list_of_dicts, when) - nodes = _merge_dictionaries(list_of_dicts) - return nodes + return _merge_dictionaries(list_of_dicts) @define(frozen=True) @@ -153,9 +151,8 @@ def _check_that_names_are_not_used_multiple_times( duplicated = find_duplicates(names) if duplicated: - raise ValueError( - f"'@pytask.mark.{when}' has nodes with the same name: {duplicated}" - ) + msg = f"'@pytask.mark.{when}' has nodes with the same name: {duplicated}" + raise ValueError(msg) def _union_of_dictionaries(dicts: list[dict[Any, Any]]) -> dict[Any, Any]: @@ -309,10 +306,11 @@ def _find_args_with_node_annotation(func: Callable[..., Any]) -> dict[str, Node] for name, meta in metas.items(): annot = [i for i in meta if not isinstance(i, ProductType)] if len(annot) >= 2: # noqa: PLR2004 - raise ValueError( + msg = ( f"Parameter {name!r} has multiple node annotations although only one " f"is allowed. Annotations: {annot}" ) + raise ValueError(msg) if annot: args_with_node_annotation[name] = annot[0] @@ -522,9 +520,8 @@ def _collect_decorator_node( session=session, path=path, node_info=node_info ) if collected_node is None: - raise NodeNotCollectedError( - f"{node!r} cannot be parsed as a {kind} for task {name!r} in {path!r}." - ) + msg = f"{node!r} cannot be parsed as a {kind} for task {name!r} in {path!r}." + raise NodeNotCollectedError(msg) return collected_node @@ -546,10 +543,10 @@ def _collect_dependency( session=session, path=path, node_info=node_info ) if collected_node is None: - raise NodeNotCollectedError( - f"{node!r} cannot be parsed as a dependency for task " - f"{name!r} in {path!r}." + msg = ( + f"{node!r} cannot be parsed as a dependency for task {name!r} in {path!r}." ) + raise NodeNotCollectedError(msg) return collected_node @@ -574,11 +571,12 @@ def _collect_product( node = node_info.value # For historical reasons, task.kwargs is like the deco and supports str and Path. if not isinstance(node, (str, Path)) and is_string_allowed: - raise ValueError( - "`@pytask.mark.task(kwargs={'produces': ...}` can only accept values of " - "type 'str' and 'pathlib.Path' or the same values nested in " - f"tuples, lists, and dictionaries. Here, {node} has type {type(node)}." + msg = ( + f"`@pytask.mark.task(kwargs={{'produces': ...}}` can only accept values of " + "type 'str' and 'pathlib.Path' or the same values nested in tuples, lists, " + f"and dictionaries. Here, {node} has type {type(node)}." ) + raise ValueError(msg) # If we encounter a string and it is allowed, convert it to a path. if isinstance(node, str) and is_string_allowed: @@ -589,9 +587,10 @@ def _collect_product( session=session, path=path, node_info=node_info ) if collected_node is None: - raise NodeNotCollectedError( + msg = ( f"{node!r} can't be parsed as a product for task {task_name!r} in {path!r}." ) + raise NodeNotCollectedError(msg) return collected_node @@ -602,10 +601,11 @@ def _evolve_instance(x: Any, instance_from_annot: Node | None) -> Any: return x if not hasattr(instance_from_annot, "from_annot"): - raise AttributeError( + msg = ( f"The node {instance_from_annot!r} does not define '.from_annot' which is " f"necessary to complete the node with the value {x!r}." ) + raise AttributeError(msg) instance_from_annot.from_annot(x) # type: ignore[attr-defined] return instance_from_annot diff --git a/src/_pytask/compat.py b/src/_pytask/compat.py index 61ad6459..c5213c3d 100644 --- a/src/_pytask/compat.py +++ b/src/_pytask/compat.py @@ -1,14 +1,17 @@ -"""This module contains functions to assess compatibility and optional dependencies.""" +"""Contains functions to assess compatibility and optional dependencies.""" from __future__ import annotations import importlib import shutil import sys -import types import warnings +from typing import TYPE_CHECKING from packaging.version import parse as parse_version +if TYPE_CHECKING: + import types + __all__ = ["check_for_optional_program", "import_optional_dependency"] @@ -26,7 +29,8 @@ def _get_version(module: types.ModuleType) -> str: """Get version from a package.""" version = getattr(module, "__version__", None) if version is None: - raise ImportError(f"Can't determine version for {module.__name__}") + msg = f"Can't determine version for {module.__name__}" + raise ImportError(msg) return version @@ -72,7 +76,8 @@ def import_optional_dependency( """ if errors not in ("warn", "raise", "ignore"): # pragma: no cover - raise ValueError("'errors' must be one of 'warn', 'raise' or 'ignore'.") + msg = "'errors' must be one of 'warn', 'raise' or 'ignore'." + raise ValueError(msg) package_name = _IMPORT_TO_PACKAGE_NAME.get(name) install_name = package_name if package_name is not None else name @@ -124,9 +129,8 @@ def check_for_optional_program( ) -> bool | None: """Check whether an optional program exists.""" if errors not in ("warn", "raise", "ignore"): - raise ValueError( - f"'errors' must be one of 'warn', 'raise' or 'ignore' and not {errors!r}." - ) + msg = f"'errors' must be one of 'warn', 'raise' or 'ignore' and not {errors!r}." + raise ValueError(msg) msg = f"{caller} requires the optional program {name!r}. {extra}" diff --git a/src/_pytask/config_utils.py b/src/_pytask/config_utils.py index c8052dfd..6f825136 100644 --- a/src/_pytask/config_utils.py +++ b/src/_pytask/config_utils.py @@ -1,4 +1,4 @@ -"""This module contains helper functions for the configuration.""" +"""Contains helper functions for the configuration.""" from __future__ import annotations import os diff --git a/src/_pytask/console.py b/src/_pytask/console.py index 4ffd52d5..7261533a 100644 --- a/src/_pytask/console.py +++ b/src/_pytask/console.py @@ -1,11 +1,10 @@ -"""This module contains the code to format output on the command line.""" +"""Contains the code to format output on the command line.""" from __future__ import annotations import functools import inspect import os import sys -from enum import Enum from pathlib import Path from typing import Any from typing import Callable @@ -13,7 +12,6 @@ from typing import TYPE_CHECKING import rich -from _pytask.node_protocols import PTask from _pytask.nodes import Task from rich.console import Console from rich.padding import Padding @@ -27,6 +25,8 @@ if TYPE_CHECKING: + from _pytask.node_protocols import PTask + from enum import Enum from _pytask.outcomes import CollectionOutcome from _pytask.outcomes import TaskOutcome @@ -139,8 +139,7 @@ def render_to_string( else: output.append(segment.text) - rendered = "".join(output) - return rendered + return "".join(output) def format_task_name(task: PTask, editor_url_scheme: str) -> Text: @@ -166,8 +165,7 @@ def format_strings_as_flat_tree(strings: Iterable[str], title: str, icon: str) - tree = Tree(title) for name in strings: tree.add(Text.assemble(icon, name)) - text = render_to_string(tree, console=console) - return text + return render_to_string(tree, console=console) def create_url_style_for_task( @@ -278,7 +276,7 @@ def create_summary_panel( style=outcome.style_textonly, # type: ignore[attr-defined] ) - panel = Panel( + return Panel( grid, title="[bold #f2f2f2]Summary[/]", expand=False, @@ -287,5 +285,3 @@ def create_summary_panel( if counts[outcome_enum.FAIL] else outcome_enum.SUCCESS.style, ) - - return panel diff --git a/src/_pytask/dag.py b/src/_pytask/dag.py index e3d0ccb5..fbc3cd66 100644 --- a/src/_pytask/dag.py +++ b/src/_pytask/dag.py @@ -1,9 +1,10 @@ -"""This module contains code related to resolving dependencies.""" +"""Contains code related to resolving dependencies.""" from __future__ import annotations import hashlib import itertools import sys +from typing import TYPE_CHECKING import networkx as nx from _pytask.config import hookimpl @@ -29,7 +30,6 @@ from _pytask.node_protocols import PTaskWithPath from _pytask.path import find_common_ancestor_of_nodes from _pytask.report import DagReport -from _pytask.session import Session from _pytask.shared import reduce_names_of_multiple_nodes from _pytask.shared import reduce_node_name from _pytask.traceback import render_exc_info @@ -37,6 +37,9 @@ from rich.text import Text from rich.tree import Tree +if TYPE_CHECKING: + from _pytask.session import Session + @hookimpl def pytask_dag(session: Session) -> bool | None: @@ -167,12 +170,12 @@ def _check_if_dag_has_cycles(dag: nx.DiGraph) -> None: except nx.NetworkXNoCycle: pass else: - raise ResolvingDependenciesError( - "The DAG contains cycles which means a dependency is directly or " + msg = ( + f"The DAG contains cycles which means a dependency is directly or " "indirectly a product of the same task. See the following the path of " - "nodes in the graph which forms the cycle." - f"\n\n{_format_cycles(cycles)}" + f"nodes in the graph which forms the cycle.\n\n{_format_cycles(cycles)}" ) + raise ResolvingDependenciesError(msg) def _format_cycles(cycles: list[tuple[str, ...]]) -> str: @@ -183,9 +186,7 @@ def _format_cycles(cycles: list[tuple[str, ...]]) -> str: lines = chain[:1] for x in chain[1:]: lines.extend((" " + ARROW_DOWN_ICON, x)) - text = "\n".join(lines) - - return text + return "\n".join(lines) _TEMPLATE_ERROR: str = ( @@ -261,10 +262,7 @@ def _check_if_task_is_skipped(task_name: str, dag: nx.DiGraph) -> bool: return True skip_if_markers = get_marks(task, "skipif") - is_any_true = any( - _skipif(*marker.args, **marker.kwargs)[0] for marker in skip_if_markers - ) - return is_any_true + return any(_skipif(*marker.args, **marker.kwargs)[0] for marker in skip_if_markers) def _skipif(condition: bool, *, reason: str) -> tuple[bool, str]: @@ -281,8 +279,7 @@ def _format_dictionary_to_tree(dict_: dict[str, list[str]], title: str) -> str: for task in tasks: branch.add(Text.assemble(TASK_ICON, task)) - text = render_to_string(tree, console=console, strip_styles=True) - return text + return render_to_string(tree, console=console, strip_styles=True) def _check_if_tasks_have_the_same_products(dag: nx.DiGraph) -> None: @@ -312,11 +309,11 @@ def _check_if_tasks_have_the_same_products(dag: nx.DiGraph) -> None: ) dictionary[short_node_name] = short_predecessors text = _format_dictionary_to_tree(dictionary, "Products from multiple tasks:") - raise ResolvingDependenciesError( - "There are some tasks which produce the same output. See the following " - "tree which shows which products are produced by multiple tasks." - f"\n\n{text}" + msg = ( + f"There are some tasks which produce the same output. See the following " + f"tree which shows which products are produced by multiple tasks.\n\n{text}" ) + raise ResolvingDependenciesError(msg) @hookimpl diff --git a/src/_pytask/dag_utils.py b/src/_pytask/dag_utils.py index 2f6da5d9..22c2649b 100644 --- a/src/_pytask/dag_utils.py +++ b/src/_pytask/dag_utils.py @@ -4,16 +4,19 @@ import itertools from typing import Generator from typing import Iterable +from typing import TYPE_CHECKING import networkx as nx from _pytask.console import format_strings_as_flat_tree from _pytask.console import format_task_name from _pytask.console import TASK_ICON from _pytask.mark_utils import has_mark -from _pytask.node_protocols import PTask from attrs import define from attrs import field +if TYPE_CHECKING: + from _pytask.node_protocols import PTask + def descending_tasks(task_name: str, dag: nx.DiGraph) -> Generator[str, None, None]: """Yield only descending tasks.""" @@ -73,7 +76,8 @@ class TopologicalSorter: def from_dag(cls, dag: nx.DiGraph) -> TopologicalSorter: """Instantiate from a DAG.""" if not dag.is_directed(): - raise ValueError("Only directed graphs have a topological order.") + msg = "Only directed graphs have a topological order." + raise ValueError(msg) tasks = [ dag.nodes[node]["task"] for node in dag.nodes if "task" in dag.nodes[node] @@ -93,16 +97,19 @@ def prepare(self) -> None: except nx.NetworkXNoCycle: pass else: - raise ValueError("The DAG contains cycles.") + msg = "The DAG contains cycles." + raise ValueError(msg) self._is_prepared = True def get_ready(self, n: int = 1) -> list[str]: """Get up to ``n`` tasks which are ready.""" if not self._is_prepared: - raise ValueError("The TopologicalSorter needs to be prepared.") + msg = "The TopologicalSorter needs to be prepared." + raise ValueError(msg) if not isinstance(n, int) or n < 1: - raise ValueError("'n' must be an integer greater or equal than 1.") + msg = "'n' must be an integer greater or equal than 1." + raise ValueError(msg) ready_nodes = {v for v, d in self.dag.in_degree() if d == 0} - self._nodes_out prioritized_nodes = sorted( @@ -168,16 +175,15 @@ def _extract_priorities_from_tasks(tasks: list[PTask]) -> dict[str, int]: text = format_strings_as_flat_tree( reduced_names, "Tasks with mixed priorities", TASK_ICON ) - raise ValueError( - "'try_first' and 'try_last' cannot be applied on the same task. See the " + msg = ( + f"'try_first' and 'try_last' cannot be applied on the same task. See the " f"following tasks for errors:\n\n{text}" ) + raise ValueError(msg) # Recode to numeric values for sorting. numeric_mapping = {(True, False): 1, (False, False): 0, (False, True): -1} - numeric_priorities = { + return { name: numeric_mapping[(p["try_first"], p["try_last"])] for name, p in priorities.items() } - - return numeric_priorities diff --git a/src/_pytask/database_utils.py b/src/_pytask/database_utils.py index ffb48a06..c5129d6c 100644 --- a/src/_pytask/database_utils.py +++ b/src/_pytask/database_utils.py @@ -1,18 +1,21 @@ -"""This module contains utilities for the database.""" +"""Contains utilities for the database.""" from __future__ import annotations import hashlib +from typing import TYPE_CHECKING from _pytask.dag_utils import node_and_neighbors from _pytask.node_protocols import PPathNode from _pytask.node_protocols import PTaskWithPath -from _pytask.session import Session from sqlalchemy import Column from sqlalchemy import create_engine from sqlalchemy import String from sqlalchemy.orm import declarative_base from sqlalchemy.orm import sessionmaker +if TYPE_CHECKING: + from _pytask.session import Session + __all__ = [ "create_database", diff --git a/src/_pytask/debugging.py b/src/_pytask/debugging.py index 13ceadf7..f1d43c23 100644 --- a/src/_pytask/debugging.py +++ b/src/_pytask/debugging.py @@ -1,28 +1,28 @@ -"""This module contains everything related to debugging.""" +"""Contains everything related to debugging.""" from __future__ import annotations import functools import pdb # noqa: T100 import sys -from types import FrameType -from types import TracebackType from typing import Any from typing import ClassVar from typing import Generator from typing import TYPE_CHECKING import click -import pluggy from _pytask.config import hookimpl from _pytask.console import console from _pytask.node_protocols import PTask from _pytask.outcomes import Exit -from _pytask.session import Session from _pytask.traceback import remove_internal_traceback_frames_from_exc_info from _pytask.traceback import render_exc_info if TYPE_CHECKING: + import pluggy + from _pytask.session import Session + from types import TracebackType + from types import FrameType from _pytask.capture import CaptureManager from _pytask.live import LiveManager @@ -153,9 +153,8 @@ def _import_pdb_cls( pdb_cls = getattr(pdb_cls, part) except Exception as exc: # noqa: BLE001 value = f"{modname}:{classname}" - raise ValueError( - f"--pdbcls: could not import {value!r}: {exc}." - ) from exc + msg = f"--pdbcls: could not import {value!r}: {exc}." + raise ValueError(msg) from exc else: import pdb # noqa: T100 @@ -223,7 +222,8 @@ def do_quit(self, arg: Any) -> int: ret = super().do_quit(arg) if cls._recursive_debug == 0: - raise Exit("Quitting debugger") + msg = "Quitting debugger" + raise Exit(msg) return ret @@ -292,9 +292,7 @@ def _init_pdb( else: console.rule(f"PDB {method}", characters=">", style=None) - _pdb = cls._import_pdb_cls(capman, live_manager)(**kwargs) - - return _pdb + return cls._import_pdb_cls(capman, live_manager)(**kwargs) @classmethod def set_trace(cls, *args: Any, **kwargs: Any) -> None: @@ -422,4 +420,5 @@ def post_mortem(t: TracebackType) -> None: p.reset() p.interaction(None, t) if p.quitting: - raise Exit("Quitting debugger") + msg = "Quitting debugger" + raise Exit(msg) diff --git a/src/_pytask/exceptions.py b/src/_pytask/exceptions.py index a3df8c22..a1641e32 100644 --- a/src/_pytask/exceptions.py +++ b/src/_pytask/exceptions.py @@ -1,4 +1,4 @@ -"""This module contains custom exceptions.""" +"""Contains custom exceptions.""" from __future__ import annotations diff --git a/src/_pytask/execute.py b/src/_pytask/execute.py index 2a473f01..902fe352 100644 --- a/src/_pytask/execute.py +++ b/src/_pytask/execute.py @@ -1,10 +1,11 @@ -"""This module contains hook implementations concerning the execution.""" +"""Contains hook implementations concerning the execution.""" from __future__ import annotations import inspect import sys import time from typing import Any +from typing import TYPE_CHECKING from _pytask.config import hookimpl from _pytask.console import console @@ -28,7 +29,6 @@ from _pytask.outcomes import TaskOutcome from _pytask.outcomes import WouldBeExecuted from _pytask.report import ExecutionReport -from _pytask.session import Session from _pytask.shared import reduce_node_name from _pytask.traceback import format_exception_without_traceback from _pytask.traceback import remove_traceback_from_exc_info @@ -38,6 +38,9 @@ from _pytask.tree_util import tree_structure from rich.text import Text +if TYPE_CHECKING: + from _pytask.session import Session + @hookimpl def pytask_post_parse(config: dict[str, Any]) -> None: @@ -162,11 +165,12 @@ def pytask_execute_task(session: Session, task: PTask) -> bool: structure_return = tree_structure(task.produces["return"]) # strict must be false when none is leaf. if not structure_return.is_prefix(structure_out, strict=False): - raise ValueError( - "The structure of the return annotation is not a subtree of the " - "structure of the function return.\n\nFunction return: " - f"{structure_out}\n\nReturn annotation: {structure_return}" + msg = ( + f"The structure of the return annotation is not a subtree of the " + f"structure of the function return.\n\nFunction return: {structure_out}" + f"\n\nReturn annotation: {structure_return}" ) + raise ValueError(msg) nodes = tree_leaves(task.produces["return"]) values = structure_return.flatten_up_to(out) diff --git a/src/_pytask/git.py b/src/_pytask/git.py index bfb74474..bba7c6de 100644 --- a/src/_pytask/git.py +++ b/src/_pytask/git.py @@ -1,11 +1,14 @@ -"""This module contains all functions related to git.""" +"""Contains all functions related to git.""" from __future__ import annotations import shutil import subprocess -from os import PathLike from pathlib import Path from typing import Any +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from os import PathLike def is_git_installed() -> bool: @@ -37,8 +40,7 @@ def zsplit(s: str) -> list[str]: def get_all_files(cwd: PathLike[str] | None = None) -> list[Path]: """Get all files tracked by git - even new, staged files.""" str_paths = zsplit(cmd_output("git", "ls-files", "-z", cwd=cwd)[1]) - paths = [Path(x) for x in str_paths] - return paths + return [Path(x) for x in str_paths] def get_root(cwd: PathLike[str] | None) -> Path | None: diff --git a/src/_pytask/graph.py b/src/_pytask/graph.py index c8466616..84ea6d7c 100644 --- a/src/_pytask/graph.py +++ b/src/_pytask/graph.py @@ -1,4 +1,4 @@ -"""This file contains the command and code for drawing the DAG.""" +"""Contains the command and code for drawing the DAG.""" from __future__ import annotations import enum @@ -240,8 +240,7 @@ def _shorten_node_labels(dag: nx.DiGraph, paths: list[Path]) -> nx.DiGraph: 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] old_to_new = dict(zip(node_names, short_names)) - dag = nx.relabel_nodes(dag, old_to_new) - return dag + return nx.relabel_nodes(dag, old_to_new) def _clean_dag(dag: nx.DiGraph) -> nx.DiGraph: diff --git a/src/_pytask/hookspecs.py b/src/_pytask/hookspecs.py index 84093926..294ae929 100644 --- a/src/_pytask/hookspecs.py +++ b/src/_pytask/hookspecs.py @@ -6,20 +6,20 @@ """ from __future__ import annotations -import pathlib from typing import Any from typing import TYPE_CHECKING -import click -import networkx as nx import pluggy -from _pytask.models import NodeInfo -from _pytask.node_protocols import MetaNode -from _pytask.node_protocols import Node -from _pytask.node_protocols import PTask if TYPE_CHECKING: + from _pytask.node_protocols import MetaNode + from _pytask.models import NodeInfo + from _pytask.node_protocols import Node + import click + from _pytask.node_protocols import PTask + import networkx as nx + import pathlib from _pytask.session import Session from _pytask.nodes import Task from _pytask.outcomes import CollectionOutcome diff --git a/src/_pytask/live.py b/src/_pytask/live.py index 00ca9f30..149bd5fe 100644 --- a/src/_pytask/live.py +++ b/src/_pytask/live.py @@ -1,20 +1,17 @@ -"""This module contains code related to live objects.""" +"""Contains code related to live objects.""" from __future__ import annotations from typing import Any from typing import Generator from typing import NamedTuple +from typing import TYPE_CHECKING import click from _pytask.config import hookimpl from _pytask.console import console from _pytask.console import format_task_name -from _pytask.node_protocols import PTask from _pytask.outcomes import CollectionOutcome from _pytask.outcomes import TaskOutcome -from _pytask.report import CollectionReport -from _pytask.report import ExecutionReport -from _pytask.session import Session from attrs import define from attrs import field from rich.box import ROUNDED @@ -24,6 +21,12 @@ from rich.table import Table from rich.text import Text +if TYPE_CHECKING: + from _pytask.node_protocols import PTask + from _pytask.report import ExecutionReport + from _pytask.session import Session + from _pytask.report import CollectionReport + @hookimpl def pytask_extend_command_line_interface(cli: click.Group) -> None: @@ -314,5 +317,4 @@ def _generate_status(self) -> Status: msg = f"Collected {self._n_collected_tasks} tasks." if self._n_errors > 0: msg += f" {self._n_errors} errors." - status = Status(msg, spinner="dots") - return status + return Status(msg, spinner="dots") diff --git a/src/_pytask/logging.py b/src/_pytask/logging.py index 0db95358..18025032 100644 --- a/src/_pytask/logging.py +++ b/src/_pytask/logging.py @@ -1,11 +1,13 @@ """Add general logging capabilities.""" from __future__ import annotations +import contextlib import platform import sys import warnings from typing import Any from typing import NamedTuple +from typing import TYPE_CHECKING import _pytask import click @@ -13,16 +15,17 @@ from _pytask.config import hookimpl from _pytask.console import console from _pytask.console import IS_WINDOWS_TERMINAL -from _pytask.outcomes import CollectionOutcome -from _pytask.outcomes import TaskOutcome -from _pytask.session import Session from rich.text import Text - -try: +if TYPE_CHECKING: from pluggy._manager import DistFacade -except ImportError: - from pluggy.manager import DistFacade + from _pytask.outcomes import TaskOutcome + from _pytask.session import Session + from _pytask.outcomes import CollectionOutcome + + +with contextlib.suppress(ImportError): + pass class _TimeUnit(NamedTuple): @@ -124,8 +127,7 @@ def _format_duration(duration: float) -> str: i for i in duration_tuples if i[1] not in ("second", "seconds") ] - formatted_duration = ", ".join([" ".join(map(str, i)) for i in duration_tuples]) - return formatted_duration + return ", ".join([" ".join(map(str, i)) for i in duration_tuples]) def _humanize_time( # noqa: C901, PLR0912 @@ -155,7 +157,8 @@ def _humanize_time( # noqa: C901, PLR0912 index = i break else: - raise ValueError(f"The time unit {unit!r} is not known.") + msg = f"The time unit {unit!r} is not known." + raise ValueError(msg) seconds = amount * _TIME_UNITS[index].in_seconds result: list[tuple[float, str]] = [] diff --git a/src/_pytask/mark/__init__.py b/src/_pytask/mark/__init__.py index 3839dbab..9cd1184b 100644 --- a/src/_pytask/mark/__init__.py +++ b/src/_pytask/mark/__init__.py @@ -1,4 +1,4 @@ -"""This module contains the main code for the markers plugin.""" +"""Contains the main code for the markers plugin.""" from __future__ import annotations import sys @@ -7,7 +7,6 @@ from typing import TYPE_CHECKING import click -import networkx as nx from _pytask.click import ColoredCommand from _pytask.config import hookimpl from _pytask.console import console @@ -19,7 +18,6 @@ from _pytask.mark.structures import MARK_GEN from _pytask.mark.structures import MarkDecorator from _pytask.mark.structures import MarkGenerator -from _pytask.node_protocols import PTask from _pytask.outcomes import ExitCode from _pytask.pluginmanager import get_plugin_manager from _pytask.session import Session @@ -29,6 +27,8 @@ if TYPE_CHECKING: + from _pytask.node_protocols import PTask + import networkx as nx from typing import NoReturn @@ -163,9 +163,8 @@ def select_by_keyword(session: Session, dag: nx.DiGraph) -> set[str]: try: expression = Expression.compile_(keywordexpr) except ParseError as e: - raise ValueError( - f"Wrong expression passed to '-k': {keywordexpr}: {e}" - ) from None + msg = f"Wrong expression passed to '-k': {keywordexpr}: {e}" + raise ValueError(msg) from None remaining: set[str] = set() for task in session.tasks: @@ -203,7 +202,8 @@ def select_by_mark(session: Session, dag: nx.DiGraph) -> set[str]: try: expression = Expression.compile_(matchexpr) except ParseError as e: - raise ValueError(f"Wrong expression passed to '-m': {matchexpr}: {e}") from None + msg = f"Wrong expression passed to '-m': {matchexpr}: {e}" + raise ValueError(msg) from None remaining: set[str] = set() for task in session.tasks: diff --git a/src/_pytask/mark/expression.py b/src/_pytask/mark/expression.py index 78880669..93184401 100644 --- a/src/_pytask/mark/expression.py +++ b/src/_pytask/mark/expression.py @@ -26,7 +26,6 @@ import ast import enum import re -import types from typing import Callable from typing import Iterator from typing import Mapping @@ -37,6 +36,7 @@ if TYPE_CHECKING: + import types from typing import NoReturn diff --git a/src/_pytask/mark/structures.py b/src/_pytask/mark/structures.py index 62109f49..242fd1e9 100644 --- a/src/_pytask/mark/structures.py +++ b/src/_pytask/mark/structures.py @@ -148,7 +148,8 @@ def normalize_mark_list(mark_list: Iterable[Mark | MarkDecorator]) -> list[Mark] extracted = [getattr(mark, "mark", mark) for mark in mark_list] for mark in extracted: if not isinstance(mark, Mark): - raise TypeError(f"Got {mark!r} instead of Mark.") + msg = f"Got {mark!r} instead of Mark." + raise TypeError(msg) return [x for x in extracted if isinstance(x, Mark)] @@ -195,7 +196,8 @@ class MarkGenerator: def __getattr__(self, name: str) -> MarkDecorator | Any: if name[0] == "_": - raise AttributeError("Marker name must NOT start with underscore") + msg = "Marker name must NOT start with underscore" + raise AttributeError(msg) if name in ("depends_on", "produces"): warnings.warn( @@ -208,14 +210,16 @@ def __getattr__(self, name: str) -> MarkDecorator | Any: # then it really is time to issue a warning or an error. if self.config is not None and name not in self.config["markers"]: if self.config["strict_markers"]: - raise ValueError(f"Unknown pytask.mark.{name}.") + msg = f"Unknown pytask.mark.{name}." + raise ValueError(msg) if name in ("parametrize", "parameterize", "parametrise", "parameterise"): - raise NotImplementedError( + msg = ( "@pytask.mark.parametrize has been removed since pytask v0.4. " - "Upgrade your parametrized tasks to the new syntax defined in" + "Upgrade your parametrized tasks to the new syntax defined in " "https://tinyurl.com/pytask-loops or revert to v0.3." - ) from None + ) + raise NotImplementedError(msg) from None warnings.warn( f"Unknown pytask.mark.{name} - is this a typo? You can register " diff --git a/src/_pytask/mark_utils.py b/src/_pytask/mark_utils.py index 348493c5..cb6ba4e5 100644 --- a/src/_pytask/mark_utils.py +++ b/src/_pytask/mark_utils.py @@ -1,4 +1,4 @@ -"""This module contains utility functions related to marker. +"""Contains utility functions related to marker. The utility functions are stored here to be separate from the plugin. diff --git a/src/_pytask/models.py b/src/_pytask/models.py index d0080c3f..7d442783 100644 --- a/src/_pytask/models.py +++ b/src/_pytask/models.py @@ -1,15 +1,15 @@ -"""This module contains code on models, containers and there like.""" +"""Contains code on models, containers and there like.""" from __future__ import annotations from typing import Any from typing import NamedTuple from typing import TYPE_CHECKING -from _pytask.tree_util import PyTree from attrs import define from attrs import field if TYPE_CHECKING: + from _pytask.tree_util import PyTree from _pytask.mark import Mark diff --git a/src/_pytask/node_protocols.py b/src/_pytask/node_protocols.py index f0463dc4..c2328b7b 100644 --- a/src/_pytask/node_protocols.py +++ b/src/_pytask/node_protocols.py @@ -1,16 +1,16 @@ from __future__ import annotations from abc import abstractmethod -from pathlib import Path from typing import Any from typing import Callable from typing import Protocol from typing import runtime_checkable from typing import TYPE_CHECKING -from _pytask.tree_util import PyTree if TYPE_CHECKING: + from _pytask.tree_util import PyTree + from pathlib import Path from _pytask.mark import Mark diff --git a/src/_pytask/nodes.py b/src/_pytask/nodes.py index 8275a025..831542ee 100644 --- a/src/_pytask/nodes.py +++ b/src/_pytask/nodes.py @@ -11,12 +11,12 @@ from _pytask.node_protocols import MetaNode from _pytask.node_protocols import Node from _pytask.node_protocols import PPathNode -from _pytask.tree_util import PyTree from attrs import define from attrs import field if TYPE_CHECKING: + from _pytask.tree_util import PyTree from _pytask.mark import Mark @@ -73,8 +73,7 @@ def state(self) -> str | None: def execute(self, **kwargs: Any) -> None: """Execute the task.""" - out = self.function(**kwargs) - return out + return self.function(**kwargs) @define(kw_only=True) @@ -99,7 +98,8 @@ def task_example(value: Annotated[Any, PathNode(name="value")]): """ if not isinstance(value, Path): - raise TypeError("'value' must be a 'pathlib.Path'.") + msg = "'value' must be a 'pathlib.Path'." + raise TypeError(msg) if not self.name: self.name = value.as_posix() self.path = value @@ -113,7 +113,8 @@ def from_path(cls, path: Path) -> PathNode: """ if not path.is_absolute(): - raise ValueError("Node must be instantiated from absolute path.") + msg = "Node must be instantiated from absolute path." + raise ValueError(msg) return cls(name=path.as_posix(), path=path) def state(self) -> str | None: @@ -137,9 +138,8 @@ def save(self, value: bytes | str) -> None: elif isinstance(value, bytes): self.path.write_bytes(value) else: - raise TypeError( - f"'PathNode' can only save 'str' and 'bytes', not {type(value)}" - ) + msg = f"'PathNode' can only save 'str' and 'bytes', not {type(value)}" + raise TypeError(msg) @define(kw_only=True) diff --git a/src/_pytask/outcomes.py b/src/_pytask/outcomes.py index 92948287..75a35ba9 100644 --- a/src/_pytask/outcomes.py +++ b/src/_pytask/outcomes.py @@ -1,4 +1,4 @@ -"""This module contains code related to outcomes.""" +"""Contains code related to outcomes.""" from __future__ import annotations from enum import auto diff --git a/src/_pytask/parameters.py b/src/_pytask/parameters.py index 2ec31f53..32baae42 100644 --- a/src/_pytask/parameters.py +++ b/src/_pytask/parameters.py @@ -82,11 +82,8 @@ def _database_url_callback( try: return make_url(value) except ArgumentError: - raise click.BadParameter( - "The 'database_url' must conform to sqlalchemy's url standard: " - "https://docs.sqlalchemy.org/en/latest/core/engines.html" - "#backend-specific-urls." - ) from None + msg = "The 'database_url' must conform to sqlalchemy's url standard: https://docs.sqlalchemy.org/en/latest/core/engines.html#backend-specific-urls." + raise click.BadParameter(msg) from None _DATABASE_URL_OPTION = click.Option( diff --git a/src/_pytask/path.py b/src/_pytask/path.py index 576f2f79..71c80b50 100644 --- a/src/_pytask/path.py +++ b/src/_pytask/path.py @@ -1,4 +1,4 @@ -"""This module contains code to handle paths.""" +"""Contains code to handle paths.""" from __future__ import annotations import functools @@ -96,8 +96,7 @@ def find_common_ancestor_of_nodes(*names: str) -> Path: def find_common_ancestor(*paths: str | Path) -> Path: """Find a common ancestor of many paths.""" - common_ancestor = Path(os.path.commonpath(paths)) - return common_ancestor + return Path(os.path.commonpath(paths)) @functools.lru_cache @@ -121,8 +120,7 @@ def find_case_sensitive_path(path: Path, platform: str) -> Path: a case-sensitive path which it does on Windows. """ - out = path.resolve() if platform == "win32" else path - return out + return path.resolve() if platform == "win32" else path def import_path(path: Path, root: Path) -> ModuleType: @@ -138,7 +136,8 @@ def import_path(path: Path, root: Path) -> ModuleType: spec = importlib.util.spec_from_file_location(module_name, str(path)) if spec is None: - raise ImportError(f"Can't find module {module_name!r} at location {path}.") + msg = f"Can't find module {module_name!r} at location {path}." + raise ImportError(msg) mod = importlib.util.module_from_spec(spec) sys.modules[module_name] = mod diff --git a/src/_pytask/persist.py b/src/_pytask/persist.py index f76faa27..6e5915af 100644 --- a/src/_pytask/persist.py +++ b/src/_pytask/persist.py @@ -8,12 +8,12 @@ from _pytask.dag_utils import node_and_neighbors from _pytask.database_utils import update_states_in_database from _pytask.mark_utils import has_mark -from _pytask.node_protocols import PTask from _pytask.outcomes import Persisted from _pytask.outcomes import TaskOutcome if TYPE_CHECKING: + from _pytask.node_protocols import PTask from _pytask.session import Session from _pytask.report import ExecutionReport diff --git a/src/_pytask/pluginmanager.py b/src/_pytask/pluginmanager.py index 0ba78c51..390e2fca 100644 --- a/src/_pytask/pluginmanager.py +++ b/src/_pytask/pluginmanager.py @@ -1,4 +1,4 @@ -"""This module holds the plugin manager.""" +"""Contains the plugin manager.""" from __future__ import annotations import pluggy diff --git a/src/_pytask/profile.py b/src/_pytask/profile.py index ce7a1881..0dc5c079 100644 --- a/src/_pytask/profile.py +++ b/src/_pytask/profile.py @@ -1,4 +1,4 @@ -"""This module contains the code to profile the execution.""" +"""Contains the code to profile the execution.""" from __future__ import annotations import csv @@ -8,7 +8,6 @@ import time from contextlib import suppress from pathlib import Path -from types import TracebackType from typing import Any from typing import Generator from typing import TYPE_CHECKING @@ -28,7 +27,6 @@ from _pytask.outcomes import ExitCode from _pytask.outcomes import TaskOutcome from _pytask.pluginmanager import get_plugin_manager -from _pytask.report import ExecutionReport from _pytask.session import Session from _pytask.traceback import render_exc_info from rich.table import Table @@ -38,6 +36,8 @@ if TYPE_CHECKING: + from _pytask.report import ExecutionReport + from types import TracebackType from typing import NoReturn @@ -280,7 +280,8 @@ def pytask_profile_export_profile( elif export == _ExportFormats.NO: pass else: # pragma: no cover - raise ValueError(f"The export option {export.value!r} cannot be handled.") + msg = f"The export option {export.value!r} cannot be handled." + raise ValueError(msg) def _export_to_csv(profile: dict[str, dict[str, Any]]) -> None: diff --git a/src/_pytask/report.py b/src/_pytask/report.py index 1f597f79..c29cdec4 100644 --- a/src/_pytask/report.py +++ b/src/_pytask/report.py @@ -1,4 +1,4 @@ -"""This module contains everything related to reports.""" +"""Contains everything related to reports.""" from __future__ import annotations from types import TracebackType @@ -7,7 +7,6 @@ from typing import TYPE_CHECKING from typing import Union -from _pytask.node_protocols import PTask from _pytask.outcomes import CollectionOutcome from _pytask.outcomes import TaskOutcome from _pytask.traceback import remove_internal_traceback_frames_from_exc_info @@ -16,6 +15,7 @@ if TYPE_CHECKING: + from _pytask.node_protocols import PTask from _pytask.node_protocols import MetaNode diff --git a/src/_pytask/session.py b/src/_pytask/session.py index 9d1adf78..1b6e4f09 100644 --- a/src/_pytask/session.py +++ b/src/_pytask/session.py @@ -1,24 +1,19 @@ -"""This module contains code related to the session object.""" +"""Contains code related to the session object.""" from __future__ import annotations from typing import Any from typing import TYPE_CHECKING -import networkx as nx -from _pytask.node_protocols import PTask from _pytask.outcomes import ExitCode -from _pytask.warnings_utils import WarningReport from attrs import define from attrs import field -# Location was moved from pluggy v0.13.1 to v1.0.0. -try: - from pluggy._hooks import _HookRelay -except ImportError: - from pluggy.hooks import _HookRelay - if TYPE_CHECKING: + from _pytask.node_protocols import PTask + from _pytask.warnings_utils import WarningReport + from pluggy._hooks import _HookRelay + import networkx as nx from _pytask.report import CollectionReport from _pytask.report import ExecutionReport from _ptytask.report import DagReport diff --git a/src/_pytask/shared.py b/src/_pytask/shared.py index 56dae2a2..f07a1b22 100644 --- a/src/_pytask/shared.py +++ b/src/_pytask/shared.py @@ -6,9 +6,9 @@ from typing import Any from typing import Iterable from typing import Sequence +from typing import TYPE_CHECKING import click -import networkx as nx from _pytask.console import format_task_name from _pytask.node_protocols import MetaNode from _pytask.node_protocols import PPathNode @@ -18,6 +18,9 @@ from _pytask.path import find_common_ancestor from _pytask.path import relative_to +if TYPE_CHECKING: + import networkx as nx + def to_list(scalar_or_iter: Any) -> list[Any]: """Convert scalars and iterables to list. @@ -78,8 +81,7 @@ def reduce_node_name(node: MetaNode, paths: Sequence[str | Path]) -> str: except ValueError: ancestor = node.path.parents[-1] - name = relative_to(node.path, ancestor).as_posix() - return name + return relative_to(node.path, ancestor).as_posix() return node.name @@ -96,7 +98,8 @@ def reduce_names_of_multiple_nodes( elif isinstance(node, MetaNode): short_name = reduce_node_name(node, paths) else: - raise TypeError(f"Requires 'Task' or 'Node' and not {type(node)!r}.") + msg = f"Requires 'Task' or 'Node' and not {type(node)!r}." + raise TypeError(msg) short_names.append(short_name) @@ -132,14 +135,12 @@ def parse_markers(x: dict[str, str] | list[str] | tuple[str, ...]) -> dict[str, elif isinstance(x, dict): mapping = {name.strip(): description.strip() for name, description in x.items()} else: - raise click.BadParameter( - "'markers' must be a mapping from markers to descriptions." - ) + msg = "'markers' must be a mapping from markers to descriptions." + raise click.BadParameter(msg) for name in mapping: if not name.isidentifier(): - raise click.BadParameter( - f"{name} is not a valid Python name and cannot be used as a marker." - ) + msg = f"{name} is not a valid Python name and cannot be used as a marker." + raise click.BadParameter(msg) return mapping diff --git a/src/_pytask/skipping.py b/src/_pytask/skipping.py index 30892a2c..8f837273 100644 --- a/src/_pytask/skipping.py +++ b/src/_pytask/skipping.py @@ -1,4 +1,4 @@ -"""This module contains everything related to skipping tasks.""" +"""Contains everything related to skipping tasks.""" from __future__ import annotations from typing import Any @@ -9,7 +9,6 @@ from _pytask.mark import Mark from _pytask.mark_utils import get_marks from _pytask.mark_utils import has_mark -from _pytask.node_protocols import PTask from _pytask.outcomes import Skipped from _pytask.outcomes import SkippedAncestorFailed from _pytask.outcomes import SkippedUnchanged @@ -18,6 +17,7 @@ if TYPE_CHECKING: + from _pytask.node_protocols import PTask from _pytask.session import Session from _pytask.report import ExecutionReport diff --git a/src/_pytask/task.py b/src/_pytask/task.py index 7b806af7..9f742e3d 100644 --- a/src/_pytask/task.py +++ b/src/_pytask/task.py @@ -1,15 +1,18 @@ -"""This module contain hooks related to the ``@pytask.mark.task`` decorator.""" +"""Contain hooks related to the ``@pytask.mark.task`` decorator.""" from __future__ import annotations -from pathlib import Path from typing import Any +from typing import TYPE_CHECKING from _pytask.config import hookimpl -from _pytask.report import CollectionReport -from _pytask.session import Session from _pytask.task_utils import COLLECTED_TASKS from _pytask.task_utils import parse_collected_tasks_with_task_marker +if TYPE_CHECKING: + from _pytask.report import CollectionReport + from _pytask.session import Session + from pathlib import Path + @hookimpl def pytask_parse_config(config: dict[str, Any]) -> None: diff --git a/src/_pytask/task_utils.py b/src/_pytask/task_utils.py index adfd7d20..5481debe 100644 --- a/src/_pytask/task_utils.py +++ b/src/_pytask/task_utils.py @@ -1,4 +1,4 @@ -"""This module contains utilities related to the ``@pytask.mark.task`` decorator.""" +"""Contains utilities related to the ``@pytask.mark.task`` decorator.""" from __future__ import annotations import inspect @@ -6,12 +6,15 @@ from pathlib import Path from typing import Any from typing import Callable +from typing import TYPE_CHECKING import attrs from _pytask.mark import Mark from _pytask.models import CollectionMetadata from _pytask.shared import find_duplicates -from _pytask.tree_util import PyTree + +if TYPE_CHECKING: + from _pytask.tree_util import PyTree __all__ = ["parse_keyword_arguments_from_signature_defaults"] @@ -60,10 +63,11 @@ def task( def wrapper(func: Callable[..., Any]) -> None: for arg, arg_name in ((name, "name"), (id, "id")): if not (isinstance(arg, str) or arg is None): - raise ValueError( + msg = ( f"Argument {arg_name!r} of @pytask.mark.task must be a str, but it " f"is {arg!r}." ) + raise ValueError(msg) unwrapped = inspect.unwrap(func) @@ -144,10 +148,11 @@ def _parse_task(task: Callable[..., Any]) -> tuple[str, Callable[..., Any]]: meta = task.pytask_meta # type: ignore[attr-defined] if meta.name is None and task.__name__ == "_": - raise ValueError( + msg = ( "A task function either needs 'name' passed by the ``@pytask.mark.task`` " "decorator or the function name of the task function must not be '_'." ) + raise ValueError(msg) parsed_name = task.__name__ if meta.name is None else meta.name parsed_kwargs = _parse_task_kwargs(meta.kwargs) @@ -167,10 +172,11 @@ def _parse_task_kwargs(kwargs: Any) -> dict[str, Any]: return kwargs._asdict() if attrs.has(type(kwargs)): return attrs.asdict(kwargs) - raise ValueError( + msg = ( "'@pytask.mark.task(kwargs=...) needs to be a dictionary, namedtuple or an " "instance of an attrs class." ) + raise ValueError(msg) def parse_keyword_arguments_from_signature_defaults( diff --git a/src/_pytask/warnings.py b/src/_pytask/warnings.py index 81728186..9ecee42f 100644 --- a/src/_pytask/warnings.py +++ b/src/_pytask/warnings.py @@ -1,25 +1,28 @@ -"""This module contains code for capturing warnings.""" +"""Contains code for capturing warnings.""" from __future__ import annotations from collections import defaultdict from typing import Any from typing import Generator +from typing import TYPE_CHECKING import click from _pytask.config import hookimpl from _pytask.console import console -from _pytask.node_protocols import PTask -from _pytask.session import Session from _pytask.warnings_utils import catch_warnings_for_item from _pytask.warnings_utils import parse_filterwarnings from _pytask.warnings_utils import WarningReport from attrs import define -from rich.console import Console -from rich.console import ConsoleOptions -from rich.console import RenderResult from rich.padding import Padding from rich.panel import Panel +if TYPE_CHECKING: + from rich.console import Console + from _pytask.node_protocols import PTask + from rich.console import ConsoleOptions + from _pytask.session import Session + from rich.console import RenderResult + @hookimpl def pytask_extend_command_line_interface(cli: click.Group) -> None: diff --git a/src/_pytask/warnings_utils.py b/src/_pytask/warnings_utils.py index 984bfe15..4d0292e9 100644 --- a/src/_pytask/warnings_utils.py +++ b/src/_pytask/warnings_utils.py @@ -1,4 +1,4 @@ -"""This module contains utility functions for warnings.""" +"""Contains utility functions for warnings.""" from __future__ import annotations import functools @@ -12,11 +12,11 @@ from typing import TYPE_CHECKING from _pytask.mark_utils import get_marks -from _pytask.node_protocols import PTask from _pytask.outcomes import Exit if TYPE_CHECKING: + from _pytask.node_protocols import PTask from _pytask.session import Session @@ -91,7 +91,8 @@ def parse_warning_filter( # noqa: PLR0912 try: lineno = int(lineno_) if lineno < 0: - raise ValueError("number is negative") + msg = "number is negative" + raise ValueError(msg) except ValueError as e: raise Exit( # noqa: B904 error_template.format(error=f"invalid lineno {lineno_!r}: {e}") @@ -122,20 +123,20 @@ def _resolve_warning_category(category: str) -> type[Warning]: m = __import__(module, None, None, [klass]) cat = getattr(m, klass) if not issubclass(cat, Warning): - raise TypeError(f"{cat} is not a Warning subclass") + msg = f"{cat} is not a Warning subclass" + raise TypeError(msg) return cast(type[Warning], cat) def warning_record_to_str(warning_message: warnings.WarningMessage) -> str: """Convert a warnings.WarningMessage to a string.""" - msg = warnings.formatwarning( + return warnings.formatwarning( message=warning_message.message, category=warning_message.category, filename=warning_message.filename, lineno=warning_message.lineno, line=warning_message.line, ) - return msg def parse_filterwarnings(x: str | list[str] | None) -> list[str]: @@ -144,7 +145,8 @@ def parse_filterwarnings(x: str | list[str] | None) -> list[str]: return [] if isinstance(x, (list, tuple)): return [i.strip() for i in x] - raise TypeError("'filterwarnings' must be a str, list[str] or None.") + msg = "'filterwarnings' must be a str, list[str] or None." + raise TypeError(msg) @contextmanager diff --git a/tests/test_collect_utils.py b/tests/test_collect_utils.py index e4dbc4c1..d2728023 100644 --- a/tests/test_collect_utils.py +++ b/tests/test_collect_utils.py @@ -15,7 +15,7 @@ from pytask import depends_on from pytask import produces from pytask import Product -from typing_extensions import Annotated +from typing_extensions import Annotated # noqa: TCH002 ERROR = "'@pytask.mark.depends_on' has nodes with the same name:"