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