m2m模型翻译
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

838 lines
31 KiB

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