Last active
March 22, 2020 12:30
-
-
Save vst/f377006ebe30f3a90d5d54366430ccee to your computer and use it in GitHub Desktop.
Various functions to inspect and normalize value inputs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
This module provides various functions to inspect and normalize value inputs. | |
Reference: https://gist.github.com/vst/f377006ebe30f3a90d5d54366430ccee | |
""" | |
__all__ = ["identity", "strnorm", "is_positive_integer", "as_positive_integer"] | |
import re | |
from functools import wraps | |
from typing import Any, Callable, Optional, TypeVar, Union | |
#: Consecutive whitespace regex. | |
_RE_WHITESPACES = re.compile(r"\s+") | |
#: Consecutive digits which constitute a valid decimal number together. | |
_RE_DIGITS = re.compile(r"^[1-9][0-9]*$") | |
#: Defines a generic type variable. | |
_T = TypeVar("_T") | |
def identity(x: _T) -> _T: | |
""" | |
Identity function. | |
>>> identity(None) | |
>>> identity(1) | |
1 | |
>>> identity('a') | |
'a' | |
""" | |
return x | |
def nonesafe(f: Callable[..., _T]) -> Callable[..., Optional[_T]]: | |
""" | |
Provides a None-safety decorator. | |
:param f: Function to decorate. | |
:return: None-safe function. | |
>>> nonesafe(lambda x: x + 1)(None) | |
>>> nonesafe(lambda x: x + 1)(1) | |
2 | |
>>> nonesafe(lambda x, y: x + y + 1)(None, 1) | |
>>> nonesafe(lambda x, y: x + y + 1)(1, 1) | |
3 | |
""" | |
@wraps(f) | |
def _f(*args: Any, **kwargs: Any) -> Optional[_T]: | |
return None if args[0] is None else f(*args, **kwargs) | |
return _f | |
def asint(f: Callable[[int], _T]) -> Callable[[Union[int, str]], _T]: | |
""" | |
Provides a decorator which ensures that the argument is an integer. | |
:param f: Function to decorate. | |
:return: New function with parameter to be integer. | |
:raises ValueError: In case that the string can not be cast to integer. | |
>>> asint(lambda x: x + 1)(1) | |
2 | |
>>> asint(lambda x: x + 1)("1") | |
2 | |
>>> asint(lambda x: x + 1)(" 1") | |
2 | |
>>> asint(lambda x: x + 1)("1 ") | |
2 | |
>>> asint(lambda x: x + 1)(" 1 ") | |
2 | |
""" | |
def _f(x: Union[int, str]) -> _T: | |
return f(x if isinstance(x, int) else int(x.strip())) | |
return _f | |
def strnorm(x: str, upper: bool = False) -> Optional[str]: | |
r""" | |
Normalizes a string by removing leading and trailing whitespace, reducing | |
consecutive whitespaces to a single space and optionally making the string | |
all uppercase. | |
:param x: String to normalize. | |
:param upper: Indicates if the resulting string should be in uppercase. | |
:return: Normalized string value if length is greater than 0, ``None`` otherwise. | |
>>> strnorm("") | |
>>> strnorm("a") | |
'a' | |
>>> strnorm(" a") | |
'a' | |
>>> strnorm("a ") | |
'a' | |
>>> strnorm(" a ") | |
'a' | |
>>> strnorm(" a ") | |
'a' | |
>>> strnorm("\r\n\ta\r\n\t") | |
'a' | |
>>> strnorm(" \r\r\n\n\t\ta \r\r\n\n\t\ta \r\r\n\n\t\ta \r\r\n\n\t\t") | |
'a a a' | |
>>> strnorm(" \r\r\n\n\t\ta \r\r\n\n\t\ta \r\r\n\n\t\ta \r\r\n\n\t\t", upper=True) | |
'A A A' | |
""" | |
## Reduce whitespaces: | |
x = _RE_WHITESPACES.sub(" ", x.strip()) | |
## Check, upper if required and return: | |
return x and (x.upper() if upper else x) or None | |
def is_positive_integer(x: Any) -> bool: | |
""" | |
Checks if the value is a positive integer (no ``0``), or a string which represents a positive integer in digits | |
without leading zeros and leading and trailing spaces. | |
This is useful to deal with internal database identifiers. | |
:param x: Value to check. | |
:return: ``True`` if value is a positive integer representation. | |
>>> is_positive_integer(1) | |
True | |
>>> is_positive_integer(1.0) | |
False | |
>>> is_positive_integer(0) | |
False | |
>>> is_positive_integer("1") | |
True | |
>>> is_positive_integer(" 1 ") | |
False | |
>>> is_positive_integer(strnorm(" 1 ")) | |
True | |
>>> is_positive_integer("01") | |
False | |
""" | |
return (isinstance(x, int) and x > 0) or (isinstance(x, str) and bool(_RE_DIGITS.match(x))) | |
def as_positive_integer(x: Any, pre: Callable[[Any], Any] = identity) -> int: | |
""" | |
Attempts to convert the value to a positive integer. | |
This is useful to deal with internal database identifiers. | |
:param x: Value to convert to a positive integer. | |
:param pre: Value transformation prior to conversion attempt. | |
:return: Positive integer. | |
:raises ValueError: In case that the value can not be converted to positive integer. | |
>>> as_positive_integer(0) | |
Traceback (most recent call last): | |
... | |
ValueError: Value does not represent a positive integer: 0 | |
>>> as_positive_integer(-1) | |
Traceback (most recent call last): | |
... | |
ValueError: Value does not represent a positive integer: -1 | |
>>> as_positive_integer(1) | |
1 | |
>>> as_positive_integer("1") | |
1 | |
>>> as_positive_integer(" 1") | |
Traceback (most recent call last): | |
... | |
ValueError: Value does not represent a positive integer: 1 | |
>>> as_positive_integer(" 1 ", lambda x: strnorm(x)) | |
1 | |
""" | |
x = pre(x) | |
if is_positive_integer(x): | |
return int(x) | |
raise ValueError(f"Value does not represent a positive integer: {x}") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment