From 4b22d17188c9eae4613891fdcc433ec02f138df8 Mon Sep 17 00:00:00 2001 From: Dima Gerasimov Date: Sun, 24 May 2020 22:20:29 +0100 Subject: [PATCH] core: add @warn_if_empty decorator --- my/core/__init__.py | 1 + my/core/common.py | 40 ++++++++++++++++++++++++++++++++++++++-- tests/misc.py | 22 ++++++++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/my/core/__init__.py b/my/core/__init__.py index 4515235..678df85 100644 --- a/my/core/__init__.py +++ b/my/core/__init__.py @@ -1,4 +1,5 @@ # this file only keeps the most common & critical types/utility functions from .common import PathIsh, Paths, Json from .common import get_files, LazyLogger +from .common import warn_if_empty from .cfg import make_config diff --git a/my/core/common.py b/my/core/common.py index cfadc04..16a730b 100644 --- a/my/core/common.py +++ b/my/core/common.py @@ -3,7 +3,7 @@ from pathlib import Path from datetime import datetime import functools import types -from typing import Union, Callable, Dict, Iterable, TypeVar, Sequence, List, Optional, Any, cast, Tuple +from typing import Union, Callable, Dict, Iterable, TypeVar, Sequence, List, Optional, Any, cast, Tuple, TYPE_CHECKING import warnings # some helper functions @@ -161,7 +161,6 @@ def get_files(pp: Paths, glob: str=DEFAULT_GLOB, sort: bool=True) -> Tuple[Path, # TODO annotate it, perhaps use 'dependent' type (for @doublewrap stuff) -from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Callable, TypeVar from typing_extensions import Protocol @@ -269,3 +268,40 @@ import re def get_valid_filename(s: str) -> str: s = str(s).strip().replace(' ', '_') return re.sub(r'(?u)[^-\w.]', '', s) + + +from typing import Generic, Sized, Callable + + +def _warn_iterator(it): + emitted = False + for i in it: + yield i + emitted = True + if not emitted: + warnings.warn(f"Function hasn't emitted any data, make sure your config paths are correct") + + +def _warn_iterable(it): + if isinstance(it, Sized): + sz = len(it) + if sz == 0: + warnings.warn(f"Function is returning empty container, make sure your config paths are correct") + return it + else: + return _warn_iterator(it) + + +from functools import wraps +X = TypeVar('X') + +class G(Generic[X], Iterable[X]): + pass + +CC = Callable[[], G] +def warn_if_empty(f: CC) -> CC: + @wraps(f) + def wrapped(*args, **kwargs) -> G[X]: + res = f(*args, **kwargs) + return _warn_iterable(res) + return wrapped diff --git a/tests/misc.py b/tests/misc.py index 40d63a4..a20399e 100644 --- a/tests/misc.py +++ b/tests/misc.py @@ -48,3 +48,25 @@ def prepare(tmp_path: Path): # meh from my.core.error import test_sort_res_by + + +from typing import Iterable, List +import warnings +from my.core import warn_if_empty +def test_warn_if_empty(): + + @warn_if_empty + def nonempty() -> Iterable[str]: + yield 'a' + yield 'aba' + + @warn_if_empty + def empty(arg: str) -> List[str]: + return [] + + with warnings.catch_warnings(record=True) as w: + assert list(nonempty()) == ['a', 'aba'] + assert len(w) == 0 + + assert empty('whatever') == [] + assert len(w) == 1