| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838 |
- # Human friendly input/output in Python.
- #
- # Author: Peter Odding <peter@peterodding.com>
- # Last Change: September 17, 2021
- # 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__ = '10.0'
- # 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('.')
- negative_sign = integer_part.startswith('-')
- reversed_digits = ''.join(reversed(integer_part.lstrip('-')))
- 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
- if negative_sign:
- formatted_number = '-' + formatted_number
- 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',
- )
|