util.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. # coding: utf-8
  2. """
  3. some helper functions that might be generally useful
  4. """
  5. from __future__ import absolute_import, print_function
  6. from functools import partial
  7. import re
  8. from .compat import text_type, binary_type
  9. if False: # MYPY
  10. from typing import Any, Dict, Optional, List, Text # NOQA
  11. from .compat import StreamTextType # NOQA
  12. class LazyEval(object):
  13. """
  14. Lightweight wrapper around lazily evaluated func(*args, **kwargs).
  15. func is only evaluated when any attribute of its return value is accessed.
  16. Every attribute access is passed through to the wrapped value.
  17. (This only excludes special cases like method-wrappers, e.g., __hash__.)
  18. The sole additional attribute is the lazy_self function which holds the
  19. return value (or, prior to evaluation, func and arguments), in its closure.
  20. """
  21. def __init__(self, func, *args, **kwargs):
  22. # type: (Any, Any, Any) -> None
  23. def lazy_self():
  24. # type: () -> Any
  25. return_value = func(*args, **kwargs)
  26. object.__setattr__(self, 'lazy_self', lambda: return_value)
  27. return return_value
  28. object.__setattr__(self, 'lazy_self', lazy_self)
  29. def __getattribute__(self, name):
  30. # type: (Any) -> Any
  31. lazy_self = object.__getattribute__(self, 'lazy_self')
  32. if name == 'lazy_self':
  33. return lazy_self
  34. return getattr(lazy_self(), name)
  35. def __setattr__(self, name, value):
  36. # type: (Any, Any) -> None
  37. setattr(self.lazy_self(), name, value)
  38. RegExp = partial(LazyEval, re.compile)
  39. # originally as comment
  40. # https://github.com/pre-commit/pre-commit/pull/211#issuecomment-186466605
  41. # if you use this in your code, I suggest adding a test in your test suite
  42. # that check this routines output against a known piece of your YAML
  43. # before upgrades to this code break your round-tripped YAML
  44. def load_yaml_guess_indent(stream, **kw):
  45. # type: (StreamTextType, Any) -> Any
  46. """guess the indent and block sequence indent of yaml stream/string
  47. returns round_trip_loaded stream, indent level, block sequence indent
  48. - block sequence indent is the number of spaces before a dash relative to previous indent
  49. - if there are no block sequences, indent is taken from nested mappings, block sequence
  50. indent is unset (None) in that case
  51. """
  52. from .main import round_trip_load
  53. # load a YAML document, guess the indentation, if you use TABs you're on your own
  54. def leading_spaces(line):
  55. # type: (Any) -> int
  56. idx = 0
  57. while idx < len(line) and line[idx] == ' ':
  58. idx += 1
  59. return idx
  60. if isinstance(stream, text_type):
  61. yaml_str = stream # type: Any
  62. elif isinstance(stream, binary_type):
  63. # most likely, but the Reader checks BOM for this
  64. yaml_str = stream.decode('utf-8')
  65. else:
  66. yaml_str = stream.read()
  67. map_indent = None
  68. indent = None # default if not found for some reason
  69. block_seq_indent = None
  70. prev_line_key_only = None
  71. key_indent = 0
  72. for line in yaml_str.splitlines():
  73. rline = line.rstrip()
  74. lline = rline.lstrip()
  75. if lline.startswith('- '):
  76. l_s = leading_spaces(line)
  77. block_seq_indent = l_s - key_indent
  78. idx = l_s + 1
  79. while line[idx] == ' ': # this will end as we rstripped
  80. idx += 1
  81. if line[idx] == '#': # comment after -
  82. continue
  83. indent = idx - key_indent
  84. break
  85. if map_indent is None and prev_line_key_only is not None and rline:
  86. idx = 0
  87. while line[idx] in ' -':
  88. idx += 1
  89. if idx > prev_line_key_only:
  90. map_indent = idx - prev_line_key_only
  91. if rline.endswith(':'):
  92. key_indent = leading_spaces(line)
  93. idx = 0
  94. while line[idx] == ' ': # this will end on ':'
  95. idx += 1
  96. prev_line_key_only = idx
  97. continue
  98. prev_line_key_only = None
  99. if indent is None and map_indent is not None:
  100. indent = map_indent
  101. return round_trip_load(yaml_str, **kw), indent, block_seq_indent
  102. def configobj_walker(cfg):
  103. # type: (Any) -> Any
  104. """
  105. walks over a ConfigObj (INI file with comments) generating
  106. corresponding YAML output (including comments
  107. """
  108. from configobj import ConfigObj # type: ignore
  109. assert isinstance(cfg, ConfigObj)
  110. for c in cfg.initial_comment:
  111. if c.strip():
  112. yield c
  113. for s in _walk_section(cfg):
  114. if s.strip():
  115. yield s
  116. for c in cfg.final_comment:
  117. if c.strip():
  118. yield c
  119. def _walk_section(s, level=0):
  120. # type: (Any, int) -> Any
  121. from configobj import Section
  122. assert isinstance(s, Section)
  123. indent = u' ' * level
  124. for name in s.scalars:
  125. for c in s.comments[name]:
  126. yield indent + c.strip()
  127. x = s[name]
  128. if u'\n' in x:
  129. i = indent + u' '
  130. x = u'|\n' + i + x.strip().replace(u'\n', u'\n' + i)
  131. elif ':' in x:
  132. x = u"'" + x.replace(u"'", u"''") + u"'"
  133. line = u'{0}{1}: {2}'.format(indent, name, x)
  134. c = s.inline_comments[name]
  135. if c:
  136. line += u' ' + c
  137. yield line
  138. for name in s.sections:
  139. for c in s.comments[name]:
  140. yield indent + c.strip()
  141. line = u'{0}{1}:'.format(indent, name)
  142. c = s.inline_comments[name]
  143. if c:
  144. line += u' ' + c
  145. yield line
  146. for val in _walk_section(s[name], level=level + 1):
  147. yield val
  148. # def config_obj_2_rt_yaml(cfg):
  149. # from .comments import CommentedMap, CommentedSeq
  150. # from configobj import ConfigObj
  151. # assert isinstance(cfg, ConfigObj)
  152. # #for c in cfg.initial_comment:
  153. # # if c.strip():
  154. # # pass
  155. # cm = CommentedMap()
  156. # for name in s.sections:
  157. # cm[name] = d = CommentedMap()
  158. #
  159. #
  160. # #for c in cfg.final_comment:
  161. # # if c.strip():
  162. # # yield c
  163. # return cm