diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index def8cca..8cebca0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,6 +5,9 @@ concurrency: group: ${{ github.head_ref || github.run_id }} cancel-in-progress: true +env: + CONDA_EXE: mamba + on: push: branches: @@ -24,18 +27,20 @@ jobs: fail-fast: false matrix: os: ['ubuntu-latest', 'macos-latest', 'windows-latest'] - python-version: ['3.7', '3.8', '3.9', '3.10'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v2 - uses: conda-incubator/setup-miniconda@v2 with: - auto-update-conda: true + auto-update-conda: false python-version: ${{ matrix.python-version }} + channels: conda-forge,nodefaults + miniforge-variant: Mambaforge - name: Install core dependencies. shell: bash -l {0} - run: conda install -c conda-forge tox-conda coverage + run: mamba install -c conda-forge tox-conda coverage mamba # Unit, integration, and end-to-end tests. @@ -44,7 +49,7 @@ jobs: run: tox -e pytest -- -m "unit or (not integration and not end_to_end)" --cov=./ --cov-report=xml -n auto - name: Upload coverage report for unit tests and doctests. - if: runner.os == 'Linux' && matrix.python-version == '3.9' + if: runner.os == 'Linux' && matrix.python-version == '3.8' shell: bash -l {0} run: bash <(curl -s https://codecov.io/bash) -F unit -c @@ -53,6 +58,6 @@ jobs: run: tox -e pytest -- -m end_to_end --cov=./ --cov-report=xml -n auto - name: Upload coverage reports of end-to-end tests. - if: runner.os == 'Linux' && matrix.python-version == '3.9' + if: runner.os == 'Linux' && matrix.python-version == '3.8' shell: bash -l {0} run: bash <(curl -s https://codecov.io/bash) -F end_to_end -c diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4c09c56..e753d5c 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.2.0 + rev: v4.4.0 hooks: - id: check-added-large-files args: ['--maxkb=100'] @@ -9,7 +9,7 @@ repos: - id: debug-statements - id: end-of-file-fixer - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.9.0 # Use the ref you want to point at + rev: v1.10.0 hooks: - id: python-check-blanket-noqa - id: python-check-mock-methods @@ -17,52 +17,40 @@ repos: - id: python-no-log-warn - id: python-use-type-annotations - id: text-unicode-replacement-char -- repo: https://github.com/asottile/pyupgrade - rev: v2.32.0 - hooks: - - id: pyupgrade - args: [--py37-plus] - repo: https://github.com/asottile/reorder_python_imports - rev: v3.0.1 + rev: v3.9.0 hooks: - id: reorder-python-imports args: [--py37-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.20.1 + rev: v2.2.0 hooks: - id: setup-cfg-fmt +- repo: https://github.com/PyCQA/docformatter + rev: v1.5.1 + hooks: + - id: docformatter + args: [--in-place, --wrap-summaries, "88", --wrap-descriptions, "88", --blank] - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 22.12.0 hooks: - id: black -- repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 +- repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.0.223 hooks: - - id: flake8 - types: [python] - additional_dependencies: [ - flake8-alfred, - flake8-bugbear, - flake8-builtins, - flake8-comprehensions, - flake8-docstrings, - flake8-eradicate, - flake8-print, - flake8-pytest-style, - flake8-todo, - flake8-typing-imports, - flake8-unused-arguments, - pep8-naming, - pydocstyle, - Pygments, - ] + - id: ruff +- repo: https://github.com/dosisod/refurb + rev: v1.10.0 + hooks: + - id: refurb + args: [--ignore, FURB126] - repo: https://github.com/econchick/interrogate rev: 1.5.0 hooks: - id: interrogate args: [-v, --fail-under=40, src, tests] - repo: https://github.com/executablebooks/mdformat - rev: 0.7.14 + rev: 0.7.16 hooks: - id: mdformat additional_dependencies: [ @@ -71,11 +59,26 @@ repos: ] args: [--wrap, "88"] - repo: https://github.com/codespell-project/codespell - rev: v2.1.0 + rev: v2.2.2 hooks: - id: codespell +- repo: https://github.com/pre-commit/mirrors-mypy + rev: 'v0.991' + hooks: + - id: mypy + args: [ + --no-strict-optional, + --ignore-missing-imports, + ] + additional_dependencies: [ + attrs>=21.3.0, + click, + types-PyYAML, + types-setuptools + ] + pass_filenames: false - repo: https://github.com/mgedmin/check-manifest - rev: "0.48" + rev: "0.49" hooks: - id: check-manifest args: [--no-build-isolation] diff --git a/CHANGES.md b/CHANGES.md index e44d3e5..6ff3ff0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,9 +5,16 @@ chronological order. Releases follow [semantic versioning](https://semver.org/) releases are available on [PyPI](https://pypi.org/project/pytask-r) and [Anaconda.org](https://anaconda.org/conda-forge/pytask-r). -## 0.2.0 - 2022-xx-xx +## 0.3.0 - 2023-xx-xx + +- {pull}`33` deprecates INI configurations and aligns the plugin with pytask v0.3. +- {pull}`34` adds mypy, ruff and refurb. +- {pull}`35` publishes types. + +## 0.2.0 - 2022-04-16 - {pull}`24` removes an unnecessary hook implementation. +- {pull}`26` implements the new interface for v0.2. ## 0.1.1 - 2022-02-08 diff --git a/MANIFEST.in b/MANIFEST.in index ebba16e..438b3ea 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,3 +8,5 @@ exclude tox.ini include LICENSE include README.md + +recursive-include src py.typed diff --git a/README.md b/README.md index 7d90f84..0ffeef9 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![image](https://img.shields.io/conda/vn/conda-forge/pytask-r.svg)](https://anaconda.org/conda-forge/pytask-r) [![image](https://img.shields.io/conda/pn/conda-forge/pytask-r.svg)](https://anaconda.org/conda-forge/pytask-r) [![PyPI - License](https://img.shields.io/pypi/l/pytask-r)](https://pypi.org/project/pytask-r) -[![image](https://img.shields.io/github/workflow/status/pytask-dev/pytask-r/main/main)](https://github.com/pytask-dev/pytask-r/actions?query=branch%3Amain) +[![image](https://img.shields.io/github/actions/workflow/status/pytask-dev/pytask-r/main.yml?branch=main)](https://github.com/pytask-dev/pytask-r/actions?query=branch%3Amain) [![image](https://codecov.io/gh/pytask-dev/pytask-r/branch/main/graph/badge.svg)](https://codecov.io/gh/pytask-dev/pytask-r) [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/pytask-dev/pytask-r/main.svg)](https://results.pre-commit.ci/latest/github/pytask-dev/pytask-r/main) [![image](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) diff --git a/environment.yml b/environment.yml index 836ac2c..e08ca6f 100644 --- a/environment.yml +++ b/environment.yml @@ -16,8 +16,8 @@ dependencies: - conda-verify # Package dependencies - - pytask >=0.2 - - pytask-parallel >=0.2 + - pytask >=0.3 + - pytask-parallel >=0.3 - r-base >4 - r-jsonlite diff --git a/pyproject.toml b/pyproject.toml index afa5c36..fba8e55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,3 +5,59 @@ build-backend = "setuptools.build_meta" [tool.setuptools_scm] write_to = "src/pytask_r/_version.py" + + +[tool.mypy] +files = ["src", "tests"] +check_untyped_defs = true +disallow_any_generics = true +disallow_incomplete_defs = true +disallow_untyped_defs = true +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 = "py37" +select = ["ALL"] +fix = true +extend-ignore = [ + # 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. + "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 +] + + +[tool.ruff.per-file-ignores] +"tests/*" = ["D", "ANN"] + + +[tool.ruff.pydocstyle] +convention = "numpy" diff --git a/setup.cfg b/setup.cfg index e0abd28..35ed3b7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,10 +15,6 @@ classifiers = Operating System :: OS Independent Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 Programming Language :: R project_urls = Changelog = https://github.com/pytask-dev/pytask-r/blob/main/CHANGES.md @@ -31,7 +27,7 @@ packages = find: install_requires = click pybaum>=0.1.1 - pytask>=0.2 + pytask>=0.3 python_requires = >=3.7 include_package_data = True package_dir = =src diff --git a/src/pytask_r/__init__.py b/src/pytask_r/__init__.py index f493641..917f1d8 100644 --- a/src/pytask_r/__init__.py +++ b/src/pytask_r/__init__.py @@ -1,3 +1,4 @@ +"""This module contains the main namespace of pytask-r.""" from __future__ import annotations try: diff --git a/src/pytask_r/collect.py b/src/pytask_r/collect.py index 739a511..0e084ba 100644 --- a/src/pytask_r/collect.py +++ b/src/pytask_r/collect.py @@ -5,6 +5,7 @@ import subprocess from pathlib import Path from types import FunctionType +from typing import Any from pytask import depends_on from pytask import has_mark @@ -13,20 +14,24 @@ from pytask import parse_nodes from pytask import produces from pytask import remove_marks +from pytask import Session from pytask import Task -from pytask_r.serialization import SERIALIZER +from pytask_r.serialization import SERIALIZERS from pytask_r.shared import r +from pytask_r.shared import R_SCRIPT_KEY def run_r_script(script: Path, options: list[str], serialized: Path) -> None: """Run an R script.""" cmd = ["Rscript", script.as_posix(), *options, str(serialized)] - print("Executing " + " ".join(cmd) + ".") # noqa: T001 + print("Executing " + " ".join(cmd) + ".") # noqa: T201 subprocess.run(cmd, check=True) @hookimpl -def pytask_collect_task(session, path, name, obj): +def pytask_collect_task( + session: Session, path: Path, name: str, obj: Any +) -> Task | None: """Perform some checks.""" __tracebackhide__ = True @@ -62,7 +67,7 @@ def pytask_collect_task(session, path, name, obj): task = Task( base_name=name, path=path, - function=_copy_func(run_r_script), + function=_copy_func(run_r_script), # type: ignore[arg-type] depends_on=dependencies, produces=products, markers=markers, @@ -74,39 +79,44 @@ def pytask_collect_task(session, path, name, obj): ) if isinstance(task.depends_on, dict): - task.depends_on["__script"] = script_node + task.depends_on[R_SCRIPT_KEY] = script_node task.attributes["r_keep_dict"] = True else: - task.depends_on = {0: task.depends_on, "__script": script_node} + task.depends_on = {0: task.depends_on, R_SCRIPT_KEY: script_node} task.attributes["r_keep_dict"] = False task.function = functools.partial( - task.function, script=task.depends_on["__script"].path, options=options + task.function, script=task.depends_on[R_SCRIPT_KEY].path, options=options ) return task + return None -def _parse_r_mark(mark, default_options, default_serializer, default_suffix): +def _parse_r_mark( + mark: Mark, + default_options: list[str] | None, + default_serializer: str, + default_suffix: str, +) -> Mark: """Parse a Julia mark.""" script, options, serializer, suffix = r(**mark.kwargs) parsed_kwargs = {} - for arg_name, value, default in [ + for arg_name, value, default in ( ("script", script, None), ("options", options, default_options), ("serializer", serializer, default_serializer), - ]: - parsed_kwargs[arg_name] = value if value else default - - if ( - isinstance(parsed_kwargs["serializer"], str) - and parsed_kwargs["serializer"] in SERIALIZER ): - proposed_suffix = SERIALIZER[parsed_kwargs["serializer"]]["suffix"] - else: - proposed_suffix = default_suffix - parsed_kwargs["suffix"] = suffix if suffix else proposed_suffix + parsed_kwargs[arg_name] = value or default + + proposed_suffix = ( + SERIALIZERS[parsed_kwargs["serializer"]]["suffix"] + if isinstance(parsed_kwargs["serializer"], str) + and parsed_kwargs["serializer"] in SERIALIZERS + else default_suffix + ) + parsed_kwargs["suffix"] = suffix or proposed_suffix # type: ignore[assignment] mark = Mark("r", (), parsed_kwargs) return mark diff --git a/src/pytask_r/config.py b/src/pytask_r/config.py index faabb81..f107589 100644 --- a/src/pytask_r/config.py +++ b/src/pytask_r/config.py @@ -1,27 +1,30 @@ """Configure pytask.""" from __future__ import annotations +from typing import Any + from pytask import hookimpl +from pytask_r.serialization import SERIALIZERS @hookimpl -def pytask_parse_config(config, config_from_file): +def pytask_parse_config(config: dict[str, Any]) -> None: """Register the r marker.""" config["markers"]["r"] = "Tasks which are executed with Rscript." - config["r_serializer"] = config_from_file.get("r_serializer", "json") - config["r_suffix"] = config_from_file.get("r_suffix", "") - config["r_options"] = _parse_value_or_whitespace_option( - config_from_file.get("r_options") - ) + config["r_serializer"] = config.get("r_serializer", "json") + if config["r_serializer"] not in SERIALIZERS: + raise ValueError( + f"'r_serializer' is {config['r_serializer']} and not one of " + f"{list(SERIALIZERS)}." + ) + config["r_suffix"] = config.get("r_suffix", "") + config["r_options"] = _parse_value_or_whitespace_option(config.get("r_options")) -def _parse_value_or_whitespace_option(value: str | None) -> None | str | list[str]: +def _parse_value_or_whitespace_option(value: Any) -> list[str] | None: """Parse option which can hold a single value or values separated by new lines.""" - if value in ["none", "None", None, ""]: + if value is None: return None - elif isinstance(value, list): + if isinstance(value, list): return list(map(str, value)) - elif isinstance(value, str): - return [v.strip() for v in value.split(" ") if v.strip()] - else: - raise ValueError(f"Input {value!r} is neither a 'str' nor 'None'.") + raise ValueError(f"'r_options' is {value} and not a list.") diff --git a/src/pytask_r/execute.py b/src/pytask_r/execute.py index 8d26982..70c0a7e 100644 --- a/src/pytask_r/execute.py +++ b/src/pytask_r/execute.py @@ -12,10 +12,11 @@ from pytask_r.serialization import create_path_to_serialized from pytask_r.serialization import serialize_keyword_arguments from pytask_r.shared import r +from pytask_r.shared import R_SCRIPT_KEY @hookimpl -def pytask_execute_task_setup(task): +def pytask_execute_task_setup(task: Task) -> None: """Perform some checks when a task marked with the r marker is executed.""" marks = get_marks(task, "r") if marks: @@ -44,13 +45,16 @@ def collect_keyword_arguments(task: Task) -> dict[str, Any]: kwargs = dict(task.kwargs) task.kwargs = {} - if len(task.depends_on) == 1 and "__script" in task.depends_on: + if len(task.depends_on) == 1 and R_SCRIPT_KEY in task.depends_on: pass - elif not task.attributes["r_keep_dict"] and len(task.depends_on) == 2: + elif ( + not task.attributes["r_keep_dict"] + and len(task.depends_on) == 2 # noqa: PLR2004 + ): kwargs["depends_on"] = str(task.depends_on[0].value) else: kwargs["depends_on"] = tree_map(lambda x: str(x.value), task.depends_on) - kwargs["depends_on"].pop("__script") + kwargs["depends_on"].pop(R_SCRIPT_KEY) if task.produces: kwargs["produces"] = tree_map(lambda x: str(x.value), task.produces) diff --git a/src/pytask_r/parametrize.py b/src/pytask_r/parametrize.py index 0f5cefc..f1040da 100644 --- a/src/pytask_r/parametrize.py +++ b/src/pytask_r/parametrize.py @@ -1,13 +1,14 @@ """Parametrize tasks.""" from __future__ import annotations +from typing import Any + import pytask from pytask import hookimpl @hookimpl -def pytask_parametrize_kwarg_to_marker(obj, kwargs): +def pytask_parametrize_kwarg_to_marker(obj: Any, kwargs: dict[Any, Any]) -> None: """Attach parametrized r arguments to the function with a marker.""" - if callable(obj): - if "r" in kwargs: - pytask.mark.r(**kwargs.pop("r"))(obj) + if callable(obj) and "r" in kwargs: # noqa: PLR2004 + pytask.mark.r(**kwargs.pop("r"))(obj) diff --git a/src/pytask_r/plugin.py b/src/pytask_r/plugin.py index 5440ed8..4c10eec 100644 --- a/src/pytask_r/plugin.py +++ b/src/pytask_r/plugin.py @@ -1,6 +1,7 @@ """Register hook specifications and implementations.""" from __future__ import annotations +from pluggy import PluginManager from pytask import hookimpl from pytask_r import collect from pytask_r import config @@ -9,7 +10,7 @@ @hookimpl -def pytask_add_hooks(pm): +def pytask_add_hooks(pm: PluginManager) -> None: """Register hook implementations.""" pm.register(collect) pm.register(config) diff --git a/src/pytask_r/py.typed b/src/pytask_r/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/pytask_r/serialization.py b/src/pytask_r/serialization.py index 9bb0499..195772d 100644 --- a/src/pytask_r/serialization.py +++ b/src/pytask_r/serialization.py @@ -12,7 +12,7 @@ _HIDDEN_FOLDER = ".pytask" -SERIALIZER = {"json": {"serializer": json.dumps, "suffix": ".json"}} +SERIALIZERS = {"json": {"serializer": json.dumps, "suffix": ".json"}} try: @@ -20,8 +20,8 @@ except ImportError: # pragma: no cover pass else: - SERIALIZER["yaml"] = {"serializer": yaml.dump, "suffix": ".yaml"} - SERIALIZER["yml"] = {"serializer": yaml.dump, "suffix": ".yml"} + SERIALIZERS["yaml"] = {"serializer": yaml.dump, "suffix": ".yaml"} + SERIALIZERS["yml"] = {"serializer": yaml.dump, "suffix": ".yml"} def create_path_to_serialized(task: Task, suffix: str) -> Path: @@ -50,14 +50,17 @@ def create_file_name(task: Task, suffix: str) -> str: def serialize_keyword_arguments( - serializer: str | Callable[dict[str, Any], str], + serializer: str | Callable[..., str], path_to_serialized: Path, kwargs: dict[str, Any], ) -> None: + """Serialize keyword arguments.""" if callable(serializer): serializer_func = serializer - elif isinstance(serializer, str) and serializer in SERIALIZER: - serializer_func = SERIALIZER[serializer]["serializer"] + elif isinstance(serializer, str) and serializer in SERIALIZERS: + serializer_func = SERIALIZERS[serializer][ + "serializer" + ] # type: ignore[assignment] else: raise ValueError(f"Serializer {serializer!r} is not known.") diff --git a/src/pytask_r/shared.py b/src/pytask_r/shared.py index 36026af..c0aab5f 100644 --- a/src/pytask_r/shared.py +++ b/src/pytask_r/shared.py @@ -1,48 +1,19 @@ +"""This module contains shared functions.""" from __future__ import annotations from pathlib import Path +from typing import Any from typing import Callable from typing import Iterable from typing import Sequence -_ERROR_MSG = """The old syntax for @pytask.mark.r was suddenly deprecated starting \ -with pytask-r v0.2 to provide a better user experience. Thank you for your \ -understanding! - -It is recommended to upgrade to the new syntax, so you enjoy all the benefits of v0.2 \ -of pytask and pytask-r which is among others access to 'depends_on' and 'produces', \ -and other kwargs inside the R script. - -You can find a manual here: \ -https://github.com/pytask-dev/pytask-r/blob/v0.2.0/README.md - -Upgrading can be as easy as rewriting your current task from - - @pytask.mark.r(["--option", "path_to_dependency.txt"]) - @pytask.mark.depends_on("script.R") - @pytask.mark.produces("out.csv") - def task_r(): - ... - -to - - @pytask.mark.r(script="script.r", options="--option") - @pytask.mark.depends_on("path_to_dependency.txt") - @pytask.mark.produces("out.csv") - def task_r(): - ... - -You can also fix the version of pytask and pytask-r to <0.2, so you do not have to \ -to upgrade. At the same time, you will not enjoy the improvements released with \ -version v0.2 of pytask and pytask-r. - -""" +R_SCRIPT_KEY = "__script" def r( - *args, - script: str | Path = None, + *, + script: str | Path, options: str | Iterable[str] | None = None, serializer: str | Callable[..., str] | str | None = None, suffix: str | None = None, @@ -56,7 +27,7 @@ def r( Parameters ---------- - script : Union[str, Path] + script : str | Path The path to the R script which is executed. options : str | Iterable[str] One or multiple command line options passed to Rscript. @@ -70,24 +41,13 @@ def r( ``".json"``. """ - if args or script is None: - raise RuntimeError(_ERROR_MSG) - options = [] if options is None else list(map(str, _to_list(options))) return script, options, serializer, suffix -def _to_list(scalar_or_iter): +def _to_list(scalar_or_iter: Any) -> list[Any]: """Convert scalars and iterables to list. - Parameters - ---------- - scalar_or_iter : str or list - - Returns - ------- - list - Examples -------- >>> _to_list("a") diff --git a/tests/test_collect.py b/tests/test_collect.py index fa78c14..2ce9752 100644 --- a/tests/test_collect.py +++ b/tests/test_collect.py @@ -6,26 +6,13 @@ from pytask import Mark from pytask_r.collect import _parse_r_mark from pytask_r.collect import r -from pytask_r.serialization import SERIALIZER +from pytask_r.serialization import SERIALIZERS -@pytest.mark.unit +@pytest.mark.unit() @pytest.mark.parametrize( - "args, kwargs, expectation, expected", + ("args", "kwargs", "expectation", "expected"), [ - ((), {}, pytest.raises(RuntimeError, match="The old syntax"), None), - ( - ("-o"), - {"script": "script.r"}, - pytest.raises(RuntimeError, match="The old syntax"), - None, - ), - ( - (), - {"options": ("-o")}, - pytest.raises(RuntimeError, match="The old syntax"), - None, - ), ( (), { @@ -56,35 +43,17 @@ def test_r(args, kwargs, expectation, expected): assert result == expected -@pytest.mark.unit +@pytest.mark.unit() @pytest.mark.parametrize( - "mark, default_options, default_serializer, default_suffix, expectation, expected", + ( + "mark", + "default_options", + "default_serializer", + "default_suffix", + "expectation", + "expected", + ), [ - ( - Mark("r", (), {}), - [], - None, - ".json", - pytest.raises(RuntimeError, match="The old syntax for @pytask.mark.r"), - Mark( - "r", - (), - { - "script": None, - "options": [], - "serializer": None, - "suffix": ".json", - }, - ), - ), - ( - Mark("r", ("-o"), {}), - [], - None, - ".json", - pytest.raises(RuntimeError, match="The old syntax for @pytask.mark.r"), - None, - ), ( Mark("r", (), {"script": "script.r"}), [], @@ -122,7 +91,7 @@ def test_r(args, kwargs, expectation, expected): "script": "script.r", "options": [], "serializer": "json", - "suffix": SERIALIZER["json"]["suffix"], + "suffix": SERIALIZERS["json"]["suffix"], }, ), ), diff --git a/tests/test_config.py b/tests/test_config.py index 9624b64..c1a6157 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -4,8 +4,8 @@ from pytask import main -@pytest.mark.end_to_end +@pytest.mark.end_to_end() def test_marker_is_configured(tmp_path): session = main({"paths": tmp_path}) - assert "r" in session.config["markers"] + assert "r" in session.config["markers"] # noqa: PLR2004 diff --git a/tests/test_execute.py b/tests/test_execute.py index 291b704..c0af857 100644 --- a/tests/test_execute.py +++ b/tests/test_execute.py @@ -15,11 +15,11 @@ from tests.conftest import parametrize_parse_code_serializer_suffix -@pytest.mark.unit +@pytest.mark.unit() def test_pytask_execute_task_setup(monkeypatch): """Make sure that the task setup raises errors.""" # Act like r is installed since we do not test this. - monkeypatch.setattr("pytask_r.execute.shutil.which", lambda x: None) # noqa: U100 + monkeypatch.setattr("pytask_r.execute.shutil.which", lambda x: None) # noqa: ARG005 task = Task( base_name="task_example", @@ -33,7 +33,7 @@ def test_pytask_execute_task_setup(monkeypatch): @needs_rscript -@pytest.mark.end_to_end +@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_r_script( @@ -71,7 +71,7 @@ def task_run_r_script(): @needs_rscript -@pytest.mark.end_to_end +@pytest.mark.end_to_end() @parametrize_parse_code_serializer_suffix def test_run_r_script_w_task_decorator( runner, tmp_path, parse_config_code, serializer, suffix @@ -103,7 +103,7 @@ def run_r_script(): @needs_rscript -@pytest.mark.end_to_end +@pytest.mark.end_to_end() @parametrize_parse_code_serializer_suffix def test_raise_error_if_rscript_is_not_found( tmp_path, monkeypatch, parse_config_code, serializer, suffix @@ -127,7 +127,7 @@ def task_run_r_script(): tmp_path.joinpath("script.r").write_text(textwrap.dedent(r_script)) # Hide Rscript if available. - monkeypatch.setattr("pytask_r.execute.shutil.which", lambda x: None) # noqa: U100 + monkeypatch.setattr("pytask_r.execute.shutil.which", lambda x: None) # noqa: ARG005 session = main({"paths": tmp_path}) @@ -136,7 +136,7 @@ def task_run_r_script(): @needs_rscript -@pytest.mark.end_to_end +@pytest.mark.end_to_end() @parametrize_parse_code_serializer_suffix def test_run_r_script_w_saving_workspace( runner, tmp_path, parse_config_code, serializer, suffix @@ -172,7 +172,7 @@ def task_run_r_script(): @needs_rscript -@pytest.mark.end_to_end +@pytest.mark.end_to_end() @parametrize_parse_code_serializer_suffix def test_run_r_script_w_wrong_cmd_option( runner, tmp_path, parse_config_code, serializer, suffix @@ -208,7 +208,7 @@ def task_run_r_script(): @needs_rscript -@pytest.mark.end_to_end +@pytest.mark.end_to_end() def test_run_r_script_w_custom_serializer(runner, tmp_path): task_source = """ import pytask @@ -239,7 +239,7 @@ def task_run_r_script(): @needs_rscript -@pytest.mark.end_to_end +@pytest.mark.end_to_end() def test_run_r_script_fails_w_multiple_markers(runner, tmp_path): task_source = """ import pytask @@ -256,4 +256,4 @@ def task_run_r_script(): result = runner.invoke(cli, [tmp_path.as_posix()]) assert result.exit_code == ExitCode.COLLECTION_FAILED - assert "has multiple @pytask.mark.r marks" in result.output + assert "has multiple @pytask.mark.r marks" in result.output # noqa: PLR2004 diff --git a/tests/test_normal_execution_w_plugin.py b/tests/test_normal_execution_w_plugin.py index 4a810eb..78d67cc 100644 --- a/tests/test_normal_execution_w_plugin.py +++ b/tests/test_normal_execution_w_plugin.py @@ -8,12 +8,12 @@ from pytask import ExitCode -@pytest.mark.end_to_end +@pytest.mark.end_to_end() @pytest.mark.parametrize( "dependencies", - [[], ["in.txt"], ["in_1.txt", "in_2.txt"]], + [(), ("in.txt",), ("in_1.txt", "in_2.txt")], ) -@pytest.mark.parametrize("products", [["out.txt"], ["out_1.txt", "out_2.txt"]]) +@pytest.mark.parametrize("products", [("out.txt",), ("out_1.txt", "out_2.txt")]) def test_execution_w_varying_dependencies_products( runner, tmp_path, dependencies, products ): diff --git a/tests/test_parallel.py b/tests/test_parallel.py index b7dbc6c..539057f 100644 --- a/tests/test_parallel.py +++ b/tests/test_parallel.py @@ -37,7 +37,7 @@ @needs_rscript -@pytest.mark.end_to_end +@pytest.mark.end_to_end() @parametrize_parse_code_serializer_suffix def test_parallel_parametrization_over_source_files_w_parametrize( runner, tmp_path, parse_config_code, serializer, suffix @@ -95,7 +95,7 @@ def task_execute_r_script(): assert result.exit_code == ExitCode.OK duration_normal = time.time() - start - for name in ["1.rds", "2.rds"]: + for name in ("1.rds", "2.rds"): tmp_path.joinpath(name).unlink() start = time.time() @@ -107,7 +107,7 @@ def task_execute_r_script(): @needs_rscript -@pytest.mark.end_to_end +@pytest.mark.end_to_end() @parametrize_parse_code_serializer_suffix def test_parallel_parametrization_over_source_files_w_loop( runner, tmp_path, parse_config_code, serializer, suffix @@ -153,7 +153,7 @@ def task_execute_r_script(): assert result.exit_code == ExitCode.OK duration_normal = time.time() - start - for name in ["1.rds", "2.rds"]: + for name in ("1.rds", "2.rds"): tmp_path.joinpath(name).unlink() start = time.time() @@ -165,7 +165,7 @@ def task_execute_r_script(): @needs_rscript -@pytest.mark.end_to_end +@pytest.mark.end_to_end() @parametrize_parse_code_serializer_suffix def test_parallel_parametrization_over_source_file_w_parametrize( runner, tmp_path, parse_config_code, serializer, suffix @@ -201,7 +201,7 @@ def task_execute_r_script(): assert result.exit_code == ExitCode.OK duration_normal = time.time() - start - for name in ["0.rds", "1.rds"]: + for name in ("0.rds", "1.rds"): tmp_path.joinpath(name).unlink() start = time.time() @@ -213,7 +213,7 @@ def task_execute_r_script(): @needs_rscript -@pytest.mark.end_to_end +@pytest.mark.end_to_end() @parametrize_parse_code_serializer_suffix def test_parallel_parametrization_over_source_file_w_loop( runner, tmp_path, parse_config_code, serializer, suffix @@ -252,7 +252,7 @@ def execute_r_script(): assert result.exit_code == ExitCode.OK duration_normal = time.time() - start - for name in ["0.rds", "1.rds"]: + for name in ("0.rds", "1.rds"): tmp_path.joinpath(name).unlink() start = time.time() diff --git a/tests/test_parametrize.py b/tests/test_parametrize.py index 075bc18..9697964 100644 --- a/tests/test_parametrize.py +++ b/tests/test_parametrize.py @@ -23,7 +23,7 @@ @needs_rscript -@pytest.mark.end_to_end +@pytest.mark.end_to_end() @parametrize_parse_code_serializer_suffix def test_parametrized_execution_of_r_script_w_parametrize( runner, tmp_path, parse_config_code, serializer, suffix @@ -54,10 +54,10 @@ def task_run_r_script(): """ tmp_path.joinpath("task_dummy.py").write_text(textwrap.dedent(task_source)) - for name, content in [ + for name, content in ( ("script_1.r", "Cities breaking down on a camel's back"), ("script_2.r", "They just have to go 'cause they don't know whack"), - ]: + ): r_script = f""" {parse_config_code} file_descr <- file(config$produces) @@ -74,7 +74,7 @@ def task_run_r_script(): @needs_rscript -@pytest.mark.end_to_end +@pytest.mark.end_to_end() @parametrize_parse_code_serializer_suffix def test_parametrized_execution_of_r_script_w_loop( runner, tmp_path, parse_config_code, serializer, suffix @@ -96,10 +96,10 @@ def task_run_r_script(): """ tmp_path.joinpath("task_dummy.py").write_text(textwrap.dedent(task_source)) - for name, content in [ + for name, content in ( ("script_1.r", "Cities breaking down on a camel's back"), ("script_2.r", "They just have to go 'cause they don't know whack"), - ]: + ): r_script = f""" {parse_config_code} file_descr <- file(config$produces) @@ -116,7 +116,7 @@ def task_run_r_script(): @needs_rscript -@pytest.mark.end_to_end +@pytest.mark.end_to_end() @parametrize_parse_code_serializer_suffix def test_parametrize_r_options_and_product_paths_w_parametrize( runner, tmp_path, parse_config_code, serializer, suffix @@ -149,7 +149,7 @@ def task_execute_r_script(): @needs_rscript -@pytest.mark.end_to_end +@pytest.mark.end_to_end() @parametrize_parse_code_serializer_suffix def test_parametrize_r_options_and_product_paths_w_loop( runner, tmp_path, parse_config_code, serializer, suffix diff --git a/tox.ini b/tox.ini index 706acb7..a3c2fa8 100644 --- a/tox.ini +++ b/tox.ini @@ -9,8 +9,8 @@ basepython = python [testenv:pytest] conda_deps = - pytask >=0.1.0 - pytask-parallel >=0.1.0 + pytask >=0.3 + pytask-parallel >=0.3 pybaum >=0.1.1 # Optional dependencies.