1"""Tools for creating command-line and web-based wizards from a tree of nodes. 2""" 3import os 4import re 5import ast 6import json 7import pprint 8import fnmatch 9import builtins 10import textwrap 11import collections.abc as cabc 12 13from xonsh.tools import to_bool, to_bool_or_break, backup_file, print_color 14from xonsh.jsonutils import serialize_xonsh_json 15 16 17# 18# Nodes themselves 19# 20class Node(object): 21 """Base type of all nodes.""" 22 23 attrs = () 24 25 def __str__(self): 26 return PrettyFormatter(self).visit() 27 28 def __repr__(self): 29 return str(self).replace("\n", "") 30 31 32class Wizard(Node): 33 """Top-level node in the tree.""" 34 35 attrs = ("children", "path") 36 37 def __init__(self, children, path=None): 38 self.children = children 39 self.path = path 40 41 42class Pass(Node): 43 """Simple do-nothing node""" 44 45 46class Message(Node): 47 """Contains a simple message to report to the user.""" 48 49 attrs = "message" 50 51 def __init__(self, message): 52 self.message = message 53 54 55class Question(Node): 56 """Asks a question and then chooses the next node based on the response. 57 """ 58 59 attrs = ("question", "responses", "converter", "path") 60 61 def __init__(self, question, responses, converter=None, path=None): 62 """ 63 Parameters 64 ---------- 65 question : str 66 The question itself. 67 responses : dict with str keys and Node values 68 Mapping from user-input responses to nodes. 69 converter : callable, optional 70 Converts the string the user typed into another object 71 that serves as a key to the responses dict. 72 path : str or sequence of str, optional 73 A path within the storage object. 74 """ 75 self.question = question 76 self.responses = responses 77 self.converter = converter 78 self.path = path 79 80 81class Input(Node): 82 """Gets input from the user.""" 83 84 attrs = ("prompt", "converter", "show_conversion", "confirm", "path") 85 86 def __init__( 87 self, 88 prompt=">>> ", 89 converter=None, 90 show_conversion=False, 91 confirm=False, 92 retry=False, 93 path=None, 94 ): 95 """ 96 Parameters 97 ---------- 98 prompt : str, optional 99 Prompt string prior to input 100 converter : callable, optional 101 Converts the string the user typed into another object 102 prior to storage. 103 show_conversion : bool, optional 104 Flag for whether or not to show the results of the conversion 105 function if the conversion function was meaningfully executed. 106 Default False. 107 confirm : bool, optional 108 Whether the input should be confirmed until true or broken, 109 default False. 110 retry : bool, optional 111 In the event that the conversion operation fails, should 112 users be re-prompted until they provide valid input. Default False. 113 path : str or sequence of str, optional 114 A path within the storage object. 115 """ 116 self.prompt = prompt 117 self.converter = converter 118 self.show_conversion = show_conversion 119 self.confirm = confirm 120 self.retry = retry 121 self.path = path 122 123 124class While(Node): 125 """Computes a body while a condition function evaluates to true. 126 127 The condition function has the form ``cond(visitor=None, node=None)`` and 128 must return an object that responds to the Python magic method ``__bool__``. 129 The beg attribute specifies the number to start the loop iteration at. 130 """ 131 132 attrs = ("cond", "body", "idxname", "beg", "path") 133 134 def __init__(self, cond, body, idxname="idx", beg=0, path=None): 135 """ 136 Parameters 137 ---------- 138 cond : callable 139 Function that determines if the next loop iteration should 140 be executed. 141 body : sequence of nodes 142 A list of node to execute on each iteration. The condition function 143 has the form ``cond(visitor=None, node=None)`` and must return an 144 object that responds to the Python magic method ``__bool__``. 145 idxname : str, optional 146 The variable name for the index. 147 beg : int, optional 148 The first index value when evaluating path format strings. 149 path : str or sequence of str, optional 150 A path within the storage object. 151 """ 152 self.cond = cond 153 self.body = body 154 self.idxname = idxname 155 self.beg = beg 156 self.path = path 157 158 159# 160# Helper nodes 161# 162 163 164class YesNo(Question): 165 """Represents a simple yes/no question.""" 166 167 def __init__(self, question, yes, no, path=None): 168 """ 169 Parameters 170 ---------- 171 question : str 172 The question itself. 173 yes : Node 174 Node to execute if the response is True. 175 no : Node 176 Node to execute if the response is False. 177 path : str or sequence of str, optional 178 A path within the storage object. 179 """ 180 responses = {True: yes, False: no} 181 super().__init__(question, responses, converter=to_bool, path=path) 182 183 184class TrueFalse(Input): 185 """Input node the returns a True or False value.""" 186 187 def __init__(self, prompt="yes or no [default: no]? ", path=None): 188 super().__init__( 189 prompt=prompt, 190 converter=to_bool, 191 show_conversion=False, 192 confirm=False, 193 path=path, 194 ) 195 196 197class TrueFalseBreak(Input): 198 """Input node the returns a True, False, or 'break' value.""" 199 200 def __init__(self, prompt="yes, no, or break [default: no]? ", path=None): 201 super().__init__( 202 prompt=prompt, 203 converter=to_bool_or_break, 204 show_conversion=False, 205 confirm=False, 206 path=path, 207 ) 208 209 210class StoreNonEmpty(Input): 211 """Stores the user input only if the input was not an empty string. 212 This works by wrapping the converter function. 213 """ 214 215 def __init__( 216 self, 217 prompt=">>> ", 218 converter=None, 219 show_conversion=False, 220 confirm=False, 221 retry=False, 222 path=None, 223 store_raw=False, 224 ): 225 def nonempty_converter(x): 226 """Converts non-empty values and converts empty inputs to 227 Unstorable. 228 """ 229 if len(x) == 0: 230 x = Unstorable 231 elif converter is None: 232 pass 233 elif store_raw: 234 converter(x) # make sure str is valid, even if storing raw 235 else: 236 x = converter(x) 237 return x 238 239 super().__init__( 240 prompt=prompt, 241 converter=nonempty_converter, 242 show_conversion=show_conversion, 243 confirm=confirm, 244 path=path, 245 retry=retry, 246 ) 247 248 249class StateFile(Input): 250 """Node for representing the state as a file under a default or user 251 given file name. This node type is likely not useful on its own. 252 """ 253 254 attrs = ("default_file", "check", "ask_filename") 255 256 def __init__(self, default_file=None, check=True, ask_filename=True): 257 """ 258 Parameters 259 ---------- 260 default_file : str, optional 261 The default filename to save the file as. 262 check : bool, optional 263 Whether to print the current state and ask if it should be 264 saved/loaded prior to asking for the file name and saving the 265 file, default=True. 266 ask_filename : bool, optional 267 Whether to ask for the filename (if ``False``, always use the 268 default filename) 269 """ 270 self._df = None 271 super().__init__(prompt="filename: ", converter=None, confirm=False, path=None) 272 self.ask_filename = ask_filename 273 self.default_file = default_file 274 self.check = check 275 276 @property 277 def default_file(self): 278 return self._df 279 280 @default_file.setter 281 def default_file(self, val): 282 self._df = val 283 if val is None: 284 self.prompt = "filename: " 285 else: 286 self.prompt = "filename [default={0!r}]: ".format(val) 287 288 289class SaveJSON(StateFile): 290 """Node for saving the state as a JSON file under a default or user 291 given file name. 292 """ 293 294 295class LoadJSON(StateFile): 296 """Node for loading the state as a JSON file under a default or user 297 given file name. 298 """ 299 300 301class FileInserter(StateFile): 302 """Node for inserting the state into a file in between a prefix and suffix. 303 The state is converted according to some dumper rules. 304 """ 305 306 attrs = ("prefix", "suffix", "dump_rules", "default_file", "check", "ask_filename") 307 308 def __init__( 309 self, 310 prefix, 311 suffix, 312 dump_rules, 313 default_file=None, 314 check=True, 315 ask_filename=True, 316 ): 317 """ 318 Parameters 319 ---------- 320 prefix : str 321 Starting unique string in file to find and begin the insertion at, 322 e.g. '# XONSH WIZARD START\n' 323 suffix : str 324 Ending unique string to find in the file and end the replacement at, 325 e.g. '\n# XONSH WIZARD END' 326 dump_rules : dict of strs to functions 327 This is a dictionary that maps the path-like match strings to functions 328 that take the flat path and the value as arguments and convert the state 329 value at a path to a string. The keys here may use wildcards (as seen in 330 the standard library fnmatch module). For example:: 331 332 dump_rules = { 333 '/path/to/exact': lambda path, x: str(x), 334 '/otherpath/*': lambda path, x: x, 335 '*ending': lambda path x: repr(x), 336 '/': None, 337 } 338 339 If a wildcard is not used in a path, then that rule will be used 340 used on an exact match. If wildcards are used, the deepest and longest 341 match is used. If None is given instead of a the function, it means to 342 skip generating that key. 343 default_file : str, optional 344 The default filename to save the file as. 345 check : bool, optional 346 Whether to print the current state and ask if it should be 347 saved/loaded prior to asking for the file name and saving the 348 file, default=True. 349 ask_filename : bool, optional 350 Whether to ask for the filename (if ``False``, always use the 351 default filename) 352 """ 353 self._dr = None 354 super().__init__( 355 default_file=default_file, check=check, ask_filename=ask_filename 356 ) 357 self.prefix = prefix 358 self.suffix = suffix 359 self.dump_rules = self.string_rules = dump_rules 360 361 @property 362 def dump_rules(self): 363 return self._dr 364 365 @dump_rules.setter 366 def dump_rules(self, value): 367 dr = {} 368 for key, func in value.items(): 369 key_trans = fnmatch.translate(key) 370 r = re.compile(key_trans) 371 dr[r] = func 372 self._dr = dr 373 374 @staticmethod 375 def _find_rule_key(x): 376 """Key function for sorting regular expression rules""" 377 return (x[0], len(x[1].pattern)) 378 379 def find_rule(self, path): 380 """For a path, find the key and conversion function that should be used to 381 dump a value. 382 """ 383 if path in self.string_rules: 384 return path, self.string_rules[path] 385 len_funcs = [] 386 for rule, func in self.dump_rules.items(): 387 m = rule.match(path) 388 if m is None: 389 continue 390 i, j = m.span() 391 len_funcs.append((j - i, rule, func)) 392 if len(len_funcs) == 0: 393 # No dump rule function for path 394 return path, None 395 len_funcs.sort(reverse=True, key=self._find_rule_key) 396 _, rule, func = len_funcs[0] 397 return rule, func 398 399 def dumps(self, flat): 400 """Dumps a flat mapping of (string path keys, values) pairs and returns 401 a formatted string. 402 """ 403 lines = [self.prefix] 404 for path, value in sorted(flat.items()): 405 rule, func = self.find_rule(path) 406 if func is None: 407 continue 408 line = func(path, value) 409 lines.append(line) 410 lines.append(self.suffix) 411 new = "\n".join(lines) + "\n" 412 return new 413 414 415def create_truefalse_cond(prompt="yes or no [default: no]? ", path=None): 416 """This creates a basic condition function for use with nodes like While 417 or other conditions. The condition function creates and visits a TrueFalse 418 node and returns the result. This TrueFalse node takes the prompt and 419 path that is passed in here. 420 """ 421 422 def truefalse_cond(visitor, node=None): 423 """Prompts the user for a true/false condition.""" 424 tf = TrueFalse(prompt=prompt, path=path) 425 rtn = visitor.visit(tf) 426 return rtn 427 428 return truefalse_cond 429 430 431# 432# Tools for trees of nodes. 433# 434 435 436def _lowername(cls): 437 return cls.__name__.lower() 438 439 440class Visitor(object): 441 """Super-class for all classes that should walk over a tree of nodes. 442 This implements the visit() method. 443 """ 444 445 def __init__(self, tree=None): 446 self.tree = tree 447 448 def visit(self, node=None): 449 """Walks over a node. If no node is provided, the tree is used.""" 450 if node is None: 451 node = self.tree 452 if node is None: 453 raise RuntimeError("no node or tree given!") 454 for clsname in map(_lowername, type.mro(node.__class__)): 455 meth = getattr(self, "visit_" + clsname, None) 456 if callable(meth): 457 rtn = meth(node) 458 break 459 else: 460 msg = "could not find valid visitor method for {0} on {1}" 461 nodename = node.__class__.__name__ 462 selfname = self.__class__.__name__ 463 raise AttributeError(msg.format(nodename, selfname)) 464 return rtn 465 466 467class PrettyFormatter(Visitor): 468 """Formats a tree of nodes into a pretty string""" 469 470 def __init__(self, tree=None, indent=" "): 471 super().__init__(tree=tree) 472 self.level = 0 473 self.indent = indent 474 475 def visit_node(self, node): 476 s = node.__class__.__name__ + "(" 477 if len(node.attrs) == 0: 478 return s + ")" 479 s += "\n" 480 self.level += 1 481 t = [] 482 for aname in node.attrs: 483 a = getattr(node, aname) 484 t.append(self.visit(a) if isinstance(a, Node) else pprint.pformat(a)) 485 t = ["{0}={1}".format(n, x) for n, x in zip(node.attrs, t)] 486 s += textwrap.indent(",\n".join(t), self.indent) 487 self.level -= 1 488 s += "\n)" 489 return s 490 491 def visit_wizard(self, node): 492 s = "Wizard(children=[" 493 if len(node.children) == 0: 494 if node.path is None: 495 return s + "])" 496 else: 497 return s + "], path={0!r})".format(node.path) 498 s += "\n" 499 self.level += 1 500 s += textwrap.indent(",\n".join(map(self.visit, node.children)), self.indent) 501 self.level -= 1 502 if node.path is None: 503 s += "\n])" 504 else: 505 s += "{0}],\n{0}path={1!r}\n)".format(self.indent, node.path) 506 return s 507 508 def visit_message(self, node): 509 return "Message({0!r})".format(node.message) 510 511 def visit_question(self, node): 512 s = node.__class__.__name__ + "(\n" 513 self.level += 1 514 s += self.indent + "question={0!r},\n".format(node.question) 515 s += self.indent + "responses={" 516 if len(node.responses) == 0: 517 s += "}" 518 else: 519 s += "\n" 520 t = sorted(node.responses.items()) 521 t = ["{0!r}: {1}".format(k, self.visit(v)) for k, v in t] 522 s += textwrap.indent(",\n".join(t), 2 * self.indent) 523 s += "\n" + self.indent + "}" 524 if node.converter is not None: 525 s += ",\n" + self.indent + "converter={0!r}".format(node.converter) 526 if node.path is not None: 527 s += ",\n" + self.indent + "path={0!r}".format(node.path) 528 self.level -= 1 529 s += "\n)" 530 return s 531 532 def visit_input(self, node): 533 s = "{0}(prompt={1!r}".format(node.__class__.__name__, node.prompt) 534 if node.converter is None and node.path is None: 535 return s + "\n)" 536 if node.converter is not None: 537 s += ",\n" + self.indent + "converter={0!r}".format(node.converter) 538 s += ",\n" + self.indent + "show_conversion={0!r}".format(node.show_conversion) 539 s += ",\n" + self.indent + "confirm={0!r}".format(node.confirm) 540 s += ",\n" + self.indent + "retry={0!r}".format(node.retry) 541 if node.path is not None: 542 s += ",\n" + self.indent + "path={0!r}".format(node.path) 543 s += "\n)" 544 return s 545 546 def visit_statefile(self, node): 547 s = "{0}(default_file={1!r}, check={2}, ask_filename={3})" 548 s = s.format( 549 node.__class__.__name__, node.default_file, node.check, node.ask_filename 550 ) 551 return s 552 553 def visit_while(self, node): 554 s = "{0}(cond={1!r}".format(node.__class__.__name__, node.cond) 555 s += ",\n" + self.indent + "body=[" 556 if len(node.body) > 0: 557 s += "\n" 558 self.level += 1 559 s += textwrap.indent(",\n".join(map(self.visit, node.body)), self.indent) 560 self.level -= 1 561 s += "\n" + self.indent 562 s += "]" 563 s += ",\n" + self.indent + "idxname={0!r}".format(node.idxname) 564 s += ",\n" + self.indent + "beg={0!r}".format(node.beg) 565 if node.path is not None: 566 s += ",\n" + self.indent + "path={0!r}".format(node.path) 567 s += "\n)" 568 return s 569 570 571def ensure_str_or_int(x): 572 """Creates a string or int.""" 573 if isinstance(x, int): 574 return x 575 x = x if isinstance(x, str) else str(x) 576 try: 577 x = ast.literal_eval(x) 578 except (ValueError, SyntaxError): 579 pass 580 if not isinstance(x, (int, str)): 581 msg = "{0!r} could not be converted to int or str".format(x) 582 raise ValueError(msg) 583 return x 584 585 586def canon_path(path, indices=None): 587 """Returns the canonical form of a path, which is a tuple of str or ints. 588 Indices may be optionally passed in. 589 """ 590 if not isinstance(path, str): 591 return tuple(map(ensure_str_or_int, path)) 592 if indices is not None: 593 path = path.format(**indices) 594 path = path[1:] if path.startswith("/") else path 595 path = path[:-1] if path.endswith("/") else path 596 if len(path) == 0: 597 return () 598 return tuple(map(ensure_str_or_int, path.split("/"))) 599 600 601class UnstorableType(object): 602 """Represents an unstorable return value for when no input was given 603 or such input was skipped. Typically represented by the Unstorable 604 singleton. 605 """ 606 607 _inst = None 608 609 def __new__(cls, *args, **kwargs): 610 if cls._inst is None: 611 cls._inst = super(UnstorableType, cls).__new__(cls, *args, **kwargs) 612 return cls._inst 613 614 615Unstorable = UnstorableType() 616 617 618class StateVisitor(Visitor): 619 """This class visits the nodes and stores the results in a top-level 620 dict of data according to the state path of the node. The the node 621 does not have a path or the path does not exist, the storage is skipped. 622 This class can be optionally initialized with an existing state. 623 """ 624 625 def __init__(self, tree=None, state=None, indices=None): 626 super().__init__(tree=tree) 627 self.state = {} if state is None else state 628 self.indices = {} if indices is None else indices 629 630 def visit(self, node=None): 631 if node is None: 632 node = self.tree 633 if node is None: 634 raise RuntimeError("no node or tree given!") 635 rtn = super().visit(node) 636 path = getattr(node, "path", None) 637 if callable(path): 638 path = path(visitor=self, node=node, val=rtn) 639 if path is not None and rtn is not Unstorable: 640 self.store(path, rtn, indices=self.indices) 641 return rtn 642 643 def store(self, path, val, indices=None): 644 """Stores a value at the path location.""" 645 path = canon_path(path, indices=indices) 646 loc = self.state 647 for p, n in zip(path[:-1], path[1:]): 648 if isinstance(p, str) and p not in loc: 649 loc[p] = {} if isinstance(n, str) else [] 650 elif isinstance(p, int) and abs(p) + (p >= 0) > len(loc): 651 i = abs(p) + (p >= 0) - len(loc) 652 if isinstance(n, str): 653 ex = [{} for _ in range(i)] 654 else: 655 ex = [[] for _ in range(i)] 656 loc.extend(ex) 657 loc = loc[p] 658 p = path[-1] 659 if isinstance(p, int) and abs(p) + (p >= 0) > len(loc): 660 i = abs(p) + (p >= 0) - len(loc) 661 ex = [None] * i 662 loc.extend(ex) 663 loc[p] = val 664 665 def flatten(self, path="/", value=None, flat=None): 666 """Returns a dict version of the store whose keys are paths. 667 Note that list and dict entries will always end in '/', allowing 668 disambiquation in dump_rules. 669 """ 670 value = self.state if value is None else value 671 flat = {} if flat is None else flat 672 if isinstance(value, cabc.Mapping): 673 path = path if path.endswith("/") else path + "/" 674 flat[path] = value 675 for k, v in value.items(): 676 p = path + k 677 self.flatten(path=p, value=v, flat=flat) 678 elif isinstance(value, (str, bytes)): 679 flat[path] = value 680 elif isinstance(value, cabc.Sequence): 681 path = path if path.endswith("/") else path + "/" 682 flat[path] = value 683 for i, v in enumerate(value): 684 p = path + str(i) 685 self.flatten(path=p, value=v, flat=flat) 686 else: 687 flat[path] = value 688 return flat 689 690 691YN = "{GREEN}yes{NO_COLOR} or {RED}no{NO_COLOR} [default: no]? " 692YNB = ( 693 "{GREEN}yes{NO_COLOR}, {RED}no{NO_COLOR}, or " 694 "{YELLOW}break{NO_COLOR} [default: no]? " 695) 696 697 698class PromptVisitor(StateVisitor): 699 """Visits the nodes in the tree via the a command-line prompt.""" 700 701 def __init__(self, tree=None, state=None, **kwargs): 702 """ 703 Parameters 704 ---------- 705 tree : Node, optional 706 Tree of nodes to start visitor with. 707 state : dict, optional 708 Initial state to begin with. 709 kwargs : optional 710 Options that are passed through to the prompt via the shell's 711 singleline() method. See BaseShell for mor details. 712 """ 713 super().__init__(tree=tree, state=state) 714 self.env = builtins.__xonsh_env__ 715 self.shell = builtins.__xonsh_shell__.shell 716 self.shell_kwargs = kwargs 717 718 def visit_wizard(self, node): 719 for child in node.children: 720 self.visit(child) 721 722 def visit_pass(self, node): 723 pass 724 725 def visit_message(self, node): 726 print_color(node.message) 727 728 def visit_question(self, node): 729 self.env["PROMPT"] = node.question 730 r = self.shell.singleline(**self.shell_kwargs) 731 if callable(node.converter): 732 r = node.converter(r) 733 self.visit(node.responses[r]) 734 return r 735 736 def visit_input(self, node): 737 need_input = True 738 while need_input: 739 self.env["PROMPT"] = node.prompt 740 raw = self.shell.singleline(**self.shell_kwargs) 741 if callable(node.converter): 742 try: 743 x = node.converter(raw) 744 except KeyboardInterrupt: 745 raise 746 except Exception: 747 if node.retry: 748 msg = ( 749 "{{BOLD_RED}}Invalid{{NO_COLOR}} input {0!r}, " 750 "please retry." 751 ) 752 print_color(msg.format(raw)) 753 continue 754 else: 755 raise 756 if node.show_conversion and x is not Unstorable and str(x) != raw: 757 msg = "{{BOLD_PURPLE}}Converted{{NO_COLOR}} input {0!r} to {1!r}." 758 print_color(msg.format(raw, x)) 759 else: 760 x = raw 761 if node.confirm: 762 msg = "Would you like to keep the input: {0}" 763 print(msg.format(pprint.pformat(x))) 764 confirmer = TrueFalseBreak(prompt=YNB) 765 status = self.visit(confirmer) 766 if isinstance(status, str) and status == "break": 767 x = Unstorable 768 break 769 else: 770 need_input = not status 771 else: 772 need_input = False 773 return x 774 775 def visit_while(self, node): 776 rtns = [] 777 origidx = self.indices.get(node.idxname, None) 778 self.indices[node.idxname] = idx = node.beg 779 while node.cond(visitor=self, node=node): 780 rtn = list(map(self.visit, node.body)) 781 rtns.append(rtn) 782 idx += 1 783 self.indices[node.idxname] = idx 784 if origidx is None: 785 del self.indices[node.idxname] 786 else: 787 self.indices[node.idxname] = origidx 788 return rtns 789 790 def visit_savejson(self, node): 791 jstate = json.dumps( 792 self.state, indent=1, sort_keys=True, default=serialize_xonsh_json 793 ) 794 if node.check: 795 msg = "The current state is:\n\n{0}\n" 796 print(msg.format(textwrap.indent(jstate, " "))) 797 ap = "Would you like to save this state, " + YN 798 asker = TrueFalse(prompt=ap) 799 do_save = self.visit(asker) 800 if not do_save: 801 return Unstorable 802 fname = None 803 if node.ask_filename: 804 fname = self.visit_input(node) 805 if fname is None or len(fname) == 0: 806 fname = node.default_file 807 if os.path.isfile(fname): 808 backup_file(fname) 809 else: 810 os.makedirs(os.path.dirname(fname), exist_ok=True) 811 with open(fname, "w") as f: 812 f.write(jstate) 813 return fname 814 815 def visit_loadjson(self, node): 816 if node.check: 817 ap = "Would you like to load an existing file, " + YN 818 asker = TrueFalse(prompt=ap) 819 do_load = self.visit(asker) 820 if not do_load: 821 return Unstorable 822 fname = self.visit_input(node) 823 if fname is None or len(fname) == 0: 824 fname = node.default_file 825 if os.path.isfile(fname): 826 with open(fname, "r") as f: 827 self.state = json.load(f) 828 print_color("{{GREEN}}{0!r} loaded.{{NO_COLOR}}".format(fname)) 829 else: 830 print_color( 831 ("{{RED}}{0!r} could not be found, " "continuing.{{NO_COLOR}}").format( 832 fname 833 ) 834 ) 835 return fname 836 837 def visit_fileinserter(self, node): 838 # perform the dumping operation. 839 new = node.dumps(self.flatten()) 840 # check if we should write this out 841 if node.check: 842 msg = "The current state to insert is:\n\n{0}\n" 843 print(msg.format(textwrap.indent(new, " "))) 844 ap = "Would you like to write out the current state, " + YN 845 asker = TrueFalse(prompt=ap) 846 do_save = self.visit(asker) 847 if not do_save: 848 return Unstorable 849 # get and backup the file. 850 fname = None 851 if node.ask_filename: 852 fname = self.visit_input(node) 853 if fname is None or len(fname) == 0: 854 fname = node.default_file 855 if os.path.isfile(fname): 856 with open(fname, "r") as f: 857 s = f.read() 858 before, _, s = s.partition(node.prefix) 859 _, _, after = s.partition(node.suffix) 860 backup_file(fname) 861 else: 862 before = after = "" 863 dname = os.path.dirname(fname) 864 if dname: 865 os.makedirs(dname, exist_ok=True) 866 # write out the file 867 with open(fname, "w") as f: 868 f.write(before + new + after) 869 return fname 870