More sanity with exception types and raises docs.
authorScott Gasch <[email protected]>
Mon, 12 Jun 2023 22:24:48 +0000 (15:24 -0700)
committerScott Gasch <[email protected]>
Mon, 12 Jun 2023 22:24:48 +0000 (15:24 -0700)
27 files changed:
src/pyutils/argparse_utils.py
src/pyutils/collectionz/interval_tree.py
src/pyutils/collectionz/trie.py
src/pyutils/compress/letter_compress.py
src/pyutils/config.py
src/pyutils/datetimes/dateparse_utils.py
src/pyutils/datetimes/datetime_utils.py
src/pyutils/decorator_utils.py
src/pyutils/dict_utils.py
src/pyutils/exec_utils.py
src/pyutils/files/directory_filter.py
src/pyutils/files/file_utils.py
src/pyutils/files/lockfile.py
src/pyutils/logging_utils.py
src/pyutils/math_utils.py
src/pyutils/parallelize/executors.py
src/pyutils/parallelize/thread_utils.py
src/pyutils/search/logical_search.py
src/pyutils/security/acl.py
src/pyutils/string_utils.py
src/pyutils/text_utils.py
src/pyutils/typez/centcount.py
src/pyutils/typez/histogram.py
src/pyutils/typez/money.py
src/pyutils/typez/persistent.py
src/pyutils/typez/rate.py
src/pyutils/typez/type_utils.py

index 01c37f54544d376f883992d17eebd2bf5526c8f4..97a57746bdedadc2bae1f8c35ab0d7818b33925c 100644 (file)
@@ -45,6 +45,10 @@ class ActionNoYes(argparse.Action):
 
     These arguments can be used to indicate the inclusion or exclusion of
     binary exclusive behaviors.
+
+    Raises:
+        ValueError: illegal argument value or combination
+
     """
 
     def __init__(self, option_strings, dest, default=None, required=False, help=None):
@@ -90,7 +94,10 @@ def valid_bool(v: Any) -> bool:
         v: data passed to an argument expecting a bool on the cmdline.
 
     Returns:
-        The boolean value of v or raises an ArgumentTypeError on error.
+        The boolean value of v
+
+    Raises:
+        ArgumentTypeError: parse error (e.g. not a valid bool string)
 
     Sample usage::
 
@@ -145,7 +152,10 @@ def valid_ip(ip: str) -> str:
         ip: data passed to a commandline arg expecting an IP(v4) address.
 
     Returns:
-        The IP address, if valid.  Raises ArgumentTypeError otherwise.
+        The IP address, if valid.
+
+    Raises:
+        ArgumentTypeError: parse error (e.g. not a valid IP address string)
 
     Sample usage::
 
@@ -187,6 +197,9 @@ def valid_mac(mac: str) -> str:
     Returns:
         The MAC address passed or raises ArgumentTypeError on error.
 
+    Raises:
+        ArgumentTypeError: parse error (e.g. not a valid MAC address)
+
     Sample usage::
 
         group.add_argument(
@@ -231,6 +244,9 @@ def valid_percentage(num: str) -> float:
     Returns:
         The number if valid, otherwise raises ArgumentTypeError.
 
+    Raises:
+        ArgumentTypeError: parse error (e.g. not a valid percentage)
+
     Sample usage::
 
         args.add_argument(
@@ -277,6 +293,9 @@ def valid_filename(filename: str) -> str:
     Returns:
         The filename if valid, otherwise raises ArgumentTypeError.
 
+    Raises:
+        ArgumentTypeError: parse error (e.g. file doesn't exist)
+
     Sample usage::
 
         args.add_argument(
@@ -314,6 +333,9 @@ def valid_date(txt: str) -> datetime.date:
     Returns:
         the datetime.date described by txt or raises ArgumentTypeError on error.
 
+    Raises:
+        ArgumentTypeError: parse error (e.g. date not valid)
+
     Sample usage::
 
         cfg.add_argument(
@@ -356,6 +378,9 @@ def valid_datetime(txt: str) -> datetime.datetime:
     Returns:
         The datetime.datetime described by txt or raises ArgumentTypeError on error.
 
+    Raises:
+        ArgumentTypeError: parse error (e.g. invalid datetime string)
+
     Sample usage::
 
         cfg.add_argument(
@@ -422,6 +447,9 @@ def valid_duration(txt: str) -> datetime.timedelta:
         The datetime.timedelta described by txt or raises ArgumentTypeError
         on error.
 
+    Raises:
+        ArgumentTypeError: parse error (e.g. invalid duration string)
+
     Sample usage::
 
         cfg.add_argument(
@@ -468,8 +496,6 @@ def valid_byte_count(txt: str) -> int:
         - plain numbers (123456)
         - numbers with ISO suffixes (Mb, Gb, Pb, etc...)
 
-    If the byte count is not parsable, raise an ArgumentTypeError.
-
     Args:
         txt: data passed to a commandline arg expecting a duration.
 
@@ -477,6 +503,9 @@ def valid_byte_count(txt: str) -> int:
         An integer number of bytes or raises ArgumentTypeError on
         error.
 
+    Raises:
+        ArgumentTypeError: parse error (e.g. byte count not parsable)
+
     Sample usage::
 
         cfg.add_argument(
index 7cd40cfa79492f42ded10ff36838e4331acc1d14..c4e4e9ae75beaa9fd85260675c7b1001df6c753e 100644 (file)
@@ -83,7 +83,7 @@ class AugmentedIntervalTree(bst.BinarySearchTree):
     @staticmethod
     def _assert_value_must_be_range(value: Any) -> NumericRange:
         if not isinstance(value, NumericRange):
-            raise Exception(
+            raise TypeError(
                 "AugmentedIntervalTree expects to use NumericRanges, see bst for a "
                 + "general purpose tree usable for other types."
             )
index ba7031b0ae378874b0ab83347049d248a4c3d95f..93138dc59ae24a1b2b54155ec452508203a3c9c0 100644 (file)
@@ -118,8 +118,7 @@ class Trie(object):
 
     def __getitem__(self, item: Sequence[Any]) -> Dict[Any, Any]:
         """Given an item, return its trie node which contains all
-        of the successor (child) node pointers.  If the item is not
-        a node in the Trie, raise a KeyError.
+        of the successor (child) node pointers.
 
         Args:
             item: the item whose node is to be retrieved
index d2dfa7abbb6bc5557c44f2961b2194bc97618452..9725353552f1a91ee7a63fd36bdb62cbba83f331 100644 (file)
@@ -35,6 +35,9 @@ def compress(uncompressed: str) -> bytes:
     Returns:
         the compressed bytes
 
