Add doctests to some of this stuff.
authorScott Gasch <[email protected]>
Thu, 9 Sep 2021 06:29:05 +0000 (23:29 -0700)
committerScott Gasch <[email protected]>
Thu, 9 Sep 2021 06:29:05 +0000 (23:29 -0700)
17 files changed:
dict_utils.py
executors.py
file_utils.py
google_assistant.py
id_generator.py
list_utils.py
lockfile.py
logical_search.py
math_utils.py
misc_utils.py
stopwatch.py
string_utils.py
tests/parallelize_test.py
tests/run_all_tests.sh
text_utils.py
thread_utils.py
type_utils.py

index 74e8fdab22749917f21ae88c150b838120bb0820..6dd79f3e5b7618b93f12eb75dbae41483041b44b 100644 (file)
@@ -11,6 +11,21 @@ def init_or_inc(
     init_value: Any = 1,
     inc_function: Callable[..., Any] = lambda x: x + 1
 ) -> bool:
     init_value: Any = 1,
     inc_function: Callable[..., Any] = lambda x: x + 1
 ) -> bool:
+    """
+    Initialize a dict value (if it doesn't exist) or increments it (using the
+    inc_function, which is customizable) if it already does exist.  Returns
+    True if the key already existed or False otherwise.
+
+    >>> d = {}
+    >>> init_or_inc(d, "test")
+    False
+    >>> init_or_inc(d, "test")
+    True
+    >>> init_or_inc(d, 'ing')
+    False
+    >>> d
+    {'test': 2, 'ing': 1}
+    """
     if key in d.keys():
         d[key] = inc_function(d[key])
         return True
     if key in d.keys():
         d[key] = inc_function(d[key])
         return True
@@ -19,6 +34,10 @@ def init_or_inc(
 
 
 def shard(d: Dict[Any, Any], size: int) -> Iterator[Dict[Any, Any]]:
 
 
 def shard(d: Dict[Any, Any], size: int) -> Iterator[Dict[Any, Any]]:
+    """
+    Shards a dict into N subdicts which, together, contain all keys/values
+    from the original unsharded dict.
+    """
     items = d.items()
     for x in range(0, len(d), size):
         yield {key: value for (key, value) in islice(items, x, x + size)}
     items = d.items()
     for x in range(0, len(d), size):
         yield {key: value for (key, value) in islice(items, x, x + size)}
@@ -42,11 +61,22 @@ def coalesce(
         *,
         aggregation_function: Callable[[Any, Any], Any] = coalesce_by_creating_list
 ) -> Dict[Any, Any]:
         *,
         aggregation_function: Callable[[Any, Any], Any] = coalesce_by_creating_list
 ) -> Dict[Any, Any]:
+    """Merge N dicts into one dict containing the union of all keys/values in
+    the input dicts.  When keys collide, apply the aggregation_function which,
+    by default, creates a list of values.  See also coalesce_by_creating_set or
+    provide a user defined aggregation_function.
+
+    >>> a = {'a': 1, 'b': 2}
+    >>> b = {'b': 1, 'c': 2, 'd': 3}
+    >>> c = {'c': 1, 'd': 2}
+    >>> coalesce([a, b, c])
+    {'a': 1, 'b': [1, 2], 'c': [1, 2], 'd': [2, 3]}
+    """
     out: Dict[Any, Any] = {}
     for d in inputs:
         for key in d:
             if key in out:
     out: Dict[Any, Any] = {}
     for d in inputs:
         for key in d:
             if key in out:
-                value = aggregation_function(d[key], out[key])
+                value = aggregation_function(key, d[key], out[key])
             else:
                 value = d[key]
             out[key] = value
             else:
                 value = d[key]
             out[key] = value
@@ -54,32 +84,89 @@ def coalesce(
 
 
 def item_with_max_value(d: Dict[Any, Any]) -> Tuple[Any, Any]:
 
 
 def item_with_max_value(d: Dict[Any, Any]) -> Tuple[Any, Any]:
+    """Returns the key and value with the max value in a dict.
+
+    >>> d = {'a': 1, 'b': 2, 'c': 3}
+    >>> item_with_max_value(d)
+    ('c', 3)
+    >>> item_with_max_value({})
+    Traceback (most recent call last):
+    ...
+    ValueError: max() arg is an empty sequence
+    """
     return max(d.items(), key=lambda _: _[1])
 
 
 def item_with_min_value(d: Dict[Any, Any]) -> Tuple[Any, Any]:
     return max(d.items(), key=lambda _: _[1])
 
 
 def item_with_min_value(d: Dict[Any, Any]) -> Tuple[Any, Any]:
+    """Returns the key and value with the min value in a dict.
+
+    >>> d = {'a': 1, 'b': 2, 'c': 3}
+    >>> item_with_min_value(d)
+    ('a', 1)
+    """
     return min(d.items(), key=lambda _: _[1])
 
 
 def key_with_max_value(d: Dict[Any, Any]) -> Any:
     return min(d.items(), key=lambda _: _[1])
 
 
 def key_with_max_value(d: Dict[Any, Any]) -> Any:
+    """Returns the key with the max value in the dict.
+
+    >>> d = {'a': 1, 'b': 2, 'c': 3}
+    >>> key_with_max_value(d)
+    'c'
+    """
     return item_with_max_value(d)[0]
 
 
 def key_with_min_value(d: Dict[Any, Any]) -> Any:
     return item_with_max_value(d)[0]
 
 
 def key_with_min_value(d: Dict[Any, Any]) -> Any:
+    """Returns the key with the min value in the dict.
+
+    >>> d = {'a': 1, 'b': 2, 'c': 3}
+    >>> key_with_min_value(d)
+    'a'
+    """
     return item_with_min_value(d)[0]
 
 
 def max_value(d: Dict[Any, Any]) -> Any:
     return item_with_min_value(d)[0]
 
 
 def max_value(d: Dict[Any, Any]) -> Any:
+    """Returns the maximum value in the dict.
+
+    >>> d = {'a': 1, 'b': 2, 'c': 3}
+    >>> max_value(d)
+    3
+    """
     return item_with_max_value(d)[1]
 
 
 def min_value(d: Dict[Any, Any]) -> Any:
     return item_with_max_value(d)[1]
 
 
 def min_value(d: Dict[Any, Any]) -> Any:
+    """Returns the minimum value in the dict.
+
+    >>> d = {'a': 1, 'b': 2, 'c': 3}
+    >>> min_value(d)
+    1
+    """
     return item_with_min_value(d)[1]
 
 
 def max_key(d: Dict[Any, Any]) -> Any:
     return item_with_min_value(d)[1]
 
 
 def max_key(d: Dict[Any, Any]) -> Any:
+    """Returns the maximum key in dict (ignoring values totally)
+
+    >>> d = {'a': 3, 'b': 2, 'c': 1}
+    >>> max_key(d)
+    'c'
+    """
     return max(d.keys())
 
 
 def min_key(d: Dict[Any, Any]) -> Any:
     return max(d.keys())
 
 
 def min_key(d: Dict[Any, Any]) -> Any:
+    """Returns the minimum key in dict (ignoring values totally)
+
+    >>> d = {'a': 3, 'b': 2, 'c': 1}
+    >>> min_key(d)
+    'a'
+    """
     return min(d.keys())
     return min(d.keys())
+
+
+if __name__ == '__main__':
+    import doctest
+    doctest.testmod()
index 63efd812e54b5b796daff892f9ff1e75fd4e6569..e074c306f08fe676e8a64aa695cdbb92fc75c29e 100644 (file)
@@ -619,7 +619,7 @@ class RemoteExecutor(BaseExecutor):
 
         while True:
             try:
 
         while True:
             try:
-                p.wait(timeout=0.5)
+                p.wait(timeout=0.25)
             except subprocess.TimeoutExpired:
                 self.heartbeat()
 
             except subprocess.TimeoutExpired:
                 self.heartbeat()
 
@@ -882,7 +882,7 @@ class DefaultExecutors(object):
                     RemoteWorkerRecord(
                         username = 'scott',
                         machine = 'meerkat.cabin',
                     RemoteWorkerRecord(
                         username = 'scott',
                         machine = 'meerkat.cabin',
-                        weight = 7,
+                        weight = 6,
                         count = 2,
                     ),
                 )
                         count = 2,
                     ),
                 )
index 525a1afb0e262e93082f91dc8860a932575ed27a..7270e30b1fe1513746aaf986e87269968787e842 100644 (file)
@@ -50,26 +50,48 @@ def create_path_if_not_exist(path, on_error=None):
 
 
 def does_file_exist(filename: str) -> bool:
 
 
 def does_file_exist(filename: str) -> bool:
+    """Returns True if a file exists and is a normal file.
+
+    >>> does_file_exist(__file__)
+    True
+    """
     return os.path.exists(filename) and os.path.isfile(filename)
 
 
 def does_directory_exist(dirname: str) -> bool:
     return os.path.exists(filename) and os.path.isfile(filename)
 
 
 def does_directory_exist(dirname: str) -> bool:
+    """Returns True if a file exists and is a directory.
+
+    >>> does_directory_exist('/tmp')
+    True
+    """
     return os.path.exists(dirname) and os.path.isdir(dirname)
 
 
 def does_path_exist(pathname: str) -> bool:
     return os.path.exists(dirname) and os.path.isdir(dirname)
 
 
 def does_path_exist(pathname: str) -> bool:
+    """Just a more verbose wrapper around os.path.exists."""
     return os.path.exists(pathname)
 
 
 def get_file_size(filename: str) -> int:
     return os.path.exists(pathname)
 
 
 def get_file_size(filename: str) -> int:
+    """Returns the size of a file in bytes."""
     return os.path.getsize(filename)
 
 
 def is_normal_file(filename: str) -> bool:
     return os.path.getsize(filename)
 
 
 def is_normal_file(filename: str) -> bool:
