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 )