SF.net SVN: docutils:[9684 ] trunk/docutils

milde--- via Docutils-checkins <[email protected]>
Newsgroups gmane.text.docutils.cvs
Message-ID <[email protected]>
Revision: 9684
          http://sourceforge.net/p/docutils/code/9684
Author:   milde
Date:     2024-05-04 09:35:10 +0000 (Sat, 04 May 2024)
Log Message:
-----------
Doctree validation: Declare/check valid Element attribute names.

New method `nodes.Element.validate()`.

Allows validating a Document Tree or individual Elements,
e.g. from XML input or custom directives. Cf. [feature-requests:#94].

Part 1: validate attribute names.

Modified Paths:
--------------
    trunk/docutils/HISTORY.txt
    trunk/docutils/RELEASE-NOTES.txt
    trunk/docutils/docutils/nodes.py
    trunk/docutils/test/test_nodes.py

Modified: trunk/docutils/HISTORY.txt
===================================================================
--- trunk/docutils/HISTORY.txt	2024-05-04 08:58:36 UTC (rev 9683)
+++ trunk/docutils/HISTORY.txt	2024-05-04 09:35:10 UTC (rev 9684)
@@ -26,6 +26,7 @@
 
   - New `SubStructural` element category class.
   - Fix element categories.
+  - New method `Element.validate()` (work in progress).
 
 * docutils/transforms/frontmatter.py
 

Modified: trunk/docutils/RELEASE-NOTES.txt
===================================================================
--- trunk/docutils/RELEASE-NOTES.txt	2024-05-04 08:58:36 UTC (rev 9683)
+++ trunk/docutils/RELEASE-NOTES.txt	2024-05-04 09:35:10 UTC (rev 9684)
@@ -154,6 +154,10 @@
 * Remove the "rawsource" argument from `nodes.Text.__init__()`
   in Docutils 2.0.
 
+* Remove the deprecated attributes `nodes.Element.known_attributes`,
+  `nodes.Element.basic_attributes`, and `nodes.Element.local_attributes`,
+  in Docutils 2.0.
+
 * Drop support for `old-format configuration files`_ in Docutils 2.0.
 
 * Remove the ``--html-writer`` option of the `buildhtml.py`_ application

Modified: trunk/docutils/docutils/nodes.py
===================================================================
--- trunk/docutils/docutils/nodes.py	2024-05-04 08:58:36 UTC (rev 9683)
+++ trunk/docutils/docutils/nodes.py	2024-05-04 09:35:10 UTC (rev 9684)
@@ -398,7 +398,10 @@
     def lstrip(self, chars=None):
         return self.__class__(str.lstrip(self, chars))
 
+    def validate(self):
+        pass  # Text nodes have no attributes and no children.
 
+
 class Element(Node):
     """
     `Element` is the superclass to all specific elements.
@@ -447,24 +450,35 @@
     This is equivalent to ``element.extend([node1, node2])``.
     """
 
-    basic_attributes = ('ids', 'classes', 'names', 'dupnames')
-    """Tuple of attributes which are defined for every Element-derived class
-    instance and can be safely transferred to a different node."""
+    list_attributes = ('ids', 'classes', 'names', 'dupnames')
+    """Tuple of attributes that are initialized to empty lists.
 
-    local_attributes = ('backrefs',)
-    """Tuple of class-specific attributes that should not be copied with the
-    standard attributes when replacing a node.
+    NOTE: Derived classes should update this value when supporting
+          additional list attributes.
+    """
 
-    NOTE: Derived classes should override this value to prevent any of its
-    attributes being copied by adding to the value in its parent class."""
+    valid_attributes = list_attributes + ('source',)
+    """Tuple of attributes that are valid for elements of this class.
 
-    list_attributes = basic_attributes + local_attributes
-    """Tuple of attributes that are automatically initialized to empty lists
-    for all nodes."""
+    NOTE: Derived classes should update this value when supporting
+          additional attributes.
+    """
 
-    known_attributes = list_attributes + ('source',)
-    """Tuple of attributes that are known to the Element base class."""
+    common_attributes = valid_attributes
+    """Tuple of `common attributes`__  known to all Doctree Element classes.
 
+    __ https://docutils.sourceforge.io/docs/ref/doctree.html#common-attributes
+    """
+
+    known_attributes = common_attributes
+    """Alias for `common_attributes`. Will be removed in Docutils 2.0."""
+
+    basic_attributes = list_attributes
+    """Common list attributes. Deprecated. Will be removed in Docutils 2.0."""
+
+    local_attributes = ('backrefs',)
+    """Obsolete. Will be removed in Docutils 2.0."""
+
     tagname = None
     """The element generic identifier.
 
@@ -478,6 +492,8 @@
         self.rawsource = rawsource
         """The raw text from which this element was constructed.
 
+        For informative and debugging purposes. Don't rely on its value!
+
         NOTE: some elements do not set this value (default '').
         """
 
@@ -715,6 +731,8 @@
         """
         Update basic attributes ('ids', 'names', 'classes',
         'dupnames', but not 'source') from node or dictionary `dict_`.
+
+        Provisional.
         """
         if isinstance(dict_, Node):
             dict_ = dict_.attributes
@@ -953,6 +971,8 @@
         """
         Replace `self` node with `new`, where `new` is a node or a
         list of nodes.
+
+        Provisional: the handling of node attributes will be revised.
         """
         update = new
         if not isinstance(new, Node):
@@ -1066,12 +1086,31 @@
     @classmethod
     def is_not_known_attribute(cls, attr):
         """
-        Returns True if and only if the given attribute is NOT recognized by
-        this class.
+        Return True if `attr` is NOT defined for all Element instances.
+
+        Provisional. May be removed in Docutils 2.0.
         """
-        return attr not in cls.known_attributes
+        return attr not in cls.common_attributes
 
+    def validate_attributes(self):
+        # check for undeclared attributes
+        # TODO: check attribute values
+        for key, value in self.attributes.items():
+            if key.startswith('internal:'):
+                continue  # see docs/user/config.html#expose-internals
+            if key not in self.valid_attributes:
+                raise ValueError(
+                    f'Element <{self.tagname}> has invalid attribute "{key}".')
 
+    def validate(self):
+        # print(f'validating', self.tagname)
+        self.validate_attributes()
+        # TODO: check number of children
+        for child in self.children:
+            # TODO: check whether child has allowed type
+            child.validate()
+
+
 # ========
 #  Mixins
 # ========
@@ -1083,6 +1122,9 @@
 class BackLinkable:
     """Mixin for Elements that accept a "backrefs" attribute."""
 
+    list_attributes = Element.list_attributes + ('backrefs',)
+    valid_attributes = Element.valid_attributes + ('backrefs',)
+
     def add_backref(self, refid):
         self['backrefs'].append(refid)
 
@@ -1215,6 +1257,8 @@
 class FixedTextElement(TextElement):
     """An element which directly contains preformatted text."""
 
+    valid_attributes = Element.valid_attributes + ('xml:space',)
+
     def __init__(self, rawsource='', text='', *children, **attributes):
         super().__init__(rawsource, text, *children, **attributes)
         self.attributes['xml:space'] = 'preserve'
@@ -1238,6 +1282,7 @@
     Do not instantiate this class directly; use
     `docutils.utils.new_document()` instead.
     """
+    valid_attributes = Element.valid_attributes + ('title',)
 
     def __init__(self, settings, reporter, *args, **kwargs):
         Element.__init__(self, *args, **kwargs)
@@ -1591,7 +1636,10 @@
 #  Title Elements
 # ================
 
-class title(Titular, PreBibliographic, SubStructural, TextElement): pass
+class title(Titular, PreBibliographic, SubStructural, TextElement):
+    valid_attributes = Element.valid_attributes + ('auto', 'refid')
+
+
 class subtitle(Titular, PreBibliographic, SubStructural, TextElement): pass
 class rubric(Titular, General, TextElement): pass
 
@@ -1602,6 +1650,8 @@
 
 class meta(PreBibliographic, SubStructural, Element):
     """Container for "invisible" bibliographic data, or meta-data."""
+    valid_attributes = Element.valid_attributes + (
+        'content', 'dir', 'http-equiv', 'lang', 'media', 'name', 'scheme')
 
 
 # ========================
@@ -1666,6 +1716,8 @@
     inside topics, sidebars, or body elements; you can't have a topic inside a
     table, list, block quote, etc.
     """
+    # "depth" and "local" attributes may be added by the "Contents" transform:
+    valid_attributes = Element.valid_attributes + ('depth', 'local')
 
 
 class sidebar(Structural, Element):
@@ -1699,8 +1751,17 @@
 class paragraph(General, TextElement): pass
 class compound(General, Element): pass
 class container(General, Element): pass
-class bullet_list(Sequential, Element): pass
-class enumerated_list(Sequential, Element): pass
+
+
+class bullet_list(Sequential, Element):
+    valid_attributes = Element.valid_attributes + ('bullet',)
+
+
+class enumerated_list(Sequential, Element):
+    valid_attributes = Element.valid_attributes + (
+        'enumtype', 'prefix', 'suffix', 'start')
+
+
 class list_item(Part, Element): pass
 class definition_list(Sequential, Element): pass
 class definition_list_item(Part, Element): pass
@@ -1723,12 +1784,14 @@
 
 class option_argument(Part, TextElement):
     """Placeholder text for option arguments."""
+    valid_attributes = Element.valid_attributes + ('delimiter',)
+
     def astext(self):
         return self.get('delimiter', ' ') + TextElement.astext(self)
 
 
 class option_group(Part, Element):
-    """Groups together one or more <option> elements, all synonyms."""
+    """Groups together one or more `option` elements, all synonyms."""
     child_text_separator = ', '
 
 
@@ -1737,7 +1800,8 @@
 
 
 class option_list_item(Part, Element):
-    """Container for a pair of `option_group` and `description` elements."""
+    """Container for a pair of `option_group` and `description` elements.
+    """
     child_text_separator = '  '
 
 
@@ -1767,23 +1831,58 @@
 class warning(Admonition, Element): pass
 class admonition(Admonition, Element): pass
 class comment(Special, Invisible, FixedTextElement): pass
-class substitution_definition(Special, Invisible, TextElement): pass
-class target(Special, Invisible, Inline, TextElement, Targetable): pass
-class footnote(General, BackLinkable, Element, Labeled, Targetable): pass
+
+
+class substitution_definition(Special, Invisible, TextElement):
+    valid_attributes = Element.valid_attributes + ('ltrim', 'rtrim')
+
+
+class target(Special, Invisible, Inline, TextElement, Targetable):
+    valid_attributes = Element.valid_attributes + (
+        'anonymous', 'refid', 'refname', 'refuri')
+
+
+class footnote(General, BackLinkable, Element, Labeled, Targetable):
+    valid_attributes = Element.valid_attributes + ('auto', 'backrefs')
+
+
 class citation(General, BackLinkable, Element, Labeled, Targetable): pass
 class label(Part, TextElement): pass
-class figure(General, Element): pass
+
+
+class figure(General, Element):
+    valid_attributes = Element.valid_attributes + ('align', 'width')
+
+
 class caption(Part, TextElement): pass
 class legend(Part, Element): pass
-class table(General, Element): pass
-class tgroup(Part, Element): pass
-class colspec(Part, Element): pass
+
+
+class table(General, Element):
+    valid_attributes = Element.valid_attributes + (
+        'align', 'colsep', 'frame', 'pgwide', 'rowsep', 'width')
+
+
+class tgroup(Part, Element):
+    valid_attributes = Element.valid_attributes + (
+        'align', 'cols', 'colsep', 'rowsep')
+
+
+class colspec(Part, Element):
+    valid_attributes = Element.valid_attributes + (
+        'align', 'char', 'charoff', 'colname', 'colnum',
+        'colsep', 'colwidth', 'rowsep', 'stub')
+
+
 class thead(Part, Element): pass
 class tbody(Part, Element): pass
 class row(Part, Element): pass
-class entry(Part, Element): pass
 
 
+class entry(Part, Element):
+    valid_attributes = Element.valid_attributes + ('morecols', 'morerows')
+
+
 class system_message(Special, BackLinkable, PreBibliographic, Element):
     """
     System message element.
@@ -1791,6 +1890,8 @@
     Do not instantiate this class directly; use
     ``document.reporter.info/warning/error/severe()`` instead.
     """
+    valid_attributes = BackLinkable.valid_attributes + (
+                           'level', 'line', 'type')
 
     def __init__(self, message=None, *children, **attributes):
         rawsource = attributes.pop('rawsource', '')
@@ -1883,7 +1984,9 @@
 
 
 class raw(Special, Inline, PreBibliographic, FixedTextElement):
-    """Raw data that is to be passed untouched to the Writer."""
+    """Raw data that is to be passed untouched to the Writer.
+    """
+    valid_attributes = Element.valid_attributes + ('format', 'xml:space')
 
 
 # =================
@@ -1893,10 +1996,25 @@
 class emphasis(Inline, TextElement): pass
 class strong(Inline, TextElement): pass
 class literal(Inline, TextElement): pass
-class reference(General, Inline, Referential, TextElement): pass
-class footnote_reference(Inline, Referential, TextElement): pass
-class citation_reference(Inline, Referential, TextElement): pass
-class substitution_reference(Inline, TextElement): pass
+
+
+class reference(General, Inline, Referential, TextElement):
+    valid_attributes = Element.valid_attributes + (
+        'anonymous', 'name', 'refid', 'refname', 'refuri')
+
+
+class footnote_reference(Inline, Referential, TextElement):
+    valid_attributes = Element.valid_attributes + ('auto', 'refid', 'refname')
+
+
+class citation_reference(Inline, Referential, TextElement):
+    valid_attributes = Element.valid_attributes + ('refid', 'refname')
+
+
+class substitution_reference(Inline, TextElement):
+    valid_attributes = Element.valid_attributes + ('refname',)
+
+
 class title_reference(Inline, TextElement): pass
 class abbreviation(Inline, TextElement): pass
 class acronym(Inline, TextElement): pass
@@ -1907,12 +2025,22 @@
 
 class image(General, Inline, Element):
     """Reference to an image resource."""
+
+    valid_attributes = Element.valid_attributes + (
+        'uri', 'alt', 'align', 'height', 'width', 'scale', 'loading')
+
     def astext(self):
         return self.get('alt', '')
 
 
 class inline(Inline, TextElement): pass
-class problematic(Inline, TextElement): pass
+
+
+class problematic(Inline, TextElement):
+    valid_attributes = Element.valid_attributes + (
+                           'refid', 'refname', 'refuri')
+
+
 class generated(Inline, TextElement): pass
 
 

Modified: trunk/docutils/test/test_nodes.py
===================================================================
--- trunk/docutils/test/test_nodes.py	2024-05-04 08:58:36 UTC (rev 9683)
+++ trunk/docutils/test/test_nodes.py	2024-05-04 09:35:10 UTC (rev 9684)
@@ -469,7 +469,19 @@
         with self.assertWarns(DeprecationWarning):
             node.set_class('parrot')
 
+    def test_validate(self):
+        node = nodes.paragraph('', 'plain text', classes='my test classes')
+        node.append(nodes.emphasis('', 'emphasised text', ids='emphtext'))
+        node.validate()
 
+    def test_validate_wrong_attribute(self):
+        node = nodes.paragraph('', 'text', id='test-paragraph')
+        with self.assertRaisesRegexp(ValueError,
+                                     'Element <paragraph> '
+                                     'has invalid attribute "id".'):
+            node.validate()
+
+
 class MiscTests(unittest.TestCase):
 
     def test_node_class_names(self):

This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.



_______________________________________________
Docutils-checkins mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/docutils-checkins
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.