PYTHON   19

buildtree

Guest on 16th May 2022 05:08:38 PM

  1. """This module explores a different approach to building doctree elements...
  2.  
  3. The "normal" way of building a DOCUTILS tree involves, well, building a tree
  4. structure. So one might do::
  5.  
  6.    section = docutils.nodes.section()
  7.    section += docutils.nodes.title(text="Title")
  8.    para = docutils.nodes.paragraph()
  9.    section += para
  10.    para += docutils.nodes.Text("Some ")
  11.    para += docutils.nodes.strong(text="strong text.")
  12.  
  13. That's all very nice, if one is *thinking* in terms of a tree structure,
  14. but it is not, for me, a very natural way to construct a text.
  15.  
  16.    (OK - I *know* in practice one would also have imported `paragraph`,
  17.    etc., from `docutils.nodes`, but that is not my point.)
  18.  
  19. This module allows one to use a more LaTex style of construction, with
  20. begin and end delimitors for DOCUTILS nodes. Thus the above example becomes::
  21.  
  22.    build.start("section")
  23.    build.add("title","Title")
  24.    build.start("paragraph","Some ")
  25.    build.add("strong","Strong text.")
  26.    build.end("section")
  27.  
  28. (As a convenience, paragraphs are automatically ended.)
  29.  
  30. A slightly shorter, and possibly more obfuscated, way of writing this
  31. would be::
  32.  
  33.    build.start("section",build.make("title","Title")
  34.    build.start("paragraph","Some ",build.make("strong","Strong text."))
  35.    build.end("section")
  36.  
  37. Sometimes I think that sort of approach makes more sense, sometimes not.
  38. """ # we need a " to keep [X]Emacs Python mode happy.
  39.  
  40. import string
  41. import docutils.nodes
  42. import docutils.utils
  43.  
  44. __docformat__ = "reST"
  45.  
  46. # ----------------------------------------------------------------------
  47. class group(docutils.nodes.Element):
  48.     """Group is a way of grouping together elements.
  49.  
  50.    Compare it to HTML <div> and <span>, or to TeX (?check?)
  51.    \begingroup and \endgroup.
  52.  
  53.    It takes the special attribute `style`, which indicates what
  54.    sort of thing it is grouping - for instance, "docstring" or
  55.    "attributes".
  56.  
  57.    Although it (should be) supplied by the standard DOCUTILS tree,
  58.    reST itself does not use `group`. It is solely used by
  59.    extensions, such as ``pysource``.
  60.  
  61.    In the default HTML Writer, `group` renders invisibly
  62.    (that is, it has no effect at all on the formatted output).
  63.    """
  64.  
  65.     pass
  66.  
  67. # ----------------------------------------------------------------------
  68. class BuildTree:
  69.  
  70.     def __init__(self, with_groups=1, root=None):
  71.         self.stack = []
  72.         """A stack of tuples of the form ("classname",classinstance).
  73.        """
  74.  
  75.         self.root = root
  76.         """A memory of the first item on the stack (notionally, the
  77.        "document") - we need this because if we `start` a document,
  78.        fill it up, and then `end` it, that final `end` will remove
  79.        the appropriate instance from the stack, leaving no record.
  80.        Thus this is that record.
  81.        """
  82.         if root is not None:
  83.             self._stack_add(root)
  84.  
  85.         self.with_groups = with_groups
  86.  
  87.     def finish(self):
  88.         """Call this to indicate we have finished.
  89.  
  90.        It will grumble if anything is left unclosed, but will
  91.        return the "root" instance of the DOCUTILS tree we've been
  92.        building if all is well...
  93.        """
  94.         if len(self.stack) > 0:
  95.             raise ValueError,"Items still outstanding on stack: %s"%\
  96.                   self._stack_as_string()
  97.         else:
  98.             return self.root
  99.  
  100.     def add(self,thing,*args,**keywords):
  101.         """Add a `thing` DOCUTILS node at the current level.
  102.  
  103.        For instance::
  104.  
  105.            build.add("paragraph","Some simple text.")
  106.  
  107.        If `thing` is "text" then it will automagically be converted
  108.        to "Text" (this makes life easier for the user, as all of the
  109.        other DOCUTILS node classes they are likely to use start with a
  110.        lowercase letter, and "Text" is the sole exception).
  111.  
  112.        See `make` (which this uses) for more details of the arguments.
  113.        """
  114.         if thing == "group" and not self.with_groups:
  115.             return
  116.  
  117.         instance = self.make(thing,*args,**keywords)
  118.         self._stack_append(instance)
  119.  
  120.     def addsubtree(self,subtree):
  121.         """Add a DOCUTILS subtree to the current item.
  122.        """
  123.         self._stack_append(subtree)
  124.  
  125.     def current(self):
  126.         """Return the "current" item.
  127.  
  128.        That is, return the item to which `add()` will add DOCUTILS nodes.
  129.        """
  130.         return self._stack_current()
  131.  
  132.     def start(self,thing,*args,**keywords):
  133.         """Add a `thing` DOCUTILS node, starting a new level.
  134.  
  135.        `thing` should be either the name of a docutils.nodes class, or
  136.        else a class itself.
  137.  
  138.        If `thing` is "text" then it will automagically be converted
  139.        to "Text" (this makes life easier for the user, as all of the
  140.        other DOCUTILS node classes they are likely to use start with a
  141.        lowercase letter, and "Text" is the sole exception).
  142.  
  143.        For instance::
  144.  
  145.            build.start("bullet_list")
  146.  
  147.        As a convenience, if `thing` is a paragraph, and if the current
  148.        item is another paragraph, this method will end the old paragraph
  149.        before starting the new.
  150.  
  151.        Note that if `thing` is "document", some extra magic is worked
  152.        internally. If the keywords `warninglevel` and `errorlevel` are
  153.        given, they will be passed to a docutils.utils.Reporter instance,
  154.        as well as being passed down to the `document` class's initialiser.
  155.  
  156.        See `make` (which this uses) for more details of the arguments.
  157.        """
  158.         name = self._nameof(thing)
  159.  
  160.         if name == "group" and not self.with_groups:
  161.             return
  162.  
  163.         if name == "paragraph" and self._stack_ends("paragraph"):
  164.             self.end("paragraph")
  165.  
  166.         if name == "document":
  167.             if self.root:
  168.                 return
  169.             if len(self.stack) > 0:
  170.                 raise ValueError,\
  171.                       "Cannot insert 'document' except at root of stack"
  172.             warninglevel = keywords.get("warninglevel",2)
  173.             errorlevel = keywords.get("errorlevel",4)
  174.             reporter = docutils.utils.Reporter('fubar', warninglevel,
  175.                 errorlevel)
  176.             instance = docutils.nodes.document(reporter,"en")
  177.         else:
  178.             instance = self.make(thing,*args,**keywords)
  179.  
  180.         if len(self.stack) == 0:
  181.             self.root = instance
  182.         else:
  183.             self._stack_append(instance)
  184.  
  185.         self._stack_add(instance)
  186.  
  187.     def end(self,thing):
  188.         """End the level started below a `thing` DOCUTILS node.
  189.  
  190.        `thing` should be either the name of a docutils.nodes class, or
  191.        else a class itself.
  192.  
  193.        For instance::
  194.  
  195.            build.end("bullet_list")
  196.  
  197.        As a convenience, if the last item constructed was actually
  198.        a paragraph, and `thing` is the container for said paragraph,
  199.        then the paragraph will be automatically ended.
  200.  
  201.        Otherwise, for the moment at least, the `thing` being ended
  202.        must be the last thing that was begun (in the future, we *might*
  203.        support automatic "unrolling" of the stack, but not at the
  204.        moment).
  205.        """
  206.         name = self._nameof(thing)
  207.  
  208.         if thing == "group" and not self.with_groups:
  209.             return
  210.  
  211.         if self._stack_ends("paragraph") and name != "paragraph":
  212.             self.end("paragraph")
  213.  
  214.         self._stack_remove(name)
  215.  
  216.     def make(self,thing,*args,**keywords):
  217.         """Return an instance of `docutils.nodes.thing`
  218.  
  219.        Attempts to regularise the initialisation of putting initial
  220.        text into an Element and a TextElement...
  221.  
  222.        `thing` should be either the name of a docutils.nodes class, or
  223.        else a class itself (so, for instance, one might call
  224.        ``build.make("paragraph")`` or
  225.        ``build.make(docutils.nodes.paragraph)``),
  226.        or else None.
  227.  
  228.        If `thing` is "text" then it will automagically be converted
  229.        to "Text" (this makes life easier for the user, as all of the
  230.        other DOCUTILS node classes they are likely to use start with a
  231.        lowercase letter, and "Text" is the sole exception).
  232.  
  233.        If `thing` is an Element subclass, then the arguments are just
  234.        passed straight through - any *args list is taken to be children
  235.        for the element (strings are coerced to Text instances), and any
  236.        **keywords are taken as attributes.
  237.  
  238.        If `thing` is an TextElement subclass, then if the first
  239.        item in *args is a string, it is passed down as the `text`
  240.        parameter. Any remaining items from *args are used as child
  241.        nodes, and any **keywords as attributes.
  242.  
  243.        If `thing` is a Text subclass, then a single argument is expected
  244.        within *args, which must be a string, to be used as the Text's
  245.        content.
  246.  
  247.        For instance::
  248.  
  249.            n1 = build.make("paragraph","Some ",
  250.                           build.make("emphasis","text"),
  251.                           ".",align="center")
  252.            n2 = build.make(None,"Just plain text")
  253.        """
  254.  
  255.         #print "make: %s, %s, %s"%(thing,args,keywords)
  256.  
  257.         # Temporary special case - since group is not (yet) in docutils.nodes...
  258.         if thing == "group":
  259.             thing = group
  260.  
  261.         if thing == None:
  262.             dps_class = docutils.nodes.Text
  263.         elif type(thing) == type(""):
  264.             if thing == "text":
  265.                 thing = "Text"
  266.             try:
  267.                 dps_class = getattr(docutils.nodes,thing)
  268.             except AttributeError:
  269.                 raise ValueError,"docutils.nodes does not define '%s'"%thing
  270.         else:
  271.             dps_class = thing
  272.  
  273.         # NB: check for TextElement before checking for Element,
  274.         # since TextElement is itself a subclass of Element!
  275.         if issubclass(dps_class,docutils.nodes.TextElement):
  276.             # Force the use of the argument list as such, by insisting
  277.             # that the `rawsource` and `text` arguments are empty strings
  278.             args = self._convert_args(args)
  279.             dps_instance = dps_class("","",*args,**keywords)
  280.         elif issubclass(dps_class,docutils.nodes.Element):
  281.             # Force the use of the argument list as such, by insisting
  282.             # that the `rawsource` arguments is an empty string
  283.             args = self._convert_args(args)
  284.             dps_instance = dps_class("",*args,**keywords)
  285.         elif issubclass(dps_class,docutils.nodes.Text):
  286.             if len(args) > 1:
  287.                 raise ValueError,\
  288.                       "Text subclass %s may only take one argument"%\
  289.                       self._nameof(thing)
  290.             elif len(args) == 1:
  291.                 text = args[0]
  292.             else:
  293.                 text = ""
  294.             if keywords:
  295.                 raise ValueError,\
  296.                       "Text subclass %s cannot use keyword arguments"%\
  297.                       self._nameof(thing)
  298.             dps_instance = dps_class(text)
  299.         else:
  300.             raise ValueError,"%s is not an Element or TextElement"%\
  301.                   self._nameof(thing)
  302.  
  303.         #print "   ",dps_instance
  304.         return dps_instance
  305.  
  306.     def _convert_args(self,args):
  307.         """Return the arguments, with strings converted to Texts.
  308.        """
  309.         newargs = []
  310.         for arg in args:
  311.             if type(arg) == type(""):
  312.                 newargs.append(docutils.nodes.Text(arg))
  313.             else:
  314.                 newargs.append(arg)
  315.         return newargs
  316.  
  317.     def __getattr__(self,name):
  318.         """Return an appropriate DOCUTILS class, for instantiation.
  319.        """
  320.         return getattr(docutils.nodes,name)
  321.  
  322.     # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  323.     def _nameof(self,thing):
  324.         if thing is None:
  325.             return "Text"
  326.         elif type(thing) == type(""):
  327.             return thing
  328.         else:
  329.             return thing.__name__
  330.  
  331.     # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  332.     # Stack maintenance
  333.  
  334.     def _stack_ends(self,name):
  335.         """Return true if the stack ends with the named entity.
  336.        """
  337.         return self.stack[-1][0] == name
  338.  
  339.     def _stack_add(self,instance):
  340.         """Add a new level to the stack.
  341.        """
  342.         self.stack.append((instance.__class__.__name__,instance))
  343.  
  344.     def _stack_remove(self,name):
  345.         """Remove the last level from the stack
  346.  
  347.        (but only if it is of the right sort).
  348.        """
  349.         if len(self.stack) == 0:
  350.             raise ValueError,"Cannot end %s - nothing outstanding to end"%\
  351.                   (name)
  352.         if name != self.stack[-1][0]:
  353.             raise ValueError,"Cannot end %s - last thing begun was %s"%\
  354.                   (name,self.stack[-1][0])
  355.         del self.stack[-1]
  356.  
  357.     def _stack_append(self,instance):
  358.         """Append an instance to the last item on the stack.
  359.        """
  360.         if len(self.stack) > 0:
  361.             self.stack[-1][1].append(instance)
  362.         else:
  363.             raise ValueError,"Cannot add %s to current level" \
  364.                   " - nothing current"%(instance.__class__.__name__)
  365.  
  366.     def _stack_current(self):
  367.         """Return the "current" element from the stack
  368.  
  369.        That is, the element to which we would append any new instances
  370.        with `_stack_append()`
  371.        """
  372.         return self.stack[-1][1]
  373.  
  374.     def _stack_as_string(self):
  375.         names = []
  376.         for name,inst in self.stack:
  377.             names.append(name)
  378.         return string.join(names,",")
  379.  
  380. # ----------------------------------------------------------------------
  381. if __name__ == "__main__":
  382.     build = BuildTree()
  383.     #print build.make("paragraph",text="fred")
  384.     #print build.paragraph(text="fred")
  385.  
  386.     print "Building a section"
  387.     build.start("section")
  388.     build.add("title","Fred")
  389.     build.start("paragraph")
  390.     build.add("text","This is some text.")
  391.     build.add("strong","Really.")
  392.     build.start("paragraph","Another paragraph")
  393.     build.end("section")
  394.     print build.finish()
  395.  
  396.     #print "Building a broken section"
  397.     #build.start("section")
  398.     #build.add("title","Fred")
  399.     #build.start("paragraph")
  400.     #print build.finish()

Raw Paste


Login or Register to edit or fork this paste. It's free.