Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,25 +52,25 @@ jobs:
shell: bash -l {0}
run: tox -e pytest -- -m "unit or (not integration and not end_to_end)" --cov=src --cov=tests --cov-report=xml -n auto

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

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

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

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

- name: Upload coverage reports of end-to-end tests.
if: runner.os == 'Linux' && matrix.python-version == '3.10'
shell: bash -l {0}
run: bash <(curl -s https://codecov.io/bash) -F end_to_end -c
- name: Upload end_to_end test coverage reports to Codecov with GitHub Action
uses: codecov/codecov-action@v3
with:
flags: end_to_end
1 change: 1 addition & 0 deletions docs/source/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and
- {pull}`523` refactors `_pytask.console._get_file`.
- {pull}`524` improves some linting and formatting rules.
- {pull}`525` enables pytask to work with remote files using universal_pathlib.
- {pull}`528` improves the codecov setup and coverage.

## 0.4.4 - 2023-12-04

Expand Down
11 changes: 5 additions & 6 deletions src/_pytask/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
from typing import TYPE_CHECKING

import click
from _pytask.capture import CaptureMethod
from _pytask.capture_utils import CaptureMethod
from _pytask.capture_utils import ShowCapture
from _pytask.click import ColoredCommand
from _pytask.config import hookimpl
from _pytask.config_utils import find_project_root_and_config
Expand Down Expand Up @@ -63,7 +64,7 @@ def pytask_unconfigure(session: Session) -> None:

def build( # noqa: C901, PLR0912, PLR0913
*,
capture: Literal["fd", "no", "sys", "tee-sys"] | CaptureMethod = CaptureMethod.NO,
capture: Literal["fd", "no", "sys", "tee-sys"] | CaptureMethod = CaptureMethod.FD,
check_casing_of_paths: bool = True,
config: Path | None = None,
database_url: str = "",
Expand All @@ -82,7 +83,8 @@ def build( # noqa: C901, PLR0912, PLR0913
pdb: bool = False,
pdb_cls: str = "",
s: bool = False,
show_capture: bool = True,
show_capture: Literal["no", "stdout", "stderr", "all"]
| ShowCapture = ShowCapture.ALL,
show_errors_immediately: bool = False,
show_locals: bool = False,
show_traceback: bool = True,
Expand Down Expand Up @@ -223,9 +225,6 @@ def build( # noqa: C901, PLR0912, PLR0913
raw_config["config"] = Path(raw_config["config"]).resolve()
raw_config["root"] = raw_config["config"].parent
else:
if raw_config["paths"] is None:
raw_config["paths"] = (Path.cwd(),)

raw_config["paths"] = parse_paths(raw_config["paths"])
(
raw_config["root"],
Expand Down
17 changes: 5 additions & 12 deletions src/_pytask/capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
from __future__ import annotations

import contextlib
import enum
import functools
import io
import os
Expand All @@ -42,21 +41,16 @@
from typing import TYPE_CHECKING

import click
from _pytask.capture_utils import CaptureMethod
from _pytask.capture_utils import ShowCapture
from _pytask.click import EnumChoice
from _pytask.config import hookimpl
from _pytask.enums import ShowCapture
from _pytask.shared import convert_to_enum

if TYPE_CHECKING:
from _pytask.node_protocols import PTask


class CaptureMethod(enum.Enum):
FD = "fd"
NO = "no"
SYS = "sys"
TEE_SYS = "tee-sys"


@hookimpl
def pytask_extend_command_line_interface(cli: click.Group) -> None:
"""Add CLI options for capturing output."""
Expand Down Expand Up @@ -90,11 +84,10 @@ def pytask_parse_config(config: dict[str, Any]) -> None:
Note that, ``-s`` is a shortcut for ``--capture=no``.

"""
if isinstance(config["capture"], str):
config["capture"] = CaptureMethod(config["capture"])

config["capture"] = convert_to_enum(config["capture"], CaptureMethod)
if config["s"]:
config["capture"] = CaptureMethod.NO
config["show_capture"] = convert_to_enum(config["show_capture"], ShowCapture)


@hookimpl
Expand Down
7 changes: 7 additions & 0 deletions src/_pytask/enums.py → src/_pytask/capture_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,10 @@ class ShowCapture(enum.Enum):
STDOUT = "stdout"
STDERR = "stderr"
ALL = "all"


class CaptureMethod(enum.Enum):
FD = "fd"
NO = "no"
SYS = "sys"
TEE_SYS = "tee-sys"
2 changes: 1 addition & 1 deletion src/_pytask/click.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ def _format_help_text( # noqa: C901, PLR0912, PLR0915
help_text = Text.from_markup(getattr(param, "help", None) or "")
extra = []

if getattr(param, "show_envvar", None):
if getattr(param, "show_envvar", None): # pragma: no cover
envvar = getattr(param, "envvar", None)

if envvar is None and (
Expand Down
7 changes: 2 additions & 5 deletions src/_pytask/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,7 @@ def render_to_string(

def format_task_name(task: PTask, editor_url_scheme: str) -> Text:
"""Format a task id."""
if task.function is None:
url_style = Style()
else:
url_style = create_url_style_for_task(task.function, editor_url_scheme)
url_style = create_url_style_for_task(task.function, editor_url_scheme)

if isinstance(task, PTaskWithPath):
path, task_name = task.name.split("::")
Expand Down Expand Up @@ -224,7 +221,7 @@ def get_file( # noqa: PLR0911
if source_file and Path(source_file) in skipped_paths:
return get_file(function.__wrapped__)
source_file = inspect.getsourcefile(function)
if source_file:
if source_file: # pragma: no cover
if "<stdin>" in source_file:
return None
if "<string>" in source_file:
Expand Down
3 changes: 1 addition & 2 deletions src/_pytask/mark/expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,7 @@ def not_expr(s: Scanner) -> ast.expr:
ident = s.accept(TokenType.IDENT)
if ident:
return ast.Name(IDENT_PREFIX + ident.value, ast.Load())
s.reject((TokenType.NOT, TokenType.LPAREN, TokenType.IDENT))
return None
s.reject((TokenType.NOT, TokenType.LPAREN, TokenType.IDENT)) # noqa: RET503


class MatcherAdapter(Mapping[str, bool]):
Expand Down
3 changes: 2 additions & 1 deletion src/_pytask/mark/structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@ def __getattr__(self, name: str) -> MarkDecorator | Any:

warnings.warn(
"'@pytask.mark.task' is deprecated starting pytask v0.4.0 and will be "
"removed in v0.5.0. Use '@pytask.task' instead.",
"removed in v0.5.0. Use '@task' with 'from pytask import task' "
"instead.",
category=FutureWarning,
stacklevel=1,
)
Expand Down
2 changes: 1 addition & 1 deletion src/_pytask/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
from typing import ClassVar
from typing import TYPE_CHECKING

from _pytask.capture_utils import ShowCapture
from _pytask.console import format_task_name
from _pytask.enums import ShowCapture
from _pytask.outcomes import CollectionOutcome
from _pytask.outcomes import TaskOutcome
from _pytask.traceback import OptionalExceptionInfo
Expand Down
21 changes: 21 additions & 0 deletions src/_pytask/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,20 @@
from _pytask.node_protocols import PTask

if TYPE_CHECKING:
from enum import Enum
import networkx as nx


__all__ = [
"convert_to_enum",
"find_duplicates",
"parse_markers",
"parse_paths",
"reduce_names_of_multiple_nodes",
"to_list",
]


def to_list(scalar_or_iter: Any) -> list[Any]:
"""Convert scalars and iterables to list.

Expand Down Expand Up @@ -131,3 +142,13 @@ def parse_markers(x: dict[str, str] | list[str] | tuple[str, ...]) -> dict[str,
raise click.BadParameter(msg)

return mapping


def convert_to_enum(value: Any, enum: type[Enum]) -> Enum:
"""Convert value to enum."""
try:
return enum(value)
except ValueError:
values = [e.value for e in enum]
msg = f"Value {value!r} is not a valid {enum!r}. Valid values are {values}."
raise ValueError(msg) from None
8 changes: 8 additions & 0 deletions src/_pytask/task_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,14 @@ def _generate_ids_for_tasks(
]
id_ = "-".join(stringified_args)
id_ = f"{name}[{id_}]"

if id_ in out:
msg = (
f"The task {name!r} with the id {id_!r} is duplicated. This can happen "
"if you create the exact same tasks multiple times or passed the same "
"the same id to multiple tasks via '@task(id=...)'."
)
raise ValueError(msg)
out[id_] = task
return out

Expand Down
5 changes: 4 additions & 1 deletion src/_pytask/warnings_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class WarningReport(NamedTuple):


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

if not isinstance(arg, str):
raise Exit(error_template.format(error="arg is not a string."))

parts = arg.split(":")
if len(parts) > 5: # noqa: PLR2004
doc_url = (
Expand Down
4 changes: 4 additions & 0 deletions src/pytask/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from _pytask import __version__
from _pytask._hashlib import hash_value
from _pytask.build import build
from _pytask.capture_utils import CaptureMethod
from _pytask.capture_utils import ShowCapture
from _pytask.click import ColoredCommand
from _pytask.click import ColoredGroup
from _pytask.click import EnumChoice
Expand Down Expand Up @@ -78,6 +80,7 @@

__all__ = [
"BaseTable",
"CaptureMethod",
"CollectionError",
"CollectionMetadata",
"CollectionOutcome",
Expand Down Expand Up @@ -112,6 +115,7 @@
"ResolvingDependenciesError",
"Runtime",
"Session",
"ShowCapture",
"Skipped",
"SkippedAncestorFailed",
"SkippedUnchanged",
Expand Down
8 changes: 1 addition & 7 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from contextlib import contextmanager
from pathlib import Path
from typing import Any
from typing import Callable

import pytest
from click.testing import CliRunner
Expand Down Expand Up @@ -62,15 +61,10 @@ def restore(self) -> None:
class SysModulesSnapshot:
"""A snapshot for sys.modules."""

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

def restore(self) -> None:
if self.__preserve:
self.__saved.update(
(k, m) for k, m in sys.modules.items() if self.__preserve(k)
)
sys.modules.clear()
sys.modules.update(self.__saved)

Expand Down
12 changes: 12 additions & 0 deletions tests/test_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,15 @@ def func(a):
assert value == 2
assert cache.cache_info.hits == 1
assert cache.cache_info.misses == 1


def test_make_memoize_key():
def func(a, b): # pragma: no cover
return a + b

argspec = inspect.getfullargspec(func)
# typed makes the key different each run.
key = _make_memoize_key(
(1,), {"b": 2}, typed=True, argspec=argspec, prefix="prefix"
)
assert key.startswith("prefix")
Loading