# coding: utf-8
"""Duck-typing tests"""
from __future__ import annotations
import collections
import warnings
import numpy as np
from typing import Any
[docs]
def is_string(s: Any) -> bool:
"""True if s behaves like a string (duck typing test)."""
try:
s + " "
return True
except Exception:
return False
[docs]
def is_intlike(obj: Any) -> bool:
"""
True if obj represents an integer (floats such as 1.0 are included as well).
"""
# isinstance(i, numbers.Integral)
try:
# This to get rid of warnings about casting complex to real.
if np.iscomplexobj(obj) and np.isreal(obj):
return int(obj.real) == obj
else:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
#print("hello", int(obj) == obj)
return int(obj) == obj
except (ValueError, TypeError) as exc:
#print(exc)
return False
return False
[docs]
def is_number_like(obj: Any) -> bool:
"""True if obj represents a number."""
try:
obj - 1
return True
except TypeError:
return False
[docs]
def is_listlike(obj: Any) -> bool:
"""True if obj is list-like."""
#if isinstance(branch, (list, tuple, np.ndarray)):
if isinstance(obj, np.ndarray): return True
if not isinstance(obj, collections.abc.Sequence): return False
if is_string(obj): return False
try:
obj[:0]
return True
except TypeError:
return False
[docs]
def list_ints(arg: Any) -> list:
"""
Always return a list of int, given a int or list of integers as input.
:Examples:
>>> list_ints(1)
[1]
"""
l = np.array(arg, dtype=int)
return [int(l)] if l.size == 1 else l.tolist()
[docs]
def torange(obj: Any) -> range:
"""
Convert obj into a range. Accepts integer, slice object or any object
with an __iter__ method. Note that an integer is converted into range(int, int+1)
>>> list(torange(1))
[1]
>>> list(torange(slice(0, 4, 2)))
[0, 2]
>>> list(torange([1, 4, 2]))
[1, 4, 2]
"""
if is_intlike(obj):
return range(obj, obj + 1)
elif isinstance(obj, slice):
start = obj.start if obj.start is not None else 0
step = obj.step if obj.step is not None else 1
return range(start, obj.stop, step)
else:
try:
return obj.__iter__()
except Exception:
raise TypeError("Don't know how to convert %s into a range object" % str(obj))
[docs]
def as_slice(obj: Any) -> slice:
"""
Convert an integer, a string or a slice object into slice.
>>> assert as_slice(5) == slice(5, 6, 1)
>>> assert as_slice("[1:4]") == slice(1, 4, 1)
>>> assert as_slice("1::2") == slice(1, None, 2)
"""
if isinstance(obj, slice) or obj is None: return obj
try:
# integer.
if int(obj) == float(obj): return slice(int(obj), int(obj)+1, 1)
except Exception:
# assume string defining a python slice [start:stop:step]
if not obj: return None
if obj.count("[") + obj.count("]") not in (0, 2):
raise ValueError("Invalid string %s" % obj)
obj = obj.replace("[", "").replace("]", "")
n = obj.count(":")
if n == 0:
obj = int(obj)
return slice(obj, obj+1)
tokens = [int(f) if f else None for f in obj.split(":")]
if len(tokens) == 2: tokens.append(1)
if tokens[2] is None: tokens[2] = 1
return slice(*tokens)
raise ValueError("Cannot convert %s into a slice:\n%s" % (type(obj), obj))
[docs]
class NoDefaultProvided:
pass
[docs]
def hasattrd(obj: Any, name: str) -> bool:
"""
The arguments are an object and a string.
The result is True if the string is the name of one of the object’s attributes, False if not.
Unlike the builtin hasattr, hasattrd supports dot notation e.g. hasattr(int, "__class__.__name__")
(This is implemented by calling getattrd(object, name) and seeing whether it raises an exception or not.)
"""
try:
getattrd(obj, name)
return True
except AttributeError:
return False
[docs]
def getattrd(obj: Any, name: str, default=NoDefaultProvided) -> Any:
"""
Same as getattr(), but allows dot notation lookup e.g. getattrd(obj, "a.b")
Raises: AttributeError if ``name`` is not found and ``default`` is not given.
Discussed in: http://stackoverflow.com/questions/11975781
"""
from functools import reduce
try:
return reduce(getattr, name.split("."), obj)
except AttributeError:
if default is not NoDefaultProvided:
return default
raise