+    """Returns True if filename is a normal file.
+
+    >>> is_normal_file(__file__)
+    True
+    """
     return os.path.isfile(filename)
 
 
 def is_directory(filename: str) -> bool:
     return os.path.isfile(filename)
 
 
 def is_directory(filename: str) -> bool:
+    """Returns True if filename is a directory.
+
+    >>> is_directory('/tmp')
+    True
+    """
     return os.path.isdir(filename)
 
 
     return os.path.isdir(filename)
 
 
@@ -274,3 +296,8 @@ class FileWriter(object):
             if (ret >> 8) != 0:
                 raise Exception(f'{cmd} failed, exit value {ret>>8}')
         return None
             if (ret >> 8) != 0:
                 raise Exception(f'{cmd} failed, exit value {ret>>8}')
         return None
+
+
+if __name__ == '__main__':
+    import doctest
+    doctest.testmod()
index a50003c7eb2a41e8326714ad24e4eccd2ec6cc34..b34197a3e60d0db9eff5c9cc6609ebe04e581bab 100644 (file)
@@ -45,10 +45,16 @@ audio_url: {self.audio_url}"""
 
 
 def tell_google(cmd: str, *, recognize_speech=True) -> GoogleResponse:
 
 
 def tell_google(cmd: str, *, recognize_speech=True) -> GoogleResponse:
+    """Alias for ask_google."""
     return ask_google(cmd, recognize_speech=recognize_speech)
 
 
 def ask_google(cmd: str, *, recognize_speech=True) -> GoogleResponse:
     return ask_google(cmd, recognize_speech=recognize_speech)
 
 
 def ask_google(cmd: str, *, recognize_speech=True) -> GoogleResponse:
+    """Send a command string to Google via the google_assistant_bridge as the
+    user google_assistant_username and return the response.  If recognize_speech
+    is True, perform speech recognition on the audio response from Google so as
+    to translate it into text (best effort, YMMV).
+    """
     logging.debug(f"Asking google: '{cmd}'")
     payload = {
         "command": cmd,
     logging.debug(f"Asking google: '{cmd}'")
     payload = {
         "command": cmd,
index c5a0d93e6908838c1f382b386a64957f3c2ea3fc..4e650dca9f13662a1ace7013d4f00bcd3a0082e2 100644 (file)
@@ -12,11 +12,22 @@ generators = {}
 
 def get(name: str) -> int:
     """
 
 def get(name: str) -> int:
     """
-    def __init__(self):
-        self.my_unique_id = id_generator.get("student_id")
+    Returns a thread safe monotonically increasing id suitable for use
+    as a globally unique identifier.
+
+    >>> import id_generator
+    >>> id_generator.get('student_id')
+    0
+    >>> id_generator.get('student_id')
+    1
     """
     if name not in generators:
         generators[name] = itertools.count()
     x = next(generators[name])
     logger.debug(f"Generated next id {x}")
     return x
     """
     if name not in generators:
         generators[name] = itertools.count()
     x = next(generators[name])
     logger.debug(f"Generated next id {x}")
     return x
+
+
+if __name__ == '__main__':
+    import doctest
+    doctest.testmod()
index 7d3355cc85a72a047aacaa0c3f06430a9e8e8dd7..993ca8af9ca9f9a633c02162e0f155fd3b3cab74 100644 (file)
@@ -5,16 +5,28 @@ from typing import Any, Iterator, List
 
 
 def shard(lst: List[Any], size: int) -> Iterator[Any]:
 
 
 def shard(lst: List[Any], size: int) -> Iterator[Any]:
-    """Yield successive size-sized shards from lst."""
+    """
+    Yield successive size-sized shards from lst.
+
+    >>> for sublist in shard([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 3):
+    ...     [_ for _ in sublist]
+    [1, 2, 3]
+    [4, 5, 6]
+    [7, 8, 9]
+    [10, 11, 12]
+
+    """
     for x in range(0, len(lst), size):
         yield islice(lst, x, x + size)
 
 
 def flatten(lst: List[Any]) -> List[Any]:
     for x in range(0, len(lst), size):
         yield islice(lst, x, x + size)
 
 
 def flatten(lst: List[Any]) -> List[Any]:
-    """Flatten out a list:
+    """
+    Flatten out a list:
+
+    >>> flatten([ 1, [2, 3, 4, [5], 6], 7, [8, [9]]])
+    [1, 2, 3, 4, 5, 6, 7, 8, 9]
 
 
-        >>> flatten([ 1, [2, 3, 4, [5], 6], 7, [8, [9]]])
-        [1, 2, 3, 4, 5, 6, 7, 8, 9]
     """
     if len(lst) == 0:
         return lst
     """
     if len(lst) == 0:
         return lst
@@ -24,6 +36,17 @@ def flatten(lst: List[Any]) -> List[Any]:
 
 
 def prepend(item: Any, lst: List[Any]) -> List[Any]:
 
 
 def prepend(item: Any, lst: List[Any]) -> List[Any]:
-    """Prepend an item to a list."""
-    lst = list.insert(0, item)
+    """
+    Prepend an item to a list.
+
+    >>> prepend('foo', ['bar', 'baz'])
+    ['foo', 'bar', 'baz']
+
+    """
+    lst.insert(0, item)
     return lst
     return lst
+
+
+if __name__ == '__main__':
+    import doctest
+    doctest.testmod()
index 34279ba8392c0e538ede3bfbc09b7af882657c16..1e0516bf75a340b4a15629141cdfbfb83fc3485c 100644 (file)
@@ -28,7 +28,14 @@ class LockFileContents:
 
 class LockFile(object):
     """A file locking mechanism that has context-manager support so you
 
 class LockFile(object):
     """A file locking mechanism that has context-manager support so you
-    can use it in a with statement.
+    can use it in a with statement.  e.g.
+
+    with LockFile('./foo.lock'):
+        # do a bunch of stuff... if the process dies we have a signal
+        # handler to do cleanup.  Other code (in this process or another)
+        # that tries to take the same lockfile will block.  There is also
+        # some logic for detecting stale locks.
+
     """
 
     def __init__(
     """
 
     def __init__(
index 86c6352972dec2f3330d7b2796fc07fc1e69b28f..805ec223010b93b2a1bf68e1fdee9467daac14aa 100644 (file)
@@ -63,7 +63,32 @@ class Operation(enum.Enum):
 
 
 class Corpus(object):
 
 
 class Corpus(object):
-    """A collection of searchable documents."""
+    """A collection of searchable documents.
+
+    >>> c = Corpus()
+    >>> c.add_doc(Document(
+    ...                    docid=1,
+    ...                    tags=set(['urgent', 'important']),
+    ...                    properties=[
+    ...                                ('author', 'Scott'),
+    ...                                ('subject', 'your anniversary')
+    ...                    ],
+    ...                    reference=None,
+    ...                   )
+    ...          )
+    >>> c.add_doc(Document(
+    ...                    docid=2,
+    ...                    tags=set(['important']),
+    ...                    properties=[
+    ...                                ('author', 'Joe'),
+    ...                                ('subject', 'your performance at work')
+    ...                    ],
+    ...                    reference=None,
+    ...                   )
+    ...          )
+    >>> c.query('author:Scott and important')
+    {1}
+    """
 
     def __init__(self) -> None:
         self.docids_by_tag: Dict[str, Set[str]] = defaultdict(set)
 
     def __init__(self) -> None:
         self.docids_by_tag: Dict[str, Set[str]] = defaultdict(set)
@@ -133,15 +158,15 @@ class Corpus(object):
     def get_docids_with_property(self, key: str) -> Set[str]:
         """Return the set of docids that have a particular property no matter
         what that property's value.
     def get_docids_with_property(self, key: str) -> Set[str]:
         """Return the set of docids that have a particular property no matter
         what that property's value.
-        """
 
 
+        """
         return self.docids_with_property[key]
 
     def get_docids_by_property(self, key: str, value: str) -> Set[str]:
         """Return the set of docids that have a particular property with a
         particular value..
         return self.docids_with_property[key]
 
     def get_docids_by_property(self, key: str, value: str) -> Set[str]:
         """Return the set of docids that have a particular property with a
         particular value..
-        """
 
 
+        """
         return self.docids_by_property[(key, value)]
 
     def invert_docid_set(self, original: Set[str]) -> Set[str]:
         return self.docids_by_property[(key, value)]
 
     def invert_docid_set(self, original: Set[str]) -> Set[str]:
@@ -205,7 +230,6 @@ class Corpus(object):
             return operator_precedence(token) is not None
 
         def lex(query: str):
             return operator_precedence(token) is not None
 
         def lex(query: str):
-            query = query.lower()
             tokens = query.split()
             for token in tokens:
                 # Handle ( and ) operators stuck to the ends of tokens
             tokens = query.split()
             for token in tokens:
                 # Handle ( and ) operators stuck to the ends of tokens
@@ -365,3 +389,8 @@ class Node(object):
             else:
                 raise ParseError(f"Unexpected negation operand {_} ({type(_)})")
         return retval
             else:
                 raise ParseError(f"Unexpected negation operand {_} ({type(_)})")
         return retval
+
+
+if __name__ == '__main__':
+    import doctest
+    doctest.testmod()
index 62771231bb67925483bcbf714fe2a8373b591058..fa0bc0e7bdbbb7789ed56420f17d64b66375cd92 100644 (file)
@@ -7,6 +7,20 @@ from heapq import heappush, heappop
 
 
 class RunningMedian:
 
 
 class RunningMedian:
+    """A running median computer.
+
+    >>> median = RunningMedian()
+    >>> median.add_number(1)
+    >>> median.add_number(10)
+    >>> median.add_number(3)
+    >>> median.get_median()
+    3
+    >>> median.add_number(7)
+    >>> median.add_number(5)
+    >>> median.get_median()
+    5
+    """
+
     def __init__(self):
         self.lowers, self.highers = [], []
 
     def __init__(self):
         self.lowers, self.highers = [], []
 
@@ -55,19 +69,46 @@ def gcd_float_sequence(lst: List[float]) -> float:
 
 
 def truncate_float(n: float, decimals: int = 2):
 
 
 def truncate_float(n: float, decimals: int = 2):
-    """Truncate a float to a particular number of decimals."""
+    """
+    Truncate a float to a particular number of decimals.
+
+    >>> truncate_float(3.1415927, 3)
+    3.141
+
+    """
     assert decimals > 0 and decimals < 10
     multiplier = 10 ** decimals
     return int(n * multiplier) / multiplier
 
 
 def percentage_to_multiplier(percent: float) -> float:
     assert decimals > 0 and decimals < 10
     multiplier = 10 ** decimals
     return int(n * multiplier) / multiplier
 
 
 def percentage_to_multiplier(percent: float) -> float:
+    """Given a percentage (e.g. 155%), return a factor needed to scale a
+    number by that percentage.
+
+    >>> percentage_to_multiplier(155)
+    2.55
+    >>> percentage_to_multiplier(45)
+    1.45
+    >>> percentage_to_multiplier(-25)
+    0.75
+
+    """
     multiplier = percent / 100
     multiplier += 1.0
     return multiplier
 
 
 def multiplier_to_percent(multiplier: float) -> float:
     multiplier = percent / 100
     multiplier += 1.0
     return multiplier
 
 
 def multiplier_to_percent(multiplier: float) -> float:
+    """Convert a multiplicative factor into a percent change.
+
+    >>> multiplier_to_percent(0.75)
+    -25.0
+    >>> multiplier_to_percent(1.0)
+    0.0
+    >>> multiplier_to_percent(1.99)
+    99.0
+
+    """
     percent = multiplier
     if percent > 0.0:
         percent -= 1.0
     percent = multiplier
     if percent > 0.0:
         percent -= 1.0
@@ -79,7 +120,18 @@ def multiplier_to_percent(multiplier: float) -> float:
 
 @functools.lru_cache(maxsize=1024, typed=True)
 def is_prime(n: int) -> bool:
 
 @functools.lru_cache(maxsize=1024, typed=True)
 def is_prime(n: int) -> bool:
-    """Returns True if n is prime and False otherwise"""
+    """
+    Returns True if n is prime and False otherwise.  Obviously(?) very slow for
+    very large input numbers.
+
+    >>> is_prime(13)
+    True
+    >>> is_prime(22)
+    False
+    >>> is_prime(51602981)
+    True
+
+    """
     if not isinstance(n, int):
         raise TypeError("argument passed to is_prime is not of 'int' type")
 
     if not isinstance(n, int):
         raise TypeError("argument passed to is_prime is not of 'int' type")
 
@@ -100,3 +152,8 @@ def is_prime(n: int) -> bool:
             return False
         i = i + 6
     return True
             return False
         i = i + 6
     return True
+
+
+if __name__ == '__main__':
+    import doctest
+    doctest.testmod()
index a4757bd3a0f6b37a1111b320ec907c9aea5f6592..fc1d5c28e86c5201ec25a800c93f4ce749578402 100644 (file)
@@ -2,5 +2,7 @@
 
 import os
 
 
 import os
 
+
 def is_running_as_root() -> bool:
 def is_running_as_root() -> bool:
+    """Returns True if running as root."""
     return os.geteuid() == 0
     return os.geteuid() == 0
index 1326cb1fec8ffb41db461f99aac59372ded655f1..cdd405b558213a9fb7c0ca38eec72ad41ba47a14 100644 (file)
@@ -6,11 +6,15 @@ from typing import Callable
 
 class Timer(object):
     """
 
 class Timer(object):
     """
-    with timer.Timer() as t:
-        do_the_thing()
+    A stopwatch to time how long something takes (walltime).
 
 
-    walltime = t()
-    print(f'That took {walltime}s.')
+    e.g.
+
+        with timer.Timer() as t:
+            do_the_thing()
+
+        walltime = t()
+        print(f'That took {walltime} seconds.')
     """
 
     def __init__(self) -> None:
     """
 
     def __init__(self) -> None:
@@ -18,6 +22,9 @@ class Timer(object):
         self.end = None
 
     def __enter__(self) -> Callable[[], float]:
         self.end = None
 
     def __enter__(self) -> Callable[[], float]:
+        """Returns a functor that, when called, returns the walltime of the
+        operation in seconds.
+        """
         self.start = time.perf_counter()
         self.end = 0.0
         return lambda: self.end - self.start
         self.start = time.perf_counter()
         self.end = 0.0
         return lambda: self.end - self.start
index bca2b70d5cd18bc8bb1198782d356f2707c1cbd5..5eb03d275e184af8709a87da3885b5827588c501 100644 (file)
@@ -6,6 +6,7 @@ import io
 from itertools import zip_longest
 import json
 import logging
 from itertools import zip_longest
 import json
 import logging
+import numbers
 import random
 import re
 import string
 import random
 import re
 import string
@@ -89,10 +90,18 @@ UUID_HEX_OK_RE = re.compile(
 
 SHALLOW_IP_V4_RE = re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$")
 
 
 SHALLOW_IP_V4_RE = re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$")
 
+ANYWHERE_IP_V4_RE = re.compile(r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}")
+
 IP_V6_RE = re.compile(r"^([a-z\d]{0,4}:){7}[a-z\d]{0,4}$", re.IGNORECASE)
 
 IP_V6_RE = re.compile(r"^([a-z\d]{0,4}:){7}[a-z\d]{0,4}$", re.IGNORECASE)
 
+ANYWHERE_IP_V6_RE = re.compile(r"([a-z\d]{0,4}:){7}[a-z\d]{0,4}", re.IGNORECASE)
+
 MAC_ADDRESS_RE = re.compile(
 MAC_ADDRESS_RE = re.compile(
-    r"^([0-9A-F]{2}[:-]){5}([0-9A-F]{2})", re.IGNORECASE
+    r"^([0-9A-F]{2}[:-]){5}([0-9A-F]{2})$", re.IGNORECASE
+)
+
+ANYWHERE_MAC_ADDRESS_RE = re.compile(
+    r"([0-9A-F]{2}[:-]){5}([0-9A-F]{2})", re.IGNORECASE
 )
 
 WORDS_COUNT_RE = re.compile(
 )
 
 WORDS_COUNT_RE = re.compile(
@@ -134,27 +143,91 @@ NUM_SUFFIXES = {
 
 
 def is_none_or_empty(in_str: Optional[str]) -> bool:
 
 
 def is_none_or_empty(in_str: Optional[str]) -> bool:
+    """
+    Returns true if the input string is either None or an empty string.
+
+    >>> is_none_or_empty("")
+    True
+    >>> is_none_or_empty(None)
+    True
+    >>> is_none_or_empty(" ")
+    True
+    >>> is_none_or_empty('Test')
+    False
+    """
     return in_str is None or len(in_str.strip()) == 0
 
 
 def is_string(obj: Any) -> bool:
     """
     Checks if an object is a string.
     return in_str is None or len(in_str.strip()) == 0
 
 
 def is_string(obj: Any) -> bool:
     """
     Checks if an object is a string.
+
+    >>> is_string('test')
+    True
+    >>> is_string(123)
+    False
+    >>> is_string(100.3)
+    False
+    >>> is_string([1, 2, 3])
+    False
     """
     return isinstance(obj, str)
 
 
 def is_empty_string(in_str: Any) -> bool:
     """
     return isinstance(obj, str)
 
 
 def is_empty_string(in_str: Any) -> bool:
+    """
+    Checks if input is a string and empty or only whitespace.
+
+    >>> is_empty_string('')
+    True
+    >>> is_empty_string('    \t\t    ')
+    True
+    >>> is_empty_string('test')
+    False
+    >>> is_empty_string(100.88)
+    False
+    >>> is_empty_string([1, 2, 3])
+    False
+    """
     return is_string(in_str) and in_str.strip() == ""
 
 
 def is_full_string(in_str: Any) -> bool:
     return is_string(in_str) and in_str.strip() == ""
 
 
 def is_full_string(in_str: Any) -> bool:
+    """
+    Checks that input is a string and is not empty ('') or only whitespace.
+
+    >>> is_full_string('test!')
+    True
+    >>> is_full_string('')
+    False
+    >>> is_full_string('      ')
+    False
+    >>> is_full_string(100.999)
+    False
+    >>> is_full_string({"a": 1, "b": 2})
+    False
+    """
     return is_string(in_str) and in_str.strip() != ""
 
 
 def is_number(in_str: str) -> bool:
     """
     Checks if a string is a valid number.
     return is_string(in_str) and in_str.strip() != ""
 
 
 def is_number(in_str: str) -> bool:
     """
     Checks if a string is a valid number.
+
+    >>> is_number(100.5)
+    Traceback (most recent call last):
+    ...
+    ValueError: 100.5
+    >>> is_number("100.5")
+    True
+    >>> is_number("test")
+    False
+    >>> is_number("99")
+    True
+    >>> is_number([1, 2, 3])
+    Traceback (most recent call last):
+    ...
+    ValueError: [1, 2, 3]
     """
     if not is_string(in_str):
         raise ValueError(in_str)
     """
     if not is_string(in_str):
         raise ValueError(in_str)
@@ -167,10 +240,10 @@ def is_integer_number(in_str: str) -> bool:
 
     An integer may be signed or unsigned or use a "scientific notation".
 
 
     An integer may be signed or unsigned or use a "scientific notation".
 
-    *Examples:*
-
-    >>> is_integer('42') # returns true
-    >>> is_integer('42.0') # returns false
+    >>> is_integer_number('42')
+    True
+    >>> is_integer_number('42.0')
+    False
     """
     return (
         (is_number(in_str) and "." not in in_str) or
     """
     return (
         (is_number(in_str) and "." not in in_str) or
@@ -181,24 +254,89 @@ def is_integer_number(in_str: str) -> bool:
 
 
 def is_hexidecimal_integer_number(in_str: str) -> bool:
 
 
 def is_hexidecimal_integer_number(in_str: str) -> bool:
+    """
+    Checks whether a string is a hex integer number.
+
+    >>> is_hexidecimal_integer_number('0x12345')
+    True
+    >>> is_hexidecimal_integer_number('0x1A3E')
+    True
+    >>> is_hexidecimal_integer_number('1234')  # Needs 0x
+    False
+    >>> is_hexidecimal_integer_number('-0xff')
+    True
+    >>> is_hexidecimal_integer_number('test')
+    False
+    >>> is_hexidecimal_integer_number(12345)  # Not a string
+    Traceback (most recent call last):
+    ...
+    ValueError: 12345
+    >>> is_hexidecimal_integer_number(101.4)
+    Traceback (most recent call last):
+    ...
+    ValueError: 101.4
+    >>> is_hexidecimal_integer_number(0x1A3E)
+    Traceback (most recent call last):
+    ...
+    ValueError: 6718
+    """
     if not is_string(in_str):
         raise ValueError(in_str)
     return HEX_NUMBER_RE.match(in_str) is not None
 
 
 def is_octal_integer_number(in_str: str) -> bool:
     if not is_string(in_str):
         raise ValueError(in_str)
     return HEX_NUMBER_RE.match(in_str) is not None
 
 
 def is_octal_integer_number(in_str: str) -> bool:
+    """
+    Checks whether a string is an octal number.
+
+    >>> is_octal_integer_number('0o777')
+    True
+    >>> is_octal_integer_number('-0O115')
+    True
+    >>> is_octal_integer_number('0xFF')  # Not octal, needs 0o
+    False
+    >>> is_octal_integer_number('7777')  # Needs 0o
+    False
+    >>> is_octal_integer_number('test')
+    False
+    """
     if not is_string(in_str):
         raise ValueError(in_str)
     return OCT_NUMBER_RE.match(in_str) is not None
 
 
 def is_binary_integer_number(in_str: str) -> bool:
     if not is_string(in_str):
         raise ValueError(in_str)
     return OCT_NUMBER_RE.match(in_str) is not None
 
 
 def is_binary_integer_number(in_str: str) -> bool:
+    """
+    Returns whether a string contains a binary number.
+
+    >>> is_binary_integer_number('0b10111')
+    True
+    >>> is_binary_integer_number('-0b111')
+    True
+    >>> is_binary_integer_number('0B10101')
+    True
+    >>> is_binary_integer_number('0b10102')
+    False
+    >>> is_binary_integer_number('0xFFF')
+    False
+    >>> is_binary_integer_number('test')
+    False
+    """
     if not is_string(in_str):
         raise ValueError(in_str)
     return BIN_NUMBER_RE.match(in_str) is not None
 
 
 def to_int(in_str: str) -> int:
     if not is_string(in_str):
         raise ValueError(in_str)
     return BIN_NUMBER_RE.match(in_str) is not None
 
 
 def to_int(in_str: str) -> int:
+    """Returns the integral value of the string or raises on error.
+
+    >>> to_int('1234')
+    1234
+    >>> to_int('test')
+    Traceback (most recent call last):
+    ...
+    ValueError: invalid literal for int() with base 10: 'test'
+    """
     if not is_string(in_str):
         raise ValueError(in_str)
     if is_binary_integer_number(in_str):
     if not is_string(in_str):
         raise ValueError(in_str)
     if is_binary_integer_number(in_str):
@@ -216,13 +354,21 @@ def is_decimal_number(in_str: str) -> bool:
 
     A decimal may be signed or unsigned or use a "scientific notation".
 
 
     A decimal may be signed or unsigned or use a "scientific notation".
 
-    >>> is_decimal('42.0') # returns true
-    >>> is_decimal('42') # returns false
+    >>> is_decimal_number('42.0')
+    True
+    >>> is_decimal_number('42')
+    False
     """
     return is_number(in_str) and "." in in_str
 
 
 def strip_escape_sequences(in_str: str) -> str:
     """
     return is_number(in_str) and "." in in_str
 
 
 def strip_escape_sequences(in_str: str) -> str:
+    """
+    Remove escape sequences in the input string.
+
+    >>> strip_escape_sequences('\e[12;11;22mthis is a test!')
+    'this is a test!'
+    """
     in_str = ESCAPE_SEQUENCE_RE.sub("", in_str)
     return in_str
 
     in_str = ESCAPE_SEQUENCE_RE.sub("", in_str)
     return in_str
 
@@ -233,7 +379,22 @@ def add_thousands_separator(
         separator_char = ',',
         places = 3
 ) -> str:
         separator_char = ',',
         places = 3
 ) -> str:
-    if isinstance(in_str, int):
+    """
+    Add thousands separator to a numeric string.  Also handles numbers.
+
+    >>> add_thousands_separator('12345678')
+    '12,345,678'
+    >>> add_thousands_separator(12345678)
+    '12,345,678'
+    >>> add_thousands_separator(12345678.99)
+    '12,345,678.99'
+    >>> add_thousands_separator('test')
+    Traceback (most recent call last):
+    ...
+    ValueError: test
+
+    """
+    if isinstance(in_str, numbers.Number):
         in_str = f'{in_str}'
     if is_number(in_str):
         return _add_thousands_separator(
         in_str = f'{in_str}'
     if is_number(in_str):
         return _add_thousands_separator(
@@ -263,11 +424,12 @@ def is_url(in_str: Any, allowed_schemes: Optional[List[str]] = None) -> bool:
     """
     Check if a string is a valid url.
 
     """
     Check if a string is a valid url.
 
-    *Examples:*
-
-    >>> is_url('http://www.mysite.com') # returns true
-    >>> is_url('https://mysite.com') # returns true
-    >>> is_url('.mysite.com') # returns false
+    >>> is_url('http://www.mysite.com')
+    True
+    >>> is_url('https://mysite.com')
+    True
+    >>> is_url('.mysite.com')
+    False
     """
     if not is_full_string(in_str):
         return False
     """
     if not is_full_string(in_str):
         return False
@@ -285,10 +447,10 @@ def is_email(in_str: Any) -> bool:
 
     Reference: https://tools.ietf.org/html/rfc3696#section-3
 
 
     Reference: https://tools.ietf.org/html/rfc3696#section-3
 
-    *Examples:*
-
-    >>> is_email('[email protected]') # returns true
-    >>> is_email('@gmail.com') # returns false
+    >>> is_email('[email protected]')
+    True
+    >>> is_email('@gmail.com')
+    False
     """
     if (
         not is_full_string(in_str)
     """
     if (
         not is_full_string(in_str)
@@ -331,8 +493,12 @@ def is_email(in_str: Any) -> bool:
 def suffix_string_to_number(in_str: str) -> Optional[int]:
     """Take a string like "33Gb" and convert it into a number (of bytes)
     like 34603008.  Return None if the input string is not valid.
 def suffix_string_to_number(in_str: str) -> Optional[int]:
     """Take a string like "33Gb" and convert it into a number (of bytes)
     like 34603008.  Return None if the input string is not valid.
-    """
 
 
+    >>> suffix_string_to_number('1Mb')
+    1048576
+    >>> suffix_string_to_number('13.1Gb')
+    14066017894
+    """
     def suffix_capitalize(s: str) -> str:
         if len(s) == 1:
             return s.upper()
     def suffix_capitalize(s: str) -> str:
         if len(s) == 1:
             return s.upper()
@@ -352,13 +518,21 @@ def suffix_string_to_number(in_str: str) -> Optional[int]:
             if multiplier is not None:
                 r = rest[x]
                 if is_integer_number(r):
             if multiplier is not None:
                 r = rest[x]
                 if is_integer_number(r):
-                    return int(r) * multiplier
+                    return to_int(r) * multiplier
+                if is_decimal_number(r):
+                    return int(float(r) * multiplier)
     return None
 
 
 def number_to_suffix_string(num: int) -> Optional[str]:
     """Take a number (of bytes) and returns a string like "43.8Gb".
     Returns none if the input is invalid.
     return None
 
 
 def number_to_suffix_string(num: int) -> Optional[str]:
     """Take a number (of bytes) and returns a string like "43.8Gb".
     Returns none if the input is invalid.
+
+    >>> number_to_suffix_string(14066017894)
+    '13.1Gb'
+    >>> number_to_suffix_string(1024 * 1024)
+    '1.0Mb'
+
     """
     d = 0.0
     suffix = None
     """
     d = 0.0
     suffix = None
@@ -427,6 +601,16 @@ def is_snake_case(in_str: Any, *, separator: str = "_") -> bool:
     - it's composed only by lowercase/uppercase letters and digits
     - it contains at least one underscore (or provided separator)
     - it does not start with a number
     - it's composed only by lowercase/uppercase letters and digits
     - it contains at least one underscore (or provided separator)
     - it does not start with a number
+
+    >>> is_snake_case('this_is_a_test')
+    True
+    >>> is_snake_case('___This_Is_A_Test_1_2_3___')
+    True
+    >>> is_snake_case('this-is-a-test')
+    False
+    >>> is_snake_case('this-is-a-test', separator='-')
+    True
+
     """
     if is_full_string(in_str):
         re_map = {"_": SNAKE_CASE_TEST_RE, "-": SNAKE_CASE_TEST_DASH_RE}
     """
     if is_full_string(in_str):
         re_map = {"_": SNAKE_CASE_TEST_RE, "-": SNAKE_CASE_TEST_DASH_RE}
@@ -447,11 +631,12 @@ def is_json(in_str: Any) -> bool:
     """
     Check if a string is a valid json.
 
     """
     Check if a string is a valid json.
 
-    *Examples:*
-
-    >>> is_json('{"name": "Peter"}') # returns true
-    >>> is_json('[1, 2, 3]') # returns true
-    >>> is_json('{nope}') # returns false
+    >>> is_json('{"name": "Peter"}')
+    True
+    >>> is_json('[1, 2, 3]')
+    True
+    >>> is_json('{nope}')
+    False
     """
     if is_full_string(in_str) and JSON_WRAPPER_RE.match(in_str) is not None:
         try:
     """
     if is_full_string(in_str) and JSON_WRAPPER_RE.match(in_str) is not None:
         try:
@@ -465,11 +650,12 @@ def is_uuid(in_str: Any, allow_hex: bool = False) -> bool:
     """
     Check if a string is a valid UUID.
 
     """
     Check if a string is a valid UUID.
 
-    *Example:*
-
-    >>> is_uuid('6f8aa2f9-686c-4ac3-8766-5712354a04cf') # returns true
-    >>> is_uuid('6f8aa2f9686c4ac387665712354a04cf') # returns false
-    >>> is_uuid('6f8aa2f9686c4ac387665712354a04cf', allow_hex=True) # returns true
+    >>> is_uuid('6f8aa2f9-686c-4ac3-8766-5712354a04cf')
+    True
+    >>> is_uuid('6f8aa2f9686c4ac387665712354a04cf')
+    False
+    >>> is_uuid('6f8aa2f9686c4ac387665712354a04cf', allow_hex=True)
+    True
     """
     # string casting is used to allow UUID itself as input data type
     s = str(in_str)
     """
     # string casting is used to allow UUID itself as input data type
     s = str(in_str)
@@ -482,11 +668,12 @@ def is_ip_v4(in_str: Any) -> bool:
     """
     Checks if a string is a valid ip v4.
 
     """
     Checks if a string is a valid ip v4.
 
-    *Examples:*
-
-    >>> is_ip_v4('255.200.100.75') # returns true
-    >>> is_ip_v4('nope') # returns false (not an ip)
-    >>> is_ip_v4('255.200.100.999') # returns false (999 is out of range)
+    >>> is_ip_v4('255.200.100.75')
+    True
+    >>> is_ip_v4('nope')
+    False
+    >>> is_ip_v4('255.200.100.999')  # 999 out of range
+    False
     """
     if not is_full_string(in_str) or SHALLOW_IP_V4_RE.match(in_str) is None:
         return False
     """
     if not is_full_string(in_str) or SHALLOW_IP_V4_RE.match(in_str) is None:
         return False
@@ -501,11 +688,14 @@ def is_ip_v4(in_str: Any) -> bool:
 def extract_ip_v4(in_str: Any) -> Optional[str]:
     """
     Extracts the IPv4 chunk of a string or None.
 def extract_ip_v4(in_str: Any) -> Optional[str]:
     """
     Extracts the IPv4 chunk of a string or None.
+
+    >>> extract_ip_v4('   The secret IP address: 127.0.0.1 (use it wisely)   ')
+    '127.0.0.1'
+    >>> extract_ip_v4('Your mom dresses you funny.')
     """
     if not is_full_string(in_str):
         return None
     """
     if not is_full_string(in_str):
         return None
-    in_str.strip()
-    m = SHALLOW_IP_V4_RE.match(in_str)
+    m = ANYWHERE_IP_V4_RE.search(in_str)
     if m is not None:
         return m.group(0)
     return None
     if m is not None:
         return m.group(0)
     return None
@@ -515,10 +705,10 @@ def is_ip_v6(in_str: Any) -> bool:
     """
     Checks if a string is a valid ip v6.
 
     """
     Checks if a string is a valid ip v6.
 
-    *Examples:*
-
-    >>> is_ip_v6('2001:db8:85a3:0000:0000:8a2e:370:7334') # returns true
-    >>> is_ip_v6('2001:db8:85a3:0000:0000:8a2e:370:?') # returns false (invalid "?")
+    >>> is_ip_v6('2001:db8:85a3:0000:0000:8a2e:370:7334')
+    True
+    >>> is_ip_v6('2001:db8:85a3:0000:0000:8a2e:370:?')    # invalid "?"
+    False
     """
     return is_full_string(in_str) and IP_V6_RE.match(in_str) is not None
 
     """
     return is_full_string(in_str) and IP_V6_RE.match(in_str) is not None
 
@@ -526,11 +716,14 @@ def is_ip_v6(in_str: Any) -> bool:
 def extract_ip_v6(in_str: Any) -> Optional[str]:
     """
     Extract IPv6 chunk or None.
 def extract_ip_v6(in_str: Any) -> Optional[str]:
     """
     Extract IPv6 chunk or None.
+
+    >>> extract_ip_v6('IP: 2001:db8:85a3:0000:0000:8a2e:370:7334')
+    '2001:db8:85a3:0000:0000:8a2e:370:7334'
+    >>> extract_ip_v6("(and she's ugly too, btw)")
     """
     if not is_full_string(in_str):
         return None
     """
     if not is_full_string(in_str):
         return None
-    in_str.strip()
-    m = IP_V6_RE.match(in_str)
+    m = ANYWHERE_IP_V6_RE.search(in_str)
     if m is not None:
         return m.group(0)
     return None
     if m is not None:
         return m.group(0)
     return None
@@ -542,15 +735,29 @@ def is_ip(in_str: Any) -> bool:
 
     *Examples:*
 
 
     *Examples:*
 
-    >>> is_ip('255.200.100.75') # returns true
-    >>> is_ip('2001:db8:85a3:0000:0000:8a2e:370:7334') # returns true
-    >>> is_ip('1.2.3') # returns false
+    >>> is_ip('255.200.100.75')
+    True
+    >>> is_ip('2001:db8:85a3:0000:0000:8a2e:370:7334')
+    True
+    >>> is_ip('1.2.3')
+    False
+    >>> is_ip('1.2.3.999')
+    False
     """
     return is_ip_v6(in_str) or is_ip_v4(in_str)
 
 
 def extract_ip(in_str: Any) -> Optional[str]:
     """
     return is_ip_v6(in_str) or is_ip_v4(in_str)
 
 
 def extract_ip(in_str: Any) -> Optional[str]:
-    """Extract the IP address or None."""
+    """
+    Extract the IP address or None.
+
+    >>> extract_ip('Attacker: 255.200.100.75')
+    '255.200.100.75'
+    >>> extract_ip('Remote host: 2001:db8:85a3:0000:0000:8a2e:370:7334')
+    '2001:db8:85a3:0000:0000:8a2e:370:7334'
+    >>> extract_ip('1.2.3')
+
+    """
     ip = extract_ip_v4(in_str)
     if ip is None:
         ip = extract_ip_v6(in_str)
     ip = extract_ip_v4(in_str)
     if ip is None:
         ip = extract_ip_v6(in_str)
@@ -558,16 +765,32 @@ def extract_ip(in_str: Any) -> Optional[str]:
 
 
 def is_mac_address(in_str: Any) -> bool:
 
 
 def is_mac_address(in_str: Any) -> bool:
-    """Return True if in_str is a valid MAC address false otherwise."""
+    """Return True if in_str is a valid MAC address false otherwise.
+
+    >>> is_mac_address("34:29:8F:12:0D:2F")
+    True
+    >>> is_mac_address('34:29:8f:12:0d:2f')
+    True
+    >>> is_mac_address('34-29-8F-12-0D-2F')
+    True
+    >>> is_mac_address("test")
+    False
+    """
     return is_full_string(in_str) and MAC_ADDRESS_RE.match(in_str) is not None
 
 
 def extract_mac_address(in_str: Any, *, separator: str = ":") -> Optional[str]:
     return is_full_string(in_str) and MAC_ADDRESS_RE.match(in_str) is not None
 
 
 def extract_mac_address(in_str: Any, *, separator: str = ":") -> Optional[str]:
-    """Extract the MAC address from in_str"""
+    """
+    Extract the MAC address from in_str.
+
+    >>> extract_mac_address(' MAC Address: 34:29:8F:12:0D:2F')
+    '34:29:8F:12:0D:2F'
+
+    """
     if not is_full_string(in_str):
         return None
     in_str.strip()
     if not is_full_string(in_str):
         return None
     in_str.strip()
-    m = MAC_ADDRESS_RE.match(in_str)
+    m = ANYWHERE_MAC_ADDRESS_RE.search(in_str)
     if m is not None:
         mac = m.group(0)
         mac.replace(":", separator)
     if m is not None:
         mac = m.group(0)
         mac.replace(":", separator)
@@ -580,16 +803,11 @@ def is_slug(in_str: Any, separator: str = "-") -> bool:
     """
     Checks if a given string is a slug (as created by `slugify()`).
 
     """
     Checks if a given string is a slug (as created by `slugify()`).
 
-    *Examples:*
-
-    >>> is_slug('my-blog-post-title') # returns true
-    >>> is_slug('My blog post title') # returns false
+    >>> is_slug('my-blog-post-title')
+    True
+    >>> is_slug('My blog post title')
+    False
 
 
-    :param in_str: String to check.
-    :type in_str: str
-    :param separator: Join sign used by the slug.
-    :type separator: str
-    :return: True if slug, false otherwise.
     """
     if not is_full_string(in_str):
         return False
     """
     if not is_full_string(in_str):
         return False
@@ -604,10 +822,11 @@ def contains_html(in_str: str) -> bool:
     By design, this function matches ANY type of tag, so don't expect to use it
     as an HTML validator, its goal is to detect "malicious" or undesired tags in the text.
 
     By design, this function matches ANY type of tag, so don't expect to use it
     as an HTML validator, its goal is to detect "malicious" or undesired tags in the text.
 
-    *Examples:*
+    >>> contains_html('my string is <strong>bold</strong>')
+    True
+    >>> contains_html('my string is not bold')
+    False
 
 
-    >>> contains_html('my string is <strong>bold</strong>') # returns true
-    >>> contains_html('my string is not bold') # returns false
     """
     if not is_string(in_str):
         raise ValueError(in_str)
     """
     if not is_string(in_str):
         raise ValueError(in_str)
@@ -623,10 +842,11 @@ def words_count(in_str: str) -> int:
     Moreover it is aware of punctuation, so the count for a string like "one,two,three.stop"
     will be 4 not 1 (even if there are no spaces in the string).
 
     Moreover it is aware of punctuation, so the count for a string like "one,two,three.stop"
     will be 4 not 1 (even if there are no spaces in the string).
 
-    *Examples:*
+    >>> words_count('hello world')
+    2
+    >>> words_count('one,two,three.stop')
+    4
 
 
-    >>> words_count('hello world') # returns 2
-    >>> words_count('one,two,three.stop') # returns 4
     """
     if not is_string(in_str):
         raise ValueError(in_str)
     """
     if not is_string(in_str):
         raise ValueError(in_str)
@@ -637,10 +857,9 @@ def generate_uuid(as_hex: bool = False) -> str:
     """
     Generated an UUID string (using `uuid.uuid4()`).
 
     """
     Generated an UUID string (using `uuid.uuid4()`).
 
-    *Examples:*
+    generate_uuid() # possible output: '97e3a716-6b33-4ab9-9bb1-8128cb24d76b'
+    generate_uuid(as_hex=True) # possible output: '97e3a7166b334ab99bb18128cb24d76b'
 
 
-    >>> uuid() # possible output: '97e3a716-6b33-4ab9-9bb1-8128cb24d76b'
-    >>> uuid(as_hex=True) # possible output: '97e3a7166b334ab99bb18128cb24d76b'
     """
     uid = uuid4()
     if as_hex:
     """
     uid = uuid4()
     if as_hex:
@@ -653,9 +872,8 @@ def generate_random_alphanumeric_string(size: int) -> str:
     Returns a string of the specified size containing random
     characters (uppercase/lowercase ascii letters and digits).
 
     Returns a string of the specified size containing random
     characters (uppercase/lowercase ascii letters and digits).
 
-    *Example:*
+    random_string(9) # possible output: "cx3QQbzYg"
 
 
-    >>> random_string(9) # possible output: "cx3QQbzYg"
     """
     if size < 1:
         raise ValueError("size must be >= 1")
     """
     if size < 1:
         raise ValueError("size must be >= 1")
@@ -667,6 +885,10 @@ def generate_random_alphanumeric_string(size: int) -> str:
 def reverse(in_str: str) -> str:
     """
     Returns the string with its chars reversed.
 def reverse(in_str: str) -> str:
     """
     Returns the string with its chars reversed.
+
+    >>> reverse('test')
+    'tset'
+
     """
     if not is_string(in_str):
         raise ValueError(in_str)
     """
     if not is_string(in_str):
         raise ValueError(in_str)
@@ -677,6 +899,11 @@ def camel_case_to_snake_case(in_str, *, separator="_"):
     """
     Convert a camel case string into a snake case one.
     (The original string is returned if is not a valid camel case string)
     """
     Convert a camel case string into a snake case one.
     (The original string is returned if is not a valid camel case string)
+
+    >>> camel_case_to_snake_case('MacAddressExtractorFactory')
+    'mac_address_extractor_factory'
+    >>> camel_case_to_snake_case('Luke Skywalker')
+    'Luke Skywalker'
     """
     if not is_string(in_str):
         raise ValueError(in_str)
     """
     if not is_string(in_str):
         raise ValueError(in_str)
@@ -693,6 +920,11 @@ def snake_case_to_camel_case(
     """
     Convert a snake case string into a camel case one.
     (The original string is returned if is not a valid snake case string)
     """
     Convert a snake case string into a camel case one.
     (The original string is returned if is not a valid snake case string)
+
+    >>> snake_case_to_camel_case('this_is_a_test')
+    'ThisIsATest'
+    >>> snake_case_to_camel_case('Han Solo')
+    'Han Solo'
     """
     if not is_string(in_str):
         raise ValueError(in_str)
     """
     if not is_string(in_str):
         raise ValueError(in_str)
@@ -705,12 +937,22 @@ def snake_case_to_camel_case(
 
 
 def to_char_list(in_str: str) -> List[str]:
 
 
 def to_char_list(in_str: str) -> List[str]:
+    """Convert a string into a list of chars.
+
+    >>> to_char_list('test')
+    ['t', 'e', 's', 't']
+    """
     if not is_string(in_str):
         return []
     return list(in_str)
 
 
 def from_char_list(in_list: List[str]) -> str:
     if not is_string(in_str):
         return []
     return list(in_str)
 
 
 def from_char_list(in_list: List[str]) -> str:
+    """Convert a char list into a string.
+
+    >>> from_char_list(['t', 'e', 's', 't'])
+    'test'
+    """
     return "".join(in_list)
 
 
     return "".join(in_list)
 
 
@@ -731,10 +973,10 @@ def strip_html(in_str: str, keep_tag_content: bool = False) -> str:
     """
     Remove html code contained into the given string.
 
     """
     Remove html code contained into the given string.
 
-    *Examples:*
-
-    >>> strip_html('test: <a href="foo/bar">click here</a>') # returns 'test: '
-    >>> strip_html('test: <a href="foo/bar">click here</a>', keep_tag_content=True) # returns 'test: click here'
+    >>> strip_html('test: <a href="foo/bar">click here</a>')
+    'test: '
+    >>> strip_html('test: <a href="foo/bar">click here</a>', keep_tag_content=True)
+    'test: click here'
     """
     if not is_string(in_str):
         raise ValueError(in_str)
     """
     if not is_string(in_str):
         raise ValueError(in_str)
@@ -744,14 +986,14 @@ def strip_html(in_str: str, keep_tag_content: bool = False) -> str:
 
 def asciify(in_str: str) -> str:
     """
 
 def asciify(in_str: str) -> str:
     """
-    Force string content to be ascii-only by translating all non-ascii chars into the closest possible representation
-    (eg: ó -> o, Ë -> E, ç -> c...).
-
-    **Bear in mind**: Some chars may be lost if impossible to translate.
+    Force string content to be ascii-only by translating all non-ascii
+    chars into the closest possible representation (eg: ó -> o, Ë ->
+    E, ç -> c...).
 
 
-    *Example:*
+    N.B. Some chars may be lost if impossible to translate.
 
 
-    >>> asciify('èéùúòóäåëýñÅÀÁÇÌÍÑÓË') # returns 'eeuuooaaeynAAACIINOE'
+    >>> asciify('èéùúòóäåëýñÅÀÁÇÌÍÑÓË')
+    'eeuuooaaeynAAACIINOE'
     """
     if not is_string(in_str):
         raise ValueError(in_str)
     """
     if not is_string(in_str):
         raise ValueError(in_str)
@@ -780,10 +1022,10 @@ def slugify(in_str: str, *, separator: str = "-") -> str:
     - all chars are encoded as ascii (by using `asciify()`)
     - is safe for URL
 
     - all chars are encoded as ascii (by using `asciify()`)
     - is safe for URL
 
-    *Examples:*
-
-    >>> slugify('Top 10 Reasons To Love Dogs!!!') # returns: 'top-10-reasons-to-love-dogs'
-    >>> slugify('Mönstér Mägnët') # returns 'monster-magnet'
+    >>> slugify('Top 10 Reasons To Love Dogs!!!')
+    'top-10-reasons-to-love-dogs'
+    >>> slugify('Mönstér Mägnët')
+    'monster-magnet'
     """
     if not is_string(in_str):
         raise ValueError(in_str)
     """
     if not is_string(in_str):
         raise ValueError(in_str)
@@ -803,7 +1045,8 @@ def to_bool(in_str: str) -> bool:
     """
     Turns a string into a boolean based on its content (CASE INSENSITIVE).
 
     """
     Turns a string into a boolean based on its content (CASE INSENSITIVE).
 
-    A positive boolean (True) is returned if the string value is one of the following:
+    A positive boolean (True) is returned if the string value is one
+    of the following:
 
     - "true"
     - "1"
 
     - "true"
     - "1"
@@ -811,6 +1054,17 @@ def to_bool(in_str: str) -> bool:
     - "y"
 
     Otherwise False is returned.
     - "y"
 
     Otherwise False is returned.
+
+    >>> to_bool('True')
+    True
+    >>> to_bool('1')
+    True
+    >>> to_bool('yes')
+    True
+    >>> to_bool('no')
+    False
+    >>> to_bool('huh?')
+    False
     """
     if not is_string(in_str):
         raise ValueError(in_str)
     """
     if not is_string(in_str):
         raise ValueError(in_str)
@@ -818,6 +1072,9 @@ def to_bool(in_str: str) -> bool:
 
 
 def to_date(in_str: str) -> Optional[datetime.date]:
 
 
 def to_date(in_str: str) -> Optional[datetime.date]:
+    """
+    Parses a date string.  See DateParser docs for details.
+    """
     import dateparse.dateparse_utils as dp
     try:
         d = dp.DateParser()
     import dateparse.dateparse_utils as dp
     try:
         d = dp.DateParser()
@@ -829,6 +1086,9 @@ def to_date(in_str: str) -> Optional[datetime.date]:
 
 
 def valid_date(in_str: str) -> bool:
 
 
 def valid_date(in_str: str) -> bool:
+    """
+    True if the string represents a valid date.
+    """
     import dateparse.dateparse_utils as dp
     try:
         d = dp.DateParser()
     import dateparse.dateparse_utils as dp
     try:
         d = dp.DateParser()
@@ -840,6 +1100,9 @@ def valid_date(in_str: str) -> bool:
 
 
 def to_datetime(in_str: str) -> Optional[datetime.datetime]:
 
 
 def to_datetime(in_str: str) -> Optional[datetime.datetime]:
+    """
+    Parses a datetime string.  See DateParser docs for more info.
+    """
     import dateparse.dateparse_utils as dp
     try:
         d = dp.DateParser()
     import dateparse.dateparse_utils as dp
     try:
         d = dp.DateParser()
@@ -852,6 +1115,9 @@ def to_datetime(in_str: str) -> Optional[datetime.datetime]:
 
 
 def valid_datetime(in_str: str) -> bool:
 
 
 def valid_datetime(in_str: str) -> bool:
+    """
+    True if the string represents a valid datetime.
+    """
     _ = to_datetime(in_str)
     if _ is not None:
         return True
     _ = to_datetime(in_str)
     if _ is not None:
         return True
@@ -862,20 +1128,6 @@ def valid_datetime(in_str: str) -> bool:
 def dedent(in_str: str) -> str:
     """
     Removes tab indentation from multi line strings (inspired by analogous Scala function).
 def dedent(in_str: str) -> str:
     """
     Removes tab indentation from multi line strings (inspired by analogous Scala function).
-
-    *Example:*
-
-    >>> strip_margin('''
-    >>>                 line 1
-    >>>                 line 2
-    >>>                 line 3
-    >>> ''')
-    >>> # returns:
-    >>> '''
-    >>> line 1
-    >>> line 2
-    >>> line 3
-    >>> '''
     """
     if not is_string(in_str):
         raise ValueError(in_str)
     """
     if not is_string(in_str):
         raise ValueError(in_str)
@@ -885,6 +1137,13 @@ def dedent(in_str: str) -> str:
 
 
 def indent(in_str: str, amount: int) -> str:
 
 
 def indent(in_str: str, amount: int) -> str:
+    """
+    Indents string by prepending amount spaces.
+
+    >>> indent('This is a test', 4)
+    '    This is a test'
+
+    """
     if not is_string(in_str):
         raise ValueError(in_str)
     line_separator = '\n'
     if not is_string(in_str):
         raise ValueError(in_str)
     line_separator = '\n'
@@ -893,6 +1152,7 @@ def indent(in_str: str, amount: int) -> str:
 
 
 def sprintf(*args, **kwargs) -> str:
 
 
 def sprintf(*args, **kwargs) -> str:
+    """String printf, like in C"""
     ret = ""
 
     sep = kwargs.pop("sep", None)
     ret = ""
 
     sep = kwargs.pop("sep", None)
@@ -924,6 +1184,15 @@ def sprintf(*args, **kwargs) -> str:
 
 
 class SprintfStdout(object):
 
 
 class SprintfStdout(object):
+    """
+    A context manager that captures outputs to stdout.
+
+    with SprintfStdout() as buf:
+        print("test")
+    print(buf())
+
+    'test\n'
+    """
     def __init__(self) -> None:
         self.destination = io.StringIO()
         self.recorder = None
     def __init__(self) -> None:
         self.destination = io.StringIO()
         self.recorder = None
@@ -940,18 +1209,48 @@ class SprintfStdout(object):
 
 
 def is_are(n: int) -> str:
 
 
 def is_are(n: int) -> str:
+    """Is or are?
+
+    >>> is_are(1)
+    'is'
+    >>> is_are(2)
+    'are'
+
+    """
     if n == 1:
         return "is"
     return "are"
 
 
 def pluralize(n: int) -> str:
     if n == 1:
         return "is"
     return "are"
 
 
 def pluralize(n: int) -> str:
+    """Add an s?
+
+    >>> pluralize(15)
+    's'
+    >>> count = 1
+    >>> print(f'There {is_are(count)} {count} file{pluralize(count)}.')
+    There is 1 file.
+    >>> count = 4
+    >>> print(f'There {is_are(count)} {count} file{pluralize(count)}.')
+    There are 4 files.
+
+    """
     if n == 1:
         return ""
     return "s"
 
 
 def thify(n: int) -> str:
     if n == 1:
         return ""
     return "s"
 
 
 def thify(n: int) -> str:
+    """Return the proper cardinal suffix for a number.
+
+    >>> thify(1)
+    'st'
+    >>> thify(33)
+    'rd'
+    >>> thify(16)
+    'th'
+
+    """
     digit = str(n)
     assert is_integer_number(digit)
     digit = digit[-1:]
     digit = str(n)
     assert is_integer_number(digit)
     digit = digit[-1:]
@@ -966,6 +1265,12 @@ def thify(n: int) -> str:
 
 
 def ngrams(txt: str, n: int):
 
 
 def ngrams(txt: str, n: int):
+    """Return the ngrams from a string.
+
+    >>> [x for x in ngrams('This is a test', 2)]
+    ['This is', 'is a', 'a test']
+
+    """
     words = txt.split()
     return ngrams_presplit(words, n)
 
     words = txt.split()
     return ngrams_presplit(words, n)
 
@@ -983,35 +1288,79 @@ def trigrams(txt: str):
     return ngrams(txt, 3)
 
 
     return ngrams(txt, 3)
 
 
-def shuffle_columns(
-        txt: Iterable[str],
-        specs: Iterable[Iterable[int]],
+def shuffle_columns_into_list(
+        input_lines: Iterable[str],
+        column_specs: Iterable[Iterable[int]],
         delim=''
 ) -> Iterable[str]:
         delim=''
 ) -> Iterable[str]:
+    """Helper to shuffle / parse columnar data and return the results as a
+    list.  The column_specs argument is an iterable collection of
+    numeric sequences that indicate one or more column numbers to
+    copy.
+
+    >>> cols = '-rwxr-xr-x 1 scott wheel 3.1K Jul  9 11:34 acl_test.py'.split()
+    >>> shuffle_columns_into_list(
+    ...     cols,
+    ...     [ [8], [2, 3], [5, 6, 7] ],
+    ...     delim=' ',
+    ... )
+    ['acl_test.py', 'scott wheel', 'Jul 9 11:34']
+
+    """
     out = []
     out = []
-    for spec in specs:
+
+    # Column specs map input lines' columns into outputs.
+    # [col1, col2...]
+    for spec in column_specs:
         chunk = ''
         for n in spec:
         chunk = ''
         for n in spec:
-            chunk = chunk + delim + txt[n]
+            chunk = chunk + delim + input_lines[n]
         chunk = chunk.strip(delim)
         out.append(chunk)
     return out
 
 
 def shuffle_columns_into_dict(
         chunk = chunk.strip(delim)
         out.append(chunk)
     return out
 
 
 def shuffle_columns_into_dict(
-        txt: Iterable[str],
-        specs: Iterable[Tuple[str, Iterable[int]]],
+        input_lines: Iterable[str],
+        column_specs: Iterable[Tuple[str, Iterable[int]]],
         delim=''
 ) -> Dict[str, str]:
         delim=''
 ) -> Dict[str, str]:
+    """Helper to shuffle / parse columnar data and return the results
+    as a dict.
+
+    >>> cols = '-rwxr-xr-x 1 scott wheel 3.1K Jul  9 11:34 acl_test.py'.split()
+    >>> shuffle_columns_into_dict(
+    ...     cols,
+    ...     [ ('filename', [8]), ('owner', [2, 3]), ('mtime', [5, 6, 7]) ],
+    ...     delim=' ',
+    ... )
+    {'filename': 'acl_test.py', 'owner': 'scott wheel', 'mtime': 'Jul 9 11:34'}
+
+    """
     out = {}
     out = {}
-    for spec in specs:
+
+    # Column specs map input lines' columns into outputs.
+    # "key", [col1, col2...]
+    for spec in column_specs:
         chunk = ''
         for n in spec[1]:
         chunk = ''
         for n in spec[1]:
-            chunk = chunk + delim + txt[n]
+            chunk = chunk + delim + input_lines[n]
         chunk = chunk.strip(delim)
         out[spec[0]] = chunk
     return out
 
 
 def interpolate_using_dict(txt: str, values: Dict[str, str]) -> str:
         chunk = chunk.strip(delim)
         out[spec[0]] = chunk
     return out
 
 
 def interpolate_using_dict(txt: str, values: Dict[str, str]) -> str:
+    """Interpolate a string with data from a dict.
+
+    >>> interpolate_using_dict('This is a {adjective} {noun}.',
+    ...                        {'adjective': 'good', 'noun': 'example'})
+    'This is a good example.'
+
+    """
     return sprintf(txt.format(**values), end='')
     return sprintf(txt.format(**values), end='')
+
+
+if __name__ == '__main__':
+    import doctest
+    doctest.testmod()
index d87b5e76bac766a666ce0bc636a1da0cd5c7a2e3..9d9871053aaa927c1af5508a17db1522b0bbd65a 100755 (executable)
@@ -37,20 +37,30 @@ def list_primes(n):
 
 
 @decorator_utils.timed
 
 
 @decorator_utils.timed
-def driver() -> None:
+def test_thread_parallelization() -> None:
     results = []
     results = []
-    for _ in range(20):
-        results.append(compute_factorial_process(_))
-    for future in smart_future.wait_any(results):
-        print(f'Process: {future}')
-
-    results = []
-    for _ in range(20):
+    for _ in range(50):
         results.append(compute_factorial_thread(_))
     smart_future.wait_all(results)
     for future in results:
         print(f'Thread: {future}')
         results.append(compute_factorial_thread(_))
     smart_future.wait_all(results)
     for future in results:
         print(f'Thread: {future}')
+    texecutor = executors.DefaultExecutors().thread_pool()
+    texecutor.shutdown()
+
+
+@decorator_utils.timed
+def test_process_parallelization() -> None:
+    results = []
+    for _ in range(50):
+        results.append(compute_factorial_process(_))
+    for future in smart_future.wait_any(results):
+        print(f'Process: {future}')
+    pexecutor = executors.DefaultExecutors().process_pool()
+    pexecutor.shutdown()
 
 
+
+@decorator_utils.timed
+def test_remote_parallelization() -> None:
     results = {}
     for _ in range(50):
         n = random.randint(0, 100000)
     results = {}
     for _ in range(50):
         n = random.randint(0, 100000)
@@ -59,66 +69,17 @@ def driver() -> None:
     for _ in results[n]:
         tot += _
     print(tot)
     for _ in results[n]:
         tot += _
     print(tot)
+    rexecutor = executors.DefaultExecutors().remote_pool()
+    rexecutor.shutdown()
 
 
 @bootstrap.initialize
 def main() -> None:
 
 
 @bootstrap.initialize
 def main() -> None:
-    print(driver())
-    pexecutor = executors.DefaultExecutors().process_pool()
-    pexecutor.shutdown()
-    texecutor = executors.DefaultExecutors().thread_pool()
-    texecutor.shutdown()
-    rexecutor = executors.DefaultExecutors().remote_pool()
-    rexecutor.shutdown()
+    test_thread_parallelization()
+    test_process_parallelization()
+    test_remote_parallelization()
     sys.exit(0)
 
 
 if __name__ == '__main__':
     main()
     sys.exit(0)
 
 
 if __name__ == '__main__':
     main()
-
-# print """Usage: python sum_primes.py [ncpus]
-#     [ncpus] - the number of workers to run in parallel,
-#     if omitted it will be set to the number of processors in the system
-# """
-
-# # tuple of all parallel python servers to connect with
-# ppservers = ()
-# #ppservers = ("10.0.0.1",)
-
-# if len(sys.argv) > 1:
-#     ncpus = int(sys.argv[1])
-#     # Creates jobserver with ncpus workers
-# job_server = pp.Server(ncpus, ppservers=ppservers)
-# else:
-#     # Creates jobserver with automatically detected number of workers
-# job_server = pp.Server(ppservers=ppservers)
-
-# print "Starting pp with", job_server.get_ncpus(), "workers"
-
-# # Submit a job of calulating sum_primes(100) for execution.
-# # sum_primes - the function
-# # (100,) - tuple with arguments for sum_primes
-# # (isprime,) - tuple with functions on which function sum_primes depends
-# # ("math",) - tuple with module names which must be imported before sum_primes execution
-# # Execution starts as soon as one of the workers will become available
-# job1 = job_server.submit(sum_primes, (100,), (isprime,), ("math",))
-
-# # Retrieves the result calculated by job1
-# # The value of job1() is the same as sum_primes(100)
-# # If the job has not been finished yet, execution will wait here until result is available
-# result = job1()
-
-# print "Sum of primes below 100 is", result
-
-# start_time = time.time()
-
-# # The following submits 8 jobs and then retrieves the results
-# inputs = (100000, 100100, 100200, 100300, 100400, 100500, 100600, 100700)
-# jobs = [(input, job_server.submit(sum_primes,(input,), (isprime,), ("math",))) for input in inputs]
-# for input, job in jobs:
-#     print "Sum of primes below", input, "is", job()
-
-# print "Time elapsed: ", time.time() - start_time, "s"
-# job_server.print_stats()
-
-# # Parallel Python Software: http://www.parallelpython.com
index c2f9f93013e8ccf2f1dc69dabac5fc7391a0c69d..25365bbd7894fb5de198ca1aa120f87ae76a89dc 100755 (executable)
@@ -1,5 +1,10 @@
 #!/bin/bash
 
 #!/bin/bash
 
+for doctest in $(grep -l doctest ../*.py); do
+    echo "------------------------- ${doctest} -------------------------"
+    python3 ${doctest}
+done
+
 for test in $(ls *_test.py); do
     if [ "${test}" != "parallelize_test.py" ]; then
         echo "------------------------- ${test} -------------------------"
 for test in $(ls *_test.py); do
     if [ "${test}" != "parallelize_test.py" ]; then
         echo "------------------------- ${test} -------------------------"
index 3be32ff49ec05b2d7ca0978e6cb34b65da64162e..8ea6e196001e795daec223e166e01d9aed33a009 100644 (file)
@@ -16,6 +16,8 @@ class RowsColumns(NamedTuple):
 
 
 def get_console_rows_columns() -> RowsColumns:
 
 
 def get_console_rows_columns() -> RowsColumns:
+    """Returns the number of rows/columns on the current console."""
+
     from exec_utils import cmd
     rows, columns = cmd("stty size").split()
     return RowsColumns(int(rows), int(columns))
     from exec_utils import cmd
     rows, columns = cmd("stty size").split()
     return RowsColumns(int(rows), int(columns))
@@ -31,6 +33,8 @@ def progress_graph(
     right_end="]",
     redraw=True,
 ) -> None:
     right_end="]",
     redraw=True,
 ) -> None:
+    """Draws a progress graph."""
+
     percent = current / total
     ret = "\r" if redraw else "\n"
     bar = bar_graph(
     percent = current / total
     ret = "\r" if redraw else "\n"
     bar = bar_graph(
@@ -53,9 +57,17 @@ def bar_graph(
     include_text=True,
     width=70,
     fgcolor=fg("school bus yellow"),
     include_text=True,
     width=70,
     fgcolor=fg("school bus yellow"),
+    reset=reset(),
     left_end="[",
     right_end="]",
 ) -> None:
     left_end="[",
     right_end="]",
 ) -> None:
+    """Returns a string containing a bar graph.
+
+    >>> bar_graph(0.5, fgcolor='', reset='')
+    '[███████████████████████████████████                                   ] 50.0%'
+
+    """
+
     if percentage < 0.0 or percentage > 1.0:
         raise ValueError(percentage)
     if include_text:
     if percentage < 0.0 or percentage > 1.0:
         raise ValueError(percentage)
     if include_text:
@@ -75,7 +87,7 @@ def bar_graph(
         fgcolor +
         "█" * whole_width + part_char +
         " " * (width - whole_width - 1) +
         fgcolor +
         "█" * whole_width + part_char +
         " " * (width - whole_width - 1) +
-        reset() +
+        reset +
         right_end + " " +
         text)
 
         right_end + " " +
         text)
 
@@ -87,6 +99,17 @@ def distribute_strings(
     alignment: str = "c",
     padding: str = " ",
 ) -> str:
     alignment: str = "c",
     padding: str = " ",
 ) -> str:
+    """
+    Distributes strings into a line with a particular justification.
+
+    >>> distribute_strings(['this', 'is', 'a', 'test'], width=40)
+    '   this       is         a       test   '
+    >>> distribute_strings(['this', 'is', 'a', 'test'], width=40, alignment='l')
+    'this      is        a         test      '
+    >>> distribute_strings(['this', 'is', 'a', 'test'], width=40, alignment='r')
+    '      this        is         a      test'
+
+    """
     subwidth = math.floor(width / len(strings))
     retval = ""
     for string in strings:
     subwidth = math.floor(width / len(strings))
     retval = ""
     for string in strings:
@@ -100,6 +123,15 @@ def distribute_strings(
 def justify_string_by_chunk(
     string: str, width: int = 80, padding: str = " "
 ) -> str:
 def justify_string_by_chunk(
     string: str, width: int = 80, padding: str = " "
 ) -> str:
+    """
+    Justifies a string.
+
+    >>> justify_string_by_chunk("This is a test", 40)
+    'This       is              a        test'
+    >>> justify_string_by_chunk("This is a test", 20)
+    'This  is    a   test'
+
+    """
     padding = padding[0]
     first, *rest, last = string.split()
     w = width - (len(first) + 1 + len(last) + 1)
     padding = padding[0]
     first, *rest, last = string.split()
     w = width - (len(first) + 1 + len(last) + 1)
@@ -115,6 +147,18 @@ def justify_string_by_chunk(
 def justify_string(
     string: str, *, width: int = 80, alignment: str = "c", padding: str = " "
 ) -> str:
 def justify_string(
     string: str, *, width: int = 80, alignment: str = "c", padding: str = " "
 ) -> str:
+    """Justify a string.
+
+    >>> justify_string('This is another test', width=40, alignment='c')
+    '          This is another test          '
+    >>> justify_string('This is another test', width=40, alignment='l')
+    'This is another test                    '
+    >>> justify_string('This is another test', width=40, alignment='r')
+    '                    This is another test'
+    >>> justify_string('This is another test', width=40, alignment='j')
+    'This       is           another     test'
+
+    """
     alignment = alignment[0]
     padding = padding[0]
     while len(string) < width:
     alignment = alignment[0]
     padding = padding[0]
     while len(string) < width:
@@ -139,7 +183,13 @@ def justify_string(
 
 
 def justify_text(text: str, *, width: int = 80, alignment: str = "c") -> str:
 
 
 def justify_text(text: str, *, width: int = 80, alignment: str = "c") -> str:
-    print("-" * width)
+    """
+    Justifies text.
+
+    >>> justify_text('This is a test of the emergency broadcast system.  This is only a test.',
+    ...              width=40, alignment='j')  #doctest: +NORMALIZE_WHITESPACE
+    'This  is    a  test  of   the  emergency\\nbroadcast system. This is only a test.'
+    """
     retval = ""
     line = ""
     for word in text.split():
     retval = ""
     line = ""
     for word in text.split():
@@ -216,3 +266,8 @@ class Indenter:
         import string_utils
         text = string_utils.sprintf(*arg, **kwargs)
         print(self.pad_prefix + self.padding * self.level + text, end='')
         import string_utils
         text = string_utils.sprintf(*arg, **kwargs)
         print(self.pad_prefix + self.padding * self.level + text, end='')
+
+
+if __name__ == '__main__':
+    import doctest
+    doctest.testmod()
index bb15c034b9e4e02f273b98dbc77410072878e089..0130cdc510547196d418d6699d1b46b84a6ddf7c 100644 (file)
@@ -20,6 +20,9 @@ def current_thread_id() -> str:
 
 
 def is_current_thread_main_thread() -> bool:
 
 
 def is_current_thread_main_thread() -> bool:
+    """Returns True is the current (calling) thread is the process' main
+    thread and False otherwise.
+    """
     return threading.current_thread() is threading.main_thread()
 
 
     return threading.current_thread() is threading.main_thread()
 
 
index 7b79af08f934f0b93153c5c14065a3a884de9a91..ee52444e7828c1d2b7920749a800dbf77e653983 100644 (file)
@@ -7,6 +7,11 @@ logger = logging.getLogger(__name__)
 
 
 def unwrap_optional(x: Optional[Any]) -> Any:
 
 
 def unwrap_optional(x: Optional[Any]) -> Any:
+    """Unwrap an Optional[Type] argument returning a Type value back.
+    If the Optional[Type] argument is None, however, raise an exception.
+    Use this to satisfy most type checkers that a value that could
+    be None isn't so as to drop the Optional.
+    """
     if x is None:
         msg = 'Argument to unwrap_optional was unexpectedly None'
         logger.critical(msg)
     if x is None:
         msg = 'Argument to unwrap_optional was unexpectedly None'
         logger.critical(msg)