Mercurial > repos > shellac > sam_consensus_v3
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 ) |