comments.py 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166
  1. # coding: utf-8
  2. """
  3. stuff to deal with comments and formatting on dict/list/ordereddict/set
  4. these are not really related, formatting could be factored out as
  5. a separate base
  6. """
  7. import sys
  8. import copy
  9. from ruamel.yaml.compat import ordereddict
  10. from ruamel.yaml.compat import MutableSliceableSequence, nprintf # NOQA
  11. from ruamel.yaml.scalarstring import ScalarString
  12. from ruamel.yaml.anchor import Anchor
  13. from ruamel.yaml.tag import Tag
  14. from collections.abc import MutableSet, Sized, Set, Mapping
  15. from typing import Any, Dict, Optional, List, Union, Optional, Iterator # NOQA
  16. # fmt: off
  17. __all__ = ['CommentedSeq', 'CommentedKeySeq',
  18. 'CommentedMap', 'CommentedOrderedMap',
  19. 'CommentedSet', 'comment_attrib', 'merge_attrib',
  20. 'C_POST', 'C_PRE', 'C_SPLIT_ON_FIRST_BLANK', 'C_BLANK_LINE_PRESERVE_SPACE',
  21. ]
  22. # fmt: on
  23. # splitting of comments by the scanner
  24. # an EOLC (End-Of-Line Comment) is preceded by some token
  25. # an FLC (Full Line Comment) is a comment not preceded by a token, i.e. # is
  26. # the first non-blank on line
  27. # a BL is a blank line i.e. empty or spaces/tabs only
  28. # bits 0 and 1 are combined, you can choose only one
  29. C_POST = 0b00
  30. C_PRE = 0b01
  31. C_SPLIT_ON_FIRST_BLANK = 0b10 # as C_POST, but if blank line then C_PRE all lines before
  32. # first blank goes to POST even if no following real FLC
  33. # (first blank -> first of post)
  34. # 0b11 -> reserved for future use
  35. C_BLANK_LINE_PRESERVE_SPACE = 0b100
  36. # C_EOL_PRESERVE_SPACE2 = 0b1000
  37. class IDX:
  38. # temporary auto increment, so rearranging is easier
  39. def __init__(self) -> None:
  40. self._idx = 0
  41. def __call__(self) -> Any:
  42. x = self._idx
  43. self._idx += 1
  44. return x
  45. def __str__(self) -> Any:
  46. return str(self._idx)
  47. cidx = IDX()
  48. # more or less in order of subjective expected likelyhood
  49. # the _POST and _PRE ones are lists themselves
  50. C_VALUE_EOL = C_ELEM_EOL = cidx()
  51. C_KEY_EOL = cidx()
  52. C_KEY_PRE = C_ELEM_PRE = cidx() # not this is not value
  53. C_VALUE_POST = C_ELEM_POST = cidx() # not this is not value
  54. C_VALUE_PRE = cidx()
  55. C_KEY_POST = cidx()
  56. C_TAG_EOL = cidx()
  57. C_TAG_POST = cidx()
  58. C_TAG_PRE = cidx()
  59. C_ANCHOR_EOL = cidx()
  60. C_ANCHOR_POST = cidx()
  61. C_ANCHOR_PRE = cidx()
  62. comment_attrib = '_yaml_comment'
  63. format_attrib = '_yaml_format'
  64. line_col_attrib = '_yaml_line_col'
  65. merge_attrib = '_yaml_merge'
  66. class Comment:
  67. # using sys.getsize tested the Comment objects, __slots__ makes them bigger
  68. # and adding self.end did not matter
  69. __slots__ = 'comment', '_items', '_post', '_pre'
  70. attrib = comment_attrib
  71. def __init__(self, old: bool = True) -> None:
  72. self._pre = None if old else [] # type: ignore
  73. self.comment = None # [post, [pre]]
  74. # map key (mapping/omap/dict) or index (sequence/list) to a list of
  75. # dict: post_key, pre_key, post_value, pre_value
  76. # list: pre item, post item
  77. self._items: Dict[Any, Any] = {}
  78. # self._start = [] # should not put these on first item
  79. self._post: List[Any] = [] # end of document comments
  80. def __str__(self) -> str:
  81. if bool(self._post):
  82. end = ',\n end=' + str(self._post)
  83. else:
  84. end = ""
  85. return f'Comment(comment={self.comment},\n items={self._items}{end})'
  86. def _old__repr__(self) -> str:
  87. if bool(self._post):
  88. end = ',\n end=' + str(self._post)
  89. else:
  90. end = ""
  91. try:
  92. ln = max([len(str(k)) for k in self._items]) + 1
  93. except ValueError:
  94. ln = '' # type: ignore
  95. it = ' '.join([f'{str(k) + ":":{ln}} {v}\n' for k, v in self._items.items()])
  96. if it:
  97. it = '\n ' + it + ' '
  98. return f'Comment(\n start={self.comment},\n items={{{it}}}{end})'
  99. def __repr__(self) -> str:
  100. if self._pre is None:
  101. return self._old__repr__()
  102. if bool(self._post):
  103. end = ',\n end=' + repr(self._post)
  104. else:
  105. end = ""
  106. try:
  107. ln = max([len(str(k)) for k in self._items]) + 1
  108. except ValueError:
  109. ln = '' # type: ignore
  110. it = ' '.join([f'{str(k) + ":":{ln}} {v}\n' for k, v in self._items.items()])
  111. if it:
  112. it = '\n ' + it + ' '
  113. return f'Comment(\n pre={self.pre},\n items={{{it}}}{end})'
  114. @property
  115. def items(self) -> Any:
  116. return self._items
  117. @property
  118. def end(self) -> Any:
  119. return self._post
  120. @end.setter
  121. def end(self, value: Any) -> None:
  122. self._post = value
  123. @property
  124. def pre(self) -> Any:
  125. return self._pre
  126. @pre.setter
  127. def pre(self, value: Any) -> None:
  128. self._pre = value
  129. def get(self, item: Any, pos: Any) -> Any:
  130. x = self._items.get(item)
  131. if x is None or len(x) < pos:
  132. return None
  133. return x[pos] # can be None
  134. def set(self, item: Any, pos: Any, value: Any) -> Any:
  135. x = self._items.get(item)
  136. if x is None:
  137. self._items[item] = x = [None] * (pos + 1)
  138. else:
  139. while len(x) <= pos:
  140. x.append(None)
  141. assert x[pos] is None
  142. x[pos] = value
  143. def __contains__(self, x: Any) -> Any:
  144. # test if a substring is in any of the attached comments
  145. if self.comment:
  146. if self.comment[0] and x in self.comment[0].value:
  147. return True
  148. if self.comment[1]:
  149. for c in self.comment[1]:
  150. if x in c.value:
  151. return True
  152. for value in self.items.values():
  153. if not value:
  154. continue
  155. for c in value:
  156. if c and x in c.value:
  157. return True
  158. if self.end:
  159. for c in self.end:
  160. if x in c.value:
  161. return True
  162. return False
  163. # to distinguish key from None
  164. class NotNone:
  165. pass # NOQA
  166. class Format:
  167. __slots__ = ('_flow_style',)
  168. attrib = format_attrib
  169. def __init__(self) -> None:
  170. self._flow_style: Any = None
  171. def set_flow_style(self) -> None:
  172. self._flow_style = True
  173. def set_block_style(self) -> None:
  174. self._flow_style = False
  175. def flow_style(self, default: Optional[Any] = None) -> Any:
  176. """if default (the flow_style) is None, the flow style tacked on to
  177. the object explicitly will be taken. If that is None as well the
  178. default flow style rules the format down the line, or the type
  179. of the constituent values (simple -> flow, map/list -> block)"""
  180. if self._flow_style is None:
  181. return default
  182. return self._flow_style
  183. def __repr__(self) -> str:
  184. return f'Format({self._flow_style})'
  185. class LineCol:
  186. """
  187. line and column information wrt document, values start at zero (0)
  188. """
  189. attrib = line_col_attrib
  190. def __init__(self) -> None:
  191. self.line = None
  192. self.col = None
  193. self.data: Optional[Dict[Any, Any]] = None
  194. def add_kv_line_col(self, key: Any, data: Any) -> None:
  195. if self.data is None:
  196. self.data = {}
  197. self.data[key] = data
  198. def key(self, k: Any) -> Any:
  199. return self._kv(k, 0, 1)
  200. def value(self, k: Any) -> Any:
  201. return self._kv(k, 2, 3)
  202. def _kv(self, k: Any, x0: Any, x1: Any) -> Any:
  203. if self.data is None:
  204. return None
  205. data = self.data[k]
  206. return data[x0], data[x1]
  207. def item(self, idx: Any) -> Any:
  208. if self.data is None:
  209. return None
  210. return self.data[idx][0], self.data[idx][1]
  211. def add_idx_line_col(self, key: Any, data: Any) -> None:
  212. if self.data is None:
  213. self.data = {}
  214. self.data[key] = data
  215. def __repr__(self) -> str:
  216. return f'LineCol({self.line}, {self.col})'
  217. class CommentedBase:
  218. @property
  219. def ca(self):
  220. # type: () -> Any
  221. if not hasattr(self, Comment.attrib):
  222. setattr(self, Comment.attrib, Comment())
  223. return getattr(self, Comment.attrib)
  224. def yaml_end_comment_extend(self, comment: Any, clear: bool = False) -> None:
  225. if comment is None:
  226. return
  227. if clear or self.ca.end is None:
  228. self.ca.end = []
  229. self.ca.end.extend(comment)
  230. def yaml_key_comment_extend(self, key: Any, comment: Any, clear: bool = False) -> None:
  231. r = self.ca._items.setdefault(key, [None, None, None, None])
  232. if clear or r[1] is None:
  233. if comment[1] is not None:
  234. assert isinstance(comment[1], list)
  235. r[1] = comment[1]
  236. else:
  237. r[1].extend(comment[0])
  238. r[0] = comment[0]
  239. def yaml_value_comment_extend(self, key: Any, comment: Any, clear: bool = False) -> None:
  240. r = self.ca._items.setdefault(key, [None, None, None, None])
  241. if clear or r[3] is None:
  242. if comment[1] is not None:
  243. assert isinstance(comment[1], list)
  244. r[3] = comment[1]
  245. else:
  246. r[3].extend(comment[0])
  247. r[2] = comment[0]
  248. def yaml_set_start_comment(self, comment: Any, indent: Any = 0) -> None:
  249. """overwrites any preceding comment lines on an object
  250. expects comment to be without `#` and possible have multiple lines
  251. """
  252. from .error import CommentMark
  253. from .tokens import CommentToken
  254. pre_comments = self._yaml_clear_pre_comment() # type: ignore
  255. if comment[-1] == '\n':
  256. comment = comment[:-1] # strip final newline if there
  257. start_mark = CommentMark(indent)
  258. for com in comment.split('\n'):
  259. c = com.strip()
  260. if len(c) > 0 and c[0] != '#':
  261. com = '# ' + com
  262. pre_comments.append(CommentToken(com + '\n', start_mark))
  263. def yaml_set_comment_before_after_key(
  264. self,
  265. key: Any,
  266. before: Any = None,
  267. indent: Any = 0,
  268. after: Any = None,
  269. after_indent: Any = None,
  270. ) -> None:
  271. """
  272. expects comment (before/after) to be without `#` and possible have multiple lines
  273. """
  274. from ruamel.yaml.error import CommentMark
  275. from ruamel.yaml.tokens import CommentToken
  276. def comment_token(s: Any, mark: Any) -> Any:
  277. # handle empty lines as having no comment
  278. return CommentToken(('# ' if s else "") + s + '\n', mark)
  279. if after_indent is None:
  280. after_indent = indent + 2
  281. if before and (len(before) > 1) and before[-1] == '\n':
  282. before = before[:-1] # strip final newline if there
  283. if after and after[-1] == '\n':
  284. after = after[:-1] # strip final newline if there
  285. start_mark = CommentMark(indent)
  286. c = self.ca.items.setdefault(key, [None, [], None, None])
  287. if before is not None:
  288. if c[1] is None:
  289. c[1] = []
  290. if before == '\n':
  291. c[1].append(comment_token("", start_mark)) # type: ignore
  292. else:
  293. for com in before.split('\n'):
  294. c[1].append(comment_token(com, start_mark)) # type: ignore
  295. if after:
  296. start_mark = CommentMark(after_indent)
  297. if c[3] is None:
  298. c[3] = []
  299. for com in after.split('\n'):
  300. c[3].append(comment_token(com, start_mark)) # type: ignore
  301. @property
  302. def fa(self) -> Any:
  303. """format attribute
  304. set_flow_style()/set_block_style()"""
  305. if not hasattr(self, Format.attrib):
  306. setattr(self, Format.attrib, Format())
  307. return getattr(self, Format.attrib)
  308. def yaml_add_eol_comment(
  309. self, comment: Any, key: Optional[Any] = NotNone, column: Optional[Any] = None,
  310. ) -> None:
  311. """
  312. there is a problem as eol comments should start with ' #'
  313. (but at the beginning of the line the space doesn't have to be before
  314. the #. The column index is for the # mark
  315. """
  316. from .tokens import CommentToken
  317. from .error import CommentMark
  318. if column is None:
  319. try:
  320. column = self._yaml_get_column(key)
  321. except AttributeError:
  322. column = 0
  323. if comment[0] != '#':
  324. comment = '# ' + comment
  325. if column is None:
  326. if comment[0] == '#':
  327. comment = ' ' + comment
  328. column = 0
  329. start_mark = CommentMark(column)
  330. ct = [CommentToken(comment, start_mark), None]
  331. self._yaml_add_eol_comment(ct, key=key)
  332. @property
  333. def lc(self) -> Any:
  334. if not hasattr(self, LineCol.attrib):
  335. setattr(self, LineCol.attrib, LineCol())
  336. return getattr(self, LineCol.attrib)
  337. def _yaml_set_line_col(self, line: Any, col: Any) -> None:
  338. self.lc.line = line
  339. self.lc.col = col
  340. def _yaml_set_kv_line_col(self, key: Any, data: Any) -> None:
  341. self.lc.add_kv_line_col(key, data)
  342. def _yaml_set_idx_line_col(self, key: Any, data: Any) -> None:
  343. self.lc.add_idx_line_col(key, data)
  344. @property
  345. def anchor(self) -> Any:
  346. if not hasattr(self, Anchor.attrib):
  347. setattr(self, Anchor.attrib, Anchor())
  348. return getattr(self, Anchor.attrib)
  349. def yaml_anchor(self) -> Any:
  350. if not hasattr(self, Anchor.attrib):
  351. return None
  352. return self.anchor
  353. def yaml_set_anchor(self, value: Any, always_dump: bool = False) -> None:
  354. self.anchor.value = value
  355. self.anchor.always_dump = always_dump
  356. @property
  357. def tag(self) -> Any:
  358. if not hasattr(self, Tag.attrib):
  359. setattr(self, Tag.attrib, Tag())
  360. return getattr(self, Tag.attrib)
  361. def yaml_set_ctag(self, value: Tag) -> None:
  362. setattr(self, Tag.attrib, value)
  363. def copy_attributes(self, t: Any, memo: Any = None) -> None:
  364. # fmt: off
  365. for a in [Comment.attrib, Format.attrib, LineCol.attrib, Anchor.attrib,
  366. Tag.attrib, merge_attrib]:
  367. if hasattr(self, a):
  368. if memo is not None:
  369. setattr(t, a, copy.deepcopy(getattr(self, a, memo)))
  370. else:
  371. setattr(t, a, getattr(self, a))
  372. # fmt: on
  373. def _yaml_add_eol_comment(self, comment: Any, key: Any) -> None:
  374. raise NotImplementedError
  375. def _yaml_get_pre_comment(self) -> Any:
  376. raise NotImplementedError
  377. def _yaml_get_column(self, key: Any) -> Any:
  378. raise NotImplementedError
  379. class CommentedSeq(MutableSliceableSequence, list, CommentedBase): # type: ignore
  380. __slots__ = (Comment.attrib, '_lst')
  381. def __init__(self, *args: Any, **kw: Any) -> None:
  382. list.__init__(self, *args, **kw)
  383. def __getsingleitem__(self, idx: Any) -> Any:
  384. return list.__getitem__(self, idx)
  385. def __setsingleitem__(self, idx: Any, value: Any) -> None:
  386. # try to preserve the scalarstring type if setting an existing key to a new value
  387. if idx < len(self):
  388. if (
  389. isinstance(value, str)
  390. and not isinstance(value, ScalarString)
  391. and isinstance(self[idx], ScalarString)
  392. ):
  393. value = type(self[idx])(value)
  394. list.__setitem__(self, idx, value)
  395. def __delsingleitem__(self, idx: Any = None) -> Any:
  396. list.__delitem__(self, idx)
  397. self.ca.items.pop(idx, None) # might not be there -> default value
  398. for list_index in sorted(self.ca.items):
  399. if list_index < idx:
  400. continue
  401. self.ca.items[list_index - 1] = self.ca.items.pop(list_index)
  402. def __len__(self) -> int:
  403. return list.__len__(self)
  404. def insert(self, idx: Any, val: Any) -> None:
  405. """the comments after the insertion have to move forward"""
  406. list.insert(self, idx, val)
  407. for list_index in sorted(self.ca.items, reverse=True):
  408. if list_index < idx:
  409. break
  410. self.ca.items[list_index + 1] = self.ca.items.pop(list_index)
  411. def extend(self, val: Any) -> None:
  412. list.extend(self, val)
  413. def __eq__(self, other: Any) -> bool:
  414. return list.__eq__(self, other)
  415. def _yaml_add_comment(self, comment: Any, key: Optional[Any] = NotNone) -> None:
  416. if key is not NotNone:
  417. self.yaml_key_comment_extend(key, comment)
  418. else:
  419. self.ca.comment = comment
  420. def _yaml_add_eol_comment(self, comment: Any, key: Any) -> None:
  421. self._yaml_add_comment(comment, key=key)
  422. def _yaml_get_columnX(self, key: Any) -> Any:
  423. return self.ca.items[key][0].start_mark.column
  424. def _yaml_get_column(self, key: Any) -> Any:
  425. column = None
  426. sel_idx = None
  427. pre, post = key - 1, key + 1
  428. if pre in self.ca.items:
  429. sel_idx = pre
  430. elif post in self.ca.items:
  431. sel_idx = post
  432. else:
  433. # self.ca.items is not ordered
  434. for row_idx, _k1 in enumerate(self):
  435. if row_idx >= key:
  436. break
  437. if row_idx not in self.ca.items:
  438. continue
  439. sel_idx = row_idx
  440. if sel_idx is not None:
  441. column = self._yaml_get_columnX(sel_idx)
  442. return column
  443. def _yaml_get_pre_comment(self) -> Any:
  444. pre_comments: List[Any] = []
  445. if self.ca.comment is None:
  446. self.ca.comment = [None, pre_comments]
  447. else:
  448. pre_comments = self.ca.comment[1]
  449. return pre_comments
  450. def _yaml_clear_pre_comment(self) -> Any:
  451. pre_comments: List[Any] = []
  452. if self.ca.comment is None:
  453. self.ca.comment = [None, pre_comments]
  454. else:
  455. self.ca.comment[1] = pre_comments
  456. return pre_comments
  457. def __deepcopy__(self, memo: Any) -> Any:
  458. res = self.__class__()
  459. memo[id(self)] = res
  460. for k in self:
  461. res.append(copy.deepcopy(k, memo))
  462. self.copy_attributes(res, memo=memo)
  463. return res
  464. def __add__(self, other: Any) -> Any:
  465. return list.__add__(self, other)
  466. def sort(self, key: Any = None, reverse: bool = False) -> None:
  467. if key is None:
  468. tmp_lst = sorted(zip(self, range(len(self))), reverse=reverse)
  469. list.__init__(self, [x[0] for x in tmp_lst])
  470. else:
  471. tmp_lst = sorted(
  472. zip(map(key, list.__iter__(self)), range(len(self))), reverse=reverse,
  473. )
  474. list.__init__(self, [list.__getitem__(self, x[1]) for x in tmp_lst])
  475. itm = self.ca.items
  476. self.ca._items = {}
  477. for idx, x in enumerate(tmp_lst):
  478. old_index = x[1]
  479. if old_index in itm:
  480. self.ca.items[idx] = itm[old_index]
  481. def __repr__(self) -> Any:
  482. return list.__repr__(self)
  483. class CommentedKeySeq(tuple, CommentedBase): # type: ignore
  484. """This primarily exists to be able to roundtrip keys that are sequences"""
  485. def _yaml_add_comment(self, comment: Any, key: Optional[Any] = NotNone) -> None:
  486. if key is not NotNone:
  487. self.yaml_key_comment_extend(key, comment)
  488. else:
  489. self.ca.comment = comment
  490. def _yaml_add_eol_comment(self, comment: Any, key: Any) -> None:
  491. self._yaml_add_comment(comment, key=key)
  492. def _yaml_get_columnX(self, key: Any) -> Any:
  493. return self.ca.items[key][0].start_mark.column
  494. def _yaml_get_column(self, key: Any) -> Any:
  495. column = None
  496. sel_idx = None
  497. pre, post = key - 1, key + 1
  498. if pre in self.ca.items:
  499. sel_idx = pre
  500. elif post in self.ca.items:
  501. sel_idx = post
  502. else:
  503. # self.ca.items is not ordered
  504. for row_idx, _k1 in enumerate(self):
  505. if row_idx >= key:
  506. break
  507. if row_idx not in self.ca.items:
  508. continue
  509. sel_idx = row_idx
  510. if sel_idx is not None:
  511. column = self._yaml_get_columnX(sel_idx)
  512. return column
  513. def _yaml_get_pre_comment(self) -> Any:
  514. pre_comments: List[Any] = []
  515. if self.ca.comment is None:
  516. self.ca.comment = [None, pre_comments]
  517. else:
  518. pre_comments = self.ca.comment[1]
  519. return pre_comments
  520. def _yaml_clear_pre_comment(self) -> Any:
  521. pre_comments: List[Any] = []
  522. if self.ca.comment is None:
  523. self.ca.comment = [None, pre_comments]
  524. else:
  525. self.ca.comment[1] = pre_comments
  526. return pre_comments
  527. class CommentedMapView(Sized):
  528. __slots__ = ('_mapping',)
  529. def __init__(self, mapping: Any) -> None:
  530. self._mapping = mapping
  531. def __len__(self) -> int:
  532. count = len(self._mapping)
  533. return count
  534. class CommentedMapKeysView(CommentedMapView, Set): # type: ignore
  535. __slots__ = ()
  536. @classmethod
  537. def _from_iterable(self, it: Any) -> Any:
  538. return set(it)
  539. def __contains__(self, key: Any) -> Any:
  540. return key in self._mapping
  541. def __iter__(self) -> Any:
  542. # yield from self._mapping # not in py27, pypy
  543. # for x in self._mapping._keys():
  544. for x in self._mapping:
  545. yield x
  546. class CommentedMapItemsView(CommentedMapView, Set): # type: ignore
  547. __slots__ = ()
  548. @classmethod
  549. def _from_iterable(self, it: Any) -> Any:
  550. return set(it)
  551. def __contains__(self, item: Any) -> Any:
  552. key, value = item
  553. try:
  554. v = self._mapping[key]
  555. except KeyError:
  556. return False
  557. else:
  558. return v == value
  559. def __iter__(self) -> Any:
  560. for key in self._mapping._keys():
  561. yield (key, self._mapping[key])
  562. class CommentedMapValuesView(CommentedMapView):
  563. __slots__ = ()
  564. def __contains__(self, value: Any) -> Any:
  565. for key in self._mapping:
  566. if value == self._mapping[key]:
  567. return True
  568. return False
  569. def __iter__(self) -> Any:
  570. for key in self._mapping._keys():
  571. yield self._mapping[key]
  572. class CommentedMap(ordereddict, CommentedBase):
  573. __slots__ = (Comment.attrib, '_ok', '_ref')
  574. def __init__(self, *args: Any, **kw: Any) -> None:
  575. self._ok: MutableSet[Any] = set() # own keys
  576. self._ref: List[CommentedMap] = []
  577. ordereddict.__init__(self, *args, **kw)
  578. def _yaml_add_comment(
  579. self, comment: Any, key: Optional[Any] = NotNone, value: Optional[Any] = NotNone,
  580. ) -> None:
  581. """values is set to key to indicate a value attachment of comment"""
  582. if key is not NotNone:
  583. self.yaml_key_comment_extend(key, comment)
  584. return
  585. if value is not NotNone:
  586. self.yaml_value_comment_extend(value, comment)
  587. else:
  588. self.ca.comment = comment
  589. def _yaml_add_eol_comment(self, comment: Any, key: Any) -> None:
  590. """add on the value line, with value specified by the key"""
  591. self._yaml_add_comment(comment, value=key)
  592. def _yaml_get_columnX(self, key: Any) -> Any:
  593. return self.ca.items[key][2].start_mark.column
  594. def _yaml_get_column(self, key: Any) -> Any:
  595. column = None
  596. sel_idx = None
  597. pre, post, last = None, None, None
  598. for x in self:
  599. if pre is not None and x != key:
  600. post = x
  601. break
  602. if x == key:
  603. pre = last
  604. last = x
  605. if pre in self.ca.items:
  606. sel_idx = pre
  607. elif post in self.ca.items:
  608. sel_idx = post
  609. else:
  610. # self.ca.items is not ordered
  611. for k1 in self:
  612. if k1 >= key:
  613. break
  614. if k1 not in self.ca.items:
  615. continue
  616. sel_idx = k1
  617. if sel_idx is not None:
  618. column = self._yaml_get_columnX(sel_idx)
  619. return column
  620. def _yaml_get_pre_comment(self) -> Any:
  621. pre_comments: List[Any] = []
  622. if self.ca.comment is None:
  623. self.ca.comment = [None, pre_comments]
  624. else:
  625. pre_comments = self.ca.comment[1]
  626. return pre_comments
  627. def _yaml_clear_pre_comment(self) -> Any:
  628. pre_comments: List[Any] = []
  629. if self.ca.comment is None:
  630. self.ca.comment = [None, pre_comments]
  631. else:
  632. self.ca.comment[1] = pre_comments
  633. return pre_comments
  634. def update(self, *vals: Any, **kw: Any) -> None:
  635. try:
  636. ordereddict.update(self, *vals, **kw)
  637. except TypeError:
  638. # probably a dict that is used
  639. for x in vals[0]:
  640. self[x] = vals[0][x]
  641. if vals:
  642. try:
  643. self._ok.update(vals[0].keys()) # type: ignore
  644. except AttributeError:
  645. # assume one argument that is a list/tuple of two element lists/tuples
  646. for x in vals[0]:
  647. self._ok.add(x[0])
  648. if kw:
  649. self._ok.update(*kw.keys()) # type: ignore
  650. def insert(self, pos: Any, key: Any, value: Any, comment: Optional[Any] = None) -> None:
  651. """insert key value into given position, as defined by source YAML
  652. attach comment if provided
  653. """
  654. if key in self._ok:
  655. del self[key]
  656. keys = [k for k in self.keys() if k in self._ok]
  657. try:
  658. ma0 = getattr(self, merge_attrib, [[-1]])[0]
  659. merge_pos = ma0[0]
  660. except IndexError:
  661. merge_pos = -1
  662. if merge_pos >= 0:
  663. if merge_pos >= pos:
  664. getattr(self, merge_attrib)[0] = (merge_pos + 1, ma0[1])
  665. idx_min = pos
  666. idx_max = len(self._ok)
  667. else:
  668. idx_min = pos - 1
  669. idx_max = len(self._ok)
  670. else:
  671. idx_min = pos
  672. idx_max = len(self._ok)
  673. self[key] = value # at the end
  674. # print(f'{idx_min=} {idx_max=}')
  675. for idx in range(idx_min, idx_max):
  676. self.move_to_end(keys[idx])
  677. self._ok.add(key)
  678. # for referer in self._ref:
  679. # for keytmp in keys:
  680. # referer.update_key_value(keytmp)
  681. if comment is not None:
  682. self.yaml_add_eol_comment(comment, key=key)
  683. def mlget(self, key: Any, default: Any = None, list_ok: Any = False) -> Any:
  684. """multi-level get that expects dicts within dicts"""
  685. if not isinstance(key, list):
  686. return self.get(key, default)
  687. # assume that the key is a list of recursively accessible dicts
  688. def get_one_level(key_list: Any, level: Any, d: Any) -> Any:
  689. if not list_ok:
  690. assert isinstance(d, dict)
  691. if level >= len(key_list):
  692. if level > len(key_list):
  693. raise IndexError
  694. return d[key_list[level - 1]]
  695. return get_one_level(key_list, level + 1, d[key_list[level - 1]])
  696. try:
  697. return get_one_level(key, 1, self)
  698. except KeyError:
  699. return default
  700. except (TypeError, IndexError):
  701. if not list_ok:
  702. raise
  703. return default
  704. def __getitem__(self, key: Any) -> Any:
  705. try:
  706. return ordereddict.__getitem__(self, key)
  707. except KeyError:
  708. for merged in getattr(self, merge_attrib, []):
  709. if key in merged[1]:
  710. return merged[1][key]
  711. raise
  712. def __setitem__(self, key: Any, value: Any) -> None:
  713. # try to preserve the scalarstring type if setting an existing key to a new value
  714. if key in self:
  715. if (
  716. isinstance(value, str)
  717. and not isinstance(value, ScalarString)
  718. and isinstance(self[key], ScalarString)
  719. ):
  720. value = type(self[key])(value)
  721. ordereddict.__setitem__(self, key, value)
  722. self._ok.add(key)
  723. def _unmerged_contains(self, key: Any) -> Any:
  724. if key in self._ok:
  725. return True
  726. return None
  727. def __contains__(self, key: Any) -> bool:
  728. return bool(ordereddict.__contains__(self, key))
  729. def get(self, key: Any, default: Any = None) -> Any:
  730. try:
  731. return self.__getitem__(key)
  732. except: # NOQA
  733. return default
  734. def __repr__(self) -> Any:
  735. res = '{'
  736. sep = ''
  737. for k, v in self.items():
  738. res += f'{sep}{k!r}: {v!r}'
  739. if not sep:
  740. sep = ', '
  741. res += '}'
  742. return res
  743. def non_merged_items(self) -> Any:
  744. for x in ordereddict.__iter__(self):
  745. if x in self._ok:
  746. yield x, ordereddict.__getitem__(self, x)
  747. def __delitem__(self, key: Any) -> None:
  748. # for merged in getattr(self, merge_attrib, []):
  749. # if key in merged[1]:
  750. # value = merged[1][key]
  751. # break
  752. # else:
  753. # # not found in merged in stuff
  754. # ordereddict.__delitem__(self, key)
  755. # for referer in self._ref:
  756. # referer.update=_key_value(key)
  757. # return
  758. #
  759. # ordereddict.__setitem__(self, key, value) # merge might have different value
  760. # self._ok.discard(key)
  761. self._ok.discard(key)
  762. ordereddict.__delitem__(self, key)
  763. for referer in self._ref:
  764. referer.update_key_value(key)
  765. def __iter__(self) -> Any:
  766. for x in ordereddict.__iter__(self):
  767. yield x
  768. def pop(self, key: Any, default: Any = NotNone) -> Any:
  769. try:
  770. result = self[key]
  771. except KeyError:
  772. if default is NotNone:
  773. raise
  774. return default
  775. del self[key]
  776. return result
  777. def _keys(self) -> Any:
  778. for x in ordereddict.__iter__(self):
  779. yield x
  780. def __len__(self) -> int:
  781. return int(ordereddict.__len__(self))
  782. def __eq__(self, other: Any) -> bool:
  783. return bool(dict(self) == other)
  784. def keys(self) -> Any:
  785. return CommentedMapKeysView(self)
  786. def values(self) -> Any:
  787. return CommentedMapValuesView(self)
  788. def _items(self) -> Any:
  789. for x in ordereddict.__iter__(self):
  790. yield x, ordereddict.__getitem__(self, x)
  791. def items(self) -> Any:
  792. return CommentedMapItemsView(self)
  793. @property
  794. def merge(self) -> Any:
  795. if not hasattr(self, merge_attrib):
  796. setattr(self, merge_attrib, [])
  797. return getattr(self, merge_attrib)
  798. def copy(self) -> Any:
  799. x = type(self)() # update doesn't work
  800. for k, v in self._items():
  801. x[k] = v
  802. self.copy_attributes(x)
  803. return x
  804. def add_referent(self, cm: Any) -> None:
  805. if cm not in self._ref:
  806. self._ref.append(cm)
  807. def add_yaml_merge(self, value: Any) -> None:
  808. for v in value:
  809. v[1].add_referent(self)
  810. for k1, v1 in v[1].items():
  811. if ordereddict.__contains__(self, k1):
  812. continue
  813. ordereddict.__setitem__(self, k1, v1)
  814. self.merge.extend(value)
  815. def update_key_value(self, key: Any) -> None:
  816. if key in self._ok:
  817. return
  818. for v in self.merge:
  819. if key in v[1]:
  820. ordereddict.__setitem__(self, key, v[1][key])
  821. return
  822. ordereddict.__delitem__(self, key)
  823. def __deepcopy__(self, memo: Any) -> Any:
  824. res = self.__class__()
  825. memo[id(self)] = res
  826. for k in self:
  827. res[k] = copy.deepcopy(self[k], memo)
  828. self.copy_attributes(res, memo=memo)
  829. return res
  830. # based on brownie mappings
  831. @classmethod # type: ignore
  832. def raise_immutable(cls: Any, *args: Any, **kwargs: Any) -> None:
  833. raise TypeError(f'{cls.__name__} objects are immutable')
  834. class CommentedKeyMap(CommentedBase, Mapping): # type: ignore
  835. __slots__ = Comment.attrib, '_od'
  836. """This primarily exists to be able to roundtrip keys that are mappings"""
  837. def __init__(self, *args: Any, **kw: Any) -> None:
  838. if hasattr(self, '_od'):
  839. raise_immutable(self)
  840. try:
  841. self._od = ordereddict(*args, **kw)
  842. except TypeError:
  843. raise
  844. __delitem__ = __setitem__ = clear = pop = popitem = setdefault = update = raise_immutable
  845. # need to implement __getitem__, __iter__ and __len__
  846. def __getitem__(self, index: Any) -> Any:
  847. return self._od[index]
  848. def __iter__(self) -> Iterator[Any]:
  849. for x in self._od.__iter__():
  850. yield x
  851. def __len__(self) -> int:
  852. return len(self._od)
  853. def __hash__(self) -> Any:
  854. return hash(tuple(self.items()))
  855. def __repr__(self) -> Any:
  856. if not hasattr(self, merge_attrib):
  857. return self._od.__repr__()
  858. return 'ordereddict(' + repr(list(self._od.items())) + ')'
  859. @classmethod
  860. def fromkeys(keys: Any, v: Any = None) -> Any:
  861. return CommentedKeyMap(dict.fromkeys(keys, v))
  862. def _yaml_add_comment(self, comment: Any, key: Optional[Any] = NotNone) -> None:
  863. if key is not NotNone:
  864. self.yaml_key_comment_extend(key, comment)
  865. else:
  866. self.ca.comment = comment
  867. def _yaml_add_eol_comment(self, comment: Any, key: Any) -> None:
  868. self._yaml_add_comment(comment, key=key)
  869. def _yaml_get_columnX(self, key: Any) -> Any:
  870. return self.ca.items[key][0].start_mark.column
  871. def _yaml_get_column(self, key: Any) -> Any:
  872. column = None
  873. sel_idx = None
  874. pre, post = key - 1, key + 1
  875. if pre in self.ca.items:
  876. sel_idx = pre
  877. elif post in self.ca.items:
  878. sel_idx = post
  879. else:
  880. # self.ca.items is not ordered
  881. for row_idx, _k1 in enumerate(self):
  882. if row_idx >= key:
  883. break
  884. if row_idx not in self.ca.items:
  885. continue
  886. sel_idx = row_idx
  887. if sel_idx is not None:
  888. column = self._yaml_get_columnX(sel_idx)
  889. return column
  890. def _yaml_get_pre_comment(self) -> Any:
  891. pre_comments: List[Any] = []
  892. if self.ca.comment is None:
  893. self.ca.comment = [None, pre_comments]
  894. else:
  895. self.ca.comment[1] = pre_comments
  896. return pre_comments
  897. class CommentedOrderedMap(CommentedMap):
  898. __slots__ = (Comment.attrib,)
  899. class CommentedSet(MutableSet, CommentedBase): # type: ignore # NOQA
  900. __slots__ = Comment.attrib, 'odict'
  901. def __init__(self, values: Any = None) -> None:
  902. self.odict = ordereddict()
  903. MutableSet.__init__(self)
  904. if values is not None:
  905. self |= values
  906. def _yaml_add_comment(
  907. self, comment: Any, key: Optional[Any] = NotNone, value: Optional[Any] = NotNone,
  908. ) -> None:
  909. """values is set to key to indicate a value attachment of comment"""
  910. if key is not NotNone:
  911. self.yaml_key_comment_extend(key, comment)
  912. return
  913. if value is not NotNone:
  914. self.yaml_value_comment_extend(value, comment)
  915. else:
  916. self.ca.comment = comment
  917. def _yaml_add_eol_comment(self, comment: Any, key: Any) -> None:
  918. """add on the value line, with value specified by the key"""
  919. self._yaml_add_comment(comment, value=key)
  920. def add(self, value: Any) -> None:
  921. """Add an element."""
  922. self.odict[value] = None
  923. def discard(self, value: Any) -> None:
  924. """Remove an element. Do not raise an exception if absent."""
  925. del self.odict[value]
  926. def __contains__(self, x: Any) -> Any:
  927. return x in self.odict
  928. def __iter__(self) -> Any:
  929. for x in self.odict:
  930. yield x
  931. def __len__(self) -> int:
  932. return len(self.odict)
  933. def __repr__(self) -> str:
  934. return f'set({self.odict.keys()!r})'
  935. class TaggedScalar(CommentedBase):
  936. # the value and style attributes are set during roundtrip construction
  937. def __init__(self, value: Any = None, style: Any = None, tag: Any = None) -> None:
  938. self.value = value
  939. self.style = style
  940. if tag is not None:
  941. if isinstance(tag, str):
  942. tag = Tag(suffix=tag)
  943. self.yaml_set_ctag(tag)
  944. def __str__(self) -> Any:
  945. return self.value
  946. def count(self, s: str, start: Optional[int] = None, end: Optional[int] = None) -> Any:
  947. return self.value.count(s, start, end)
  948. def __getitem__(self, pos: int) -> Any:
  949. return self.value[pos]
  950. def dump_comments(d: Any, name: str = "", sep: str = '.', out: Any = sys.stdout) -> None:
  951. """
  952. recursively dump comments, all but the toplevel preceded by the path
  953. in dotted form x.0.a
  954. """
  955. if isinstance(d, dict) and hasattr(d, 'ca'):
  956. if name:
  957. out.write(f'{name} {type(d)}\n')
  958. out.write(f'{d.ca!r}\n')
  959. for k in d:
  960. dump_comments(d[k], name=(name + sep + str(k)) if name else k, sep=sep, out=out)
  961. elif isinstance(d, list) and hasattr(d, 'ca'):
  962. if name:
  963. out.write(f'{name} {type(d)}\n')
  964. out.write(f'{d.ca!r}\n')
  965. for idx, k in enumerate(d):
  966. dump_comments(
  967. k, name=(name + sep + str(idx)) if name else str(idx), sep=sep, out=out,
  968. )