Mercurial > repos > shellac > guppy_basecaller
diff env/lib/python3.7/site-packages/humanfriendly/__init__.py @ 2:6af9afd405e9 draft
"planemo upload commit 0a63dd5f4d38a1f6944587f52a8cd79874177fc1"
author | shellac |
---|---|
date | Thu, 14 May 2020 14:56:58 -0400 |
parents | 26e78fe6e8c4 |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/lib/python3.7/site-packages/humanfriendly/__init__.py Thu May 14 14:56:58 2020 -0400 @@ -0,0 +1,835 @@ +# Human friendly input/output in Python. +# +# Author: Peter Odding <peter@peterodding.com> +# Last Change: April 19, 2020 +# URL: https://humanfriendly.readthedocs.io + +"""The main module of the `humanfriendly` package.""" + +# Standard library modules. +import collections +import datetime +import decimal +import numbers +import os +import os.path +import re +import time + +# Modules included in our package. +from humanfriendly.compat import is_string, monotonic +from humanfriendly.deprecation import define_aliases +from humanfriendly.text import concatenate, format, pluralize, tokenize + +# Public identifiers that require documentation. +__all__ = ( + 'CombinedUnit', + 'InvalidDate', + 'InvalidLength', + 'InvalidSize', + 'InvalidTimespan', + 'SizeUnit', + 'Timer', + '__version__', + 'coerce_boolean', + 'coerce_pattern', + 'coerce_seconds', + 'disk_size_units', + 'format_length', + 'format_number', + 'format_path', + 'format_size', + 'format_timespan', + 'length_size_units', + 'parse_date', + 'parse_length', + 'parse_path', + 'parse_size', + 'parse_timespan', + 'round_number', + 'time_units', +) + +# Semi-standard module versioning. +__version__ = '8.2' + +# Named tuples to define units of size. +SizeUnit = collections.namedtuple('SizeUnit', 'divider, symbol, name') +CombinedUnit = collections.namedtuple('CombinedUnit', 'decimal, binary') + +# Common disk size units in binary (base-2) and decimal (base-10) multiples. +disk_size_units = ( + CombinedUnit(SizeUnit(1000**1, 'KB', 'kilobyte'), SizeUnit(1024**1, 'KiB', 'kibibyte')), + CombinedUnit(SizeUnit(1000**2, 'MB', 'megabyte'), SizeUnit(1024**2, 'MiB', 'mebibyte')), + CombinedUnit(SizeUnit(1000**3, 'GB', 'gigabyte'), SizeUnit(1024**3, 'GiB', 'gibibyte')), + CombinedUnit(SizeUnit(1000**4, 'TB', 'terabyte'), SizeUnit(1024**4, 'TiB', 'tebibyte')), + CombinedUnit(SizeUnit(1000**5, 'PB', 'petabyte'), SizeUnit(1024**5, 'PiB', 'pebibyte')), + CombinedUnit(SizeUnit(1000**6, 'EB', 'exabyte'), SizeUnit(1024**6, 'EiB', 'exbibyte')), + CombinedUnit(SizeUnit(1000**7, 'ZB', 'zettabyte'), SizeUnit(1024**7, 'ZiB', 'zebibyte')), + CombinedUnit(SizeUnit(1000**8, 'YB', 'yottabyte'), SizeUnit(1024**8, 'YiB', 'yobibyte')), +) + +# Common length size units, used for formatting and parsing. +length_size_units = (dict(prefix='nm', divider=1e-09, singular='nm', plural='nm'), + dict(prefix='mm', divider=1e-03, singular='mm', plural='mm'), + dict(prefix='cm', divider=1e-02, singular='cm', plural='cm'), + dict(prefix='m', divider=1, singular='metre', plural='metres'), + dict(prefix='km', divider=1000, singular='km', plural='km')) + +# Common time units, used for formatting of time spans. +time_units = (dict(divider=1e-9, singular='nanosecond', plural='nanoseconds', abbreviations=['ns']), + dict(divider=1e-6, singular='microsecond', plural='microseconds', abbreviations=['us']), + dict(divider=1e-3, singular='millisecond', plural='milliseconds', abbreviations=['ms']), + dict(divider=1, singular='second', plural='seconds', abbreviations=['s', 'sec', 'secs']), + dict(divider=60, singular='minute', plural='minutes', abbreviations=['m', 'min', 'mins']), + dict(divider=60 * 60, singular='hour', plural='hours', abbreviations=['h']), + dict(divider=60 * 60 * 24, singular='day', plural='days', abbreviations=['d']), + dict(divider=60 * 60 * 24 * 7, singular='week', plural='weeks', abbreviations=['w']), + dict(divider=60 * 60 * 24 * 7 * 52, singular='year', plural='years', abbreviations=['y'])) + + +def coerce_boolean(value): + """ + Coerce any value to a boolean. + + :param value: Any Python value. If the value is a string: + + - The strings '1', 'yes', 'true' and 'on' are coerced to :data:`True`. + - The strings '0', 'no', 'false' and 'off' are coerced to :data:`False`. + - Other strings raise an exception. + + Other Python values are coerced using :class:`bool`. + :returns: A proper boolean value. + :raises: :exc:`exceptions.ValueError` when the value is a string but + cannot be coerced with certainty. + """ + if is_string(value): + normalized = value.strip().lower() + if normalized in ('1', 'yes', 'true', 'on'): + return True + elif normalized in ('0', 'no', 'false', 'off', ''): + return False + else: + msg = "Failed to coerce string to boolean! (%r)" + raise ValueError(format(msg, value)) + else: + return bool(value) + + +def coerce_pattern(value, flags=0): + """ + Coerce strings to compiled regular expressions. + + :param value: A string containing a regular expression pattern + or a compiled regular expression. + :param flags: The flags used to compile the pattern (an integer). + :returns: A compiled regular expression. + :raises: :exc:`~exceptions.ValueError` when `value` isn't a string + and also isn't a compiled regular expression. + """ + if is_string(value): + value = re.compile(value, flags) + else: + empty_pattern = re.compile('') + pattern_type = type(empty_pattern) + if not isinstance(value, pattern_type): + msg = "Failed to coerce value to compiled regular expression! (%r)" + raise ValueError(format(msg, value)) + return value + + +def coerce_seconds(value): + """ + Coerce a value to the number of seconds. + + :param value: An :class:`int`, :class:`float` or + :class:`datetime.timedelta` object. + :returns: An :class:`int` or :class:`float` value. + + When `value` is a :class:`datetime.timedelta` object the + :meth:`~datetime.timedelta.total_seconds()` method is called. + """ + if isinstance(value, datetime.timedelta): + return value.total_seconds() + if not isinstance(value, numbers.Number): + msg = "Failed to coerce value to number of seconds! (%r)" + raise ValueError(format(msg, value)) + return value + + +def format_size(num_bytes, keep_width=False, binary=False): + """ + Format a byte count as a human readable file size. + + :param num_bytes: The size to format in bytes (an integer). + :param keep_width: :data:`True` if trailing zeros should not be stripped, + :data:`False` if they can be stripped. + :param binary: :data:`True` to use binary multiples of bytes (base-2), + :data:`False` to use decimal multiples of bytes (base-10). + :returns: The corresponding human readable file size (a string). + + This function knows how to format sizes in bytes, kilobytes, megabytes, + gigabytes, terabytes and petabytes. Some examples: + + >>> from humanfriendly import format_size + >>> format_size(0) + '0 bytes' + >>> format_size(1) + '1 byte' + >>> format_size(5) + '5 bytes' + > format_size(1000) + '1 KB' + > format_size(1024, binary=True) + '1 KiB' + >>> format_size(1000 ** 3 * 4) + '4 GB' + """ + for unit in reversed(disk_size_units): + if num_bytes >= unit.binary.divider and binary: + number = round_number(float(num_bytes) / unit.binary.divider, keep_width=keep_width) + return pluralize(number, unit.binary.symbol, unit.binary.symbol) + elif num_bytes >= unit.decimal.divider and not binary: + number = round_number(float(num_bytes) / unit.decimal.divider, keep_width=keep_width) + return pluralize(number, unit.decimal.symbol, unit.decimal.symbol) + return pluralize(num_bytes, 'byte') + + +def parse_size(size, binary=False): + """ + Parse a human readable data size and return the number of bytes. + + :param size: The human readable file size to parse (a string). + :param binary: :data:`True` to use binary multiples of bytes (base-2) for + ambiguous unit symbols and names, :data:`False` to use + decimal multiples of bytes (base-10). + :returns: The corresponding size in bytes (an integer). + :raises: :exc:`InvalidSize` when the input can't be parsed. + + This function knows how to parse sizes in bytes, kilobytes, megabytes, + gigabytes, terabytes and petabytes. Some examples: + + >>> from humanfriendly import parse_size + >>> parse_size('42') + 42 + >>> parse_size('13b') + 13 + >>> parse_size('5 bytes') + 5 + >>> parse_size('1 KB') + 1000 + >>> parse_size('1 kilobyte') + 1000 + >>> parse_size('1 KiB') + 1024 + >>> parse_size('1 KB', binary=True) + 1024 + >>> parse_size('1.5 GB') + 1500000000 + >>> parse_size('1.5 GB', binary=True) + 1610612736 + """ + tokens = tokenize(size) + if tokens and isinstance(tokens[0], numbers.Number): + # Get the normalized unit (if any) from the tokenized input. + normalized_unit = tokens[1].lower() if len(tokens) == 2 and is_string(tokens[1]) else '' + # If the input contains only a number, it's assumed to be the number of + # bytes. The second token can also explicitly reference the unit bytes. + if len(tokens) == 1 or normalized_unit.startswith('b'): + return int(tokens[0]) + # Otherwise we expect two tokens: A number and a unit. + if normalized_unit: + # Convert plural units to singular units, for details: + # https://github.com/xolox/python-humanfriendly/issues/26 + normalized_unit = normalized_unit.rstrip('s') + for unit in disk_size_units: + # First we check for unambiguous symbols (KiB, MiB, GiB, etc) + # and names (kibibyte, mebibyte, gibibyte, etc) because their + # handling is always the same. + if normalized_unit in (unit.binary.symbol.lower(), unit.binary.name.lower()): + return int(tokens[0] * unit.binary.divider) + # Now we will deal with ambiguous prefixes (K, M, G, etc), + # symbols (KB, MB, GB, etc) and names (kilobyte, megabyte, + # gigabyte, etc) according to the caller's preference. + if (normalized_unit in (unit.decimal.symbol.lower(), unit.decimal.name.lower()) or + normalized_unit.startswith(unit.decimal.symbol[0].lower())): + return int(tokens[0] * (unit.binary.divider if binary else unit.decimal.divider)) + # We failed to parse the size specification. + msg = "Failed to parse size! (input %r was tokenized as %r)" + raise InvalidSize(format(msg, size, tokens)) + + +def format_length(num_metres, keep_width=False): + """ + Format a metre count as a human readable length. + + :param num_metres: The length to format in metres (float / integer). + :param keep_width: :data:`True` if trailing zeros should not be stripped, + :data:`False` if they can be stripped. + :returns: The corresponding human readable length (a string). + + This function supports ranges from nanometres to kilometres. + + Some examples: + + >>> from humanfriendly import format_length + >>> format_length(0) + '0 metres' + >>> format_length(1) + '1 metre' + >>> format_length(5) + '5 metres' + >>> format_length(1000) + '1 km' + >>> format_length(0.004) + '4 mm' + """ + for unit in reversed(length_size_units): + if num_metres >= unit['divider']: + number = round_number(float(num_metres) / unit['divider'], keep_width=keep_width) + return pluralize(number, unit['singular'], unit['plural']) + return pluralize(num_metres, 'metre') + + +def parse_length(length): + """ + Parse a human readable length and return the number of metres. + + :param length: The human readable length to parse (a string). + :returns: The corresponding length in metres (a float). + :raises: :exc:`InvalidLength` when the input can't be parsed. + + Some examples: + + >>> from humanfriendly import parse_length + >>> parse_length('42') + 42 + >>> parse_length('1 km') + 1000 + >>> parse_length('5mm') + 0.005 + >>> parse_length('15.3cm') + 0.153 + """ + tokens = tokenize(length) + if tokens and isinstance(tokens[0], numbers.Number): + # If the input contains only a number, it's assumed to be the number of metres. + if len(tokens) == 1: + return tokens[0] + # Otherwise we expect to find two tokens: A number and a unit. + if len(tokens) == 2 and is_string(tokens[1]): + normalized_unit = tokens[1].lower() + # Try to match the first letter of the unit. + for unit in length_size_units: + if normalized_unit.startswith(unit['prefix']): + return tokens[0] * unit['divider'] + # We failed to parse the length specification. + msg = "Failed to parse length! (input %r was tokenized as %r)" + raise InvalidLength(format(msg, length, tokens)) + + +def format_number(number, num_decimals=2): + """ + Format a number as a string including thousands separators. + + :param number: The number to format (a number like an :class:`int`, + :class:`long` or :class:`float`). + :param num_decimals: The number of decimals to render (2 by default). If no + decimal places are required to represent the number + they will be omitted regardless of this argument. + :returns: The formatted number (a string). + + This function is intended to make it easier to recognize the order of size + of the number being formatted. + + Here's an example: + + >>> from humanfriendly import format_number + >>> print(format_number(6000000)) + 6,000,000 + > print(format_number(6000000000.42)) + 6,000,000,000.42 + > print(format_number(6000000000.42, num_decimals=0)) + 6,000,000,000 + """ + integer_part, _, decimal_part = str(float(number)).partition('.') + reversed_digits = ''.join(reversed(integer_part)) + parts = [] + while reversed_digits: + parts.append(reversed_digits[:3]) + reversed_digits = reversed_digits[3:] + formatted_number = ''.join(reversed(','.join(parts))) + decimals_to_add = decimal_part[:num_decimals].rstrip('0') + if decimals_to_add: + formatted_number += '.' + decimals_to_add + return formatted_number + + +def round_number(count, keep_width=False): + """ + Round a floating point number to two decimal places in a human friendly format. + + :param count: The number to format. + :param keep_width: :data:`True` if trailing zeros should not be stripped, + :data:`False` if they can be stripped. + :returns: The formatted number as a string. If no decimal places are + required to represent the number, they will be omitted. + + The main purpose of this function is to be used by functions like + :func:`format_length()`, :func:`format_size()` and + :func:`format_timespan()`. + + Here are some examples: + + >>> from humanfriendly import round_number + >>> round_number(1) + '1' + >>> round_number(math.pi) + '3.14' + >>> round_number(5.001) + '5' + """ + text = '%.2f' % float(count) + if not keep_width: + text = re.sub('0+$', '', text) + text = re.sub(r'\.$', '', text) + return text + + +def format_timespan(num_seconds, detailed=False, max_units=3): + """ + Format a timespan in seconds as a human readable string. + + :param num_seconds: Any value accepted by :func:`coerce_seconds()`. + :param detailed: If :data:`True` milliseconds are represented separately + instead of being represented as fractional seconds + (defaults to :data:`False`). + :param max_units: The maximum number of units to show in the formatted time + span (an integer, defaults to three). + :returns: The formatted timespan as a string. + :raise: See :func:`coerce_seconds()`. + + Some examples: + + >>> from humanfriendly import format_timespan + >>> format_timespan(0) + '0 seconds' + >>> format_timespan(1) + '1 second' + >>> import math + >>> format_timespan(math.pi) + '3.14 seconds' + >>> hour = 60 * 60 + >>> day = hour * 24 + >>> week = day * 7 + >>> format_timespan(week * 52 + day * 2 + hour * 3) + '1 year, 2 days and 3 hours' + """ + num_seconds = coerce_seconds(num_seconds) + if num_seconds < 60 and not detailed: + # Fast path. + return pluralize(round_number(num_seconds), 'second') + else: + # Slow path. + result = [] + num_seconds = decimal.Decimal(str(num_seconds)) + relevant_units = list(reversed(time_units[0 if detailed else 3:])) + for unit in relevant_units: + # Extract the unit count from the remaining time. + divider = decimal.Decimal(str(unit['divider'])) + count = num_seconds / divider + num_seconds %= divider + # Round the unit count appropriately. + if unit != relevant_units[-1]: + # Integer rounding for all but the smallest unit. + count = int(count) + else: + # Floating point rounding for the smallest unit. + count = round_number(count) + # Only include relevant units in the result. + if count not in (0, '0'): + result.append(pluralize(count, unit['singular'], unit['plural'])) + if len(result) == 1: + # A single count/unit combination. + return result[0] + else: + if not detailed: + # Remove `insignificant' data from the formatted timespan. + result = result[:max_units] + # Format the timespan in a readable way. + return concatenate(result) + + +def parse_timespan(timespan): + """ + Parse a "human friendly" timespan into the number of seconds. + + :param value: A string like ``5h`` (5 hours), ``10m`` (10 minutes) or + ``42s`` (42 seconds). + :returns: The number of seconds as a floating point number. + :raises: :exc:`InvalidTimespan` when the input can't be parsed. + + Note that the :func:`parse_timespan()` function is not meant to be the + "mirror image" of the :func:`format_timespan()` function. Instead it's + meant to allow humans to easily and succinctly specify a timespan with a + minimal amount of typing. It's very useful to accept easy to write time + spans as e.g. command line arguments to programs. + + The time units (and abbreviations) supported by this function are: + + - ms, millisecond, milliseconds + - s, sec, secs, second, seconds + - m, min, mins, minute, minutes + - h, hour, hours + - d, day, days + - w, week, weeks + - y, year, years + + Some examples: + + >>> from humanfriendly import parse_timespan + >>> parse_timespan('42') + 42.0 + >>> parse_timespan('42s') + 42.0 + >>> parse_timespan('1m') + 60.0 + >>> parse_timespan('1h') + 3600.0 + >>> parse_timespan('1d') + 86400.0 + """ + tokens = tokenize(timespan) + if tokens and isinstance(tokens[0], numbers.Number): + # If the input contains only a number, it's assumed to be the number of seconds. + if len(tokens) == 1: + return float(tokens[0]) + # Otherwise we expect to find two tokens: A number and a unit. + if len(tokens) == 2 and is_string(tokens[1]): + normalized_unit = tokens[1].lower() + for unit in time_units: + if (normalized_unit == unit['singular'] or + normalized_unit == unit['plural'] or + normalized_unit in unit['abbreviations']): + return float(tokens[0]) * unit['divider'] + # We failed to parse the timespan specification. + msg = "Failed to parse timespan! (input %r was tokenized as %r)" + raise InvalidTimespan(format(msg, timespan, tokens)) + + +def parse_date(datestring): + """ + Parse a date/time string into a tuple of integers. + + :param datestring: The date/time string to parse. + :returns: A tuple with the numbers ``(year, month, day, hour, minute, + second)`` (all numbers are integers). + :raises: :exc:`InvalidDate` when the date cannot be parsed. + + Supported date/time formats: + + - ``YYYY-MM-DD`` + - ``YYYY-MM-DD HH:MM:SS`` + + .. note:: If you want to parse date/time strings with a fixed, known + format and :func:`parse_date()` isn't useful to you, consider + :func:`time.strptime()` or :meth:`datetime.datetime.strptime()`, + both of which are included in the Python standard library. + Alternatively for more complex tasks consider using the date/time + parsing module in the dateutil_ package. + + Examples: + + >>> from humanfriendly import parse_date + >>> parse_date('2013-06-17') + (2013, 6, 17, 0, 0, 0) + >>> parse_date('2013-06-17 02:47:42') + (2013, 6, 17, 2, 47, 42) + + Here's how you convert the result to a number (`Unix time`_): + + >>> from humanfriendly import parse_date + >>> from time import mktime + >>> mktime(parse_date('2013-06-17 02:47:42') + (-1, -1, -1)) + 1371430062.0 + + And here's how you convert it to a :class:`datetime.datetime` object: + + >>> from humanfriendly import parse_date + >>> from datetime import datetime + >>> datetime(*parse_date('2013-06-17 02:47:42')) + datetime.datetime(2013, 6, 17, 2, 47, 42) + + Here's an example that combines :func:`format_timespan()` and + :func:`parse_date()` to calculate a human friendly timespan since a + given date: + + >>> from humanfriendly import format_timespan, parse_date + >>> from time import mktime, time + >>> unix_time = mktime(parse_date('2013-06-17 02:47:42') + (-1, -1, -1)) + >>> seconds_since_then = time() - unix_time + >>> print(format_timespan(seconds_since_then)) + 1 year, 43 weeks and 1 day + + .. _dateutil: https://dateutil.readthedocs.io/en/latest/parser.html + .. _Unix time: http://en.wikipedia.org/wiki/Unix_time + """ + try: + tokens = [t.strip() for t in datestring.split()] + if len(tokens) >= 2: + date_parts = list(map(int, tokens[0].split('-'))) + [1, 1] + time_parts = list(map(int, tokens[1].split(':'))) + [0, 0, 0] + return tuple(date_parts[0:3] + time_parts[0:3]) + else: + year, month, day = (list(map(int, datestring.split('-'))) + [1, 1])[0:3] + return (year, month, day, 0, 0, 0) + except Exception: + msg = "Invalid date! (expected 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS' but got: %r)" + raise InvalidDate(format(msg, datestring)) + + +def format_path(pathname): + """ + Shorten a pathname to make it more human friendly. + + :param pathname: An absolute pathname (a string). + :returns: The pathname with the user's home directory abbreviated. + + Given an absolute pathname, this function abbreviates the user's home + directory to ``~/`` in order to shorten the pathname without losing + information. It is not an error if the pathname is not relative to the + current user's home directory. + + Here's an example of its usage: + + >>> from os import environ + >>> from os.path import join + >>> vimrc = join(environ['HOME'], '.vimrc') + >>> vimrc + '/home/peter/.vimrc' + >>> from humanfriendly import format_path + >>> format_path(vimrc) + '~/.vimrc' + """ + pathname = os.path.abspath(pathname) + home = os.environ.get('HOME') + if home: + home = os.path.abspath(home) + if pathname.startswith(home): + pathname = os.path.join('~', os.path.relpath(pathname, home)) + return pathname + + +def parse_path(pathname): + """ + Convert a human friendly pathname to an absolute pathname. + + Expands leading tildes using :func:`os.path.expanduser()` and + environment variables using :func:`os.path.expandvars()` and makes the + resulting pathname absolute using :func:`os.path.abspath()`. + + :param pathname: A human friendly pathname (a string). + :returns: An absolute pathname (a string). + """ + return os.path.abspath(os.path.expanduser(os.path.expandvars(pathname))) + + +class Timer(object): + + """ + Easy to use timer to keep track of long during operations. + """ + + def __init__(self, start_time=None, resumable=False): + """ + Remember the time when the :class:`Timer` was created. + + :param start_time: The start time (a float, defaults to the current time). + :param resumable: Create a resumable timer (defaults to :data:`False`). + + When `start_time` is given :class:`Timer` uses :func:`time.time()` as a + clock source, otherwise it uses :func:`humanfriendly.compat.monotonic()`. + """ + if resumable: + self.monotonic = True + self.resumable = True + self.start_time = 0.0 + self.total_time = 0.0 + elif start_time: + self.monotonic = False + self.resumable = False + self.start_time = start_time + else: + self.monotonic = True + self.resumable = False + self.start_time = monotonic() + + def __enter__(self): + """ + Start or resume counting elapsed time. + + :returns: The :class:`Timer` object. + :raises: :exc:`~exceptions.ValueError` when the timer isn't resumable. + """ + if not self.resumable: + raise ValueError("Timer is not resumable!") + self.start_time = monotonic() + return self + + def __exit__(self, exc_type=None, exc_value=None, traceback=None): + """ + Stop counting elapsed time. + + :raises: :exc:`~exceptions.ValueError` when the timer isn't resumable. + """ + if not self.resumable: + raise ValueError("Timer is not resumable!") + if self.start_time: + self.total_time += monotonic() - self.start_time + self.start_time = 0.0 + + def sleep(self, seconds): + """ + Easy to use rate limiting of repeating actions. + + :param seconds: The number of seconds to sleep (an + integer or floating point number). + + This method sleeps for the given number of seconds minus the + :attr:`elapsed_time`. If the resulting duration is negative + :func:`time.sleep()` will still be called, but the argument + given to it will be the number 0 (negative numbers cause + :func:`time.sleep()` to raise an exception). + + The use case for this is to initialize a :class:`Timer` inside + the body of a :keyword:`for` or :keyword:`while` loop and call + :func:`Timer.sleep()` at the end of the loop body to rate limit + whatever it is that is being done inside the loop body. + + For posterity: Although the implementation of :func:`sleep()` only + requires a single line of code I've added it to :mod:`humanfriendly` + anyway because now that I've thought about how to tackle this once I + never want to have to think about it again :-P (unless I find ways to + improve this). + """ + time.sleep(max(0, seconds - self.elapsed_time)) + + @property + def elapsed_time(self): + """ + Get the number of seconds counted so far. + """ + elapsed_time = 0 + if self.resumable: + elapsed_time += self.total_time + if self.start_time: + current_time = monotonic() if self.monotonic else time.time() + elapsed_time += current_time - self.start_time + return elapsed_time + + @property + def rounded(self): + """Human readable timespan rounded to seconds (a string).""" + return format_timespan(round(self.elapsed_time)) + + def __str__(self): + """Show the elapsed time since the :class:`Timer` was created.""" + return format_timespan(self.elapsed_time) + + +class InvalidDate(Exception): + + """ + Raised when a string cannot be parsed into a date. + + For example: + + >>> from humanfriendly import parse_date + >>> parse_date('2013-06-XY') + Traceback (most recent call last): + File "humanfriendly.py", line 206, in parse_date + raise InvalidDate(format(msg, datestring)) + humanfriendly.InvalidDate: Invalid date! (expected 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS' but got: '2013-06-XY') + """ + + +class InvalidSize(Exception): + + """ + Raised when a string cannot be parsed into a file size. + + For example: + + >>> from humanfriendly import parse_size + >>> parse_size('5 Z') + Traceback (most recent call last): + File "humanfriendly/__init__.py", line 267, in parse_size + raise InvalidSize(format(msg, size, tokens)) + humanfriendly.InvalidSize: Failed to parse size! (input '5 Z' was tokenized as [5, 'Z']) + """ + + +class InvalidLength(Exception): + + """ + Raised when a string cannot be parsed into a length. + + For example: + + >>> from humanfriendly import parse_length + >>> parse_length('5 Z') + Traceback (most recent call last): + File "humanfriendly/__init__.py", line 267, in parse_length + raise InvalidLength(format(msg, length, tokens)) + humanfriendly.InvalidLength: Failed to parse length! (input '5 Z' was tokenized as [5, 'Z']) + """ + + +class InvalidTimespan(Exception): + + """ + Raised when a string cannot be parsed into a timespan. + + For example: + + >>> from humanfriendly import parse_timespan + >>> parse_timespan('1 age') + Traceback (most recent call last): + File "humanfriendly/__init__.py", line 419, in parse_timespan + raise InvalidTimespan(format(msg, timespan, tokens)) + humanfriendly.InvalidTimespan: Failed to parse timespan! (input '1 age' was tokenized as [1, 'age']) + """ + + +# Define aliases for backwards compatibility. +define_aliases( + module_name=__name__, + # In humanfriendly 1.23 the format_table() function was added to render a + # table using characters like dashes and vertical bars to emulate borders. + # Since then support for other tables has been added and the name of + # format_table() has changed. + format_table='humanfriendly.tables.format_pretty_table', + # In humanfriendly 1.30 the following text manipulation functions were + # moved out into a separate module to enable their usage in other modules + # of the humanfriendly package (without causing circular imports). + compact='humanfriendly.text.compact', + concatenate='humanfriendly.text.concatenate', + dedent='humanfriendly.text.dedent', + format='humanfriendly.text.format', + is_empty_line='humanfriendly.text.is_empty_line', + pluralize='humanfriendly.text.pluralize', + tokenize='humanfriendly.text.tokenize', + trim_empty_lines='humanfriendly.text.trim_empty_lines', + # In humanfriendly 1.38 the prompt_for_choice() function was moved out into a + # separate module because several variants of interactive prompts were added. + prompt_for_choice='humanfriendly.prompts.prompt_for_choice', + # In humanfriendly 8.0 the Spinner class and minimum_spinner_interval + # variable were extracted to a new module and the erase_line_code, + # hide_cursor_code and show_cursor_code variables were moved. + AutomaticSpinner='humanfriendly.terminal.spinners.AutomaticSpinner', + Spinner='humanfriendly.terminal.spinners.Spinner', + erase_line_code='humanfriendly.terminal.ANSI_ERASE_LINE', + hide_cursor_code='humanfriendly.terminal.ANSI_SHOW_CURSOR', + minimum_spinner_interval='humanfriendly.terminal.spinners.MINIMUM_INTERVAL', + show_cursor_code='humanfriendly.terminal.ANSI_HIDE_CURSOR', +)