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