+    Raises:
+        ValueError: uncompressed text contains illegal character
+
     >>> import binascii
     >>> binascii.hexlify(compress('this is a test'))
     b'a2133da67b0ee859d0'
@@ -52,7 +55,7 @@ def compress(uncompressed: str) -> bytes:
             bits = ord(letter) - ord("a") + 1  # 1..26
         else:
             if letter not in special_characters:
-                raise Exception(
+                raise ValueError(
                     f'"{uncompressed}" contains uncompressable char="{letter}"'
                 )
             bits = special_characters[letter]
index f4647c45eadc4907ff95165fe479b335fa80d1f3..a1aa5f97d89d9e3ebebe044436271dafef1a37a8 100644 (file)
@@ -403,6 +403,9 @@ class Config:
 
             Otherwise False is returned.
 
+        Raises:
+            Exception: On error reading from zookeeper
+
         >>> to_bool('True')
         True
 
@@ -604,6 +607,10 @@ class Config:
             A dict containing the parsed program configuration.  Note that this can
                 be safely ignored since it is also saved in `config.config` and may
                 be used directly using that identifier.
+
+        Raises:
+            Exception: if unrecognized config argument(s) are detected and the
+                --config_rejects_unrecognized_arguments argument is enabled.
         """
         if self.config_parse_called:
             return self.config
index d6665d76fc8dbbafd86b5f7c865b922323f3568f..586008d526466537ea5c57d8dbd8ef711a6dcf01 100755 (executable)
@@ -154,7 +154,11 @@ class ParseException(Exception):
 
 
 class RaisingErrorListener(antlr4.DiagnosticErrorListener):
-    """An error listener that raises ParseExceptions."""
+    """An error listener that raises ParseExceptions.
+
+    Raises:
+        ParseException: on parse error
+    """
 
     def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e):
         raise ParseException(msg)
@@ -305,6 +309,9 @@ class DateParser(dateparse_utilsListener):
             A datetime.datetime representing the parsed date/time or
             None on error.
 
+        Raises:
+            ParseException: an exception happened during parsing
+
         .. note::
 
             Parsed date expressions without any time part return
index b2a9d10169dc9b60fc9c16b0d8c969c6f52a224a..6a38e923b454939ff62924dce3b978e0ab6945e0 100644 (file)
@@ -117,6 +117,9 @@ def add_timezone(dt: datetime.datetime, tz: datetime.tzinfo) -> datetime.datetim
         A datetime identical to dt, the input datetime, except for
         that a timezone has been added.
 
+    Raises:
+        ValueError: if dt is already a timezone aware datetime.
+
     .. warning::
 
         This doesn't change the hour, minute, second, day, month, etc...
@@ -152,7 +155,7 @@ def add_timezone(dt: datetime.datetime, tz: datetime.tzinfo) -> datetime.datetim
     if is_timezone_aware(dt):
         if dt.tzinfo == tz:
             return dt
-        raise Exception(
+        raise ValueError(
             f"{dt} is already timezone aware; use replace_timezone or translate_timezone "
             + "depending on the semantics you want.  See the pydocs / code."
         )
@@ -518,6 +521,9 @@ def n_timeunits_from_base(
         base: a datetime representing the base date the result should be
             relative to.
 
+    Raises:
+        ValueError: unit is invalid
+
     Returns:
         A datetime that is count units before of after the base datetime.
 
@@ -956,6 +962,9 @@ def minute_number(hour: int, minute: int) -> MinuteOfDay:
     Returns:
         The minute number requested.  Raises `ValueError` on bad input.
 
+    Raises:
+        ValueError: invalid hour or minute input argument
+
     >>> minute_number(0, 0)
     0
 
@@ -1055,6 +1064,9 @@ def parse_duration(duration: str, raise_on_error: bool = False) -> int:
     Returns:
         A count of seconds represented by the input string.
 
+    Raises:
+        ValueError: bad duration and raise_on_error is set.
+
     >>> parse_duration('15 days, 2 hours')
     1303200
 
@@ -1359,6 +1371,9 @@ def easter(year: int, method: int = EASTER_WESTERN):
 
     `The Calendar FAQ: Easter <https://www.tondering.dk/claus/cal/easter.php>`_
 
+    Raises:
+        ValueError if method argument is invalid
+
     """
 
     if not (1 <= method <= 3):
index f0856975930ec2b76c8df95624fbe2ef3cae8a53..06785c026abb1c7c0d027ec39586146579d0027d 100644 (file)
@@ -410,6 +410,10 @@ def predicated_retry_with_backoff(
             that we should stop calling or False to indicate a retry
             is necessary
 
+    Raises:
+        ValueError: on invalid arguments; e.g. backoff must be >= 1.0,
+            delay_sec must be >= 0.0, tries must be > 0.
+
     .. note::
 
         If after `tries` attempts the wrapped function is still
@@ -952,6 +956,9 @@ def call_probabilistically(probability_of_call: float) -> Callable:
         probability_of_call: probability with which to invoke the
             wrapped function.  Must be 0 <= probabilty <= 1.0.
 
+    Raises:
+        ValueError: invalid probability of call arg
+
     Example usage... this example would skip the invocation of
     `log_the_entire_request_message` 95% of the time and only invoke
     if 5% of the time.::
index 0dbe18a8b153114159aa8457c69fdc7066c5fe27..8e6d6cb9b3c357398104a6b48af61064d4f77217 100644 (file)
@@ -337,13 +337,16 @@ def parallel_lists_to_dict(keys: List[Hashable], values: List[Any]) -> AnyDict:
     Returns:
         A dict composed of zipping the keys list and values list together.
 
+    Raises:
+        ValueError: if keys and values lists not the same length.
+
     >>> k = ['name', 'phone', 'address', 'zip']
     >>> v = ['scott', '555-1212', '123 main st.', '12345']
     >>> parallel_lists_to_dict(k, v)
     {'name': 'scott', 'phone': '555-1212', 'address': '123 main st.', 'zip': '12345'}
     """
     if len(keys) != len(values):
-        raise Exception("Parallel keys and values lists must have the same length")
+        raise ValueError("Parallel keys and values lists must have the same length")
     return dict(zip(keys, values))
 
 
index 6839ef6adf75c30cd2ededf8cff8b342f67914ee..6fcd340a05b301680cfdf696a2f1a178355aea9f 100644 (file)
@@ -38,6 +38,9 @@ def cmd_showing_output(
         exited.  Raises `TimeoutExpired` after killing the subprocess
         if the timeout expires.
 
+    Raises:
+        TimeoutExpired: if timeout expires before child terminates
+
     Side effects:
         prints all output of the child process (stdout or stderr)
     """
@@ -93,9 +96,7 @@ def cmd_showing_output(
 
 def cmd_exitcode(command: str, timeout_seconds: Optional[float] = None) -> int:
     """Run a command silently in the background and return its exit
-    code once it has finished.  If timeout_seconds is provided and the
-    command runs longer than timeout_seconds, raise a `TimeoutExpired`
-    exception.
+    code once it has finished.
 
     Args:
         command: the command to run
@@ -106,6 +107,10 @@ def cmd_exitcode(command: str, timeout_seconds: Optional[float] = None) -> int:
         the exit status of the subprocess once the subprocess has
         exited
 
+    Raises:
+        TimeoutExpired: if timeout_seconds is provided and the child process
+            executes longer than the limit.
+
     >>> cmd_exitcode('/bin/echo foo', 10.0)
     0
 
@@ -152,10 +157,7 @@ def cmd(command: str, timeout_seconds: Optional[float] = None) -> str:
 
 
 def run_silently(command: str, timeout_seconds: Optional[float] = None) -> None:
-    """Run a command silently but raise
-    `subprocess.CalledProcessError` if it fails (i.e. returns a
-    non-zero return value) and raise a `TimeoutExpired` if it runs too
-    long.
+    """Run a command silently.
 
     Args:
         command: the command to run.
@@ -167,6 +169,10 @@ def run_silently(command: str, timeout_seconds: Optional[float] = None) -> None:
         No return value; error conditions (including non-zero child process
         exits) produce exceptions.
 
+    Raises:
+        CalledProcessError: if the child process fails (i.e. exit != 0)
+        TimeoutExpired: if the child process executes too long.
+
     >>> run_silently("/usr/bin/true")
 
     >>> run_silently("/usr/bin/false")
index 74d49fb6168dbedfd4c5dc83322e77bc7d166550..243056e9d58191150271536545b0eea5bce36198 100644 (file)
@@ -30,6 +30,9 @@ class DirectoryFileFilter(object):
     content to-be-written is identical to the contents of the file on
     disk allowing calling code to safely skip the write.
 
+    Raises:
+        ValueError: directory doesn't exist
+
     >>> testfile = '/tmp/directory_filter_text_f39e5b58-c260-40da-9448-ad1c3b2a69c2.txt'
     >>> contents = b'This is a test'
     >>> with open(testfile, 'wb') as wf:
index d05cae6b592110847fc73ee9d7e2499ff80c9481..16b71ab9b5cea1914eb91180f93e04b5ecc14156 100644 (file)
@@ -62,6 +62,9 @@ def slurp_file(
 
     Returns:
         A list of lines from the read and transformed file contents.
+
+    Raises:
+        Exception: filename not found or can't be read.
     """
 
     ret = []
@@ -310,6 +313,9 @@ def create_path_if_not_exist(
         on_error: If set, it's invoked on error conditions and passed then
             path and OSError that it caused.
 
+    Raises:
+        OSError: an exception occurred and on_error not set.
+
     See also :meth:`does_file_exist`.
 
     .. warning::
index 96bc40a0c7badff2854fccf9c131c315a684228b..1d78dd44d715df87cfe4329477dd54ea17318771 100644 (file)
@@ -104,6 +104,9 @@ class LockFile(contextlib.AbstractContextManager):
                 Note that this is required for zookeeper based locks.
             override_command: don't use argv to determine our commandline
                 rather use this instead if provided.
+
+        Raises:
+            Exception: Zookeeper lock path without an expiration timestamp
         """
         self.is_locked: bool = False
         self.lockfile: str = ""
index 8832c295e174f4996326a68706eed4d4d5477d6f..f5461d6d338abd2daccff0f35673488e08dcd00e 100644 (file)
@@ -949,6 +949,10 @@ def initialize_logging(logger=None) -> logging.Logger:
     :meth:`bootstrap.initialize` decorator on your program's entry point,
     it will call this for you.  See :meth:`pyutils.bootstrap.initialize`
     for more details.
+
+    Raises:
+        ValueError: if logging level is invalid
+
     """
 
     global LOGGING_INITIALIZED
@@ -1077,6 +1081,11 @@ class OutputMultiplexer(object):
             handles: if FILEHANDLES bit is set, this should be a list of
                 already opened filehandles you'd like to output into.  The
                 handles will remain open after the scope of the multiplexer.
+
+        Raises:
+            ValueError: invalid combination of arguments (e.g. the filenames
+                argument is present but the filenames bit isn't set, the handle
+                argument is present but the handles bit isn't set, etc...)
         """
         if logger is None:
             logger = logging.getLogger(None)
@@ -1105,7 +1114,16 @@ class OutputMultiplexer(object):
         return self.destination_bitv
 
     def set_destination_bitv(self, destination_bitv: int):
-        """Change the output destination_bitv to the one provided."""
+        """Change the output destination_bitv to the one provided.
+
+        Args:
+            destination_bitv: the new destination bitvector to set.
+
+        Raises:
+            ValueError: invalid combination of arguments (e.g. the filenames
+                argument is present but the filenames bit isn't set, the handle
+                argument is present but the handles bit isn't set, etc...)
+        """
         if destination_bitv & self.Destination.FILENAMES and self.f is None:
             raise ValueError("Filename argument is required if bitv & FILENAMES")
         if destination_bitv & self.Destination.FILEHANDLES and self.h is None:
@@ -1113,7 +1131,12 @@ class OutputMultiplexer(object):
         self.destination_bitv = destination_bitv
 
     def print(self, *args, **kwargs):
-        """Produce some output to all sinks."""
+        """Produce some output to all sinks.  Use the same arguments as the
+        print-builtin.
+
+        Raises:
+            TypeError: Illegal argument types encountered
+        """
         from pyutils.string_utils import _sprintf, strip_escape_sequences
 
         end = kwargs.pop("end", None)
index fe1e9067011f80a2926eef15d38c8b74aa5dadbe..61c8c4be81d46101e4e187d9fec3202ca3e638b2 100644 (file)
@@ -200,6 +200,9 @@ def gcd_float_sequence(lst: List[float]) -> float:
 
     Args:
         lst: a list of operands
+
+    Raises:
+        ValueError: if the list doesn't contain at least one number.
     """
     if len(lst) <= 0:
         raise ValueError("Need at least one number")
@@ -282,6 +285,9 @@ def is_prime(n: int) -> bool:
     Returns:
         True if n is prime and False otherwise.
 
+    Raises:
+        TypeError: if argument is not an into
+
     .. note::
 
          Obviously(?) very slow for very large input numbers until
index 15191193752e3250f9d8aab3acf5b61cdffb0de0..7ab4eb6e78653d06d4ac6f492009a88ab6154fbc 100644 (file)
@@ -244,6 +244,10 @@ class ThreadExecutor(BaseExecutor):
 
     @overrides
     def submit(self, function: Callable, *args, **kwargs) -> fut.Future:
+        """
+        Raises:
+            Exception: executor is shutting down already.
+        """
         if self.already_shutdown:
             raise Exception('Submitted work after shutdown.')
         self.adjust_task_count(+1)
@@ -305,6 +309,10 @@ class ProcessExecutor(BaseExecutor):
 
     @overrides
     def submit(self, function: Callable, *args, **kwargs) -> fut.Future:
+        """
+        Raises:
+            Exception: executor is shutting down already.
+        """
         if self.already_shutdown:
             raise Exception('Submitted work after shutdown.')
         start = time.time()
@@ -849,6 +857,9 @@ class RemoteExecutor(BaseExecutor):
         Args:
             workers: A list of remote workers we can call on to do tasks.
             policy: A policy for selecting remote workers for tasks.
+
+        Raises:
+            RemoteExecutorException: unable to find a place to schedule work.
         """
 
         super().__init__()
@@ -1463,7 +1474,11 @@ class RemoteExecutor(BaseExecutor):
         self, bundle: BundleDetails
     ) -> Optional[fut.Future]:
         """Something unexpectedly failed with bundle.  Either retry it
-        from the beginning or throw in the towel and give up on it."""
+        from the beginning or throw in the towel and give up on it.
+
+        Raises:
+            RemoteExecutorException: a bundle fails repeatedly.
+        """
 
         is_original = bundle.src_bundle is None
         bundle.worker = None
@@ -1500,7 +1515,11 @@ class RemoteExecutor(BaseExecutor):
     @overrides
     def submit(self, function: Callable, *args, **kwargs) -> fut.Future:
         """Submit work to be done.  This is the user entry point of this
-        class."""
+        class.
+
+        Raises:
+            Exception: executor is already shutting down.
+        """
         if self.already_shutdown:
             raise Exception('Submitted work after shutdown.')
         pickle = _make_cloud_pickle(function, *args, **kwargs)
index 4e891f1d8b813a7d3624b17907790c038f26527f..11c1c318d7ec8f8af8162bf6efe202d23bc3f0fe 100644 (file)
@@ -198,10 +198,11 @@ class ThreadWithReturnValue(threading.Thread, Runnable):
 
         A thread can be joined many times.
 
-        :meth:`join` raises a RuntimeError if an attempt is made to join the
-        current thread as that would cause a deadlock. It is also an
-        error to join a thread before it has been started and
-        attempts to do so raises the same exception.
+        Raises:
+            RuntimeError: an attempt is made to join the current thread
+                as that would cause a deadlock. It is also an error to join
+                a thread before it has been started and attempts to do so
+                raises the same exception.
         """
         threading.Thread.join(self, *args)
         return self._return
index 88191340d14726577ce6fe83e8a6e351eb507a17..b3d642a9ea8b06557ab758dec736f5851816bb1b 100644 (file)
@@ -320,6 +320,11 @@ class Corpus(object):
                 yield token
 
         def evaluate(corpus: Corpus, stack: List[str]):
+            """
+            Raises:
+                ParseError: bad number of operations, unbalanced parenthesis,
+                    unknown operators, internal errors.
+            """
             node_stack: List[Node] = []
             for token in stack:
                 node = None
@@ -403,7 +408,12 @@ class Node(object):
         self.operands = operands
 
     def eval(self) -> Set[str]:
-        """Evaluate this node."""
+        """Evaluate this node.
+
+        Raises:
+            ParseError: unexpected operands, invalid key:value syntax, wrong
+                number of operands for operation, other invalid queries.
+        """
 
         evaled_operands: List[Union[Set[str], str]] = []
         for operand in self.operands:
index 8243c8c1ffba8b512955cf8b401b0ed975b279f3..5d6abf1bd6d79d61b92da648b1b5d2df90195892 100644 (file)
@@ -116,6 +116,9 @@ class SimpleACL(ABC):
             default_answer: pass this argument to provide the ACL with a
                 default answer.
 
+        Raises:
+            ValueError: Invalid Order argument
+
         .. note::
 
             By using `order_to_check_allow_deny` and `default_answer` you
@@ -128,7 +131,7 @@ class SimpleACL(ABC):
             Order.ALLOW_DENY,
             Order.DENY_ALLOW,
         ):
-            raise Exception(
+            raise ValueError(
                 'order_to_check_allow_deny must be Order.ALLOW_DENY or '
                 + 'Order.DENY_ALLOW'
             )
index 22964c69041a5607075ce0907f59c55053b26d12..7e2b999b9b8f4d84d57e07f4e16a404fce8ecda4 100644 (file)
@@ -356,6 +356,9 @@ def is_number(in_str: str) -> bool:
         True if the string contains a valid numberic value and
         False otherwise.
 
+    Raises:
+        TypeError: the input argument isn't a string
+
     See also :meth:`is_integer_number`, :meth:`is_decimal_number`,
     :meth:`is_hexidecimal_integer_number`, :meth:`is_octal_integer_number`,
     etc...
@@ -363,7 +366,7 @@ def is_number(in_str: str) -> bool:
     >>> is_number(100.5)
     Traceback (most recent call last):
     ...
-    ValueError: 100.5
+    TypeError: 100.5
     >>> is_number("100.5")
     True
     >>> is_number("test")
@@ -373,10 +376,10 @@ def is_number(in_str: str) -> bool:
     >>> is_number([1, 2, 3])
     Traceback (most recent call last):
     ...
-    ValueError: [1, 2, 3]
+    TypeError: [1, 2, 3]
     """
     if not is_string(in_str):
-        raise ValueError(in_str)
+        raise TypeError(in_str)
     return NUMBER_RE.match(in_str) is not None
 
 
@@ -415,6 +418,9 @@ def is_hexidecimal_integer_number(in_str: str) -> bool:
     Returns:
         True if the string is a hex integer number and False otherwise.
 
+    Raises:
+        TypeError: the input argument isn't a string
+
     See also :meth:`is_integer_number`, :meth:`is_decimal_number`,
     :meth:`is_octal_integer_number`, :meth:`is_binary_integer_number`, etc...
 
@@ -431,18 +437,18 @@ def is_hexidecimal_integer_number(in_str: str) -> bool:
     >>> is_hexidecimal_integer_number(12345)  # Not a string
     Traceback (most recent call last):
     ...
-    ValueError: 12345
+    TypeError: 12345
     >>> is_hexidecimal_integer_number(101.4)
     Traceback (most recent call last):
     ...
-    ValueError: 101.4
+    TypeError: 101.4
     >>> is_hexidecimal_integer_number(0x1A3E)
     Traceback (most recent call last):
     ...
-    ValueError: 6718
+    TypeError: 6718
     """
     if not is_string(in_str):
-        raise ValueError(in_str)
+        raise TypeError(in_str)
     return HEX_NUMBER_RE.match(in_str) is not None
 
 
@@ -454,6 +460,9 @@ def is_octal_integer_number(in_str: str) -> bool:
     Returns:
         True if the string is a valid octal integral number and False otherwise.
 
+    Raises:
+        TypeError: the input argument isn't a string
+
     See also :meth:`is_integer_number`, :meth:`is_decimal_number`,
     :meth:`is_hexidecimal_integer_number`, :meth:`is_binary_integer_number`,
     etc...
@@ -470,7 +479,7 @@ def is_octal_integer_number(in_str: str) -> bool:
     False
     """
     if not is_string(in_str):
-        raise ValueError(in_str)
+        raise TypeError(in_str)
     return OCT_NUMBER_RE.match(in_str) is not None
 
 
@@ -482,6 +491,9 @@ def is_binary_integer_number(in_str: str) -> bool:
     Returns:
         True if the string contains a binary integral number and False otherwise.
 
+    Raises:
+        TypeError: the input argument isn't a string
+
     See also :meth:`is_integer_number`, :meth:`is_decimal_number`,
     :meth:`is_hexidecimal_integer_number`, :meth:`is_octal_integer_number`,
     etc...
@@ -500,7 +512,7 @@ def is_binary_integer_number(in_str: str) -> bool:
     False
     """
     if not is_string(in_str):
-        raise ValueError(in_str)
+        raise TypeError(in_str)
     return BIN_NUMBER_RE.match(in_str) is not None
 
 
@@ -512,6 +524,9 @@ def to_int(in_str: str) -> int:
     Returns:
         The integral value of the string or raises on error.
 
+    Raises:
+        TypeError: the input argument isn't a string
+
     See also :meth:`is_integer_number`, :meth:`is_decimal_number`,
     :meth:`is_hexidecimal_integer_number`, :meth:`is_octal_integer_number`,
     :meth:`is_binary_integer_number`, etc...
@@ -528,9 +543,13 @@ def to_int(in_str: str) -> int:
     Traceback (most recent call last):
     ...
     ValueError: invalid literal for int() with base 10: 'test'
+    >>> to_int(123)
+    Traceback (most recent call last):
+    ...
+    TypeError: 123
     """
     if not is_string(in_str):
-        raise ValueError(in_str)
+        raise TypeError(in_str)
     if is_binary_integer_number(in_str):
         return int(in_str, 2)
     if is_octal_integer_number(in_str):
@@ -550,6 +569,9 @@ def number_string_to_integer(in_str: str) -> int:
     Returns:
         The integer whose value was parsed from in_str.
 
+    Raises:
+        ValueError: unable to parse a chunk of the number string
+
     See also :meth:`integer_to_number_string`.
 
     .. warning::
@@ -703,6 +725,9 @@ def add_thousands_separator(
     Returns:
         A numeric string with thousands separators added appropriately.
 
+    Raises:
+        ValueError: a non-numeric string argument is presented
+
     >>> add_thousands_separator('12345678')
     '12,345,678'
     >>> add_thousands_separator(12345678)
@@ -803,7 +828,7 @@ def is_email(in_str: Any) -> bool:
             head = head.replace(" ", "")[1:-1]
         return EMAIL_RE.match(head + "@" + tail) is not None
 
-    except ValueError:
+    except (TypeError, ValueError):
         # borderline case in which we have multiple "@" signs but the
         # head part is correctly escaped.
         if ESCAPED_AT_SIGN.search(in_str) is not None:
@@ -910,6 +935,9 @@ def is_credit_card(in_str: Any, card_type: str = None) -> bool:
     Returns:
         True if in_str is a valid credit card number.
 
+    Raises:
+        KeyError: card_type is invalid
+
     .. warning::
         This code is not verifying the authenticity of the credit card (i.e.
         not checking whether it's a real card that can be charged); rather
@@ -1259,6 +1287,9 @@ def contains_html(in_str: str) -> bool:
         True if the given string contains HTML/XML tags and False
         otherwise.
 
+    Raises:
+        TypeError: the input argument isn't a string
+
     See also :meth:`strip_html`.
 
     .. warning::
@@ -1274,7 +1305,7 @@ def contains_html(in_str: str) -> bool:
 
     """
     if not is_string(in_str):
-        raise ValueError(in_str)
+        raise TypeError(in_str)
     return HTML_RE.search(in_str) is not None
 
 
@@ -1286,6 +1317,9 @@ def words_count(in_str: str) -> int:
     Returns:
         The number of words contained in the given string.
 
+    Raises:
+        TypeError: the input argument isn't a string
+
     .. note::
         This method is "smart" in that it does consider only sequences
         of one or more letter and/or numbers to be "words".  Thus a
@@ -1300,7 +1334,7 @@ def words_count(in_str: str) -> int:
     4
     """
     if not is_string(in_str):
-        raise ValueError(in_str)
+        raise TypeError(in_str)
     return len(WORDS_COUNT_RE.findall(in_str))
 
 
@@ -1357,6 +1391,9 @@ def generate_random_alphanumeric_string(size: int) -> str:
         A string of the specified size containing random characters
         (uppercase/lowercase ascii letters and digits).
 
+    Raises:
+        ValueError: size < 1
+
     See also :meth:`asciify`, :meth:`generate_uuid`.
 
     >>> random.seed(22)
@@ -1378,11 +1415,14 @@ def reverse(in_str: str) -> str:
     Returns:
         The reversed (chracter by character) string.
 
+    Raises:
+        TypeError: the input argument isn't a string
+
     >>> reverse('test')
     'tset'
     """
     if not is_string(in_str):
-        raise ValueError(in_str)
+        raise TypeError(in_str)
     return in_str[::-1]
 
 
@@ -1397,6 +1437,9 @@ def camel_case_to_snake_case(in_str: str, *, separator: str = "_"):
         original string if it is not a valid camel case string or some
         other error occurs.
 
+    Raises:
+        TypeError: the input argument isn't a string
+
     See also :meth:`is_camel_case`, :meth:`is_snake_case`, and :meth:`is_slug`.
 
     >>> camel_case_to_snake_case('MacAddressExtractorFactory')
@@ -1405,7 +1448,7 @@ def camel_case_to_snake_case(in_str: str, *, separator: str = "_"):
     'Luke Skywalker'
     """
     if not is_string(in_str):
-        raise ValueError(in_str)
+        raise TypeError(in_str)
     if not is_camel_case(in_str):
         return in_str
     return CAMEL_CASE_REPLACE_RE.sub(lambda m: m.group(1) + separator, in_str).lower()
@@ -1425,6 +1468,9 @@ def snake_case_to_camel_case(
         provided or the original string back again if it is not valid
         snake case or another error occurs.
 
+    Raises:
+        TypeError: the input argument isn't a string
+
     See also :meth:`is_camel_case`, :meth:`is_snake_case`, and :meth:`is_slug`.
 
     >>> snake_case_to_camel_case('this_is_a_test')
@@ -1433,7 +1479,7 @@ def snake_case_to_camel_case(
     'Han Solo'
     """
     if not is_string(in_str):
-        raise ValueError(in_str)
+        raise TypeError(in_str)
     if not is_snake_case(in_str, separator=separator):
         return in_str
     tokens = [s.title() for s in in_str.split(separator) if is_full_string(s)]
@@ -1529,6 +1575,9 @@ def strip_html(in_str: str, keep_tag_content: bool = False) -> str:
         A string with all HTML tags removed (optionally with tag contents
         preserved).
 
+    Raises:
+        TypeError: the input argument isn't a string
+
     See also :meth:`contains_html`.
 
     .. note::
@@ -1543,7 +1592,7 @@ def strip_html(in_str: str, keep_tag_content: bool = False) -> str:
     'test: click here'
     """
     if not is_string(in_str):
-        raise ValueError(in_str)
+        raise TypeError(in_str)
     r = HTML_TAG_ONLY_RE if keep_tag_content else HTML_RE
     return r.sub("", in_str)
 
@@ -1559,6 +1608,9 @@ def asciify(in_str: str) -> str:
         by translating all non-ascii chars into their closest possible
         ASCII representation (eg: Ã³ -> o, Ã‹ -> E, Ã§ -> c...).
 
+    Raises:
+        TypeError: the input argument isn't a string
+
     See also :meth:`to_ascii`, :meth:`generate_random_alphanumeric_string`.
 
     .. warning::
@@ -1568,7 +1620,7 @@ def asciify(in_str: str) -> str:
     'eeuuooaaeynAAACIINOE'
     """
     if not is_string(in_str):
-        raise ValueError(in_str)
+        raise TypeError(in_str)
 
     # "NFKD" is the algorithm which is able to successfully translate
     # the most of non-ascii chars.
@@ -1599,6 +1651,9 @@ def slugify(in_str: str, *, separator: str = "-") -> str:
         * all chars are encoded as ascii (by using :meth:`asciify`)
         * is safe for URL
 
+    Raises:
+        TypeError: the input argument isn't a string
+
     See also :meth:`is_slug` and :meth:`asciify`.
 
     >>> slugify('Top 10 Reasons To Love Dogs!!!')
@@ -1607,7 +1662,7 @@ def slugify(in_str: str, *, separator: str = "-") -> str:
     'monster-magnet'
     """
     if not is_string(in_str):
-        raise ValueError(in_str)
+        raise TypeError(in_str)
 
     # replace any character that is NOT letter or number with spaces
     out = NO_LETTERS_OR_NUMBERS_RE.sub(" ", in_str.lower()).strip()
@@ -1639,6 +1694,9 @@ def to_bool(in_str: str) -> bool:
 
         Otherwise False is returned.
 
+    Raises:
+        TypeError: the input argument isn't a string
+
     See also :mod:`pyutils.argparse_utils`.
 
     >>> to_bool('True')
@@ -1660,7 +1718,7 @@ def to_bool(in_str: str) -> bool:
     True
     """
     if not is_string(in_str):
-        raise ValueError(in_str)
+        raise TypeError(in_str)
     return in_str.lower() in set(["true", "1", "yes", "y", "t", "on"])
 
 
@@ -1871,13 +1929,16 @@ def indent(in_str: str, amount: int) -> str:
     Returns:
         An indented string created by prepending amount spaces.
 
+    Raises:
+        TypeError: the input argument isn't a string
+
     See also :meth:`dedent`.
 
     >>> indent('This is a test', 4)
     '    This is a test'
     """
     if not is_string(in_str):
-        raise ValueError(in_str)
+        raise TypeError(in_str)
     line_separator = '\n'
     lines = [" " * amount + line for line in in_str.split(line_separator)]
     return line_separator.join(lines)
@@ -2434,6 +2495,9 @@ def to_ascii(txt: str):
     Returns:
         txt encoded as an ASCII byte string.
 
+    Raises:
+        TypeError: the input argument isn't a string or bytes
+
     See also :meth:`to_base64`, :meth:`to_bitstring`, :meth:`to_bytes`,
     :meth:`generate_random_alphanumeric_string`, :meth:`asciify`.
 
@@ -2447,7 +2511,7 @@ def to_ascii(txt: str):
         return txt.encode('ascii')
     if isinstance(txt, bytes):
         return txt
-    raise Exception('to_ascii works with strings and bytes')
+    raise TypeError('to_ascii works with strings and bytes')
 
 
 def to_base64(
index 6cf6411d3771635c1349ef1ff91325fae9de69e4..f696c59bd0d651876aeaf74475c79bbe35326536 100644 (file)
@@ -49,6 +49,9 @@ def get_console_rows_columns() -> RowsColumns:
     Returns:
         The number of rows/columns on the current console or None
         if we can't tell or an error occurred.
+
+    Raises:
+        Exception: if the console size can't be determined.
     """
     from pyutils.exec_utils import cmd
 
@@ -172,6 +175,9 @@ def bar_graph_string(
         left_end: the character at the left side of the graph
         right_end: the character at the right side of the graph
 
+    Raises:
+        ValueError: if percentage is invalid
+
     See also :meth:`bar_graph`, :meth:`sparkline`.
 
     >>> bar_graph_string(5, 10, fgcolor='', reset_seq='')
@@ -325,6 +331,9 @@ def justify_string(
             * 'r' = right alignment
         padding: the padding character to use while justifying
 
+    Raises:
+        ValueError: if alignment argument is invalid.
+
     >>> justify_string('This is another test', width=40, alignment='c')
     '          This is another test          '
     >>> justify_string('This is another test', width=40, alignment='l')
@@ -349,7 +358,7 @@ def justify_string(
             else:
                 string = padding + string
         else:
-            raise ValueError
+            raise ValueError('alignment must be l, r, j, or c.')
     return string
 
 
index a17060d56d414e3a61416cef0ac629823986ad2a..81348baccb58a3c2200d247c084766e12cf00c9b 100644 (file)
@@ -70,16 +70,19 @@ class CentCount(object):
             currency: optionally declare the currency being represented by
                 this instance.  If provided it will guard against operations
                 such as attempting to add it to non-matching currencies.
-            strict_mode: if True, the instance created will object if you
-                compare or aggregate it with non-CentCount objects; that is,
+            strict_mode: if True, the instance created will object (raise) if
+                compared or aggregated with non-CentCount objects; that is,
                 strict_mode disallows comparison with literal numbers or
                 aggregation with literal numbers.
+
+        Raises:
+            ValueError: invalid money string passed in
         """
         self.strict_mode = strict_mode
         if isinstance(centcount, str):
             ret = CentCount._parse(centcount)
             if ret is None:
-                raise Exception(f'Unable to parse money string "{centcount}"')
+                raise ValueError(f'Unable to parse money string "{centcount}"')
             centcount = ret[0]
             currency = ret[1]
         if isinstance(centcount, float):
@@ -108,6 +111,11 @@ class CentCount(object):
         return CentCount(centcount=-self.centcount, currency=self.currency)
 
     def __add__(self, other):
+        """
+        Raises:
+            TypeError: if addend is not compatible or the object is in strict
+                mode and the addend is not another CentCount.
+        """
         if isinstance(other, CentCount):
             if self.currency == other.currency:
                 return CentCount(
@@ -123,6 +131,11 @@ class CentCount(object):
                 return self.__add__(CentCount(other, self.currency))
 
     def __sub__(self, other):
+        """
+        Raises:
+            TypeError: if amount is not compatible or the object is in strict
+                mode and the amount is not another CentCount.
+        """
         if isinstance(other, CentCount):
             if self.currency == other.currency:
                 return CentCount(
@@ -139,6 +152,9 @@ class CentCount(object):
 
     def __mul__(self, other):
         """
+        Raises:
+            TypeError: if factor is not compatible.
+
         .. note::
 
             Multiplication and division are performed by converting the
@@ -172,6 +188,9 @@ class CentCount(object):
 
     def __truediv__(self, other):
         """
+        Raises:
+            TypeError: the divisor is not compatible
+
         .. note::
 
             Multiplication and division are performed by converting the
@@ -212,6 +231,11 @@ class CentCount(object):
     __radd__ = __add__
 
     def __rsub__(self, other):
+        """
+        Raises:
+            TypeError: amount is not compatible or, if the object is in
+                strict mode, the amount is not a CentCount.
+        """
         if isinstance(other, CentCount):
             if self.currency == other.currency:
                 return CentCount(
@@ -235,6 +259,10 @@ class CentCount(object):
     # Override comparison operators to also compare currency.
     #
     def __eq__(self, other):
+        """
+        Raises:
+            TypeError: In strict mode and the other object isn't a CentCount.
+        """
         if other is None:
             return False
         if isinstance(other, CentCount):
@@ -251,6 +279,11 @@ class CentCount(object):
         return not result
 
     def __lt__(self, other):
+        """
+        Raises:
+            TypeError: amounts have different currencies or, if this object
+                is in strict mode, the amount must be a CentCount.
+        """
         if isinstance(other, CentCount):
             if self.currency == other.currency:
                 return self.centcount < other.centcount
@@ -263,6 +296,11 @@ class CentCount(object):
                 return self.centcount < int(other)
 
     def __gt__(self, other):
+        """
+        Raises:
+            TypeError: amounts have different currencies or, if this object
+                is in strict mode, the amount must be a CentCount.
+        """
         if isinstance(other, CentCount):
             if self.currency == other.currency:
                 return self.centcount > other.centcount
@@ -313,11 +351,14 @@ class CentCount(object):
 
         Args:
             s: the string to be parsed
+
+        Raises:
+            ValueError: input string cannot be parsed.
         """
         chunks = CentCount._parse(s)
         if chunks is not None:
             return CentCount(chunks[0], chunks[1])
-        raise Exception(f'Unable to parse money string "{s}"')
+        raise ValueError(f'Unable to parse money string "{s}"')
 
 
 if __name__ == "__main__":
index bcb1c5f908e193e88b1972d2a3af1544dafa159d..8de88bd37987462aceb565b74af70342df71d202 100644 (file)
@@ -79,13 +79,16 @@ class SimpleHistogram(Generic[T]):
                 buckets we are counting population in.  See also
                 :meth:`n_evenly_spaced_buckets` to generate these
                 buckets more easily.
+
+        Raises:
+            ValueError: buckets overlap
         """
         from pyutils.math_utils import NumericPopulation
 
         self.buckets: Dict[Tuple[Bound, Bound], Count] = {}
         for start_end in buckets:
             if self._get_bucket(start_end[0]) is not None:
-                raise Exception("Buckets overlap?!")
+                raise ValueError("Buckets overlap?!")
             self.buckets[start_end] = 0
         self.sigma: float = 0.0
         self.stats: NumericPopulation = NumericPopulation()
@@ -109,11 +112,14 @@ class SimpleHistogram(Generic[T]):
 
         Returns:
             A list of bounds that define N evenly spaced buckets
+
+        Raises:
+            ValueError: min is not < max
         """
         ret: List[Tuple[int, int]] = []
         stride = int((max_bound - min_bound) / n)
         if stride <= 0:
-            raise Exception("Min must be < Max")
+            raise ValueError("Min must be < Max")
         imax = math.ceil(max_bound)
         imin = math.floor(min_bound)
         for bucket_start in range(imin, imax, stride):
index 5eafd636bed88b6fba4ce6170b255d8a749a6f58..2cb9d3c66d070e348777717e4f7d52974e32cbc8 100644 (file)
@@ -43,12 +43,15 @@ class Money(object):
             strict_mode: if True, disallows comparison or arithmetic operations
                 between Money instances and any non-Money types (e.g. literal
                 numbers).
+
+        Raises:
+            ValueError: unable to parse a money string
         """
         self.strict_mode = strict_mode
         if isinstance(amount, str):
             ret = Money._parse(amount)
             if ret is None:
-                raise Exception(f'Unable to parse money string "{amount}"')
+                raise ValueError(f'Unable to parse money string "{amount}"')
             amount = ret[0]
             currency = ret[1]
         if not isinstance(amount, Decimal):
@@ -90,6 +93,11 @@ class Money(object):
         return Money(amount=-self.amount, currency=self.currency)
 
     def __add__(self, other):
+        """
+        Raises:
+            TypeError: attempt to add incompatible amounts or, if in strict
+                mode, attempt to add a Money with a literal.
+        """
         if isinstance(other, Money):
             if self.currency == other.currency:
                 return Money(amount=self.amount + other.amount, currency=self.currency)
@@ -105,14 +113,19 @@ class Money(object):
                 )
 
     def __sub__(self, other):
+        """
+        Raises:
+            TypeError: attempt to subtract incompatible amounts or, if in strict
+                mode, attempt to add a Money with a literal.
+        """
         if isinstance(other, Money):
             if self.currency == other.currency:
                 return Money(amount=self.amount - other.amount, currency=self.currency)
             else:
-                raise TypeError("Incompatible currencies in add expression")
+                raise TypeError("Incompatible currencies in sibtraction expression")
         else:
             if self.strict_mode:
-                raise TypeError("In strict_mode only two moneys can be added")
+                raise TypeError("In strict_mode only two moneys can be subtracted")
             else:
                 return Money(
                     amount=self.amount - Decimal(float(other)),
@@ -120,6 +133,10 @@ class Money(object):
                 )
 
     def __mul__(self, other):
+        """
+        Raises:
+            TypeError: attempt to multiply two Money objects.
+        """
         if isinstance(other, Money):
             raise TypeError("can not multiply monetary quantities")
         else:
@@ -129,6 +146,10 @@ class Money(object):
             )
 
     def __truediv__(self, other):
+        """
+        Raises:
+            TypeError: attempt to divide two Money objects.
+        """
         if isinstance(other, Money):
             raise TypeError("can not divide monetary quantities")
         else:
@@ -211,6 +232,11 @@ class Money(object):
     __radd__ = __add__
 
     def __rsub__(self, other):
+        """
+        Raises:
+            TypeError: attempt to subtract incompatible amounts or, if in strict
+                mode, attempt to add a Money with a literal.
+        """
         if isinstance(other, Money):
             if self.currency == other.currency:
                 return Money(amount=other.amount - self.amount, currency=self.currency)
@@ -231,6 +257,11 @@ class Money(object):
     # Override comparison operators to also compare currency.
     #
     def __eq__(self, other):
+        """
+        Raises:
+            TypeError: in strict mode, an attempt to compare a Money with a
+                non-Money object.
+        """
         if other is None:
             return False
         if isinstance(other, Money):
@@ -247,6 +278,10 @@ class Money(object):
         return not result
 
     def __lt__(self, other):
+        """
+        TypeError: attempt to compare incompatible amounts or, if in strict
+            mode, attempt to compare a Money with a literal.
+        """
         if isinstance(other, Money):
             if self.currency == other.currency:
                 return self.amount < other.amount
@@ -259,6 +294,10 @@ class Money(object):
                 return self.amount < Decimal(float(other))
 
     def __gt__(self, other):
+        """
+        TypeError: attempt to compare incompatible amounts or, if in strict
+            mode, attempt to compare a Money with a literal.
+        """
         if isinstance(other, Money):
             if self.currency == other.currency:
                 return self.amount > other.amount
@@ -309,11 +348,14 @@ class Money(object):
 
         Args:
             s: the string to parse
+
+        Raises:
+            ValueError: unable to parse a string
         """
         chunks = Money._parse(s)
         if chunks is not None:
             return Money(chunks[0], chunks[1])
-        raise Exception(f'Unable to parse money string "{s}"')
+        raise ValueError(f'Unable to parse money string "{s}"')
 
 
 if __name__ == "__main__":
index 47762601ac6fde047c75095628c9c6878414e34a..f577bd8cc14a8d00afe7dc917f9eda913635e0d4 100644 (file)
@@ -184,6 +184,10 @@ class PicklingFileBasedPersistent(FileBasedPersistent):
     def load(
         cls: type[PicklingFileBasedPersistent],
     ) -> Optional[PicklingFileBasedPersistent]:
+        """
+        Raises:
+            Exception: failure to load from file.
+        """
         filename = cls.get_filename()
         if cls.should_we_load_data(filename):
             logger.debug("Attempting to load state from %s", filename)
@@ -202,6 +206,10 @@ class PicklingFileBasedPersistent(FileBasedPersistent):
 
     @overrides
     def save(self) -> bool:
+        """
+        Raises:
+            Exception: failure to save to file.
+        """
         filename = self.get_filename()
         if self.should_we_save_data(filename):
             logger.debug("Trying to save state in %s", filename)
@@ -266,6 +274,10 @@ class JsonFileBasedPersistent(FileBasedPersistent):
     @classmethod
     @overrides
     def load(cls: type[JsonFileBasedPersistent]) -> Optional[JsonFileBasedPersistent]:
+        """
+        Raises:
+            Exception: failure to load from file.
+        """
         filename = cls.get_filename()
         if cls.should_we_load_data(filename):
             logger.debug("Trying to load state from %s", filename)
@@ -295,6 +307,10 @@ class JsonFileBasedPersistent(FileBasedPersistent):
 
     @overrides
     def save(self) -> bool:
+        """
+        Raises:
+            Exception: failure to save to file.
+        """
         filename = self.get_filename()
         if self.should_we_save_data(filename):
             logger.debug("Trying to save state in %s", filename)
index f9842bdfae3548434d6af392041fae0e74b06dc5..e68fd210c08f2e93ac717263a286b4954afb8293 100644 (file)
@@ -35,6 +35,10 @@ class Rate(object):
             percentage: provides the multiplier as a percentage
             percent_change: provides the multiplier as a percent change to
                 the base amount
+
+        Raises:
+            ValueError: if more than one of percentage, percent_change and
+                multiplier is provided
         """
         count = 0
         if multiplier is not None:
@@ -53,7 +57,7 @@ class Rate(object):
             self.multiplier = 1.0 + percent_change / 100
             count += 1
         if count != 1:
-            raise Exception(
+            raise ValueError(
                 "Exactly one of percentage, percent_change or multiplier is required."
             )
 
index e648fa00d8fea5878c9c512a9c72b71eb8b7d337..d4fcb7bf8deb25c4cecae12c59b06a6ef40d4df2 100644 (file)
@@ -23,6 +23,9 @@ def unwrap_optional(x: Optional[Any]) -> Any:
         If the Optional[Type] argument is None, however, raise an
         exception.
 
+    Raises:
+        AssertionError: the parameter is, indeed, of NoneType.
+
     >>> x: Optional[bool] = True
     >>> unwrap_optional(x)
     True