Mercurial > repos > shellac > guppy_basecaller
comparison env/lib/python3.7/site-packages/humanfriendly/__init__.py @ 0:26e78fe6e8c4 draft
"planemo upload commit c699937486c35866861690329de38ec1a5d9f783"
| author | shellac |
|---|---|
| date | Sat, 02 May 2020 07:14:21 -0400 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| -1:000000000000 | 0:26e78fe6e8c4 |
|---|---|
| 1 # Human friendly input/output in Python. | |
| 2 # | |
| 3 # Author: Peter Odding <peter@peterodding.com> | |
| 4 # Last Change: April 19, 2020 | |
| 5 # URL: https://humanfriendly.readthedocs.io | |
| 6 | |
| 7 """The main module of the `humanfriendly` package.""" | |
| 8 | |
| 9 # Standard library modules. | |
| 10 import collections | |
| 11 import datetime | |
| 12 import decimal | |
| 13 import numbers | |
| 14 import os | |
| 15 import os.path | |
| 16 import re | |
| 17 import time | |
| 18 | |
| 19 # Modules included in our package. | |
| 20 from humanfriendly.compat import is_string, monotonic | |
| 21 from humanfriendly.deprecation import define_aliases | |
| 22 from humanfriendly.text import concatenate, format, pluralize, tokenize | |
| 23 | |
| 24 # Public identifiers that require documentation. | |
| 25 __all__ = ( | |
| 26 'CombinedUnit', | |
| 27 'InvalidDate', | |
| 28 'InvalidLength', | |
| 29 'InvalidSize', | |
| 30 'InvalidTimespan', | |
| 31 'SizeUnit', | |
| 32 'Timer', | |
| 33 '__version__', | |
| 34 'coerce_boolean', | |
| 35 'coerce_pattern', | |
| 36 'coerce_seconds', | |
| 37 'disk_size_units', | |
| 38 'format_length', | |
| 39 'format_number', | |
| 40 'format_path', | |
| 41 'format_size', | |
| 42 'format_timespan', | |
| 43 'length_size_units', | |
| 44 'parse_date', | |
| 45 'parse_length', | |
| 46 'parse_path', | |
| 47 'parse_size', | |
| 48 'parse_timespan', | |
| 49 'round_number', | |
| 50 'time_units', | |
| 51 ) | |
| 52 | |
| 53 # Semi-standard module versioning. | |
| 54 __version__ = '8.2' | |
| 55 | |
| 56 # Named tuples to define units of size. | |
| 57 SizeUnit = collections.namedtuple('SizeUnit', 'divider, symbol, name') | |
| 58 CombinedUnit = collections.namedtuple('CombinedUnit', 'decimal, binary') | |
| 59 | |
| 60 # Common disk size units in binary (base-2) and decimal (base-10) multiples. | |
| 61 disk_size_units = ( | |
| 62 CombinedUnit(SizeUnit(1000**1, 'KB', 'kilobyte'), SizeUnit(1024**1, 'KiB', 'kibibyte')), | |
| 63 CombinedUnit(SizeUnit(1000**2, 'MB', 'megabyte'), SizeUnit(1024**2, 'MiB', 'mebibyte')), | |
| 64 CombinedUnit(SizeUnit(1000**3, 'GB', 'gigabyte'), SizeUnit(1024**3, 'GiB', 'gibibyte')), | |
| 65 CombinedUnit(SizeUnit(1000**4, 'TB', 'terabyte'), SizeUnit(1024**4, 'TiB', 'tebibyte')), | |
| 66 CombinedUnit(SizeUnit(1000**5, 'PB', 'petabyte'), SizeUnit(1024**5, 'PiB', 'pebibyte')), | |
| 67 CombinedUnit(SizeUnit(1000**6, 'EB', 'exabyte'), SizeUnit(1024**6, 'EiB', 'exbibyte')), | |
| 68 CombinedUnit(SizeUnit(1000**7, 'ZB', 'zettabyte'), SizeUnit(1024**7, 'ZiB', 'zebibyte')), | |
| 69 CombinedUnit(SizeUnit(1000**8, 'YB', 'yottabyte'), SizeUnit(1024**8, 'YiB', 'yobibyte')), | |
| 70 ) | |
| 71 | |
| 72 # Common length size units, used for formatting and parsing. | |
| 73 length_size_units = (dict(prefix='nm', divider=1e-09, singular='nm', plural='nm'), | |
| 74 dict(prefix='mm', divider=1e-03, singular='mm', plural='mm'), | |
| 75 dict(prefix='cm', divider=1e-02, singular='cm', plural='cm'), | |
| 76 dict(prefix='m', divider=1, singular='metre', plural='metres'), | |
| 77 dict(prefix='km', divider=1000, singular='km', plural='km')) | |
| 78 | |
| 79 # Common time units, used for formatting of time spans. | |
| 80 time_units = (dict(divider=1e-9, singular='nanosecond', plural='nanoseconds', abbreviations=['ns']), | |
| 81 dict(divider=1e-6, singular='microsecond', plural='microseconds', abbreviations=['us']), | |
| 82 dict(divider=1e-3, singular='millisecond', plural='milliseconds', abbreviations=['ms']), | |
| 83 dict(divider=1, singular='second', plural='seconds', abbreviations=['s', 'sec', 'secs']), | |
| 84 dict(divider=60, singular='minute', plural='minutes', abbreviations=['m', 'min', 'mins']), | |
| 85 dict(divider=60 * 60, singular='hour', plural='hours', abbreviations=['h']), | |
| 86 dict(divider=60 * 60 * 24, singular='day', plural='days', abbreviations=['d']), | |
| 87 dict(divider=60 * 60 * 24 * 7, singular='week', plural='weeks', abbreviations=['w']), | |
| 88 dict(divider=60 * 60 * 24 * 7 * 52, singular='year', plural='years', abbreviations=['y'])) | |
| 89 | |
| 90 | |
| 91 def coerce_boolean(value): | |
| 92 """ | |
| 93 Coerce any value to a boolean. | |
| 94 | |
| 95 :param value: Any Python value. If the value is a string: | |
| 96 | |
| 97 - The strings '1', 'yes', 'true' and 'on' are coerced to :data:`True`. | |
| 98 - The strings '0', 'no', 'false' and 'off' are coerced to :data:`False`. | |
| 99 - Other strings raise an exception. | |
| 100 | |
| 101 Other Python values are coerced using :class:`bool`. | |
| 102 :returns: A proper boolean value. | |
| 103 :raises: :exc:`exceptions.ValueError` when the value is a string but | |
| 104 cannot be coerced with certainty. | |
| 105 """ | |
| 106 if is_string(value): | |
| 107 normalized = value.strip().lower() | |
| 108 if normalized in ('1', 'yes', 'true', 'on'): | |
| 109 return True | |
| 110 elif normalized in ('0', 'no', 'false', 'off', ''): | |
| 111 return False | |
| 112 else: | |
| 113 msg = "Failed to coerce string to boolean! (%r)" | |
| 114 raise ValueError(format(msg, value)) | |
| 115 else: | |
| 116 return bool(value) | |
| 117 | |
| 118 | |
| 119 def coerce_pattern(value, flags=0): | |
| 120 """ | |
| 121 Coerce strings to compiled regular expressions. | |
| 122 | |
| 123 :param value: A string containing a regular expression pattern | |
| 124 or a compiled regular expression. | |
| 125 :param flags: The flags used to compile the pattern (an integer). | |
| 126 :returns: A compiled regular expression. | |
| 127 :raises: :exc:`~exceptions.ValueError` when `value` isn't a string | |
| 128 and also isn't a compiled regular expression. | |
| 129 """ | |
| 130 if is_string(value): | |
| 131 value = re.compile(value, flags) | |
| 132 else: | |
| 133 empty_pattern = re.compile('') | |
| 134 pattern_type = type(empty_pattern) | |
| 135 if not isinstance(value, pattern_type): | |
| 136 msg = "Failed to coerce value to compiled regular expression! (%r)" | |
| 137 raise ValueError(format(msg, value)) | |
| 138 return value | |
| 139 | |
| 140 | |
| 141 def coerce_seconds(value): | |
| 142 """ | |
| 143 Coerce a value to the number of seconds. | |
| 144 | |
| 145 :param value: An :class:`int`, :class:`float` or | |
| 146 :class:`datetime.timedelta` object. | |
| 147 :returns: An :class:`int` or :class:`float` value. | |
| 148 | |
| 149 When `value` is a :class:`datetime.timedelta` object the | |
| 150 :meth:`~datetime.timedelta.total_seconds()` method is called. | |
| 151 """ | |
| 152 if isinstance(value, datetime.timedelta): | |
| 153 return value.total_seconds() | |
| 154 if not isinstance(value, numbers.Number): | |
| 155 msg = "Failed to coerce value to number of seconds! (%r)" | |
| 156 raise ValueError(format(msg, value)) | |
| 157 return value | |
| 158 | |
| 159 | |
| 160 def format_size(num_bytes, keep_width=False, binary=False): | |
| 161 """ | |
| 162 Format a byte count as a human readable file size. | |
| 163 | |
| 164 :param num_bytes: The size to format in bytes (an integer). | |
| 165 :param keep_width: :data:`True` if trailing zeros should not be stripped, | |
| 166 :data:`False` if they can be stripped. | |
| 167 :param binary: :data:`True` to use binary multiples of bytes (base-2), | |
| 168 :data:`False` to use decimal multiples of bytes (base-10). | |
| 169 :returns: The corresponding human readable file size (a string). | |
| 170 | |
| 171 This function knows how to format sizes in bytes, kilobytes, megabytes, | |
| 172 gigabytes, terabytes and petabytes. Some examples: | |
| 173 | |
| 174 >>> from humanfriendly import format_size | |
| 175 >>> format_size(0) | |
| 176 '0 bytes' | |
| 177 >>> format_size(1) | |
| 178 '1 byte' | |
| 179 >>> format_size(5) | |
| 180 '5 bytes' | |
| 181 > format_size(1000) | |
| 182 '1 KB' | |
| 183 > format_size(1024, binary=True) | |
| 184 '1 KiB' | |
| 185 >>> format_size(1000 ** 3 * 4) | |
| 186 '4 GB' | |
| 187 """ | |
| 188 for unit in reversed(disk_size_units): | |
| 189 if num_bytes >= unit.binary.divider and binary: | |
| 190 number = round_number(float(num_bytes) / unit.binary.divider, keep_width=keep_width) | |
| 191 return pluralize(number, unit.binary.symbol, unit.binary.symbol) | |
| 192 elif num_bytes >= unit.decimal.divider and not binary: | |
| 193 number = round_number(float(num_bytes) / unit.decimal.divider, keep_width=keep_width) | |
| 194 return pluralize(number, unit.decimal.symbol, unit.decimal.symbol) | |
| 195 return pluralize(num_bytes, 'byte') | |
| 196 | |
| 197 | |
| 198 def parse_size(size, binary=False): | |
| 199 """ | |
| 200 Parse a human readable data size and return the number of bytes. | |
| 201 | |
| 202 :param size: The human readable file size to parse (a string). | |
| 203 :param binary: :data:`True` to use binary multiples of bytes (base-2) for | |
| 204 ambiguous unit symbols and names, :data:`False` to use | |
| 205 decimal multiples of bytes (base-10). | |
| 206 :returns: The corresponding size in bytes (an integer). | |
| 207 :raises: :exc:`InvalidSize` when the input can't be parsed. | |
| 208 | |
| 209 This function knows how to parse sizes in bytes, kilobytes, megabytes, | |
| 210 gigabytes, terabytes and petabytes. Some examples: | |
| 211 | |
| 212 >>> from humanfriendly import parse_size | |
| 213 >>> parse_size('42') | |
| 214 42 | |
| 215 >>> parse_size('13b') | |
| 216 13 | |
| 217 >>> parse_size('5 bytes') | |
| 218 5 | |
| 219 >>> parse_size('1 KB') | |
| 220 1000 | |
| 221 >>> parse_size('1 kilobyte') | |
| 222 1000 | |
| 223 >>> parse_size('1 KiB') | |
| 224 1024 | |
| 225 >>> parse_size('1 KB', binary=True) | |
| 226 1024 | |
| 227 >>> parse_size('1.5 GB') | |
| 228 1500000000 | |
| 229 >>> parse_size('1.5 GB', binary=True) | |
| 230 1610612736 | |
| 231 """ | |
| 232 tokens = tokenize(size) | |
| 233 if tokens and isinstance(tokens[0], numbers.Number): | |
| 234 # Get the normalized unit (if any) from the tokenized input. | |
| 235 normalized_unit = tokens[1].lower() if len(tokens) == 2 and is_string(tokens[1]) else '' | |
| 236 # If the input contains only a number, it's assumed to be the number of | |
| 237 # bytes. The second token can also explicitly reference the unit bytes. | |
| 238 if len(tokens) == 1 or normalized_unit.startswith('b'): | |
| 239 return int(tokens[0]) | |
| 240 # Otherwise we expect two tokens: A number and a unit. | |
| 241 if normalized_unit: | |
| 242 # Convert plural units to singular units, for details: | |
| 243 # https://github.com/xolox/python-humanfriendly/issues/26 | |
| 244 normalized_unit = normalized_unit.rstrip('s') | |
| 245 for unit in disk_size_units: | |
| 246 # First we check for unambiguous symbols (KiB, MiB, GiB, etc) | |
| 247 # and names (kibibyte, mebibyte, gibibyte, etc) because their | |
| 248 # handling is always the same. | |
| 249 if normalized_unit in (unit.binary.symbol.lower(), unit.binary.name.lower()): | |
| 250 return int(tokens[0] * unit.binary.divider) | |
| 251 # Now we will deal with ambiguous prefixes (K, M, G, etc), | |
| 252 # symbols (KB, MB, GB, etc) and names (kilobyte, megabyte, | |
| 253 # gigabyte, etc) according to the caller's preference. | |
| 254 if (normalized_unit in (unit.decimal.symbol.lower(), unit.decimal.name.lower()) or | |
| 255 normalized_unit.startswith(unit.decimal.symbol[0].lower())): | |
| 256 return int(tokens[0] * (unit.binary.divider if binary else unit.decimal.divider)) | |
| 257 # We failed to parse the size specification. | |
| 258 msg = "Failed to parse size! (input %r was tokenized as %r)" | |
| 259 raise InvalidSize(format(msg, size, tokens)) | |
| 260 | |
| 261 | |
| 262 def format_length(num_metres, keep_width=False): | |
| 263 """ | |
| 264 Format a metre count as a human readable length. | |
| 265 | |
| 266 :param num_metres: The length to format in metres (float / integer). | |
| 267 :param keep_width: :data:`True` if trailing zeros should not be stripped, | |
| 268 :data:`False` if they can be stripped. | |
| 269 :returns: The corresponding human readable length (a string). | |
| 270 | |
| 271 This function supports ranges from nanometres to kilometres. | |
| 272 | |
| 273 Some examples: | |
| 274 | |
| 275 >>> from humanfriendly import format_length | |
| 276 >>> format_length(0) | |
| 277 '0 metres' | |
| 278 >>> format_length(1) | |
| 279 '1 metre' | |
| 280 >>> format_length(5) | |
| 281 '5 metres' | |
| 282 >>> format_length(1000) | |
| 283 '1 km' | |
| 284 >>> format_length(0.004) | |
| 285 '4 mm' | |
| 286 """ | |
| 287 for unit in reversed(length_size_units): | |
| 288 if num_metres >= unit['divider']: | |
| 289 number = round_number(float(num_metres) / unit['divider'], keep_width=keep_width) | |
| 290 return pluralize(number, unit['singular'], unit['plural']) | |
| 291 return pluralize(num_metres, 'metre') | |
| 292 | |
| 293 | |
| 294 def parse_length(length): | |
| 295 """ | |
| 296 Parse a human readable length and return the number of metres. | |
| 297 | |
| 298 :param length: The human readable length to parse (a string). | |
| 299 :returns: The corresponding length in metres (a float). | |
| 300 :raises: :exc:`InvalidLength` when the input can't be parsed. | |
| 301 | |
| 302 Some examples: | |
| 303 | |
| 304 >>> from humanfriendly import parse_length | |
| 305 >>> parse_length('42') | |
| 306 42 | |
| 307 >>> parse_length('1 km') | |
| 308 1000 | |
| 309 >>> parse_length('5mm') | |
| 310 0.005 | |
| 311 >>> parse_length('15.3cm') | |
| 312 0.153 | |
| 313 """ | |
| 314 tokens = tokenize(length) | |
| 315 if tokens and isinstance(tokens[0], numbers.Number): | |
| 316 # If the input contains only a number, it's assumed to be the number of metres. | |
| 317 if len(tokens) == 1: | |
| 318 return tokens[0] | |
| 319 # Otherwise we expect to find two tokens: A number and a unit. | |
| 320 if len(tokens) == 2 and is_string(tokens[1]): | |
| 321 normalized_unit = tokens[1].lower() | |
| 322 # Try to match the first letter of the unit. | |
| 323 for unit in length_size_units: | |
| 324 if normalized_unit.startswith(unit['prefix']): | |
| 325 return tokens[0] * unit['divider'] | |
| 326 # We failed to parse the length specification. | |
| 327 msg = "Failed to parse length! (input %r was tokenized as %r)" | |
| 328 raise InvalidLength(format(msg, length, tokens)) | |
| 329 | |
| 330 | |
| 331 def format_number(number, num_decimals=2): | |
| 332 """ | |
| 333 Format a number as a string including thousands separators. | |
| 334 | |
| 335 :param number: The number to format (a number like an :class:`int`, | |
| 336 :class:`long` or :class:`float`). | |
| 337 :param num_decimals: The number of decimals to render (2 by default). If no | |
| 338 decimal places are required to represent the number | |
| 339 they will be omitted regardless of this argument. | |
| 340 :returns: The formatted number (a string). | |
| 341 | |
| 342 This function is intended to make it easier to recognize the order of size | |
| 343 of the number being formatted. | |
| 344 | |
| 345 Here's an example: | |
| 346 | |
| 347 >>> from humanfriendly import format_number | |
| 348 >>> print(format_number(6000000)) | |
| 349 6,000,000 | |
| 350 > print(format_number(6000000000.42)) | |
| 351 6,000,000,000.42 | |
| 352 > print(format_number(6000000000.42, num_decimals=0)) | |
| 353 6,000,000,000 | |
| 354 """ | |
| 355 integer_part, _, decimal_part = str(float(number)).partition('.') | |
| 356 reversed_digits = ''.join(reversed(integer_part)) | |
| 357 parts = [] | |
| 358 while reversed_digits: | |
| 359 parts.append(reversed_digits[:3]) | |
| 360 reversed_digits = reversed_digits[3:] | |
| 361 formatted_number = ''.join(reversed(','.join(parts))) | |
| 362 decimals_to_add = decimal_part[:num_decimals].rstrip('0') | |
| 363 if decimals_to_add: | |
| 364 formatted_number += '.' + decimals_to_add | |
| 365 return formatted_number | |
| 366 | |
| 367 | |
| 368 def round_number(count, keep_width=False): | |
| 369 """ | |
| 370 Round a floating point number to two decimal places in a human friendly format. | |
| 371 | |
| 372 :param count: The number to format. | |
| 373 :param keep_width: :data:`True` if trailing zeros should not be stripped, | |
| 374 :data:`False` if they can be stripped. | |
| 375 :returns: The formatted number as a string. If no decimal places are | |
| 376 required to represent the number, they will be omitted. | |
| 377 | |
| 378 The main purpose of this function is to be used by functions like | |
| 379 :func:`format_length()`, :func:`format_size()` and | |
| 380 :func:`format_timespan()`. | |
| 381 | |
| 382 Here are some examples: | |
| 383 | |
| 384 >>> from humanfriendly import round_number | |
| 385 >>> round_number(1) | |
| 386 '1' | |
| 387 >>> round_number(math.pi) | |
| 388 '3.14' | |
| 389 >>> round_number(5.001) | |
| 390 '5' | |
| 391 """ | |
| 392 text = '%.2f' % float(count) | |
| 393 if not keep_width: | |
| 394 text = re.sub('0+$', '', text) | |
| 395 text = re.sub(r'\.$', '', text) | |
| 396 return text | |
| 397 | |
| 398 | |
| 399 def format_timespan(num_seconds, detailed=False, max_units=3): | |
| 400 """ | |
| 401 Format a timespan in seconds as a human readable string. | |
| 402 | |
| 403 :param num_seconds: Any value accepted by :func:`coerce_seconds()`. | |
| 404 :param detailed: If :data:`True` milliseconds are represented separately | |
| 405 instead of being represented as fractional seconds | |
| 406 (defaults to :data:`False`). | |
| 407 :param max_units: The maximum number of units to show in the formatted time | |
| 408 span (an integer, defaults to three). | |
| 409 :returns: The formatted timespan as a string. | |
| 410 :raise: See :func:`coerce_seconds()`. | |
| 411 | |
| 412 Some examples: | |
| 413 | |
| 414 >>> from humanfriendly import format_timespan | |
| 415 >>> format_timespan(0) | |
| 416 '0 seconds' | |
| 417 >>> format_timespan(1) | |
| 418 '1 second' | |
| 419 >>> import math | |
| 420 >>> format_timespan(math.pi) | |
| 421 '3.14 seconds' | |
| 422 >>> hour = 60 * 60 | |
| 423 >>> day = hour * 24 | |
| 424 >>> week = day * 7 | |
| 425 >>> format_timespan(week * 52 + day * 2 + hour * 3) | |
| 426 '1 year, 2 days and 3 hours' | |
| 427 """ | |
| 428 num_seconds = coerce_seconds(num_seconds) | |
| 429 if num_seconds < 60 and not detailed: | |
| 430 # Fast path. | |
| 431 return pluralize(round_number(num_seconds), 'second') | |
| 432 else: | |
| 433 # Slow path. | |
| 434 result = [] | |
| 435 num_seconds = decimal.Decimal(str(num_seconds)) | |
| 436 relevant_units = list(reversed(time_units[0 if detailed else 3:])) | |
| 437 for unit in relevant_units: | |
| 438 # Extract the unit count from the remaining time. | |
| 439 divider = decimal.Decimal(str(unit['divider'])) | |
| 440 count = num_seconds / divider | |
| 441 num_seconds %= divider | |
| 442 # Round the unit count appropriately. | |
| 443 if unit != relevant_units[-1]: | |
| 444 # Integer rounding for all but the smallest unit. | |
| 445 count = int(count) | |
| 446 else: | |
| 447 # Floating point rounding for the smallest unit. | |
| 448 count = round_number(count) | |
| 449 # Only include relevant units in the result. | |
| 450 if count not in (0, '0'): | |
| 451 result.append(pluralize(count, unit['singular'], unit['plural'])) | |
| 452 if len(result) == 1: | |
| 453 # A single count/unit combination. | |
| 454 return result[0] | |
| 455 else: | |
| 456 if not detailed: | |
| 457 # Remove `insignificant' data from the formatted timespan. | |
| 458 result = result[:max_units] | |
| 459 # Format the timespan in a readable way. | |
| 460 return concatenate(result) | |
| 461 | |
| 462 | |
| 463 def parse_timespan(timespan): | |
| 464 """ | |
| 465 Parse a "human friendly" timespan into the number of seconds. | |
| 466 | |
| 467 :param value: A string like ``5h`` (5 hours), ``10m`` (10 minutes) or | |
| 468 ``42s`` (42 seconds). | |
| 469 :returns: The number of seconds as a floating point number. | |
| 470 :raises: :exc:`InvalidTimespan` when the input can't be parsed. | |
| 471 | |
| 472 Note that the :func:`parse_timespan()` function is not meant to be the | |
| 473 "mirror image" of the :func:`format_timespan()` function. Instead it's | |
| 474 meant to allow humans to easily and succinctly specify a timespan with a | |
| 475 minimal amount of typing. It's very useful to accept easy to write time | |
| 476 spans as e.g. command line arguments to programs. | |
| 477 | |
| 478 The time units (and abbreviations) supported by this function are: | |
| 479 | |
| 480 - ms, millisecond, milliseconds | |
| 481 - s, sec, secs, second, seconds | |
| 482 - m, min, mins, minute, minutes | |
| 483 - h, hour, hours | |
| 484 - d, day, days | |
| 485 - w, week, weeks | |
| 486 - y, year, years | |
| 487 | |
| 488 Some examples: | |
| 489 | |
| 490 >>> from humanfriendly import parse_timespan | |
| 491 >>> parse_timespan('42') | |
| 492 42.0 | |
| 493 >>> parse_timespan('42s') | |
| 494 42.0 | |
| 495 >>> parse_timespan('1m') | |
| 496 60.0 | |
| 497 >>> parse_timespan('1h') | |
| 498 3600.0 | |
| 499 >>> parse_timespan('1d') | |
| 500 86400.0 | |
| 501 """ | |
| 502 tokens = tokenize(timespan) | |
| 503 if tokens and isinstance(tokens[0], numbers.Number): | |
| 504 # If the input contains only a number, it's assumed to be the number of seconds. | |
| 505 if len(tokens) == 1: | |
| 506 return float(tokens[0]) | |
| 507 # Otherwise we expect to find two tokens: A number and a unit. | |
| 508 if len(tokens) == 2 and is_string(tokens[1]): | |
| 509 normalized_unit = tokens[1].lower() | |
| 510 for unit in time_units: | |
| 511 if (normalized_unit == unit['singular'] or | |
| 512 normalized_unit == unit['plural'] or | |
| 513 normalized_unit in unit['abbreviations']): | |
| 514 return float(tokens[0]) * unit['divider'] | |
| 515 # We failed to parse the timespan specification. | |
| 516 msg = "Failed to parse timespan! (input %r was tokenized as %r)" | |
| 517 raise InvalidTimespan(format(msg, timespan, tokens)) | |
| 518 | |
| 519 | |
| 520 def parse_date(datestring): | |
| 521 """ | |
| 522 Parse a date/time string into a tuple of integers. | |
| 523 | |
| 524 :param datestring: The date/time string to parse. | |
| 525 :returns: A tuple with the numbers ``(year, month, day, hour, minute, | |
| 526 second)`` (all numbers are integers). | |
| 527 :raises: :exc:`InvalidDate` when the date cannot be parsed. | |
| 528 | |
| 529 Supported date/time formats: | |
| 530 | |
| 531 - ``YYYY-MM-DD`` | |
| 532 - ``YYYY-MM-DD HH:MM:SS`` | |
| 533 | |
| 534 .. note:: If you want to parse date/time strings with a fixed, known | |
| 535 format and :func:`parse_date()` isn't useful to you, consider | |
| 536 :func:`time.strptime()` or :meth:`datetime.datetime.strptime()`, | |
| 537 both of which are included in the Python standard library. | |
| 538 Alternatively for more complex tasks consider using the date/time | |
| 539 parsing module in the dateutil_ package. | |
| 540 | |
| 541 Examples: | |
| 542 | |
| 543 >>> from humanfriendly import parse_date | |
| 544 >>> parse_date('2013-06-17') | |
| 545 (2013, 6, 17, 0, 0, 0) | |
| 546 >>> parse_date('2013-06-17 02:47:42') | |
| 547 (2013, 6, 17, 2, 47, 42) | |
| 548 | |
| 549 Here's how you convert the result to a number (`Unix time`_): | |
| 550 | |
| 551 >>> from humanfriendly import parse_date | |
| 552 >>> from time import mktime | |
| 553 >>> mktime(parse_date('2013-06-17 02:47:42') + (-1, -1, -1)) | |
| 554 1371430062.0 | |
| 555 | |
| 556 And here's how you convert it to a :class:`datetime.datetime` object: | |
| 557 | |
| 558 >>> from humanfriendly import parse_date | |
| 559 >>> from datetime import datetime | |
| 560 >>> datetime(*parse_date('2013-06-17 02:47:42')) | |
| 561 datetime.datetime(2013, 6, 17, 2, 47, 42) | |
| 562 | |
| 563 Here's an example that combines :func:`format_timespan()` and | |
| 564 :func:`parse_date()` to calculate a human friendly timespan since a | |
| 565 given date: | |
| 566 | |
| 567 >>> from humanfriendly import format_timespan, parse_date | |
| 568 >>> from time import mktime, time | |
| 569 >>> unix_time = mktime(parse_date('2013-06-17 02:47:42') + (-1, -1, -1)) | |
| 570 >>> seconds_since_then = time() - unix_time | |
| 571 >>> print(format_timespan(seconds_since_then)) | |
| 572 1 year, 43 weeks and 1 day | |
| 573 | |
| 574 .. _dateutil: https://dateutil.readthedocs.io/en/latest/parser.html | |
| 575 .. _Unix time: http://en.wikipedia.org/wiki/Unix_time | |
| 576 """ | |
| 577 try: | |
| 578 tokens = [t.strip() for t in datestring.split()] | |
| 579 if len(tokens) >= 2: | |
| 580 date_parts = list(map(int, tokens[0].split('-'))) + [1, 1] | |
| 581 time_parts = list(map(int, tokens[1].split(':'))) + [0, 0, 0] | |
| 582 return tuple(date_parts[0:3] + time_parts[0:3]) | |
| 583 else: | |
| 584 year, month, day = (list(map(int, datestring.split('-'))) + [1, 1])[0:3] | |
| 585 return (year, month, day, 0, 0, 0) | |
| 586 except Exception: | |
| 587 msg = "Invalid date! (expected 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS' but got: %r)" | |
| 588 raise InvalidDate(format(msg, datestring)) | |
| 589 | |
| 590 | |
| 591 def format_path(pathname): | |
| 592 """ | |
| 593 Shorten a pathname to make it more human friendly. | |
| 594 | |
| 595 :param pathname: An absolute pathname (a string). | |
| 596 :returns: The pathname with the user's home directory abbreviated. | |
| 597 | |
| 598 Given an absolute pathname, this function abbreviates the user's home | |
| 599 directory to ``~/`` in order to shorten the pathname without losing | |
| 600 information. It is not an error if the pathname is not relative to the | |
| 601 current user's home directory. | |
| 602 | |
| 603 Here's an example of its usage: | |
| 604 | |
| 605 >>> from os import environ | |
| 606 >>> from os.path import join | |
| 607 >>> vimrc = join(environ['HOME'], '.vimrc') | |
| 608 >>> vimrc | |
| 609 '/home/peter/.vimrc' | |
| 610 >>> from humanfriendly import format_path | |
| 611 >>> format_path(vimrc) | |
| 612 '~/.vimrc' | |
| 613 """ | |
| 614 pathname = os.path.abspath(pathname) | |
| 615 home = os.environ.get('HOME') | |
| 616 if home: | |
| 617 home = os.path.abspath(home) | |
| 618 if pathname.startswith(home): | |
| 619 pathname = os.path.join('~', os.path.relpath(pathname, home)) | |
| 620 return pathname | |
| 621 | |
| 622 | |
| 623 def parse_path(pathname): | |
| 624 """ | |
| 625 Convert a human friendly pathname to an absolute pathname. | |
| 626 | |
| 627 Expands leading tildes using :func:`os.path.expanduser()` and | |
| 628 environment variables using :func:`os.path.expandvars()` and makes the | |
| 629 resulting pathname absolute using :func:`os.path.abspath()`. | |
| 630 | |
| 631 :param pathname: A human friendly pathname (a string). | |
| 632 :returns: An absolute pathname (a string). | |
| 633 """ | |
| 634 return os.path.abspath(os.path.expanduser(os.path.expandvars(pathname))) | |
| 635 | |
| 636 | |
| 637 class Timer(object): | |
| 638 | |
| 639 """ | |
| 640 Easy to use timer to keep track of long during operations. | |
| 641 """ | |
| 642 | |
| 643 def __init__(self, start_time=None, resumable=False): | |
| 644 """ | |
| 645 Remember the time when the :class:`Timer` was created. | |
| 646 | |
| 647 :param start_time: The start time (a float, defaults to the current time). | |
| 648 :param resumable: Create a resumable timer (defaults to :data:`False`). | |
| 649 | |
| 650 When `start_time` is given :class:`Timer` uses :func:`time.time()` as a | |
| 651 clock source, otherwise it uses :func:`humanfriendly.compat.monotonic()`. | |
| 652 """ | |
| 653 if resumable: | |
| 654 self.monotonic = True | |
| 655 self.resumable = True | |
| 656 self.start_time = 0.0 | |
| 657 self.total_time = 0.0 | |
| 658 elif start_time: | |
| 659 self.monotonic = False | |
| 660 self.resumable = False | |
| 661 self.start_time = start_time | |
| 662 else: | |
| 663 self.monotonic = True | |
| 664 self.resumable = False | |
| 665 self.start_time = monotonic() | |
| 666 | |
| 667 def __enter__(self): | |
| 668 """ | |
| 669 Start or resume counting elapsed time. | |
| 670 | |
| 671 :returns: The :class:`Timer` object. | |
| 672 :raises: :exc:`~exceptions.ValueError` when the timer isn't resumable. | |
| 673 """ | |
| 674 if not self.resumable: | |
| 675 raise ValueError("Timer is not resumable!") | |
| 676 self.start_time = monotonic() | |
| 677 return self | |
| 678 | |
| 679 def __exit__(self, exc_type=None, exc_value=None, traceback=None): | |
| 680 """ | |
| 681 Stop counting elapsed time. | |
| 682 | |
| 683 :raises: :exc:`~exceptions.ValueError` when the timer isn't resumable. | |
| 684 """ | |
| 685 if not self.resumable: | |
| 686 raise ValueError("Timer is not resumable!") | |
| 687 if self.start_time: | |
| 688 self.total_time += monotonic() - self.start_time | |
| 689 self.start_time = 0.0 | |
| 690 | |
| 691 def sleep(self, seconds): | |
| 692 """ | |
| 693 Easy to use rate limiting of repeating actions. | |
| 694 | |
| 695 :param seconds: The number of seconds to sleep (an | |
| 696 integer or floating point number). | |
| 697 | |
| 698 This method sleeps for the given number of seconds minus the | |
| 699 :attr:`elapsed_time`. If the resulting duration is negative | |
| 700 :func:`time.sleep()` will still be called, but the argument | |
| 701 given to it will be the number 0 (negative numbers cause | |
| 702 :func:`time.sleep()` to raise an exception). | |
| 703 | |
| 704 The use case for this is to initialize a :class:`Timer` inside | |
| 705 the body of a :keyword:`for` or :keyword:`while` loop and call | |
| 706 :func:`Timer.sleep()` at the end of the loop body to rate limit | |
| 707 whatever it is that is being done inside the loop body. | |
| 708 | |
| 709 For posterity: Although the implementation of :func:`sleep()` only | |
| 710 requires a single line of code I've added it to :mod:`humanfriendly` | |
| 711 anyway because now that I've thought about how to tackle this once I | |
| 712 never want to have to think about it again :-P (unless I find ways to | |
| 713 improve this). | |
| 714 """ | |
| 715 time.sleep(max(0, seconds - self.elapsed_time)) | |
| 716 | |
| 717 @property | |
| 718 def elapsed_time(self): | |
| 719 """ | |
| 720 Get the number of seconds counted so far. | |
| 721 """ | |
| 722 elapsed_time = 0 | |
| 723 if self.resumable: | |
| 724 elapsed_time += self.total_time | |
| 725 if self.start_time: | |
| 726 current_time = monotonic() if self.monotonic else time.time() | |
| 727 elapsed_time += current_time - self.start_time | |
| 728 return elapsed_time | |
| 729 | |
| 730 @property | |
| 731 def rounded(self): | |
| 732 """Human readable timespan rounded to seconds (a string).""" | |
| 733 return format_timespan(round(self.elapsed_time)) | |
| 734 | |
| 735 def __str__(self): | |
| 736 """Show the elapsed time since the :class:`Timer` was created.""" | |
| 737 return format_timespan(self.elapsed_time) | |
| 738 | |
| 739 | |
| 740 class InvalidDate(Exception): | |
| 741 | |
| 742 """ | |
| 743 Raised when a string cannot be parsed into a date. | |
| 744 | |
| 745 For example: | |
| 746 | |
| 747 >>> from humanfriendly import parse_date | |
| 748 >>> parse_date('2013-06-XY') | |
| 749 Traceback (most recent call last): | |
| 750 File "humanfriendly.py", line 206, in parse_date | |
| 751 raise InvalidDate(format(msg, datestring)) | |
| 752 humanfriendly.InvalidDate: Invalid date! (expected 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS' but got: '2013-06-XY') | |
| 753 """ | |
| 754 | |
| 755 | |
| 756 class InvalidSize(Exception): | |
| 757 | |
| 758 """ | |
| 759 Raised when a string cannot be parsed into a file size. | |
| 760 | |
| 761 For example: | |
| 762 | |
| 763 >>> from humanfriendly import parse_size | |
| 764 >>> parse_size('5 Z') | |
| 765 Traceback (most recent call last): | |
| 766 File "humanfriendly/__init__.py", line 267, in parse_size | |
| 767 raise InvalidSize(format(msg, size, tokens)) | |
| 768 humanfriendly.InvalidSize: Failed to parse size! (input '5 Z' was tokenized as [5, 'Z']) | |
| 769 """ | |
| 770 | |
| 771 | |
| 772 class InvalidLength(Exception): | |
| 773 | |
| 774 """ | |
| 775 Raised when a string cannot be parsed into a length. | |
| 776 | |
| 777 For example: | |
| 778 | |
| 779 >>> from humanfriendly import parse_length | |
| 780 >>> parse_length('5 Z') | |
| 781 Traceback (most recent call last): | |
| 782 File "humanfriendly/__init__.py", line 267, in parse_length | |
| 783 raise InvalidLength(format(msg, length, tokens)) | |
| 784 humanfriendly.InvalidLength: Failed to parse length! (input '5 Z' was tokenized as [5, 'Z']) | |
| 785 """ | |
| 786 | |
| 787 | |
| 788 class InvalidTimespan(Exception): | |
| 789 | |
| 790 """ | |
| 791 Raised when a string cannot be parsed into a timespan. | |
| 792 | |
| 793 For example: | |
| 794 | |
| 795 >>> from humanfriendly import parse_timespan | |
| 796 >>> parse_timespan('1 age') | |
| 797 Traceback (most recent call last): | |
| 798 File "humanfriendly/__init__.py", line 419, in parse_timespan | |
| 799 raise InvalidTimespan(format(msg, timespan, tokens)) | |
| 800 humanfriendly.InvalidTimespan: Failed to parse timespan! (input '1 age' was tokenized as [1, 'age']) | |
| 801 """ | |
| 802 | |
| 803 | |
| 804 # Define aliases for backwards compatibility. | |
| 805 define_aliases( | |
| 806 module_name=__name__, | |
| 807 # In humanfriendly 1.23 the format_table() function was added to render a | |
| 808 # table using characters like dashes and vertical bars to emulate borders. | |
| 809 # Since then support for other tables has been added and the name of | |
| 810 # format_table() has changed. | |
| 811 format_table='humanfriendly.tables.format_pretty_table', | |
| 812 # In humanfriendly 1.30 the following text manipulation functions were | |
| 813 # moved out into a separate module to enable their usage in other modules | |
| 814 # of the humanfriendly package (without causing circular imports). | |
| 815 compact='humanfriendly.text.compact', | |
| 816 concatenate='humanfriendly.text.concatenate', | |
| 817 dedent='humanfriendly.text.dedent', | |
| 818 format='humanfriendly.text.format', | |
| 819 is_empty_line='humanfriendly.text.is_empty_line', | |
| 820 pluralize='humanfriendly.text.pluralize', | |
| 821 tokenize='humanfriendly.text.tokenize', | |
| 822 trim_empty_lines='humanfriendly.text.trim_empty_lines', | |
| 823 # In humanfriendly 1.38 the prompt_for_choice() function was moved out into a | |
| 824 # separate module because several variants of interactive prompts were added. | |
| 825 prompt_for_choice='humanfriendly.prompts.prompt_for_choice', | |
| 826 # In humanfriendly 8.0 the Spinner class and minimum_spinner_interval | |
| 827 # variable were extracted to a new module and the erase_line_code, | |
| 828 # hide_cursor_code and show_cursor_code variables were moved. | |
| 829 AutomaticSpinner='humanfriendly.terminal.spinners.AutomaticSpinner', | |
| 830 Spinner='humanfriendly.terminal.spinners.Spinner', | |
| 831 erase_line_code='humanfriendly.terminal.ANSI_ERASE_LINE', | |
| 832 hide_cursor_code='humanfriendly.terminal.ANSI_SHOW_CURSOR', | |
| 833 minimum_spinner_interval='humanfriendly.terminal.spinners.MINIMUM_INTERVAL', | |
| 834 show_cursor_code='humanfriendly.terminal.ANSI_HIDE_CURSOR', | |
| 835 ) |
