No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 
 
 

265 líneas
8.2 KiB

  1. """
  2. some helper functions that might be generally useful
  3. """
  4. from __future__ import annotations
  5. import datetime
  6. from functools import partial
  7. import re
  8. if False: # MYPY
  9. from typing import Any, Dict, Optional, List, Text, Callable, Union # NOQA
  10. from .compat import StreamTextType # NOQA
  11. class LazyEval:
  12. """
  13. Lightweight wrapper around lazily evaluated func(*args, **kwargs).
  14. func is only evaluated when any attribute of its return value is accessed.
  15. Every attribute access is passed through to the wrapped value.
  16. (This only excludes special cases like method-wrappers, e.g., __hash__.)
  17. The sole additional attribute is the lazy_self function which holds the
  18. return value (or, prior to evaluation, func and arguments), in its closure.
  19. """
  20. def __init__(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> None:
  21. def lazy_self() -> Any:
  22. return_value = func(*args, **kwargs)
  23. object.__setattr__(self, 'lazy_self', lambda: return_value)
  24. return return_value
  25. object.__setattr__(self, 'lazy_self', lazy_self)
  26. def __getattribute__(self, name: str) -> Any:
  27. lazy_self = object.__getattribute__(self, 'lazy_self')
  28. if name == 'lazy_self':
  29. return lazy_self
  30. return getattr(lazy_self(), name)
  31. def __setattr__(self, name: str, value: Any) -> None:
  32. setattr(self.lazy_self(), name, value)
  33. RegExp = partial(LazyEval, re.compile)
  34. timestamp_regexp = RegExp(
  35. """^(?P<year>[0-9][0-9][0-9][0-9])
  36. -(?P<month>[0-9][0-9]?)
  37. -(?P<day>[0-9][0-9]?)
  38. (?:((?P<t>[Tt])|[ \\t]+) # explictly not retaining extra spaces
  39. (?P<hour>[0-9][0-9]?)
  40. :(?P<minute>[0-9][0-9])
  41. :(?P<second>[0-9][0-9])
  42. (?:\\.(?P<fraction>[0-9]*))?
  43. (?:[ \\t]*(?P<tz>Z|(?P<tz_sign>[-+])(?P<tz_hour>[0-9][0-9]?)
  44. (?::(?P<tz_minute>[0-9][0-9]))?))?)?$""",
  45. re.X,
  46. )
  47. def create_timestamp(
  48. year: Any,
  49. month: Any,
  50. day: Any,
  51. t: Any,
  52. hour: Any,
  53. minute: Any,
  54. second: Any,
  55. fraction: Any,
  56. tz: Any,
  57. tz_sign: Any,
  58. tz_hour: Any,
  59. tz_minute: Any,
  60. ) -> Union[datetime.datetime, datetime.date]:
  61. # create a timestamp from matching against timestamp_regexp
  62. MAX_FRAC = 999999
  63. year = int(year)
  64. month = int(month)
  65. day = int(day)
  66. if hour is None:
  67. return datetime.date(year, month, day)
  68. hour = int(hour)
  69. minute = int(minute)
  70. second = int(second)
  71. frac = 0
  72. if fraction:
  73. frac_s = fraction[:6]
  74. while len(frac_s) < 6:
  75. frac_s += '0'
  76. frac = int(frac_s)
  77. if len(fraction) > 6 and int(fraction[6]) > 4:
  78. frac += 1
  79. if frac > MAX_FRAC:
  80. fraction = 0
  81. else:
  82. fraction = frac
  83. else:
  84. fraction = 0
  85. tzinfo = None
  86. delta = None
  87. if tz_sign:
  88. tz_hour = int(tz_hour)
  89. tz_minute = int(tz_minute) if tz_minute else 0
  90. td = datetime.timedelta(
  91. hours=tz_hour, minutes=tz_minute,
  92. )
  93. if tz_sign == '-':
  94. td = -td
  95. tzinfo = datetime.timezone(td, name=tz)
  96. elif tz == 'Z':
  97. tzinfo = datetime.timezone(datetime.timedelta(hours=0), name=tz)
  98. if frac > MAX_FRAC:
  99. delta = -datetime.timedelta(seconds=1)
  100. # should do something else instead (or hook this up to the preceding if statement
  101. # in reverse
  102. # if delta is None:
  103. # return datetime.datetime(year, month, day, hour, minute, second, fraction)
  104. # return datetime.datetime(year, month, day, hour, minute, second, fraction,
  105. # datetime.timezone.utc)
  106. # the above is not good enough though, should provide tzinfo. In Python3 that is easily
  107. # doable drop that kind of support for Python2 as it has not native tzinfo
  108. data = datetime.datetime(year, month, day, hour, minute, second, fraction, tzinfo)
  109. if delta:
  110. data -= delta
  111. return data
  112. # originally as comment
  113. # https://github.com/pre-commit/pre-commit/pull/211#issuecomment-186466605
  114. # if you use this in your code, I suggest adding a test in your test suite
  115. # that check this routines output against a known piece of your YAML
  116. # before upgrades to this code break your round-tripped YAML
  117. def load_yaml_guess_indent(stream: StreamTextType, **kw: Any) -> Any:
  118. """guess the indent and block sequence indent of yaml stream/string
  119. returns round_trip_loaded stream, indent level, block sequence indent
  120. - block sequence indent is the number of spaces before a dash relative to previous indent
  121. - if there are no block sequences, indent is taken from nested mappings, block sequence
  122. indent is unset (None) in that case
  123. """
  124. from .main import YAML
  125. # load a YAML document, guess the indentation, if you use TABs you are on your own
  126. def leading_spaces(line: Any) -> int:
  127. idx = 0
  128. while idx < len(line) and line[idx] == ' ':
  129. idx += 1
  130. return idx
  131. if isinstance(stream, str):
  132. yaml_str: Any = stream
  133. elif isinstance(stream, bytes):
  134. # most likely, but the Reader checks BOM for this
  135. yaml_str = stream.decode('utf-8')
  136. else:
  137. yaml_str = stream.read()
  138. map_indent = None
  139. indent = None # default if not found for some reason
  140. block_seq_indent = None
  141. prev_line_key_only = None
  142. key_indent = 0
  143. for line in yaml_str.splitlines():
  144. rline = line.rstrip()
  145. lline = rline.lstrip()
  146. if lline.startswith('- '):
  147. l_s = leading_spaces(line)
  148. block_seq_indent = l_s - key_indent
  149. idx = l_s + 1
  150. while line[idx] == ' ': # this will end as we rstripped
  151. idx += 1
  152. if line[idx] == '#': # comment after -
  153. continue
  154. indent = idx - key_indent
  155. break
  156. if map_indent is None and prev_line_key_only is not None and rline:
  157. idx = 0
  158. while line[idx] in ' -':
  159. idx += 1
  160. if idx > prev_line_key_only:
  161. map_indent = idx - prev_line_key_only
  162. if rline.endswith(':'):
  163. key_indent = leading_spaces(line)
  164. idx = 0
  165. while line[idx] == ' ': # this will end on ':'
  166. idx += 1
  167. prev_line_key_only = idx
  168. continue
  169. prev_line_key_only = None
  170. if indent is None and map_indent is not None:
  171. indent = map_indent
  172. yaml = YAML() if 'yaml' not in kw else kw.pop('yaml')
  173. return yaml.load(yaml_str, **kw), indent, block_seq_indent
  174. def configobj_walker(cfg: Any) -> Any:
  175. """
  176. walks over a ConfigObj (INI file with comments) generating
  177. corresponding YAML output (including comments
  178. """
  179. from configobj import ConfigObj # type: ignore
  180. assert isinstance(cfg, ConfigObj)
  181. for c in cfg.initial_comment:
  182. if c.strip():
  183. yield c
  184. for s in _walk_section(cfg):
  185. if s.strip():
  186. yield s
  187. for c in cfg.final_comment:
  188. if c.strip():
  189. yield c
  190. def _walk_section(s: Any, level: int = 0) -> Any:
  191. from configobj import Section
  192. assert isinstance(s, Section)
  193. indent = ' ' * level
  194. for name in s.scalars:
  195. for c in s.comments[name]:
  196. yield indent + c.strip()
  197. x = s[name]
  198. if '\n' in x:
  199. i = indent + ' '
  200. x = '|\n' + i + x.strip().replace('\n', '\n' + i)
  201. elif ':' in x:
  202. x = "'" + x.replace("'", "''") + "'"
  203. line = f'{indent}{name}: {x}'
  204. c = s.inline_comments[name]
  205. if c:
  206. line += ' ' + c
  207. yield line
  208. for name in s.sections:
  209. for c in s.comments[name]:
  210. yield indent + c.strip()
  211. line = f'{indent}{name}:'
  212. c = s.inline_comments[name]
  213. if c:
  214. line += ' ' + c
  215. yield line
  216. for val in _walk_section(s[name], level=level + 1):
  217. yield val
  218. # def config_obj_2_rt_yaml(cfg):
  219. # from .comments import CommentedMap, CommentedSeq
  220. # from configobj import ConfigObj
  221. # assert isinstance(cfg, ConfigObj)
  222. # #for c in cfg.initial_comment:
  223. # # if c.strip():
  224. # # pass
  225. # cm = CommentedMap()
  226. # for name in s.sections:
  227. # cm[name] = d = CommentedMap()
  228. #
  229. #
  230. # #for c in cfg.final_comment:
  231. # # if c.strip():
  232. # # yield c
  233. # return cm