comparison env/lib/python3.9/site-packages/humanfriendly/__init__.py @ 0:4f3585e2f14b draft default tip

"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author shellac
date Mon, 22 Mar 2021 18:12:50 +0000
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:4f3585e2f14b
1 # Human friendly input/output in Python.
2 #
3 # Author: Peter Odding <peter@peterodding.com>
4 # Last Change: December 10, 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__ = '9.1'
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 negative_sign = integer_part.startswith('-')
357 reversed_digits = ''.join(reversed(integer_part.lstrip('-')))
358 parts = []
359 while reversed_digits:
360 parts.append(reversed_digits[:3])
361 reversed_digits = reversed_digits[3:]
362 formatted_number = ''.join(reversed(','.join(parts)))
363 decimals_to_add = decimal_part[:num_decimals].rstrip('0')
364 if decimals_to_add:
365 formatted_number += '.' + decimals_to_add
366 if negative_sign:
367 formatted_number = '-' + formatted_number
368 return formatted_number
369
370
371 def round_number(count, keep_width=False):
372 """
373 Round a floating point number to two decimal places in a human friendly format.
374
375 :param count: The number to format.
376 :param keep_width: :data:`True` if trailing zeros should not be stripped,
377 :data:`False` if they can be stripped.
378 :returns: The formatted number as a string. If no decimal places are
379 required to represent the number, they will be omitted.
380
381 The main purpose of this function is to be used by functions like
382 :func:`format_length()`, :func:`format_size()` and
383 :func:`format_timespan()`.
384
385 Here are some examples:
386
387 >>> from humanfriendly import round_number
388 >>> round_number(1)
389 '1'
390 >>> round_number(math.pi)
391 '3.14'
392 >>> round_number(5.001)
393 '5'
394 """
395 text = '%.2f' % float(count)
396 if not keep_width:
397 text = re.sub('0+$', '', text)
398 text = re.sub(r'\.$', '', text)
399 return text
400
401
402 def format_timespan(num_seconds, detailed=False, max_units=3):
403 """
404 Format a timespan in seconds as a human readable string.
405
406 :param num_seconds: Any value accepted by :func:`coerce_seconds()`.
407 :param detailed: If :data:`True` milliseconds are represented separately
408 instead of being represented as fractional seconds
409 (defaults to :data:`False`).
410 :param max_units: The maximum number of units to show in the formatted time
411 span (an integer, defaults to three).
412 :returns: The formatted timespan as a string.
413 :raise: See :func:`coerce_seconds()`.
414
415 Some examples:
416
417 >>> from humanfriendly import format_timespan
418 >>> format_timespan(0)
419 '0 seconds'
420 >>> format_timespan(1)
421 '1 second'
422 >>> import math
423 >>> format_timespan(math.pi)
424 '3.14 seconds'
425 >>> hour = 60 * 60
426 >>> day = hour * 24
427 >>> week = day * 7
428 >>> format_timespan(week * 52 + day * 2 + hour * 3)
429 '1 year, 2 days and 3 hours'
430 """
431 num_seconds = coerce_seconds(num_seconds)
432 if num_seconds < 60 and not detailed:
433 # Fast path.
434 return pluralize(round_number(num_seconds), 'second')
435 else:
436 # Slow path.
437 result = []
438 num_seconds = decimal.Decimal(str(num_seconds))
439 relevant_units = list(reversed(time_units[0 if detailed else 3:]))
440 for unit in relevant_units:
441 # Extract the unit count from the remaining time.
442 divider = decimal.Decimal(str(unit['divider']))
443 count = num_seconds / divider
444 num_seconds %= divider
445 # Round the unit count appropriately.
446 if unit != relevant_units[-1]:
447 # Integer rounding for all but the smallest unit.
448 count = int(count)
449 else:
450 # Floating point rounding for the smallest unit.
451 count = round_number(count)
452 # Only include relevant units in the result.
453 if count not in (0, '0'):
454 result.append(pluralize(count, unit['singular'], unit['plural']))
455 if len(result) == 1:
456 # A single count/unit combination.
457 return result[0]
458 else:
459 if not detailed:
460 # Remove `insignificant' data from the formatted timespan.
461 result = result[:max_units]
462 # Format the timespan in a readable way.
463 return concatenate(result)
464
465
466 def parse_timespan(timespan):
467 """
468 Parse a "human friendly" timespan into the number of seconds.
469
470 :param value: A string like ``5h`` (5 hours), ``10m`` (10 minutes) or
471 ``42s`` (42 seconds).
472 :returns: The number of seconds as a floating point number.
473 :raises: :exc:`InvalidTimespan` when the input can't be parsed.
474
475 Note that the :func:`parse_timespan()` function is not meant to be the
476 "mirror image" of the :func:`format_timespan()` function. Instead it's
477 meant to allow humans to easily and succinctly specify a timespan with a
478 minimal amount of typing. It's very useful to accept easy to write time
479 spans as e.g. command line arguments to programs.
480
481 The time units (and abbreviations) supported by this function are:
482
483 - ms, millisecond, milliseconds
484 - s, sec, secs, second, seconds
485 - m, min, mins, minute, minutes
486 - h, hour, hours
487 - d, day, days
488 - w, week, weeks
489 - y, year, years
490
491 Some examples:
492
493 >>> from humanfriendly import parse_timespan
494 >>> parse_timespan('42')
495 42.0
496 >>> parse_timespan('42s')
497 42.0
498 >>> parse_timespan('1m')
499 60.0
500 >>> parse_timespan('1h')
501 3600.0
502 >>> parse_timespan('1d')
503 86400.0
504 """
505 tokens = tokenize(timespan)
506 if tokens and isinstance(tokens[0], numbers.Number):
507 # If the input contains only a number, it's assumed to be the number of seconds.
508 if len(tokens) == 1:
509 return float(tokens[0])
510 # Otherwise we expect to find two tokens: A number and a unit.
511 if len(tokens) == 2 and is_string(tokens[1]):
512 normalized_unit = tokens[1].lower()
513 for unit in time_units:
514 if (normalized_unit == unit['singular'] or
515 normalized_unit == unit['plural'] or
516 normalized_unit in unit['abbreviations']):
517 return float(tokens[0]) * unit['divider']
518 # We failed to parse the timespan specification.
519 msg = "Failed to parse timespan! (input %r was tokenized as %r)"
520 raise InvalidTimespan(format(msg, timespan, tokens))
521
522
523 def parse_date(datestring):
524 """
525 Parse a date/time string into a tuple of integers.
526
527 :param datestring: The date/time string to parse.
528 :returns: A tuple with the numbers ``(year, month, day, hour, minute,
529 second)`` (all numbers are integers).
530 :raises: :exc:`InvalidDate` when the date cannot be parsed.
531
532 Supported date/time formats:
533
534 - ``YYYY-MM-DD``
535 - ``YYYY-MM-DD HH:MM:SS``
536
537 .. note:: If you want to parse date/time strings with a fixed, known
538 format and :func:`parse_date()` isn't useful to you, consider
539 :func:`time.strptime()` or :meth:`datetime.datetime.strptime()`,
540 both of which are included in the Python standard library.
541 Alternatively for more complex tasks consider using the date/time
542 parsing module in the dateutil_ package.
543
544 Examples:
545
546 >>> from humanfriendly import parse_date
547 >>> parse_date('2013-06-17')
548 (2013, 6, 17, 0, 0, 0)
549 >>> parse_date('2013-06-17 02:47:42')
550 (2013, 6, 17, 2, 47, 42)
551
552 Here's how you convert the result to a number (`Unix time`_):
553
554 >>> from humanfriendly import parse_date
555 >>> from time import mktime
556 >>> mktime(parse_date('2013-06-17 02:47:42') + (-1, -1, -1))
557 1371430062.0
558
559 And here's how you convert it to a :class:`datetime.datetime` object:
560
561 >>> from humanfriendly import parse_date
562 >>> from datetime import datetime
563 >>> datetime(*parse_date('2013-06-17 02:47:42'))
564 datetime.datetime(2013, 6, 17, 2, 47, 42)
565
566 Here's an example that combines :func:`format_timespan()` and
567 :func:`parse_date()` to calculate a human friendly timespan since a
568 given date:
569
570 >>> from humanfriendly import format_timespan, parse_date
571 >>> from time import mktime, time
572 >>> unix_time = mktime(parse_date('2013-06-17 02:47:42') + (-1, -1, -1))
573 >>> seconds_since_then = time() - unix_time
574 >>> print(format_timespan(seconds_since_then))
575 1 year, 43 weeks and 1 day
576
577 .. _dateutil: https://dateutil.readthedocs.io/en/latest/parser.html
578 .. _Unix time: http://en.wikipedia.org/wiki/Unix_time
579 """
580 try:
581 tokens = [t.strip() for t in datestring.split()]
582 if len(tokens) >= 2:
583 date_parts = list(map(int, tokens[0].split('-'))) + [1, 1]
584 time_parts = list(map(int, tokens[1].split(':'))) + [0, 0, 0]
585 return tuple(date_parts[0:3] + time_parts[0:3])
586 else:
587 year, month, day = (list(map(int, datestring.split('-'))) + [1, 1])[0:3]
588 return (year, month, day, 0, 0, 0)
589 except Exception:
590 msg = "Invalid date! (expected 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS' but got: %r)"
591 raise InvalidDate(format(msg, datestring))
592
593
594 def format_path(pathname):
595 """
596 Shorten a pathname to make it more human friendly.
597
598 :param pathname: An absolute pathname (a string).
599 :returns: The pathname with the user's home directory abbreviated.
600
601 Given an absolute pathname, this function abbreviates the user's home
602 directory to ``~/`` in order to shorten the pathname without losing
603 information. It is not an error if the pathname is not relative to the
604 current user's home directory.
605
606 Here's an example of its usage:
607
608 >>> from os import environ
609 >>> from os.path import join
610 >>> vimrc = join(environ['HOME'], '.vimrc')
611 >>> vimrc
612 '/home/peter/.vimrc'
613 >>> from humanfriendly import format_path
614 >>> format_path(vimrc)
615 '~/.vimrc'
616 """
617 pathname = os.path.abspath(pathname)
618 home = os.environ.get('HOME')
619 if home:
620 home = os.path.abspath(home)
621 if pathname.startswith(home):
622 pathname = os.path.join('~', os.path.relpath(pathname, home))
623 return pathname
624
625
626 def parse_path(pathname):
627 """
628 Convert a human friendly pathname to an absolute pathname.
629
630 Expands leading tildes using :func:`os.path.expanduser()` and
631 environment variables using :func:`os.path.expandvars()` and makes the
632 resulting pathname absolute using :func:`os.path.abspath()`.
633
634 :param pathname: A human friendly pathname (a string).
635 :returns: An absolute pathname (a string).
636 """
637 return os.path.abspath(os.path.expanduser(os.path.expandvars(pathname)))
638
639
640 class Timer(object):
641
642 """
643 Easy to use timer to keep track of long during operations.
644 """
645
646 def __init__(self, start_time=None, resumable=False):
647 """
648 Remember the time when the :class:`Timer` was created.
649
650 :param start_time: The start time (a float, defaults to the current time).
651 :param resumable: Create a resumable timer (defaults to :data:`False`).
652
653 When `start_time` is given :class:`Timer` uses :func:`time.time()` as a
654 clock source, otherwise it uses :func:`humanfriendly.compat.monotonic()`.
655 """
656 if resumable:
657 self.monotonic = True
658 self.resumable = True
659 self.start_time = 0.0
660 self.total_time = 0.0
661 elif start_time:
662 self.monotonic = False
663 self.resumable = False
664 self.start_time = start_time
665 else:
666 self.monotonic = True
667 self.resumable = False
668 self.start_time = monotonic()
669
670 def __enter__(self):
671 """
672 Start or resume counting elapsed time.
673
674 :returns: The :class:`Timer` object.
675 :raises: :exc:`~exceptions.ValueError` when the timer isn't resumable.
676 """
677 if not self.resumable:
678 raise ValueError("Timer is not resumable!")
679 self.start_time = monotonic()
680 return self
681
682 def __exit__(self, exc_type=None, exc_value=None, traceback=None):
683 """
684 Stop counting elapsed time.
685
686 :raises: :exc:`~exceptions.ValueError` when the timer isn't resumable.
687 """
688 if not self.resumable:
689 raise ValueError("Timer is not resumable!")
690 if self.start_time:
691 self.total_time += monotonic() - self.start_time
692 self.start_time = 0.0
693
694 def sleep(self, seconds):
695 """
696 Easy to use rate limiting of repeating actions.
697
698 :param seconds: The number of seconds to sleep (an
699 integer or floating point number).
700
701 This method sleeps for the given number of seconds minus the
702 :attr:`elapsed_time`. If the resulting duration is negative
703 :func:`time.sleep()` will still be called, but the argument
704 given to it will be the number 0 (negative numbers cause
705 :func:`time.sleep()` to raise an exception).
706
707 The use case for this is to initialize a :class:`Timer` inside
708 the body of a :keyword:`for` or :keyword:`while` loop and call
709 :func:`Timer.sleep()` at the end of the loop body to rate limit
710 whatever it is that is being done inside the loop body.
711
712 For posterity: Although the implementation of :func:`sleep()` only
713 requires a single line of code I've added it to :mod:`humanfriendly`
714 anyway because now that I've thought about how to tackle this once I
715 never want to have to think about it again :-P (unless I find ways to
716 improve this).
717 """
718 time.sleep(max(0, seconds - self.elapsed_time))
719
720 @property
721 def elapsed_time(self):
722 """
723 Get the number of seconds counted so far.
724 """
725 elapsed_time = 0
726 if self.resumable:
727 elapsed_time += self.total_time
728 if self.start_time:
729 current_time = monotonic() if self.monotonic else time.time()
730 elapsed_time += current_time - self.start_time
731 return elapsed_time
732
733 @property
734 def rounded(self):
735 """Human readable timespan rounded to seconds (a string)."""
736 return format_timespan(round(self.elapsed_time))
737
738 def __str__(self):
739 """Show the elapsed time since the :class:`Timer` was created."""
740 return format_timespan(self.elapsed_time)
741
742
743 class InvalidDate(Exception):
744
745 """
746 Raised when a string cannot be parsed into a date.
747
748 For example:
749
750 >>> from humanfriendly import parse_date
751 >>> parse_date('2013-06-XY')
752 Traceback (most recent call last):
753 File "humanfriendly.py", line 206, in parse_date
754 raise InvalidDate(format(msg, datestring))
755 humanfriendly.InvalidDate: Invalid date! (expected 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS' but got: '2013-06-XY')
756 """
757
758
759 class InvalidSize(Exception):
760
761 """
762 Raised when a string cannot be parsed into a file size.
763
764 For example:
765
766 >>> from humanfriendly import parse_size
767 >>> parse_size('5 Z')
768 Traceback (most recent call last):
769 File "humanfriendly/__init__.py", line 267, in parse_size
770 raise InvalidSize(format(msg, size, tokens))
771 humanfriendly.InvalidSize: Failed to parse size! (input '5 Z' was tokenized as [5, 'Z'])
772 """
773
774
775 class InvalidLength(Exception):
776
777 """
778 Raised when a string cannot be parsed into a length.
779
780 For example:
781
782 >>> from humanfriendly import parse_length
783 >>> parse_length('5 Z')
784 Traceback (most recent call last):
785 File "humanfriendly/__init__.py", line 267, in parse_length
786 raise InvalidLength(format(msg, length, tokens))
787 humanfriendly.InvalidLength: Failed to parse length! (input '5 Z' was tokenized as [5, 'Z'])
788 """
789
790
791 class InvalidTimespan(Exception):
792
793 """
794 Raised when a string cannot be parsed into a timespan.
795
796 For example:
797
798 >>> from humanfriendly import parse_timespan
799 >>> parse_timespan('1 age')
800 Traceback (most recent call last):
801 File "humanfriendly/__init__.py", line 419, in parse_timespan
802 raise InvalidTimespan(format(msg, timespan, tokens))
803 humanfriendly.InvalidTimespan: Failed to parse timespan! (input '1 age' was tokenized as [1, 'age'])
804 """
805
806
807 # Define aliases for backwards compatibility.
808 define_aliases(
809 module_name=__name__,
810 # In humanfriendly 1.23 the format_table() function was added to render a
811 # table using characters like dashes and vertical bars to emulate borders.
812 # Since then support for other tables has been added and the name of
813 # format_table() has changed.
814 format_table='humanfriendly.tables.format_pretty_table',
815 # In humanfriendly 1.30 the following text manipulation functions were
816 # moved out into a separate module to enable their usage in other modules
817 # of the humanfriendly package (without causing circular imports).
818 compact='humanfriendly.text.compact',
819 concatenate='humanfriendly.text.concatenate',
820 dedent='humanfriendly.text.dedent',
821 format='humanfriendly.text.format',
822 is_empty_line='humanfriendly.text.is_empty_line',
823 pluralize='humanfriendly.text.pluralize',
824 tokenize='humanfriendly.text.tokenize',
825 trim_empty_lines='humanfriendly.text.trim_empty_lines',
826 # In humanfriendly 1.38 the prompt_for_choice() function was moved out into a
827 # separate module because several variants of interactive prompts were added.
828 prompt_for_choice='humanfriendly.prompts.prompt_for_choice',
829 # In humanfriendly 8.0 the Spinner class and minimum_spinner_interval
830 # variable were extracted to a new module and the erase_line_code,
831 # hide_cursor_code and show_cursor_code variables were moved.
832 AutomaticSpinner='humanfriendly.terminal.spinners.AutomaticSpinner',
833 Spinner='humanfriendly.terminal.spinners.Spinner',
834 erase_line_code='humanfriendly.terminal.ANSI_ERASE_LINE',
835 hide_cursor_code='humanfriendly.terminal.ANSI_SHOW_CURSOR',
836 minimum_spinner_interval='humanfriendly.terminal.spinners.MINIMUM_INTERVAL',
837 show_cursor_code='humanfriendly.terminal.ANSI_HIDE_CURSOR',
838 )