diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ef87f54..085dc02 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-added-large-files args: ['--maxkb=100'] @@ -24,28 +24,19 @@ repos: - id: python-no-log-warn - id: python-use-type-annotations - id: text-unicode-replacement-char -- repo: https://github.com/asottile/reorder-python-imports - rev: v3.12.0 - hooks: - - id: reorder-python-imports - args: [--py38-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/setup-cfg-fmt rev: v2.5.0 hooks: - id: setup-cfg-fmt -- repo: https://github.com/psf/black - rev: 23.9.1 - hooks: - - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.292 + rev: v0.3.4 hooks: - id: ruff + - id: ruff-format - repo: https://github.com/dosisod/refurb - rev: v1.21.0 + rev: v2.0.0 hooks: - id: refurb - args: [--ignore, FURB126] - repo: https://github.com/executablebooks/mdformat rev: 0.7.17 hooks: @@ -55,13 +46,8 @@ repos: mdformat-black, ] args: [--wrap, "88"] -- repo: https://github.com/econchick/interrogate - rev: 1.5.0 - hooks: - - id: interrogate - args: [-v, --fail-under=40, src] - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v1.5.1' + rev: 'v1.9.0' hooks: - id: mypy additional_dependencies: [ @@ -81,7 +67,7 @@ repos: hooks: - id: check-manifest args: [--no-build-isolation] - additional_dependencies: [setuptools-scm, toml] + additional_dependencies: [setuptools-scm, toml, wheel] - repo: meta hooks: - id: check-hooks-apply diff --git a/README.md b/README.md index 2806739..930ab56 100644 --- a/README.md +++ b/README.md @@ -213,8 +213,7 @@ Use the `serializer` keyword arguments of the `@pytask.mark.julia` decorator wit ```python @pytask.mark.julia(script="script.jl", serializer="yaml") -def task_example(): - ... +def task_example(): ... ``` And in your Julia script use @@ -236,8 +235,7 @@ import json @pytask.mark.julia(script="script.jl", serializer=json.dumps, suffix=".json") -def task_example(): - ... +def task_example(): ... ``` ### Configuration diff --git a/pyproject.toml b/pyproject.toml index 43ffdb9..ab86f5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,11 +2,9 @@ requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.0"] build-backend = "setuptools.build_meta" - [tool.setuptools_scm] write_to = "src/pytask_julia/_version.py" - [tool.mypy] files = ["src", "tests"] check_untyped_defs = true @@ -17,50 +15,31 @@ no_implicit_optional = true warn_redundant_casts = true warn_unused_ignores = true - [[tool.mypy.overrides]] module = "tests.*" disallow_untyped_defs = false ignore_errors = true - [tool.ruff] target-version = "py38" -select = ["ALL"] fix = true +unsafe-fixes = true + +[tool.ruff.lint] extend-ignore = [ - "I", # ignore isort - "TRY", # ignore tryceratops - # Numpy docstyle - "D107", - "D203", - "D212", - "D213", - "D402", - "D413", - "D415", - "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. - "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 + "COM812", # Comply with ruff-format. + "ISC001", # Comply with ruff-format. ] +select = ["ALL"] +[tool.ruff.lint.per-file-ignores] +"tests/*" = ["D", "ANN", "S101"] -[tool.ruff.per-file-ignores] -"tests/*" = ["D", "ANN", "PLR2004", "PLR0913"] - +[tool.ruff.lint.isort] +force-single-line = true -[tool.ruff.pydocstyle] +[tool.ruff.lint.pydocstyle] convention = "numpy" [tool.pytest.ini_options] diff --git a/setup.cfg b/setup.cfg index e05751b..bf87ad0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,7 +25,7 @@ project_urls = packages = find: install_requires = pluggy>=1.0.0 - pytask>=0.4.0 + pytask>=0.4.5 python_requires = >=3.8 include_package_data = True package_dir = =src diff --git a/src/pytask_julia/__init__.py b/src/pytask_julia/__init__.py index 2a4567b..d0fff47 100644 --- a/src/pytask_julia/__init__.py +++ b/src/pytask_julia/__init__.py @@ -1,4 +1,5 @@ -"""This module contains the main namespace.""" +"""Contains the main namespace.""" + from __future__ import annotations try: diff --git a/src/pytask_julia/collect.py b/src/pytask_julia/collect.py index 3b9a975..1aa2fb0 100644 --- a/src/pytask_julia/collect.py +++ b/src/pytask_julia/collect.py @@ -1,4 +1,5 @@ """Collect tasks.""" + from __future__ import annotations import subprocess @@ -7,26 +8,26 @@ from typing import Any from typing import Callable -from pytask import has_mark -from pytask import hookimpl -from pytask import is_task_function from pytask import Mark from pytask import NodeInfo -from pytask import parse_dependencies_from_task_function -from pytask import parse_products_from_task_function from pytask import PathNode from pytask import PTask from pytask import PythonNode -from pytask import remove_marks from pytask import Session from pytask import Task from pytask import TaskWithoutPath -from pytask_julia.serialization import create_path_to_serialized +from pytask import has_mark +from pytask import hookimpl +from pytask import is_task_function +from pytask import parse_dependencies_from_task_function +from pytask import parse_products_from_task_function +from pytask import remove_marks + from pytask_julia.serialization import SERIALIZERS +from pytask_julia.serialization import create_path_to_serialized from pytask_julia.shared import julia from pytask_julia.shared import parse_relative_path - _SEPARATOR: str = "--" """str: Separates options for the Julia executable and arguments to the file.""" @@ -62,9 +63,12 @@ def pytask_collect_task( # Parse the @pytask.mark.julia decorator. obj, marks = remove_marks(obj, "julia") if len(marks) > 1: - raise ValueError( + msg = ( f"Task {name!r} has multiple @pytask.mark.julia marks, but only one is " - "allowed.", + "allowed." + ) + raise ValueError( + msg, ) mark = _parse_julia_mark( @@ -98,10 +102,11 @@ def pytask_collect_task( ) if not (isinstance(script_node, PathNode) and script_node.path.suffix == ".jl"): - raise ValueError( + msg = ( "The 'script' keyword of the @pytask.mark.julia decorator must point " f"to Julia file with the .jl suffix, but it is {script_node}." ) + raise ValueError(msg) options_node = session.hook.pytask_collect_node( session=session, @@ -211,8 +216,7 @@ def _parse_julia_mark( else: parsed_kwargs["project"] = default_project - mark = Mark("julia", (), parsed_kwargs) - return mark + return Mark("julia", (), parsed_kwargs) def _parse_project(project: str | Path | None, root: Path) -> list[str]: diff --git a/src/pytask_julia/config.py b/src/pytask_julia/config.py index 1849fd3..af8f47d 100644 --- a/src/pytask_julia/config.py +++ b/src/pytask_julia/config.py @@ -1,9 +1,11 @@ """Configure pytask.""" + from __future__ import annotations from typing import Any from pytask import hookimpl + from pytask_julia.serialization import SERIALIZERS from pytask_julia.shared import parse_relative_path @@ -14,9 +16,12 @@ def pytask_parse_config(config: dict[str, Any]) -> None: config["markers"]["julia"] = "Tasks which are executed with Julia." config["julia_serializer"] = config.get("julia_serializer", "json") if config["julia_serializer"] not in SERIALIZERS: - raise ValueError( + msg = ( f"'julia_serializer' is {config['julia_serializer']} and not one of " - f"{list(SERIALIZERS)}.", + f"{list(SERIALIZERS)}." + ) + raise ValueError( + msg, ) config["julia_suffix"] = config.get("julia_suffix", "") config["julia_options"] = _parse_value_or_whitespace_option( @@ -35,4 +40,5 @@ def _parse_value_or_whitespace_option(value: Any) -> None | list[str]: return None if isinstance(value, list): return list(map(str, value)) - raise ValueError(f"'julia_options' is {value} and not a list.") + msg = f"'julia_options' is {value} and not a list." + raise ValueError(msg) diff --git a/src/pytask_julia/execute.py b/src/pytask_julia/execute.py index 6ea5c7d..f090df6 100644 --- a/src/pytask_julia/execute.py +++ b/src/pytask_julia/execute.py @@ -1,18 +1,23 @@ """Execute tasks.""" + from __future__ import annotations import shutil +from typing import TYPE_CHECKING from typing import Any -from pytask import get_marks -from pytask import hookimpl from pytask import PPathNode from pytask import PTask -from pytask import PythonNode +from pytask import get_marks +from pytask import hookimpl from pytask.tree_util import tree_map + from pytask_julia.serialization import serialize_keyword_arguments from pytask_julia.shared import julia +if TYPE_CHECKING: + from pathlib import Path + @hookimpl def pytask_execute_task_setup(task: PTask) -> None: @@ -20,22 +25,21 @@ def pytask_execute_task_setup(task: PTask) -> None: marks = get_marks(task, "julia") if marks: if shutil.which("julia") is None: - raise RuntimeError( + msg = ( "julia is needed to run Julia scripts, but it is not found on your " - "PATH.", + "PATH." + ) + raise RuntimeError( + msg, ) - assert len(marks) == 1 - - _, _, serializer, suffix, _ = julia(**marks[0].kwargs) - - assert serializer - assert suffix is not None + _, _, serializer, _, _ = julia(**marks[0].kwargs) - serialized_node: PythonNode = task.depends_on["_serialized"] # type: ignore[assignment] - serialized_node.value.parent.mkdir(parents=True, exist_ok=True) + serialized_node = task.depends_on["_serialized"] + path: Path = serialized_node.value # type: ignore[attr-defined] + path.parent.mkdir(parents=True, exist_ok=True) kwargs = collect_keyword_arguments(task) - serialize_keyword_arguments(serializer, serialized_node.value, kwargs) + serialize_keyword_arguments(serializer, path, kwargs) def collect_keyword_arguments(task: PTask) -> dict[str, Any]: diff --git a/src/pytask_julia/plugin.py b/src/pytask_julia/plugin.py index 8c05235..dc7b6b1 100644 --- a/src/pytask_julia/plugin.py +++ b/src/pytask_julia/plugin.py @@ -1,9 +1,11 @@ """Register hook specifications and implementations.""" + from __future__ import annotations from typing import TYPE_CHECKING from pytask import hookimpl + from pytask_julia import collect from pytask_julia import config from pytask_julia import execute diff --git a/src/pytask_julia/serialization.py b/src/pytask_julia/serialization.py index 2932dff..c2b0835 100644 --- a/src/pytask_julia/serialization.py +++ b/src/pytask_julia/serialization.py @@ -1,4 +1,5 @@ -"""This module contains the code to serialize keyword arguments to the task.""" +"""Contains the code to serialize keyword arguments to the task.""" + from __future__ import annotations import json @@ -9,7 +10,6 @@ from pytask import PTask from pytask import PTaskWithPath - _HIDDEN_FOLDER = ".pytask/pytask-julia" @@ -30,8 +30,7 @@ def create_path_to_serialized(task: PTask, suffix: str) -> Path: """Create path to serialized.""" parent = task.path.parent if isinstance(task, PTaskWithPath) else Path.cwd() file_name = create_file_name(task, suffix) - path = parent.joinpath(_HIDDEN_FOLDER, file_name).with_suffix(suffix) - return path + return parent.joinpath(_HIDDEN_FOLDER, file_name).with_suffix(suffix) def create_file_name(task: PTask, suffix: str) -> str: @@ -53,7 +52,7 @@ def create_file_name(task: PTask, suffix: str) -> str: def serialize_keyword_arguments( - serializer: str | Callable[..., str], + serializer: str | Callable[..., str] | None, path_to_serialized: Path, kwargs: dict[str, Any], ) -> None: @@ -61,11 +60,10 @@ def serialize_keyword_arguments( if callable(serializer): serializer_func = serializer elif isinstance(serializer, str) and serializer in SERIALIZERS: - serializer_func = SERIALIZERS[serializer][ - "serializer" - ] # type: ignore[assignment] + serializer_func = SERIALIZERS[serializer]["serializer"] # type: ignore[assignment] else: # pragma: no cover - raise ValueError(f"Serializer {serializer!r} is not known.") + msg = f"Serializer {serializer!r} is not known." + raise ValueError(msg) serialized = serializer_func(kwargs) path_to_serialized.write_text(serialized) diff --git a/src/pytask_julia/shared.py b/src/pytask_julia/shared.py index 079b62b..a17be29 100644 --- a/src/pytask_julia/shared.py +++ b/src/pytask_julia/shared.py @@ -1,4 +1,5 @@ -"""This module contains shared functions.""" +"""Contains shared functions.""" + from __future__ import annotations from pathlib import Path diff --git a/tests/conftest.py b/tests/conftest.py index a0529f6..74128c6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,7 +8,7 @@ import pytest from click.testing import CliRunner - +from pytask import storage ROOT = Path(__file__).parent.joinpath("..").resolve() @@ -32,7 +32,7 @@ class SysPathsSnapshot: """A snapshot for sys.path.""" def __init__(self) -> None: - self.__saved = list(sys.path), list(sys.meta_path) + self.__saved = sys.path.copy(), sys.meta_path.copy() def restore(self) -> None: sys.path[:], sys.meta_path[:] = self.__saved @@ -43,7 +43,7 @@ class SysModulesSnapshot: def __init__(self, preserve: Callable[[str], bool] | None = None) -> None: self.__preserve = preserve - self.__saved = dict(sys.modules) + self.__saved = sys.modules.copy() def restore(self) -> None: if self.__preserve: @@ -82,6 +82,7 @@ def _restore_sys_path_and_module_after_test_execution(): class CustomCliRunner(CliRunner): def invoke(self, *args, **kwargs): """Restore sys.path and sys.modules after an invocation.""" + storage.create() with restore_sys_path_and_module_after_test_execution(): return super().invoke(*args, **kwargs) diff --git a/tests/test_collect.py b/tests/test_collect.py index c0ecedc..7dafc0e 100644 --- a/tests/test_collect.py +++ b/tests/test_collect.py @@ -4,9 +4,9 @@ import pytest from pytask import Mark +from pytask_julia.collect import SERIALIZERS from pytask_julia.collect import _parse_julia_mark from pytask_julia.collect import _parse_project -from pytask_julia.collect import SERIALIZERS from tests.conftest import ROOT @@ -71,7 +71,7 @@ ), ], ) -def test_parse_julia_mark( +def test_parse_julia_mark( # noqa: PLR0913 mark, default_options, default_serializer, diff --git a/tests/test_execute.py b/tests/test_execute.py index 75168a9..e206612 100644 --- a/tests/test_execute.py +++ b/tests/test_execute.py @@ -6,16 +6,16 @@ from pathlib import Path import pytest -from pytask import build -from pytask import cli from pytask import ExitCode from pytask import Mark from pytask import Task +from pytask import build +from pytask import cli from pytask_julia.execute import pytask_execute_task_setup +from tests.conftest import ROOT from tests.conftest import needs_julia from tests.conftest import parametrize_parse_code_serializer_suffix -from tests.conftest import ROOT @pytest.mark.unit() @@ -40,7 +40,7 @@ def test_pytask_execute_task_setup_missing_julia(monkeypatch): @pytest.mark.end_to_end() @parametrize_parse_code_serializer_suffix @pytest.mark.parametrize("depends_on", ["'in_1.txt'", "['in_1.txt', 'in_2.txt']"]) -def test_run_jl_script( +def test_run_jl_script( # noqa: PLR0913 runner, tmp_path, parse_config_code, @@ -211,7 +211,7 @@ def task_run_jl_script(): @pytest.mark.end_to_end() @pytest.mark.parametrize("n_threads", [2, 3]) @parametrize_parse_code_serializer_suffix -def test_check_passing_cmd_line_options( +def test_check_passing_cmd_line_options( # noqa: PLR0913 runner, tmp_path, n_threads, @@ -256,7 +256,7 @@ def task_run_jl_script(): ) @parametrize_parse_code_serializer_suffix @pytest.mark.parametrize("path", [ROOT, "relative_from_config"]) -def test_run_jl_script_w_environment_in_config( +def test_run_jl_script_w_environment_in_config( # noqa: PLR0913 runner, tmp_path, parse_config_code, diff --git a/tests/test_normal_execution_w_plugin.py b/tests/test_normal_execution_w_plugin.py index 6ed0f0c..8825b52 100644 --- a/tests/test_normal_execution_w_plugin.py +++ b/tests/test_normal_execution_w_plugin.py @@ -1,11 +1,12 @@ """Contains tests which do not require the plugin and ensure normal execution.""" + from __future__ import annotations import textwrap import pytest -from pytask import cli from pytask import ExitCode +from pytask import cli @pytest.mark.end_to_end() diff --git a/tests/test_parallel.py b/tests/test_parallel.py index 9d042e2..8806418 100644 --- a/tests/test_parallel.py +++ b/tests/test_parallel.py @@ -1,15 +1,16 @@ """Contains test which ensure that the plugin works with pytask-parallel.""" + from __future__ import annotations import textwrap import pytest -from pytask import cli from pytask import ExitCode +from pytask import cli +from tests.conftest import ROOT from tests.conftest import needs_julia from tests.conftest import parametrize_parse_code_serializer_suffix -from tests.conftest import ROOT try: import pytask_parallel # noqa: F401 diff --git a/tests/test_parametrize.py b/tests/test_parametrize.py index 8a45128..54606ce 100644 --- a/tests/test_parametrize.py +++ b/tests/test_parametrize.py @@ -3,12 +3,12 @@ import textwrap import pytest -from pytask import cli from pytask import ExitCode +from pytask import cli +from tests.conftest import ROOT from tests.conftest import needs_julia from tests.conftest import parametrize_parse_code_serializer_suffix -from tests.conftest import ROOT @needs_julia