Skip to content

Instantly share code, notes, and snippets.

@vst

vst/inpututils.py

Last active Mar 22, 2020
Embed
What would you like to do?
Various functions to inspect and normalize value inputs
"""
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