Skip to content

Commit 7279231

Browse files
authored
Improve codecov and coverage. (#528)
1 parent f0d5994 commit 7279231

27 files changed

+320
-84
lines changed

.github/workflows/main.yml

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,25 +52,25 @@ jobs:
5252
shell: bash -l {0}
5353
run: tox -e pytest -- -m "unit or (not integration and not end_to_end)" --cov=src --cov=tests --cov-report=xml -n auto
5454

55-
- name: Upload coverage report for unit tests and doctests.
56-
if: runner.os == 'Linux' && matrix.python-version == '3.10'
57-
shell: bash -l {0}
58-
run: bash <(curl -s https://codecov.io/bash) -F unit -c
55+
- name: Upload unit test coverage reports to Codecov with GitHub Action
56+
uses: codecov/codecov-action@v3
57+
with:
58+
flags: unit
5959

6060
- name: Run integration tests.
6161
shell: bash -l {0}
6262
run: tox -e pytest -- -m integration --cov=src --cov=tests --cov-report=xml -n auto
6363

64-
- name: Upload coverage reports of integration tests.
65-
if: runner.os == 'Linux' && matrix.python-version == '3.10'
66-
shell: bash -l {0}
67-
run: bash <(curl -s https://codecov.io/bash) -F integration -c
64+
- name: Upload integration test coverage reports to Codecov with GitHub Action
65+
uses: codecov/codecov-action@v3
66+
with:
67+
flags: integration
6868

6969
- name: Run end-to-end tests.
7070
shell: bash -l {0}
7171
run: tox -e pytest -- -m end_to_end --cov=src --cov=tests --cov-report=xml -n auto
7272

73-
- name: Upload coverage reports of end-to-end tests.
74-
if: runner.os == 'Linux' && matrix.python-version == '3.10'
75-
shell: bash -l {0}
76-
run: bash <(curl -s https://codecov.io/bash) -F end_to_end -c
73+
- name: Upload end_to_end test coverage reports to Codecov with GitHub Action
74+
uses: codecov/codecov-action@v3
75+
with:
76+
flags: end_to_end

docs/source/changes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and
2020
- {pull}`523` refactors `_pytask.console._get_file`.
2121
- {pull}`524` improves some linting and formatting rules.
2222
- {pull}`525` enables pytask to work with remote files using universal_pathlib.
23+
- {pull}`528` improves the codecov setup and coverage.
2324

2425
## 0.4.4 - 2023-12-04
2526

src/_pytask/build.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
from typing import TYPE_CHECKING
1313

1414
import click
15-
from _pytask.capture import CaptureMethod
15+
from _pytask.capture_utils import CaptureMethod
16+
from _pytask.capture_utils import ShowCapture
1617
from _pytask.click import ColoredCommand
1718
from _pytask.config import hookimpl
1819
from _pytask.config_utils import find_project_root_and_config
@@ -63,7 +64,7 @@ def pytask_unconfigure(session: Session) -> None:
6364

6465
def build( # noqa: C901, PLR0912, PLR0913
6566
*,
66-
capture: Literal["fd", "no", "sys", "tee-sys"] | CaptureMethod = CaptureMethod.NO,
67+
capture: Literal["fd", "no", "sys", "tee-sys"] | CaptureMethod = CaptureMethod.FD,
6768
check_casing_of_paths: bool = True,
6869
config: Path | None = None,
6970
database_url: str = "",
@@ -82,7 +83,8 @@ def build( # noqa: C901, PLR0912, PLR0913
8283
pdb: bool = False,
8384
pdb_cls: str = "",
8485
s: bool = False,
85-
show_capture: bool = True,
86+
show_capture: Literal["no", "stdout", "stderr", "all"]
87+
| ShowCapture = ShowCapture.ALL,
8688
show_errors_immediately: bool = False,
8789
show_locals: bool = False,
8890
show_traceback: bool = True,
@@ -223,9 +225,6 @@ def build( # noqa: C901, PLR0912, PLR0913
223225
raw_config["config"] = Path(raw_config["config"]).resolve()
224226
raw_config["root"] = raw_config["config"].parent
225227
else:
226-
if raw_config["paths"] is None:
227-
raw_config["paths"] = (Path.cwd(),)
228-
229228
raw_config["paths"] = parse_paths(raw_config["paths"])
230229
(
231230
raw_config["root"],

src/_pytask/capture.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
from __future__ import annotations
2626

2727
import contextlib
28-
import enum
2928
import functools
3029
import io
3130
import os
@@ -42,21 +41,16 @@
4241
from typing import TYPE_CHECKING
4342

4443
import click
44+
from _pytask.capture_utils import CaptureMethod
45+
from _pytask.capture_utils import ShowCapture
4546
from _pytask.click import EnumChoice
4647
from _pytask.config import hookimpl
47-
from _pytask.enums import ShowCapture
48+
from _pytask.shared import convert_to_enum
4849

4950
if TYPE_CHECKING:
5051
from _pytask.node_protocols import PTask
5152

5253

53-
class CaptureMethod(enum.Enum):
54-
FD = "fd"
55-
NO = "no"
56-
SYS = "sys"
57-
TEE_SYS = "tee-sys"
58-
59-
6054
@hookimpl
6155
def pytask_extend_command_line_interface(cli: click.Group) -> None:
6256
"""Add CLI options for capturing output."""
@@ -90,11 +84,10 @@ def pytask_parse_config(config: dict[str, Any]) -> None:
9084
Note that, ``-s`` is a shortcut for ``--capture=no``.
9185
9286
"""
93-
if isinstance(config["capture"], str):
94-
config["capture"] = CaptureMethod(config["capture"])
95-
87+
config["capture"] = convert_to_enum(config["capture"], CaptureMethod)
9688
if config["s"]:
9789
config["capture"] = CaptureMethod.NO
90+
config["show_capture"] = convert_to_enum(config["show_capture"], ShowCapture)
9891

9992

10093
@hookimpl

src/_pytask/enums.py renamed to src/_pytask/capture_utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,10 @@ class ShowCapture(enum.Enum):
99
STDOUT = "stdout"
1010
STDERR = "stderr"
1111
ALL = "all"
12+
13+
14+
class CaptureMethod(enum.Enum):
15+
FD = "fd"
16+
NO = "no"
17+
SYS = "sys"
18+
TEE_SYS = "tee-sys"

src/_pytask/click.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ def _format_help_text( # noqa: C901, PLR0912, PLR0915
207207
help_text = Text.from_markup(getattr(param, "help", None) or "")
208208
extra = []
209209

210-
if getattr(param, "show_envvar", None):
210+
if getattr(param, "show_envvar", None): # pragma: no cover
211211
envvar = getattr(param, "envvar", None)
212212

213213
if envvar is None and (

src/_pytask/console.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,7 @@ def render_to_string(
126126

127127
def format_task_name(task: PTask, editor_url_scheme: str) -> Text:
128128
"""Format a task id."""
129-
if task.function is None:
130-
url_style = Style()
131-
else:
132-
url_style = create_url_style_for_task(task.function, editor_url_scheme)
129+
url_style = create_url_style_for_task(task.function, editor_url_scheme)
133130

134131
if isinstance(task, PTaskWithPath):
135132
path, task_name = task.name.split("::")
@@ -224,7 +221,7 @@ def get_file( # noqa: PLR0911
224221
if source_file and Path(source_file) in skipped_paths:
225222
return get_file(function.__wrapped__)
226223
source_file = inspect.getsourcefile(function)
227-
if source_file:
224+
if source_file: # pragma: no cover
228225
if "<stdin>" in source_file:
229226
return None
230227
if "<string>" in source_file:

src/_pytask/mark/expression.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,7 @@ def not_expr(s: Scanner) -> ast.expr:
179179
ident = s.accept(TokenType.IDENT)
180180
if ident:
181181
return ast.Name(IDENT_PREFIX + ident.value, ast.Load())
182-
s.reject((TokenType.NOT, TokenType.LPAREN, TokenType.IDENT))
183-
return None
182+
s.reject((TokenType.NOT, TokenType.LPAREN, TokenType.IDENT)) # noqa: RET503
184183

185184

186185
class MatcherAdapter(Mapping[str, bool]):

src/_pytask/mark/structures.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,8 @@ def __getattr__(self, name: str) -> MarkDecorator | Any:
226226

227227
warnings.warn(
228228
"'@pytask.mark.task' is deprecated starting pytask v0.4.0 and will be "
229-
"removed in v0.5.0. Use '@pytask.task' instead.",
229+
"removed in v0.5.0. Use '@task' with 'from pytask import task' "
230+
"instead.",
230231
category=FutureWarning,
231232
stacklevel=1,
232233
)

src/_pytask/reports.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
from typing import ClassVar
55
from typing import TYPE_CHECKING
66

7+
from _pytask.capture_utils import ShowCapture
78
from _pytask.console import format_task_name
8-
from _pytask.enums import ShowCapture
99
from _pytask.outcomes import CollectionOutcome
1010
from _pytask.outcomes import TaskOutcome
1111
from _pytask.traceback import OptionalExceptionInfo

src/_pytask/shared.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,20 @@
1616
from _pytask.node_protocols import PTask
1717

1818
if TYPE_CHECKING:
19+
from enum import Enum
1920
import networkx as nx
2021

2122

23+
__all__ = [
24+
"convert_to_enum",
25+
"find_duplicates",
26+
"parse_markers",
27+
"parse_paths",
28+
"reduce_names_of_multiple_nodes",
29+
"to_list",
30+
]
31+
32+
2233
def to_list(scalar_or_iter: Any) -> list[Any]:
2334
"""Convert scalars and iterables to list.
2435
@@ -131,3 +142,13 @@ def parse_markers(x: dict[str, str] | list[str] | tuple[str, ...]) -> dict[str,
131142
raise click.BadParameter(msg)
132143

133144
return mapping
145+
146+
147+
def convert_to_enum(value: Any, enum: type[Enum]) -> Enum:
148+
"""Convert value to enum."""
149+
try:
150+
return enum(value)
151+
except ValueError:
152+
values = [e.value for e in enum]
153+
msg = f"Value {value!r} is not a valid {enum!r}. Valid values are {values}."
154+
raise ValueError(msg) from None

src/_pytask/task_utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,14 @@ def _generate_ids_for_tasks(
283283
]
284284
id_ = "-".join(stringified_args)
285285
id_ = f"{name}[{id_}]"
286+
287+
if id_ in out:
288+
msg = (
289+
f"The task {name!r} with the id {id_!r} is duplicated. This can happen "
290+
"if you create the exact same tasks multiple times or passed the same "
291+
"the same id to multiple tasks via '@task(id=...)'."
292+
)
293+
raise ValueError(msg)
286294
out[id_] = task
287295
return out
288296

src/_pytask/warnings_utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class WarningReport(NamedTuple):
3535

3636

3737
@functools.lru_cache(maxsize=50)
38-
def parse_warning_filter( # noqa: PLR0912
38+
def parse_warning_filter( # noqa: PLR0912, C901
3939
arg: str, *, escape: bool
4040
) -> tuple[warnings._ActionKind, str, type[Warning], str, int]:
4141
"""Parse a warnings filter string.
@@ -57,6 +57,9 @@ def parse_warning_filter( # noqa: PLR0912
5757
"""
5858
)
5959

60+
if not isinstance(arg, str):
61+
raise Exit(error_template.format(error="arg is not a string."))
62+
6063
parts = arg.split(":")
6164
if len(parts) > 5: # noqa: PLR2004
6265
doc_url = (

src/pytask/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from _pytask import __version__
55
from _pytask._hashlib import hash_value
66
from _pytask.build import build
7+
from _pytask.capture_utils import CaptureMethod
8+
from _pytask.capture_utils import ShowCapture
79
from _pytask.click import ColoredCommand
810
from _pytask.click import ColoredGroup
911
from _pytask.click import EnumChoice
@@ -78,6 +80,7 @@
7880

7981
__all__ = [
8082
"BaseTable",
83+
"CaptureMethod",
8184
"CollectionError",
8285
"CollectionMetadata",
8386
"CollectionOutcome",
@@ -112,6 +115,7 @@
112115
"ResolvingDependenciesError",
113116
"Runtime",
114117
"Session",
118+
"ShowCapture",
115119
"Skipped",
116120
"SkippedAncestorFailed",
117121
"SkippedUnchanged",

tests/conftest.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from contextlib import contextmanager
66
from pathlib import Path
77
from typing import Any
8-
from typing import Callable
98

109
import pytest
1110
from click.testing import CliRunner
@@ -62,15 +61,10 @@ def restore(self) -> None:
6261
class SysModulesSnapshot:
6362
"""A snapshot for sys.modules."""
6463

65-
def __init__(self, preserve: Callable[[str], bool] | None = None) -> None:
66-
self.__preserve = preserve
64+
def __init__(self) -> None:
6765
self.__saved = dict(sys.modules)
6866

6967
def restore(self) -> None:
70-
if self.__preserve:
71-
self.__saved.update(
72-
(k, m) for k, m in sys.modules.items() if self.__preserve(k)
73-
)
7468
sys.modules.clear()
7569
sys.modules.update(self.__saved)
7670

tests/test_cache.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,15 @@ def func(a):
5050
assert value == 2
5151
assert cache.cache_info.hits == 1
5252
assert cache.cache_info.misses == 1
53+
54+
55+
def test_make_memoize_key():
56+
def func(a, b): # pragma: no cover
57+
return a + b
58+
59+
argspec = inspect.getfullargspec(func)
60+
# typed makes the key different each run.
61+
key = _make_memoize_key(
62+
(1,), {"b": 2}, typed=True, argspec=argspec, prefix="prefix"
63+
)
64+
assert key.startswith("prefix")

0 commit comments

Comments
 (0)