From f2ba3439ff00df222a9d00c24dc558b6fc64f029 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Wed, 8 Nov 2023 00:42:08 +0100 Subject: [PATCH 1/4] Improve coverage. --- src/_pytask/_hashlib.py | 4 ++-- src/_pytask/_inspect.py | 4 ++-- src/_pytask/clean.py | 4 ++-- src/_pytask/cli.py | 4 ++-- src/_pytask/collect.py | 4 ++-- src/_pytask/collect_command.py | 8 ++++---- src/_pytask/collect_utils.py | 8 ++++---- src/_pytask/config_utils.py | 6 +++--- src/_pytask/dag_command.py | 8 ++++---- src/_pytask/data_catalog.py | 2 +- src/_pytask/database_utils.py | 9 +++------ src/_pytask/execute.py | 4 ++-- src/_pytask/git.py | 2 +- src/_pytask/mark/__init__.py | 2 +- src/_pytask/mark/expression.py | 4 ++-- src/_pytask/mark/structures.py | 10 +++++----- tests/test_cache.py | 12 ++++++------ tests/test_collect.py | 17 +++++++++++++++++ tests/test_data_catalog.py | 20 ++++++++++++++++++++ tests/test_mark.py | 28 ++++++++++++++++++++++++++++ tests/test_typing.py | 2 +- 21 files changed, 112 insertions(+), 50 deletions(-) diff --git a/src/_pytask/_hashlib.py b/src/_pytask/_hashlib.py index e11c280e..0b4d530e 100644 --- a/src/_pytask/_hashlib.py +++ b/src/_pytask/_hashlib.py @@ -7,9 +7,9 @@ from typing import Any -if sys.version_info >= (3, 11): +if sys.version_info >= (3, 11): # pragma: no cover from hashlib import file_digest -else: +else: # pragma: no cover # This tuple and __get_builtin_constructor() must be modified if a new # always available algorithm is added. __always_supported = ( diff --git a/src/_pytask/_inspect.py b/src/_pytask/_inspect.py index 70f86325..10b61d8e 100644 --- a/src/_pytask/_inspect.py +++ b/src/_pytask/_inspect.py @@ -11,9 +11,9 @@ __all__ = ["get_annotations"] -if sys.version_info >= (3, 10): +if sys.version_info >= (3, 10): # pragma: no cover from inspect import get_annotations -else: +else: # pragma: no cover def get_annotations( # noqa: C901, PLR0912, PLR0915 obj: Callable[..., object] | type[Any] | types.ModuleType, diff --git a/src/_pytask/clean.py b/src/_pytask/clean.py index 41e58b32..3aaa2884 100644 --- a/src/_pytask/clean.py +++ b/src/_pytask/clean.py @@ -106,7 +106,7 @@ def clean(**raw_config: Any) -> NoReturn: # noqa: C901, PLR0912 config = pm.hook.pytask_configure(pm=pm, raw_config=raw_config) session = Session.from_config(config) - except Exception: # noqa: BLE001 + except Exception: # noqa: BLE001 # pragma: no cover session = Session(exit_code=ExitCode.CONFIGURATION_FAILED) console.print(Traceback(sys.exc_info())) @@ -160,7 +160,7 @@ def clean(**raw_config: Any) -> NoReturn: # noqa: C901, PLR0912 session.exit_code = ExitCode.COLLECTION_FAILED console.rule(style="failed") - except Exception: # noqa: BLE001 + except Exception: # noqa: BLE001 # pragma: no cover console.print(Traceback(sys.exc_info())) console.rule(style="failed") session.exit_code = ExitCode.FAILED diff --git a/src/_pytask/cli.py b/src/_pytask/cli.py index 6531fff3..f056b2e1 100644 --- a/src/_pytask/cli.py +++ b/src/_pytask/cli.py @@ -20,9 +20,9 @@ } -if parse_version(click.__version__) < parse_version("8"): +if parse_version(click.__version__) < parse_version("8"): # pragma: no cover _VERSION_OPTION_KWARGS: dict[str, Any] = {} -else: +else: # pragma: no cover _VERSION_OPTION_KWARGS = {"package_name": "pytask"} diff --git a/src/_pytask/collect.py b/src/_pytask/collect.py index f68d334e..04824c3f 100644 --- a/src/_pytask/collect.py +++ b/src/_pytask/collect.py @@ -63,7 +63,7 @@ def pytask_collect(session: Session) -> bool: try: session.hook.pytask_collect_modify_tasks(session=session, tasks=session.tasks) - except Exception: # noqa: BLE001 + except Exception: # noqa: BLE001 # pragma: no cover report = CollectionReport.from_exception( outcome=CollectionOutcome.FAIL, exc_info=sys.exc_info() ) @@ -370,7 +370,7 @@ def _raise_error_if_casing_of_path_is_wrong( path: Path, check_casing_of_paths: bool ) -> None: """Raise an error if the path does not have the correct casing.""" - if ( + if ( # pragma: no cover not IS_FILE_SYSTEM_CASE_SENSITIVE and sys.platform == "win32" and check_casing_of_paths diff --git a/src/_pytask/collect_command.py b/src/_pytask/collect_command.py index 70865d81..064d85fd 100644 --- a/src/_pytask/collect_command.py +++ b/src/_pytask/collect_command.py @@ -62,7 +62,7 @@ def collect(**raw_config: Any | None) -> NoReturn: config = pm.hook.pytask_configure(pm=pm, raw_config=raw_config) session = Session.from_config(config) - except (ConfigurationError, Exception): + except (ConfigurationError, Exception): # pragma: no cover session = Session(exit_code=ExitCode.CONFIGURATION_FAILED) console.print_exception() @@ -90,13 +90,13 @@ def collect(**raw_config: Any | None) -> NoReturn: console.print() console.rule(style="neutral") - except CollectionError: + except CollectionError: # pragma: no cover session.exit_code = ExitCode.COLLECTION_FAILED - except ResolvingDependenciesError: + except ResolvingDependenciesError: # pragma: no cover session.exit_code = ExitCode.DAG_FAILED - except Exception: # noqa: BLE001 + except Exception: # noqa: BLE001 # pragma: no cover session.exit_code = ExitCode.FAILED console.print_exception() console.rule(style="failed") diff --git a/src/_pytask/collect_utils.py b/src/_pytask/collect_utils.py index 4f7ca196..a3cf1946 100644 --- a/src/_pytask/collect_utils.py +++ b/src/_pytask/collect_utils.py @@ -34,7 +34,7 @@ if sys.version_info >= (3, 9): from typing import Annotated -else: +else: # pragma: no cover from typing_extensions import Annotated if TYPE_CHECKING: @@ -598,7 +598,7 @@ def _collect_decorator_node( collected_node = session.hook.pytask_collect_node( session=session, path=path, node_info=node_info ) - if collected_node is None: + if collected_node is None: # pragma: no cover msg = f"{node!r} cannot be parsed as a {kind} for task {name!r} in {path!r}." raise NodeNotCollectedError(msg) @@ -628,7 +628,7 @@ def _collect_dependency( collected_node = session.hook.pytask_collect_node( session=session, path=path, node_info=node_info ) - if collected_node is None: + if collected_node is None: # pragma: no cover msg = ( f"{node!r} cannot be parsed as a dependency for task {name!r} in {path!r}." ) @@ -673,7 +673,7 @@ def _collect_product( session=session, path=path, node_info=node_info ) - if collected_node is None: + if collected_node is None: # pragma: no cover msg = ( f"{node!r} can't be parsed as a product for task {task_name!r} in {path!r}." ) diff --git a/src/_pytask/config_utils.py b/src/_pytask/config_utils.py index b99d10dd..2b6bf6f1 100644 --- a/src/_pytask/config_utils.py +++ b/src/_pytask/config_utils.py @@ -10,9 +10,9 @@ import click from _pytask.shared import parse_paths -if sys.version_info >= (3, 11): +if sys.version_info >= (3, 11): # pragma: no cover import tomllib -else: +else: # pragma: no cover import tomli as tomllib @@ -98,7 +98,7 @@ def find_project_root_and_config( if path.exists(): try: read_config(path) - except (tomllib.TOMLDecodeError, OSError) as e: + except (tomllib.TOMLDecodeError, OSError) as e: # pragma: no cover raise click.FileError( filename=str(path), hint=f"Error reading {path}:\n{e}" ) from None diff --git a/src/_pytask/dag_command.py b/src/_pytask/dag_command.py index 175aa8b7..d9034791 100644 --- a/src/_pytask/dag_command.py +++ b/src/_pytask/dag_command.py @@ -84,7 +84,7 @@ def dag(**raw_config: Any) -> int: config = pm.hook.pytask_configure(pm=pm, raw_config=raw_config) session = Session.from_config(config) - except (ConfigurationError, Exception): + except (ConfigurationError, Exception): # pragma: no cover console.print_exception() session = Session(exit_code=ExitCode.CONFIGURATION_FAILED) @@ -102,10 +102,10 @@ def dag(**raw_config: Any) -> int: dag = _refine_dag(session) _write_graph(dag, session.config["output_path"], session.config["layout"]) - except CollectionError: + except CollectionError: # pragma: no cover session.exit_code = ExitCode.COLLECTION_FAILED - except ResolvingDependenciesError: + except ResolvingDependenciesError: # pragma: no cover session.exit_code = ExitCode.DAG_FAILED except Exception: # noqa: BLE001 @@ -183,7 +183,7 @@ def build_dag(raw_config: dict[str, Any]) -> nx.DiGraph: session = Session.from_config(config) - except (ConfigurationError, Exception): + except (ConfigurationError, Exception): # pragma: no cover console.print_exception() session = Session(exit_code=ExitCode.CONFIGURATION_FAILED) diff --git a/src/_pytask/data_catalog.py b/src/_pytask/data_catalog.py index 6fdad664..84c25375 100644 --- a/src/_pytask/data_catalog.py +++ b/src/_pytask/data_catalog.py @@ -122,7 +122,7 @@ def add(self, name: str, node: DataCatalog | PNode | None = None) -> None: arg_name=name, path=(), value=node, task_path=None, task_name="" ), ) - if collected_node is None: + if collected_node is None: # pragma: no cover msg = f"{node!r} cannot be parsed." raise NodeNotCollectedError(msg) self.entries[name] = collected_node diff --git a/src/_pytask/database_utils.py b/src/_pytask/database_utils.py index a0f297b2..8ea53d07 100644 --- a/src/_pytask/database_utils.py +++ b/src/_pytask/database_utils.py @@ -40,12 +40,9 @@ class State(BaseTable): # type: ignore[valid-type, misc] def create_database(url: str) -> None: """Create the database.""" - try: - engine = create_engine(url) - BaseTable.metadata.create_all(bind=engine) - DatabaseSession.configure(bind=engine) - except Exception: - raise + engine = create_engine(url) + BaseTable.metadata.create_all(bind=engine) + DatabaseSession.configure(bind=engine) def _create_or_update_state(first_key: str, second_key: str, hash_: str) -> None: diff --git a/src/_pytask/execute.py b/src/_pytask/execute.py index 47347609..b9fd8319 100644 --- a/src/_pytask/execute.py +++ b/src/_pytask/execute.py @@ -102,7 +102,7 @@ def pytask_execute_task_protocol(session: Session, task: PTask) -> ExecutionRepo session.hook.pytask_execute_task_setup(session=session, task=task) session.hook.pytask_execute_task(session=session, task=task) session.hook.pytask_execute_task_teardown(session=session, task=task) - except KeyboardInterrupt: + except KeyboardInterrupt: # pragma: no cover short_exc_info = remove_traceback_from_exc_info(sys.exc_info()) report = ExecutionReport.from_task_and_exception(task, short_exc_info) session.should_stop = True @@ -253,7 +253,7 @@ def pytask_execute_task_process_report( if session.n_tasks_failed >= session.config["max_failures"]: session.should_stop = True - if report.exc_info and isinstance(report.exc_info[1], Exit): + if report.exc_info and isinstance(report.exc_info[1], Exit): # pragma: no cover session.should_stop = True return True diff --git a/src/_pytask/git.py b/src/_pytask/git.py index f507f28e..d6e1769c 100644 --- a/src/_pytask/git.py +++ b/src/_pytask/git.py @@ -50,7 +50,7 @@ def get_root(cwd: Path) -> Path | None: try: _, stdout, _ = cmd_output("git", "rev-parse", "--show-cdup", cwd=cwd) root = Path(cwd) / stdout.strip() - except subprocess.CalledProcessError: + except subprocess.CalledProcessError: # pragma: no cover # User is not in git repo. root = None return root diff --git a/src/_pytask/mark/__init__.py b/src/_pytask/mark/__init__.py index 78665345..97ac4ed6 100644 --- a/src/_pytask/mark/__init__.py +++ b/src/_pytask/mark/__init__.py @@ -54,7 +54,7 @@ def markers(**raw_config: Any) -> NoReturn: config = pm.hook.pytask_configure(pm=pm, raw_config=raw_config) session = Session.from_config(config) - except (ConfigurationError, Exception): + except (ConfigurationError, Exception): # pragma: no cover console.print_exception() session = Session(exit_code=ExitCode.CONFIGURATION_FAILED) diff --git a/src/_pytask/mark/expression.py b/src/_pytask/mark/expression.py index 587d32e4..7c086b51 100644 --- a/src/_pytask/mark/expression.py +++ b/src/_pytask/mark/expression.py @@ -192,10 +192,10 @@ def __init__(self, matcher: Callable[[str], bool]) -> None: def __getitem__(self, key: str) -> bool: return self.matcher(key[len(IDENT_PREFIX) :]) - def __iter__(self) -> Iterator[str]: + def __iter__(self) -> Iterator[str]: # pragma: no cover raise NotImplementedError - def __len__(self) -> int: + def __len__(self) -> int: # pragma: no cover raise NotImplementedError diff --git a/src/_pytask/mark/structures.py b/src/_pytask/mark/structures.py index d7d2fb7e..de2592ab 100644 --- a/src/_pytask/mark/structures.py +++ b/src/_pytask/mark/structures.py @@ -140,7 +140,7 @@ 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): + if not isinstance(mark, Mark): # pragma: no cover msg = f"Got {mark!r} instead of Mark." raise TypeError(msg) return [x for x in extracted if isinstance(x, Mark)] @@ -202,10 +202,6 @@ def __getattr__(self, name: str) -> MarkDecorator | Any: # If the name is not in the set of known marks after updating, # 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"]: - msg = f"Unknown pytask.mark.{name}." - raise ValueError(msg) - if name in ("parametrize", "parameterize", "parametrise", "parameterise"): msg = ( "@pytask.mark.parametrize has been removed since pytask v0.4. " @@ -214,6 +210,10 @@ def __getattr__(self, name: str) -> MarkDecorator | Any: ) raise NotImplementedError(msg) from None + if self.config["strict_markers"]: + msg = f"Unknown pytask.mark.{name}." + raise ValueError(msg) + warnings.warn( f"Unknown pytask.mark.{name} - is this a typo? You can register " "custom marks to avoid this warning.", diff --git a/tests/test_cache.py b/tests/test_cache.py index 4d6fd52a..bdd18e1b 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -10,21 +10,21 @@ def test_cache(): cache = Cache() @cache.memoize - def func(a): - return a + def func(a, b): + return a + b assert func.cache.cache_info.hits == 0 assert func.cache.cache_info.misses == 0 - value = func(1) - assert value == 1 + value = func(1, b=2) + assert value == 3 assert func.cache.cache_info.hits == 0 assert func.cache.cache_info.misses == 1 assert next(i for i in cache._cache.values()) == 1 - value = func(1) - assert value == 1 + value = func(1, b=2) + assert value == 3 assert func.cache.cache_info.hits == 1 assert func.cache.cache_info.misses == 1 diff --git a/tests/test_collect.py b/tests/test_collect.py index d45097a2..f811c867 100644 --- a/tests/test_collect.py +++ b/tests/test_collect.py @@ -555,3 +555,20 @@ def task_example( result = runner.invoke(cli, [tmp_path.as_posix()]) assert result.exit_code == ExitCode.OK assert tmp_path.joinpath("subfolder", "out.txt").exists() + + +@pytest.mark.end_to_end() +def test_error_when_using_kwargs_and_node_in_annotation(runner, tmp_path): + source = """ + from pathlib import Path + from pytask import task, Product + from typing_extensions import Annotated + + @task(kwargs={"path": Path("file.txt")}) + def task_example(path: Annotated[Path, Path("file.txt"), Product]) -> None: ... + """ + tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(source)) + + result = runner.invoke(cli, [tmp_path.as_posix()]) + assert result.exit_code == ExitCode.COLLECTION_FAILED + assert "is defined twice" in result.output diff --git a/tests/test_data_catalog.py b/tests/test_data_catalog.py index 3e5dbfd6..e54b8e1a 100644 --- a/tests/test_data_catalog.py +++ b/tests/test_data_catalog.py @@ -187,3 +187,23 @@ def task_add_content() -> Annotated[str, data_catalog["new_content"]]: result = runner.invoke(cli, [tmp_path.as_posix()]) assert result.exit_code == ExitCode.OK assert len(list(tmp_path.joinpath(".data").iterdir())) == 2 + + +@pytest.mark.unit() +def test_error_when_name_of_node_is_not_string(): + data_catalog = DataCatalog() + with pytest.raises(TypeError, match="The name of a catalog entry"): + data_catalog.add(True, Path("file.txt")) + + +@pytest.mark.unit() +def test_requesting_new_node_with_python_node_as_default(): + data_catalog = DataCatalog(default_node=PythonNode) + assert isinstance(data_catalog["node"], PythonNode) + + +@pytest.mark.unit() +def test_adding_a_python_node(): + data_catalog = DataCatalog() + data_catalog.add("node", PythonNode(name="node", value=1)) + assert isinstance(data_catalog["node"], PythonNode) diff --git a/tests/test_mark.py b/tests/test_mark.py index 1d075c6f..8f27404a 100644 --- a/tests/test_mark.py +++ b/tests/test_mark.py @@ -409,3 +409,31 @@ def task_write_text(): ... result = runner.invoke(cli, [tmp_path.as_posix()]) assert result.exit_code == ExitCode.OK assert "Skipped" in result.output + + +@pytest.mark.end_to_end() +def test_error_with_unknown_marker_and_strict(runner, tmp_path): + source = """ + from pytask import mark + + @mark.unknown + def task_write_text(): ... + """ + tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(source)) + result = runner.invoke(cli, [tmp_path.as_posix(), "--strict-markers"]) + assert result.exit_code == ExitCode.COLLECTION_FAILED + assert "Unknown pytask.mark.unknown" in result.output + + +@pytest.mark.end_to_end() +def test_error_with_parametrize(runner, tmp_path): + source = """ + from pytask import mark + + @mark.parametrize + def task_write_text(): ... + """ + tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(source)) + result = runner.invoke(cli, [tmp_path.as_posix()]) + assert result.exit_code == ExitCode.COLLECTION_FAILED + assert "@pytask.mark.parametrize" in result.output diff --git a/tests/test_typing.py b/tests/test_typing.py index eca3ada4..eb60d461 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -9,7 +9,7 @@ @pytest.mark.unit() def test_is_task_function(): def func(): - pass + ... assert is_task_function(func) From 0bb36ee711be75c8a9e4cb95be5c6ad1382abaca Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Wed, 8 Nov 2023 00:43:04 +0100 Subject: [PATCH 2/4] fix. --- docs/source/changes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/changes.md b/docs/source/changes.md index 9137d7a1..25bcc6fe 100644 --- a/docs/source/changes.md +++ b/docs/source/changes.md @@ -28,6 +28,7 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and - {pull}`479` gives skips a higher precedence as an outcome than ancestor failed. - {pull}`480` removes the check for missing root nodes from the generation of the DAG. It is delegated to the check during the execution. +- {pull}`481` improves coverage. ## 0.4.1 - 2023-10-11 From af984d4b9497f970b5835f796d375e971ea5ff23 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Wed, 8 Nov 2023 00:45:23 +0100 Subject: [PATCH 3/4] move to pyproject.toml. --- .coveragerc | 6 ------ pyproject.toml | 8 ++++++++ 2 files changed, 8 insertions(+), 6 deletions(-) delete mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 8f4a99e0..00000000 --- a/.coveragerc +++ /dev/null @@ -1,6 +0,0 @@ -[report] -exclude_lines = - pragma: no cover - if TYPE_CHECKING.*: - \.\.\. - def __repr__ diff --git a/pyproject.toml b/pyproject.toml index d2ccd24b..b58d0e8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -193,3 +193,11 @@ python_version = "3.8" [tool.check-manifest] ignore = ["src/_pytask/_version.py"] + +[tool.coverage.report] +exclude_also = [ + "pragma: no cover", + "if TYPE_CHECKING.*:", + "\\.\\.\\.", + "def __repr__", +] From 814ea8822abeb1681960deb32e0b1de813456048 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Wed, 8 Nov 2023 00:46:18 +0100 Subject: [PATCH 4/4] move to pyproject.toml. --- tests/test_cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cache.py b/tests/test_cache.py index bdd18e1b..cc0439b1 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -21,7 +21,7 @@ def func(a, b): assert func.cache.cache_info.hits == 0 assert func.cache.cache_info.misses == 1 - assert next(i for i in cache._cache.values()) == 1 + assert next(i for i in cache._cache.values()) == 3 value = func(1, b=2) assert value == 3