We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Confusing draft submission error when docName is missing
### Describe the issue

On draft submit, when `rfc` element's `docName` attribute is not set datatracker produces an error: `Error parsing XML Internet-Draft: expected string or bytes-like object`.

### Code of Conduct

- [X] I agree to follow the [IETF's Code of Conduct](https://github.com/ietf-tools/.github/blob/main/CODE_OF_CONDUCT.md)

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `ietf/utils/xmldraft.py`
Content:
```
1 # Copyright The IETF Trust 2022, All Rights Reserved
2 # -*- coding: utf-8 -*-
3 import datetime
4 import io
5 import re
6 import xml2rfc
7 
8 import debug  # pyflakes: ignore
9 
10 from contextlib import ExitStack
11 from lxml.etree import XMLSyntaxError
12 from xml2rfc.util.date import augment_date, extract_date
13 from ietf.utils.timezone import date_today
14 
15 from .draft import Draft
16 
17 
18 class XMLDraft(Draft):
19     """Draft from XML source
20 
21     Not all methods from the superclass are implemented yet.
22     """
23     def __init__(self, xml_file):
24         """Initialize XMLDraft instance
25 
26         :parameter xml_file: path to file containing XML source
27         """
28         super().__init__()
29         # cast xml_file to str so, e.g., this will work with a Path
30         self.xmltree, self.xml_version = self.parse_xml(str(xml_file))
31         self.xmlroot = self.xmltree.getroot()
32         self.filename, self.revision = self._parse_docname()
33 
34     @staticmethod
35     def parse_xml(filename):
36         """Parse XML draft
37 
38         Converts to xml2rfc v3 schema, then returns the root of the v3 tree and the original
39         xml version.
40         """
41         orig_write_out = xml2rfc.log.write_out
42         orig_write_err = xml2rfc.log.write_err
43         parser_out = io.StringIO()
44         parser_err = io.StringIO()
45 
46         with ExitStack() as stack:
47             @stack.callback
48             def cleanup():  # called when context exited, even if there's an exception
49                 xml2rfc.log.write_out = orig_write_out
50                 xml2rfc.log.write_err = orig_write_err
51 
52             xml2rfc.log.write_out = parser_out
53             xml2rfc.log.write_err = parser_err
54 
55             parser = xml2rfc.XmlRfcParser(filename, quiet=True)
56             try:
57                 tree = parser.parse()
58             except XMLSyntaxError:
59                 raise InvalidXMLError()
60             except Exception as e:
61                 raise XMLParseError(parser_out.getvalue(), parser_err.getvalue()) from e
62 
63             xml_version = tree.getroot().get('version', '2')
64             if xml_version == '2':
65                 v2v3 = xml2rfc.V2v3XmlWriter(tree)
66                 tree.tree = v2v3.convert2to3()
67         return tree, xml_version
68 
69     def _document_name(self, ref):
70         """Get document name from reference."""
71         series = ["rfc", "bcp", "fyi", "std"]
72         # handle xinclude first
73         # FIXME: this assumes the xinclude is a bibxml href; if it isn't, there can
74         # still be false negatives. it would be better to expand the xinclude and parse
75         # its seriesInfo.
76         if ref.tag.endswith("}include"):
77             name = re.search(
78                 rf"reference\.({'|'.join(series).upper()})\.(\d{{4}})\.xml",
79                 ref.attrib["href"],
80             )
81             if name:
82                 return f"{name.group(1)}{int(name.group(2))}".lower()
83             name = re.search(
84                 r"reference\.I-D\.(?:draft-)?(.*)\.xml", ref.attrib["href"]
85             )
86             if name:
87                 return f"draft-{name.group(1)}"
88             # can't extract the name, give up
89             return ""
90 
91         # check the anchor next
92         anchor = ref.get("anchor").lower()  # always give back lowercase
93         label = anchor.rstrip("0123456789")  # remove trailing digits
94         maybe_number = anchor[len(label) :]
95         if label in series and maybe_number.isdigit():
96             number = int(maybe_number)
97             return f"{label}{number}"
98 
99         # if we couldn't find a match so far, try the seriesInfo
100         series_query = " or ".join(f"@name='{x.upper()}'" for x in series)
101         for info in ref.xpath(
102             f"./seriesInfo[{series_query} or @name='Internet-Draft']"
103         ):
104             if not info.attrib["value"]:
105                 continue
106             if info.attrib["name"] == "Internet-Draft":
107                 return info.attrib["value"]
108             else:
109                 return f'{info.attrib["name"].lower()}{info.attrib["value"]}'
110         return ""
111 
112     def _reference_section_type(self, section_name):
113         """Determine reference type from name of references section"""
114         if section_name:
115             section_name = section_name.lower()
116             if 'normative' in section_name:
117                 return self.REF_TYPE_NORMATIVE
118             elif 'informative' in section_name:
119                 return self.REF_TYPE_INFORMATIVE
120         return self.REF_TYPE_UNKNOWN
121 
122     def _reference_section_name(self, section_elt):
123         section_name = section_elt.findtext('name')
124         if section_name is None and 'title' in section_elt.keys():
125             section_name = section_elt.get('title')  # fall back to title if we have it
126         return section_name
127 
128     def _parse_docname(self):
129         docname = self.xmlroot.attrib.get('docName')
130         revmatch = re.match(
131             r'^(?P<filename>.+?)(?:-(?P<rev>[0-9][0-9]))?$',
132             docname,
133 
134         )
135         if revmatch is None:
136             raise ValueError('Unable to parse docName')
137         # If a group had no match it is None
138         return revmatch.group('filename'), revmatch.group('rev')
139 
140     def get_title(self):
141         return self.xmlroot.findtext('front/title').strip()
142 
143     @staticmethod
144     def parse_creation_date(date_elt):
145         if date_elt is None:
146             return None
147         today = date_today()
148         # ths mimics handling of date elements in the xml2rfc text/html writers
149         year, month, day = extract_date(date_elt, today)
150         year, month, day = augment_date(year, month, day, today)
151         if not day:
152             # Must choose a day for a datetime.date. Per RFC 7991 sect 2.17, we use
153             # today's date if it is consistent with the rest of the date. Otherwise,
154             # arbitrariy (and consistent with the text parser) assume the 15th.
155             if year == today.year and month == today.month:
156                 day = today.day
157             else:
158                 day = 15
159         return datetime.date(year, month, day)
160 
161     def get_creation_date(self):
162         return self.parse_creation_date(self.xmlroot.find("front/date"))
163 
164     # todo fix the implementation of XMLDraft.get_abstract()
165     #
166     # This code was pulled from ietf.submit.forms where it existed for some time.
167     # It does not work, at least with modern xml2rfc. This assumes that the abstract
168     # is simply text in the front/abstract node, but the XML schema wraps the actual
169     # abstract text in <t> elements (and allows <dl>, <ol>, and <ul> as well). As a
170     # result, this method normally returns an empty string, which is later replaced by
171     # the abstract parsed from the rendered text. For now, I a commenting this out
172     # and making it explicit that the abstract always comes from the text format.
173     #
174     # def get_abstract(self):
175     #     """Extract the abstract"""
176     #     abstract = self.xmlroot.findtext('front/abstract')
177     #     return abstract.strip() if abstract else ''
178 
179     def get_author_list(self):
180         """Get detailed author list
181 
182         Returns a list of dicts with the following keys:
183             name, first_name, middle_initial, last_name,
184             name_suffix, email, country, affiliation
185         Values will be None if not available
186         """
187         result = []
188         empty_author = {
189             k: None for k in [
190                 'name', 'first_name', 'middle_initial', 'last_name',
191                 'name_suffix', 'email', 'country', 'affiliation',
192             ]
193         }
194 
195         for author in self.xmlroot.findall('front/author'):
196             info = {
197                 'name': author.attrib.get('fullname'),
198                 'email': author.findtext('address/email'),
199                 'affiliation': author.findtext('organization'),
200             }
201             elem = author.find('address/postal/country')
202             if elem is not None:
203                 ascii_country = elem.get('ascii', None)
204                 info['country'] = ascii_country if ascii_country else elem.text
205             for item in info:
206                 if info[item]:
207                     info[item] = info[item].strip()
208             result.append(empty_author | info)  # merge, preferring info
209         return result
210 
211     def get_refs(self):
212         """Extract references from the draft"""
213         refs = {}
214         # accept nested <references> sections
215         for section in self.xmlroot.findall("back//references"):
216             ref_type = self._reference_section_type(
217                 self._reference_section_name(section)
218             )
219             for ref in (
220                 section.findall("./reference")
221                 + section.findall("./referencegroup")
222                 + section.findall(
223                     "./xi:include", {"xi": "http://www.w3.org/2001/XInclude"}
224                 )
225             ):
226                 name = self._document_name(ref)
227                 if name:
228                     refs[name] = ref_type
229         return refs
230 
231 
232 class XMLParseError(Exception):
233     """An error occurred while parsing"""
234     def __init__(self, out: str, err: str, *args):
235         super().__init__(*args)
236         self._out = out
237         self._err = err
238 
239     def parser_msgs(self):
240         return self._out.splitlines() + self._err.splitlines()
241 
242 
243 class InvalidXMLError(Exception):
244     """File is not valid XML"""
245     pass
246
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/ietf/utils/xmldraft.py b/ietf/utils/xmldraft.py
--- a/ietf/utils/xmldraft.py
+++ b/ietf/utils/xmldraft.py
@@ -29,7 +29,7 @@
         # cast xml_file to str so, e.g., this will work with a Path
         self.xmltree, self.xml_version = self.parse_xml(str(xml_file))
         self.xmlroot = self.xmltree.getroot()
-        self.filename, self.revision = self._parse_docname()
+        self.filename, self.revision = self.parse_docname(self.xmlroot)
 
     @staticmethod
     def parse_xml(filename):
@@ -125,8 +125,11 @@
             section_name = section_elt.get('title')  # fall back to title if we have it
         return section_name
 
-    def _parse_docname(self):
-        docname = self.xmlroot.attrib.get('docName')
+    @staticmethod
+    def parse_docname(xmlroot):
+        docname = xmlroot.attrib.get('docName')
+        if docname is None:
+            raise ValueError("Missing docName attribute in the XML root element")
         revmatch = re.match(
             r'^(?P<filename>.+?)(?:-(?P<rev>[0-9][0-9]))?$',
             docname,

{"golden_diff": "diff --git a/ietf/utils/xmldraft.py b/ietf/utils/xmldraft.py\n--- a/ietf/utils/xmldraft.py\n+++ b/ietf/utils/xmldraft.py\n@@ -29,7 +29,7 @@\n         # cast xml_file to str so, e.g., this will work with a Path\n         self.xmltree, self.xml_version = self.parse_xml(str(xml_file))\n         self.xmlroot = self.xmltree.getroot()\n-        self.filename, self.revision = self._parse_docname()\n+        self.filename, self.revision = self.parse_docname(self.xmlroot)\n \n     @staticmethod\n     def parse_xml(filename):\n@@ -125,8 +125,11 @@\n             section_name = section_elt.get('title')  # fall back to title if we have it\n         return section_name\n \n-    def _parse_docname(self):\n-        docname = self.xmlroot.attrib.get('docName')\n+    @staticmethod\n+    def parse_docname(xmlroot):\n+        docname = xmlroot.attrib.get('docName')\n+        if docname is None:\n+            raise ValueError(\"Missing docName attribute in the XML root element\")\n         revmatch = re.match(\n             r'^(?P<filename>.+?)(?:-(?P<rev>[0-9][0-9]))?$',\n             docname,\n", "issue": "Confusing draft submission error when docName is missing\n### Describe the issue\r\n\r\nOn draft submit, when `rfc` element's `docName` attribute is not set datatracker produces an error: `Error parsing XML Internet-Draft: expected string or bytes-like object`.\r\n\r\n### Code of Conduct\r\n\r\n- [X] I agree to follow the [IETF's Code of Conduct](https://github.com/ietf-tools/.github/blob/main/CODE_OF_CONDUCT.md)\n", "before_files": [{"content": "# Copyright The IETF Trust 2022, All Rights Reserved\n# -*- coding: utf-8 -*-\nimport datetime\nimport io\nimport re\nimport xml2rfc\n\nimport debug  # pyflakes: ignore\n\nfrom contextlib import ExitStack\nfrom lxml.etree import XMLSyntaxError\nfrom xml2rfc.util.date import augment_date, extract_date\nfrom ietf.utils.timezone import date_today\n\nfrom .draft import Draft\n\n\nclass XMLDraft(Draft):\n    \"\"\"Draft from XML source\n\n    Not all methods from the superclass are implemented yet.\n    \"\"\"\n    def __init__(self, xml_file):\n        \"\"\"Initialize XMLDraft instance\n\n        :parameter xml_file: path to file containing XML source\n        \"\"\"\n        super().__init__()\n        # cast xml_file to str so, e.g., this will work with a Path\n        self.xmltree, self.xml_version = self.parse_xml(str(xml_file))\n        self.xmlroot = self.xmltree.getroot()\n        self.filename, self.revision = self._parse_docname()\n\n    @staticmethod\n    def parse_xml(filename):\n        \"\"\"Parse XML draft\n\n        Converts to xml2rfc v3 schema, then returns the root of the v3 tree and the original\n        xml version.\n        \"\"\"\n        orig_write_out = xml2rfc.log.write_out\n        orig_write_err = xml2rfc.log.write_err\n        parser_out = io.StringIO()\n        parser_err = io.StringIO()\n\n        with ExitStack() as stack:\n            @stack.callback\n            def cleanup():  # called when context exited, even if there's an exception\n                xml2rfc.log.write_out = orig_write_out\n                xml2rfc.log.write_err = orig_write_err\n\n            xml2rfc.log.write_out = parser_out\n            xml2rfc.log.write_err = parser_err\n\n            parser = xml2rfc.XmlRfcParser(filename, quiet=True)\n            try:\n                tree = parser.parse()\n            except XMLSyntaxError:\n                raise InvalidXMLError()\n            except Exception as e:\n                raise XMLParseError(parser_out.getvalue(), parser_err.getvalue()) from e\n\n            xml_version = tree.getroot().get('version', '2')\n            if xml_version == '2':\n                v2v3 = xml2rfc.V2v3XmlWriter(tree)\n                tree.tree = v2v3.convert2to3()\n        return tree, xml_version\n\n    def _document_name(self, ref):\n        \"\"\"Get document name from reference.\"\"\"\n        series = [\"rfc\", \"bcp\", \"fyi\", \"std\"]\n        # handle xinclude first\n        # FIXME: this assumes the xinclude is a bibxml href; if it isn't, there can\n        # still be false negatives. it would be better to expand the xinclude and parse\n        # its seriesInfo.\n        if ref.tag.endswith(\"}include\"):\n            name = re.search(\n                rf\"reference\\.({'|'.join(series).upper()})\\.(\\d{{4}})\\.xml\",\n                ref.attrib[\"href\"],\n            )\n            if name:\n                return f\"{name.group(1)}{int(name.group(2))}\".lower()\n            name = re.search(\n                r\"reference\\.I-D\\.(?:draft-)?(.*)\\.xml\", ref.attrib[\"href\"]\n            )\n            if name:\n                return f\"draft-{name.group(1)}\"\n            # can't extract the name, give up\n            return \"\"\n\n        # check the anchor next\n        anchor = ref.get(\"anchor\").lower()  # always give back lowercase\n        label = anchor.rstrip(\"0123456789\")  # remove trailing digits\n        maybe_number = anchor[len(label) :]\n        if label in series and maybe_number.isdigit():\n            number = int(maybe_number)\n            return f\"{label}{number}\"\n\n        # if we couldn't find a match so far, try the seriesInfo\n        series_query = \" or \".join(f\"@name='{x.upper()}'\" for x in series)\n        for info in ref.xpath(\n            f\"./seriesInfo[{series_query} or @name='Internet-Draft']\"\n        ):\n            if not info.attrib[\"value\"]:\n                continue\n            if info.attrib[\"name\"] == \"Internet-Draft\":\n                return info.attrib[\"value\"]\n            else:\n                return f'{info.attrib[\"name\"].lower()}{info.attrib[\"value\"]}'\n        return \"\"\n\n    def _reference_section_type(self, section_name):\n        \"\"\"Determine reference type from name of references section\"\"\"\n        if section_name:\n            section_name = section_name.lower()\n            if 'normative' in section_name:\n                return self.REF_TYPE_NORMATIVE\n            elif 'informative' in section_name:\n                return self.REF_TYPE_INFORMATIVE\n        return self.REF_TYPE_UNKNOWN\n\n    def _reference_section_name(self, section_elt):\n        section_name = section_elt.findtext('name')\n        if section_name is None and 'title' in section_elt.keys():\n            section_name = section_elt.get('title')  # fall back to title if we have it\n        return section_name\n\n    def _parse_docname(self):\n        docname = self.xmlroot.attrib.get('docName')\n        revmatch = re.match(\n            r'^(?P<filename>.+?)(?:-(?P<rev>[0-9][0-9]))?$',\n            docname,\n\n        )\n        if revmatch is None:\n            raise ValueError('Unable to parse docName')\n        # If a group had no match it is None\n        return revmatch.group('filename'), revmatch.group('rev')\n\n    def get_title(self):\n        return self.xmlroot.findtext('front/title').strip()\n\n    @staticmethod\n    def parse_creation_date(date_elt):\n        if date_elt is None:\n            return None\n        today = date_today()\n        # ths mimics handling of date elements in the xml2rfc text/html writers\n        year, month, day = extract_date(date_elt, today)\n        year, month, day = augment_date(year, month, day, today)\n        if not day:\n            # Must choose a day for a datetime.date. Per RFC 7991 sect 2.17, we use\n            # today's date if it is consistent with the rest of the date. Otherwise,\n            # arbitrariy (and consistent with the text parser) assume the 15th.\n            if year == today.year and month == today.month:\n                day = today.day\n            else:\n                day = 15\n        return datetime.date(year, month, day)\n\n    def get_creation_date(self):\n        return self.parse_creation_date(self.xmlroot.find(\"front/date\"))\n\n    # todo fix the implementation of XMLDraft.get_abstract()\n    #\n    # This code was pulled from ietf.submit.forms where it existed for some time.\n    # It does not work, at least with modern xml2rfc. This assumes that the abstract\n    # is simply text in the front/abstract node, but the XML schema wraps the actual\n    # abstract text in <t> elements (and allows <dl>, <ol>, and <ul> as well). As a\n    # result, this method normally returns an empty string, which is later replaced by\n    # the abstract parsed from the rendered text. For now, I a commenting this out\n    # and making it explicit that the abstract always comes from the text format.\n    #\n    # def get_abstract(self):\n    #     \"\"\"Extract the abstract\"\"\"\n    #     abstract = self.xmlroot.findtext('front/abstract')\n    #     return abstract.strip() if abstract else ''\n\n    def get_author_list(self):\n        \"\"\"Get detailed author list\n\n        Returns a list of dicts with the following keys:\n            name, first_name, middle_initial, last_name,\n            name_suffix, email, country, affiliation\n        Values will be None if not available\n        \"\"\"\n        result = []\n        empty_author = {\n            k: None for k in [\n                'name', 'first_name', 'middle_initial', 'last_name',\n                'name_suffix', 'email', 'country', 'affiliation',\n            ]\n        }\n\n        for author in self.xmlroot.findall('front/author'):\n            info = {\n                'name': author.attrib.get('fullname'),\n                'email': author.findtext('address/email'),\n                'affiliation': author.findtext('organization'),\n            }\n            elem = author.find('address/postal/country')\n            if elem is not None:\n                ascii_country = elem.get('ascii', None)\n                info['country'] = ascii_country if ascii_country else elem.text\n            for item in info:\n                if info[item]:\n                    info[item] = info[item].strip()\n            result.append(empty_author | info)  # merge, preferring info\n        return result\n\n    def get_refs(self):\n        \"\"\"Extract references from the draft\"\"\"\n        refs = {}\n        # accept nested <references> sections\n        for section in self.xmlroot.findall(\"back//references\"):\n            ref_type = self._reference_section_type(\n                self._reference_section_name(section)\n            )\n            for ref in (\n                section.findall(\"./reference\")\n                + section.findall(\"./referencegroup\")\n                + section.findall(\n                    \"./xi:include\", {\"xi\": \"http://www.w3.org/2001/XInclude\"}\n                )\n            ):\n                name = self._document_name(ref)\n                if name:\n                    refs[name] = ref_type\n        return refs\n\n\nclass XMLParseError(Exception):\n    \"\"\"An error occurred while parsing\"\"\"\n    def __init__(self, out: str, err: str, *args):\n        super().__init__(*args)\n        self._out = out\n        self._err = err\n\n    def parser_msgs(self):\n        return self._out.splitlines() + self._err.splitlines()\n\n\nclass InvalidXMLError(Exception):\n    \"\"\"File is not valid XML\"\"\"\n    pass\n", "path": "ietf/utils/xmldraft.py"}], "after_files": [{"content": "# Copyright The IETF Trust 2022, All Rights Reserved\n# -*- coding: utf-8 -*-\nimport datetime\nimport io\nimport re\nimport xml2rfc\n\nimport debug  # pyflakes: ignore\n\nfrom contextlib import ExitStack\nfrom lxml.etree import XMLSyntaxError\nfrom xml2rfc.util.date import augment_date, extract_date\nfrom ietf.utils.timezone import date_today\n\nfrom .draft import Draft\n\n\nclass XMLDraft(Draft):\n    \"\"\"Draft from XML source\n\n    Not all methods from the superclass are implemented yet.\n    \"\"\"\n    def __init__(self, xml_file):\n        \"\"\"Initialize XMLDraft instance\n\n        :parameter xml_file: path to file containing XML source\n        \"\"\"\n        super().__init__()\n        # cast xml_file to str so, e.g., this will work with a Path\n        self.xmltree, self.xml_version = self.parse_xml(str(xml_file))\n        self.xmlroot = self.xmltree.getroot()\n        self.filename, self.revision = self.parse_docname(self.xmlroot)\n\n    @staticmethod\n    def parse_xml(filename):\n        \"\"\"Parse XML draft\n\n        Converts to xml2rfc v3 schema, then returns the root of the v3 tree and the original\n        xml version.\n        \"\"\"\n        orig_write_out = xml2rfc.log.write_out\n        orig_write_err = xml2rfc.log.write_err\n        parser_out = io.StringIO()\n        parser_err = io.StringIO()\n\n        with ExitStack() as stack:\n            @stack.callback\n            def cleanup():  # called when context exited, even if there's an exception\n                xml2rfc.log.write_out = orig_write_out\n                xml2rfc.log.write_err = orig_write_err\n\n            xml2rfc.log.write_out = parser_out\n            xml2rfc.log.write_err = parser_err\n\n            parser = xml2rfc.XmlRfcParser(filename, quiet=True)\n            try:\n                tree = parser.parse()\n            except XMLSyntaxError:\n                raise InvalidXMLError()\n            except Exception as e:\n                raise XMLParseError(parser_out.getvalue(), parser_err.getvalue()) from e\n\n            xml_version = tree.getroot().get('version', '2')\n            if xml_version == '2':\n                v2v3 = xml2rfc.V2v3XmlWriter(tree)\n                tree.tree = v2v3.convert2to3()\n        return tree, xml_version\n\n    def _document_name(self, ref):\n        \"\"\"Get document name from reference.\"\"\"\n        series = [\"rfc\", \"bcp\", \"fyi\", \"std\"]\n        # handle xinclude first\n        # FIXME: this assumes the xinclude is a bibxml href; if it isn't, there can\n        # still be false negatives. it would be better to expand the xinclude and parse\n        # its seriesInfo.\n        if ref.tag.endswith(\"}include\"):\n            name = re.search(\n                rf\"reference\\.({'|'.join(series).upper()})\\.(\\d{{4}})\\.xml\",\n                ref.attrib[\"href\"],\n            )\n            if name:\n                return f\"{name.group(1)}{int(name.group(2))}\".lower()\n            name = re.search(\n                r\"reference\\.I-D\\.(?:draft-)?(.*)\\.xml\", ref.attrib[\"href\"]\n            )\n            if name:\n                return f\"draft-{name.group(1)}\"\n            # can't extract the name, give up\n            return \"\"\n\n        # check the anchor next\n        anchor = ref.get(\"anchor\").lower()  # always give back lowercase\n        label = anchor.rstrip(\"0123456789\")  # remove trailing digits\n        maybe_number = anchor[len(label) :]\n        if label in series and maybe_number.isdigit():\n            number = int(maybe_number)\n            return f\"{label}{number}\"\n\n        # if we couldn't find a match so far, try the seriesInfo\n        series_query = \" or \".join(f\"@name='{x.upper()}'\" for x in series)\n        for info in ref.xpath(\n            f\"./seriesInfo[{series_query} or @name='Internet-Draft']\"\n        ):\n            if not info.attrib[\"value\"]:\n                continue\n            if info.attrib[\"name\"] == \"Internet-Draft\":\n                return info.attrib[\"value\"]\n            else:\n                return f'{info.attrib[\"name\"].lower()}{info.attrib[\"value\"]}'\n        return \"\"\n\n    def _reference_section_type(self, section_name):\n        \"\"\"Determine reference type from name of references section\"\"\"\n        if section_name:\n            section_name = section_name.lower()\n            if 'normative' in section_name:\n                return self.REF_TYPE_NORMATIVE\n            elif 'informative' in section_name:\n                return self.REF_TYPE_INFORMATIVE\n        return self.REF_TYPE_UNKNOWN\n\n    def _reference_section_name(self, section_elt):\n        section_name = section_elt.findtext('name')\n        if section_name is None and 'title' in section_elt.keys():\n            section_name = section_elt.get('title')  # fall back to title if we have it\n        return section_name\n\n    @staticmethod\n    def parse_docname(xmlroot):\n        docname = xmlroot.attrib.get('docName')\n        if docname is None:\n            raise ValueError(\"Missing docName attribute in the XML root element\")\n        revmatch = re.match(\n            r'^(?P<filename>.+?)(?:-(?P<rev>[0-9][0-9]))?$',\n            docname,\n\n        )\n        if revmatch is None:\n            raise ValueError('Unable to parse docName')\n        # If a group had no match it is None\n        return revmatch.group('filename'), revmatch.group('rev')\n\n    def get_title(self):\n        return self.xmlroot.findtext('front/title').strip()\n\n    @staticmethod\n    def parse_creation_date(date_elt):\n        if date_elt is None:\n            return None\n        today = date_today()\n        # ths mimics handling of date elements in the xml2rfc text/html writers\n        year, month, day = extract_date(date_elt, today)\n        year, month, day = augment_date(year, month, day, today)\n        if not day:\n            # Must choose a day for a datetime.date. Per RFC 7991 sect 2.17, we use\n            # today's date if it is consistent with the rest of the date. Otherwise,\n            # arbitrariy (and consistent with the text parser) assume the 15th.\n            if year == today.year and month == today.month:\n                day = today.day\n            else:\n                day = 15\n        return datetime.date(year, month, day)\n\n    def get_creation_date(self):\n        return self.parse_creation_date(self.xmlroot.find(\"front/date\"))\n\n    # todo fix the implementation of XMLDraft.get_abstract()\n    #\n    # This code was pulled from ietf.submit.forms where it existed for some time.\n    # It does not work, at least with modern xml2rfc. This assumes that the abstract\n    # is simply text in the front/abstract node, but the XML schema wraps the actual\n    # abstract text in <t> elements (and allows <dl>, <ol>, and <ul> as well). As a\n    # result, this method normally returns an empty string, which is later replaced by\n    # the abstract parsed from the rendered text. For now, I a commenting this out\n    # and making it explicit that the abstract always comes from the text format.\n    #\n    # def get_abstract(self):\n    #     \"\"\"Extract the abstract\"\"\"\n    #     abstract = self.xmlroot.findtext('front/abstract')\n    #     return abstract.strip() if abstract else ''\n\n    def get_author_list(self):\n        \"\"\"Get detailed author list\n\n        Returns a list of dicts with the following keys:\n            name, first_name, middle_initial, last_name,\n            name_suffix, email, country, affiliation\n        Values will be None if not available\n        \"\"\"\n        result = []\n        empty_author = {\n            k: None for k in [\n                'name', 'first_name', 'middle_initial', 'last_name',\n                'name_suffix', 'email', 'country', 'affiliation',\n            ]\n        }\n\n        for author in self.xmlroot.findall('front/author'):\n            info = {\n                'name': author.attrib.get('fullname'),\n                'email': author.findtext('address/email'),\n                'affiliation': author.findtext('organization'),\n            }\n            elem = author.find('address/postal/country')\n            if elem is not None:\n                ascii_country = elem.get('ascii', None)\n                info['country'] = ascii_country if ascii_country else elem.text\n            for item in info:\n                if info[item]:\n                    info[item] = info[item].strip()\n            result.append(empty_author | info)  # merge, preferring info\n        return result\n\n    def get_refs(self):\n        \"\"\"Extract references from the draft\"\"\"\n        refs = {}\n        # accept nested <references> sections\n        for section in self.xmlroot.findall(\"back//references\"):\n            ref_type = self._reference_section_type(\n                self._reference_section_name(section)\n            )\n            for ref in (\n                section.findall(\"./reference\")\n                + section.findall(\"./referencegroup\")\n                + section.findall(\n                    \"./xi:include\", {\"xi\": \"http://www.w3.org/2001/XInclude\"}\n                )\n            ):\n                name = self._document_name(ref)\n                if name:\n                    refs[name] = ref_type\n        return refs\n\n\nclass XMLParseError(Exception):\n    \"\"\"An error occurred while parsing\"\"\"\n    def __init__(self, out: str, err: str, *args):\n        super().__init__(*args)\n        self._out = out\n        self._err = err\n\n    def parser_msgs(self):\n        return self._out.splitlines() + self._err.splitlines()\n\n\nclass InvalidXMLError(Exception):\n    \"\"\"File is not valid XML\"\"\"\n    pass\n", "path": "ietf/utils/xmldraft.py"}]}