From 4a52b3c7a674f8e19ed096f6bc027bc2c582de84 Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Tue, 20 Apr 2021 07:41:21 +0100 Subject: [PATCH 01/37] Add PEP 0 parser --- .../pep_zero_generator/pep_0_parser.py | 288 ++++++++++++++++++ 1 file changed, 288 insertions(+) create mode 100644 pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py b/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py new file mode 100644 index 00000000000..308090c4cf3 --- /dev/null +++ b/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py @@ -0,0 +1,288 @@ +"""Code for handling object representation of a PEP.""" + +from __future__ import annotations + +from email.parser import HeaderParser +from pathlib import Path +import re +import textwrap +from typing import NamedTuple +import unicodedata + + +class PEPError(Exception): + def __init__(self, error: str, pep_file: Path, pep_number: int | None = None): + super().__init__(error) + self.filename = pep_file + self.number = pep_number + + def __str__(self): + error_msg = super(PEPError, self).__str__() + error_msg = f"({self.filename}): {error_msg}" + pep_str = f"PEP {self.number}" + return f"{pep_str} {error_msg}" if self.number is not None else error_msg + + +class Name(NamedTuple): + name: str = None # mononym + forename: str = None + surname: str = None + suffix: str = None + + +class Author: + """Represent PEP authors. + + Attributes: + last_first: The author's name in Surname, Forename, Suffix order. + nick: Author's nickname for PEP tables. Defaults to surname. + email: The author's email address. + _first_last: The author's full name, unchanged + + """ + __slots__ = "last_first", "nick", "email", "_first_last" + + def __init__(self, author_email_tuple: tuple[str, str], authors_exceptions: dict[str, dict[str, str]]): + """Parse the name and email address of an author.""" + name, email = author_email_tuple + self._first_last: str = name.strip() + self.email: str = email.lower() + + self.last_first: str = "" + self.nick: str = "" + + if self._first_last in authors_exceptions: + name_dict = authors_exceptions[self._first_last] + self.last_first = name_dict["Surname First"] + self.nick = name_dict["Name Reference"] + else: + name_parts = self._parse_name(self._first_last) + if name_parts.name is not None: + self.last_first = self.nick = name_parts.name + else: + if name_parts.surname[1] == ".": + # Add an escape to avoid docutils turning `v.` into `22.`. + name_parts.surname = f"\\{name_parts.surname}" + self.last_first = f"{name_parts.surname}, {name_parts.forename}" + self.nick = name_parts.surname + + if name_parts.suffix is not None: + self.last_first += f", {name_parts.suffix}" + + def __hash__(self): + return hash(self.last_first) + + def __eq__(self, other): + if not isinstance(other, Author): + return NotImplemented + return self.last_first == other.last_first + + def __len__(self): + return len(unicodedata.normalize("NFC", self.last_first)) + + @staticmethod + def _parse_name(full_name: str) -> Name: + """Decompose a full name into parts. + + If a mononym (e.g, 'Aahz') then return the full name. If there are + suffixes in the name (e.g. ', Jr.' or 'III'), then find and extract + them. If there is a middle initial followed by a full stop, then + combine the following words into a surname (e.g. N. Vander Weele). If + there is a leading, lowercase portion to the last name (e.g. 'van' or + 'von') then include it in the surname. + + """ + possible_suffixes = {"Jr", "Jr.", "II", "III"} + + pre_suffix, _, raw_suffix = full_name.partition(",") + name_parts = pre_suffix.strip().split(" ") + num_parts = len(name_parts) + suffix = raw_suffix.strip() or None + + if num_parts == 0: + raise ValueError("Name is empty!") + elif num_parts == 1: + return Name(name=name_parts[0], suffix=suffix) + elif num_parts == 2: + return Name(forename=name_parts[0].strip(), surname=name_parts[1], suffix=suffix) + + # handles rogue uncaught suffixes + if name_parts[-1] in possible_suffixes: + suffix = f"{name_parts.pop(-1)} {suffix}".strip() + + # handles von, van, v. etc. + if name_parts[-2].islower(): + forename = " ".join(name_parts[:-2]).strip() + surname = " ".join(name_parts[-2:]) + return Name(forename=forename, surname=surname, suffix=suffix) + + # handles double surnames after a middle initial (e.g. N. Vander Weele) + elif any(s.endswith(".") for s in name_parts): + split_position = [i for i, x in enumerate(name_parts) if x.endswith(".")][-1] + 1 + forename = " ".join(name_parts[:split_position]).strip() + surname = " ".join(name_parts[split_position:]) + return Name(forename=forename, surname=surname, suffix=suffix) + + # default to using the last item as the surname + else: + forename = " ".join(name_parts[:-1]).strip() + return Name(forename=forename, surname=name_parts[-1], suffix=suffix) + + +def author_sort_by(author: Author) -> str: + """Skip lower-cased words in surname when sorting.""" + surname, *_ = author.last_first.split(",") + surname_parts = surname.split() + for i, part in enumerate(surname_parts): + if part[0].isupper(): + base = " ".join(surname_parts[i:]).lower() + return unicodedata.normalize("NFKD", base) + # If no capitals, use the whole string + return unicodedata.normalize("NFKD", surname.lower()) + + +class PEP: + """Representation of PEPs. + + Attributes: + number : PEP number. + title : PEP title. + pep_type : The type of PEP. Can only be one of the values from PEP.type_values. + status : The PEP's status. Value must be found in PEP.status_values. + authors : A list of the authors. + + """ + + # The required RFC 822 headers for all PEPs. + required_headers = {"PEP", "Title", "Author", "Status", "Type", "Created"} + + # Valid values for the Type header. + type_values = {"Standards Track", "Informational", "Process"} + # Valid values for the Status header. + # Active PEPs can only be for Informational or Process PEPs. + status_values = { + "Accepted", "Provisional", "Rejected", "Withdrawn", + "Deferred", "Final", "Active", "Draft", "Superseded", + } + + def raise_pep_error(self, msg: str, pep_num: bool = False) -> None: + pep_number = self.number if pep_num else None + raise PEPError(msg, self.filename, pep_number=pep_number) + + def __init__(self, filename: Path, author_lookup: dict, title_length: int): + """Init object from an open PEP file object. + + pep_file is full text of the PEP file, filename is path of the PEP file, author_lookup is author exceptions file + + """ + self.filename: Path = filename + self.title_length: int = title_length + + # Parse the headers. + pep_text = filename.read_text("UTF8") + metadata = HeaderParser().parsestr(pep_text) + required_header_misses = self.required_headers - set(metadata.keys()) + if required_header_misses: + msg = f"PEP is missing required headers ({', '.join(required_header_misses)})" + self.raise_pep_error(msg) + + try: + self.number: int = int(metadata["PEP"]) + except ValueError: + self.raise_pep_error("PEP number isn't an integer") + + # Check PEP number matches filename + if self.number != int(filename.stem[4:]): + self.raise_pep_error(f"PEP number does not match file name ({filename})", pep_num=True) + + # Title + self.title: str = metadata["Title"] + + # Type + self.pep_type: str = metadata["Type"] + if self.pep_type not in self.type_values: + self.raise_pep_error(f"{self.pep_type} is not a valid Type value", pep_num=True) + + # Status + status = metadata["Status"] + if status not in self.status_values: + if status == "April Fool!": # See PEP 401 :) + status = "Rejected" + else: + self.raise_pep_error(f"{status} is not a valid Status value", pep_num=True) + + # Special case for Active PEPs. + if status == "Active" and self.pep_type not in {"Process", "Informational"}: + msg = "Only Process and Informational PEPs may have an Active status" + self.raise_pep_error(msg, pep_num=True) + + # Special case for Provisional PEPs. + if status == "Provisional" and self.pep_type != "Standards Track": + msg = "Only Standards Track PEPs may have a Provisional status" + self.raise_pep_error(msg, pep_num=True) + self.status: str = status + + # Parse PEP authors + self.authors: list[Author] = self.parse_authors(metadata["Author"], author_lookup) + + def parse_authors(self, author_header: str, author_lookup: dict) -> list[Author]: + """Parse Author header line""" + authors_and_emails = self._parse_author(author_header) + if not authors_and_emails: + raise self.raise_pep_error("no authors found", pep_num=True) + return [Author(author_tuple, author_lookup) for author_tuple in authors_and_emails] + + angled = re.compile(r"(?P.+?) <(?P.+?)>(,\s*)?") + paren = re.compile(r"(?P.+?) \((?P.+?)\)(,\s*)?") + simple = re.compile(r"(?P[^,]+)(,\s*)?") + + @staticmethod + def _parse_author(data: str) -> list[tuple[str, str]]: + """Return a list of author names and emails.""" + # XXX Consider using email.utils.parseaddr (doesn't work with names + # lacking an email address. + + author_list = [] + for regex in (PEP.angled, PEP.paren, PEP.simple): + for match in regex.finditer(data): + # Watch out for suffixes like 'Jr.' when they are comma-separated + # from the name and thus cause issues when *all* names are only + # separated by commas. + match_dict = match.groupdict() + author = match_dict["author"] + if not author.partition(" ")[1] and author.endswith("."): + prev_author = author_list.pop() + author = ", ".join([prev_author, author]) + if "email" not in match_dict: + email = "" + else: + email = match_dict["email"] + author_list.append((author, email)) + else: + # If authors were found then stop searching as only expect one + # style of author citation. + if author_list: + break + return author_list + + @property + def title_abbr(self) -> str: + """Shorten the title to be no longer than the max title length.""" + if len(self.title) <= self.title_length: + return self.title + wrapped_title, *_excess = textwrap.wrap(self.title, self.title_length - 4) + return f"{wrapped_title} ..." + + @property + def pep(self) -> dict[str, str | int]: + """Return the line entry for the PEP.""" + return { + # how the type is to be represented in the index + "type": self.pep_type[0].upper(), + "number": self.number, + "title": self.title_abbr, + # how the status should be represented in the index + "status": self.status[0].upper() if self.status not in {"Draft", "Active"} else " ", + # the author list as a comma-separated with only last names + "authors": ", ".join(x.nick for x in self.authors), + } From d3771d4d85ffb114178a63ad1455334b2bf3ec05 Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Tue, 20 Apr 2021 07:41:32 +0100 Subject: [PATCH 02/37] Add PEP 0 writer --- .../pep_zero_generator/pep_0_writer.py | 287 ++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 pep_sphinx_extensions/pep_zero_generator/pep_0_writer.py diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_0_writer.py b/pep_sphinx_extensions/pep_zero_generator/pep_0_writer.py new file mode 100644 index 00000000000..b5e2289ed90 --- /dev/null +++ b/pep_sphinx_extensions/pep_zero_generator/pep_0_writer.py @@ -0,0 +1,287 @@ +"""Code to handle the output of PEP 0.""" + +import datetime +import functools + +from pep_sphinx_extensions.pep_zero_generator import pep_0_parser + +title_length = 55 +author_length = 40 +table_separator = "== ==== " + "="*title_length + " " + "="*author_length + +# column format is called as a function with a mapping containing field values +column_format = functools.partial( + "{type}{status}{number: >5} {title: <{title_length}} {authors}".format, + title_length=title_length +) + +header = f"""\ +PEP: 0 +Title: Index of Python Enhancement Proposals (PEPs) +Last-Modified: {datetime.date.today()} +Author: python-dev +Status: Active +Type: Informational +Content-Type: text/x-rst +Created: 13-Jul-2000 +""" + +intro = """\ +This PEP contains the index of all Python Enhancement Proposals, +known as PEPs. PEP numbers are assigned by the PEP editors, and +once assigned are never changed [1_]. The version control history [2_] of +the PEP texts represent their historical record. +""" + +references = """\ +.. [1] PEP 1: PEP Purpose and Guidelines +.. [2] View PEP history online: https://github.com/python/peps +""" + + +class PEPZeroWriter: + # This is a list of reserved PEP numbers. Reservations are not to be used for + # the normal PEP number allocation process - just give out the next available + # PEP number. These are for "special" numbers that may be used for semantic, + # humorous, or other such reasons, e.g. 401, 666, 754. + # + # PEP numbers may only be reserved with the approval of a PEP editor. Fields + # here are the PEP number being reserved and the claimants for the PEP. + # Although the output is sorted when PEP 0 is generated, please keep this list + # sorted as well. + RESERVED = { + 801: "Warsaw", + } + + def __init__(self): + self._output: list[str] = [] + + def output(self, content: str) -> None: + # Appends content argument to the _output list + self._output.append(content) + + def emit_newline(self) -> None: + self.output("") + + def emit_table_separator(self) -> None: + self.output(table_separator) + + def emit_author_table_separator(self, max_name_len: int) -> None: + author_table_separator = "=" * max_name_len + " " + "=" * len("email address") + self.output(author_table_separator) + + def emit_column_headers(self) -> None: + """Output the column headers for the PEP indices.""" + self.emit_table_separator() + self.output(column_format( + status=".", + type=".", + number="PEP", + title="PEP Title", + authors="PEP Author(s)", + )) + self.emit_table_separator() + + @staticmethod + def sort_peps(peps: list[pep_0_parser.PEP]) -> tuple[list[pep_0_parser.PEP], ...]: + """Sort PEPs into meta, informational, accepted, open, finished, + and essentially dead.""" + meta = [] + info = [] + provisional = [] + accepted = [] + open_ = [] + finished = [] + historical = [] + deferred = [] + dead = [] + for pep in peps: + # Order of 'if' statement important. Key Status values take precedence + # over Type value, and vice-versa. + if pep.status == "Draft": + open_.append(pep) + elif pep.status == "Deferred": + deferred.append(pep) + elif pep.pep_type == "Process": + if pep.status == "Active": + meta.append(pep) + elif pep.status in {"Withdrawn", "Rejected"}: + dead.append(pep) + else: + historical.append(pep) + elif pep.status in {"Rejected", "Withdrawn", "Incomplete", "Superseded"}: + dead.append(pep) + elif pep.pep_type == "Informational": + # Hack until the conflict between the use of "Final" + # for both API definition PEPs and other (actually + # obsolete) PEPs is addressed + if pep.status == "Active" or "Release Schedule" not in pep.title: + info.append(pep) + else: + historical.append(pep) + elif pep.status == "Provisional": + provisional.append(pep) + elif pep.status in {"Accepted", "Active"}: + accepted.append(pep) + elif pep.status == "Final": + finished.append(pep) + else: + raise pep_0_parser.PEPError(f"unsorted ({pep.pep_type}/{pep.status})", pep.filename, pep.number) + return meta, info, provisional, accepted, open_, finished, historical, deferred, dead + + @staticmethod + def verify_email_addresses(peps: list[pep_0_parser.PEP]) -> dict[pep_0_parser.Author, str]: + authors_dict: dict[pep_0_parser.Author, set[str]] = {} + for pep in peps: + for author in pep.authors: + # If this is the first time we have come across an author, add them. + if author not in authors_dict: + authors_dict[author] = {author.email} if author.email else set() + else: + # If the new email is an empty string, move on. + if not author.email: + continue + # If the email has not been seen, add it to the list. + authors_dict[author].add(author.email) + + valid_authors_dict = {} + too_many_emails = [] + for author, emails in authors_dict.items(): + if len(emails) > 1: + too_many_emails.append((author.last_first, emails)) + else: + valid_authors_dict[author] = next(iter(emails), "") + if too_many_emails: + err_output = [] + for author, emails in too_many_emails: + err_output.append(" " * 4 + f"{author}: {emails}") + raise ValueError( + "some authors have more than one email address listed:\n" + + "\n".join(err_output) + ) + + return valid_authors_dict + + @staticmethod + def sort_authors(authors_dict: dict[pep_0_parser.Author, str]) -> list[pep_0_parser.Author]: + return sorted(authors_dict.keys(), key=pep_0_parser.author_sort_by) + + def emit_title(self, text: str, anchor: str, *, symbol: str = "=") -> None: + self.output(f".. _{anchor}:\n") + self.output(text) + self.output(symbol * len(text)) + self.emit_newline() + + def emit_subtitle(self, text: str, anchor: str) -> None: + self.emit_title(text, anchor, symbol="-") + + def emit_pep_category(self, category: str, anchor: str, peps: list[pep_0_parser.PEP]) -> None: + self.emit_subtitle(category, anchor) + self.emit_column_headers() + for pep in peps: + self.output(column_format(**pep.pep)) + self.emit_table_separator() + self.emit_newline() + + def write_pep0(self, peps: list[pep_0_parser.PEP]): + + # PEP metadata + self.output(header) + self.emit_newline() + + # Introduction + self.emit_title("Introduction", "intro") + self.output(intro) + self.emit_newline() + + # PEPs by category + self.emit_title("Index by Category", "by-category") + meta, info, provisional, accepted, open_, finished, historical, deferred, dead = self.sort_peps(peps) + pep_categories = [ + ("Meta-PEPs (PEPs about PEPs or Processes)", "by-category-meta", meta), + ("Other Informational PEPs", "by-category-other-info", info), + ("Provisional PEPs (provisionally accepted; interface may still change)", "by-category-provisional", provisional), + ("Accepted PEPs (accepted; may not be implemented yet)", "by-category-accepted", accepted), + ("Open PEPs (under consideration)", "by-category-open", open_), + ("Finished PEPs (done, with a stable interface)", "by-category-finished", finished), + ("Historical Meta-PEPs and Informational PEPs", "by-category-historical", historical), + ("Deferred PEPs (postponed pending further research or updates)", "by-category-deferred", deferred), + ("Abandoned, Withdrawn, and Rejected PEPs", "by-category-abandoned", dead), + ] + for (category, anchor, peps_in_category) in pep_categories: + self.emit_pep_category(category, anchor, peps_in_category) + + self.emit_newline() + + # PEPs by number + self.emit_title("Numerical Index", "by-pep-number") + self.emit_column_headers() + prev_pep = 0 + for pep in peps: + if pep.number - prev_pep > 1: + self.emit_newline() + self.output(column_format(**pep.pep)) + prev_pep = pep.number + + self.emit_table_separator() + self.emit_newline() + + # Reserved PEP numbers + self.emit_title("Reserved PEP Numbers", "reserved") + self.emit_column_headers() + for number, claimants in sorted(self.RESERVED.items()): + self.output(column_format( + type=".", + status=".", + number=number, + title="RESERVED", + authors=claimants, + )) + + self.emit_table_separator() + self.emit_newline() + + # PEP types key + self.emit_title("PEP Types Key", "type-key") + for type_ in sorted(pep_0_parser.PEP.type_values): + self.output(f" {type_[0]} - {type_} PEP") + self.emit_newline() + + self.emit_newline() + + # PEP status key + self.emit_title("PEP Status Key", "status-key") + for status in sorted(pep_0_parser.PEP.status_values): + # Draft PEPs have no status displayed, Active shares a key with Accepted + if status in {"Active", "Draft"}: + continue + if status == "Accepted": + msg = " A - Accepted (Standards Track only) or Active proposal" + else: + msg = f" {status[0]} - {status} proposal" + self.output(msg) + self.emit_newline() + + self.emit_newline() + + # PEP owners + authors_dict = self.verify_email_addresses(peps) + max_name_len = max(len(author) for author in authors_dict.keys()) + self.emit_title("Authors/Owners", "authors") + self.emit_author_table_separator(max_name_len) + self.output(f"{'Name':{max_name_len}} Email Address") + self.emit_author_table_separator(max_name_len) + for author in self.sort_authors(authors_dict): + # Use the email from authors_dict instead of the one from "author" as + # the author instance may have an empty email. + self.output(f"{author.last_first:{max_name_len}} {authors_dict[author]}") + self.emit_author_table_separator(max_name_len) + self.emit_newline() + self.emit_newline() + + # References for introduction footnotes + self.emit_title("References", "references") + self.output(references) + + pep0_string = "\n".join([str(s) for s in self._output]) + return pep0_string From c8268fb47730bf8d236063224061f7cb66cf4ad4 Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Tue, 20 Apr 2021 07:41:45 +0100 Subject: [PATCH 03/37] Add PEP 0 generator and authors override --- AUTHORS.csv | 12 ++++ .../pep_zero_generator/pep_index_generator.py | 70 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 AUTHORS.csv create mode 100644 pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py diff --git a/AUTHORS.csv b/AUTHORS.csv new file mode 100644 index 00000000000..c17ad10c73d --- /dev/null +++ b/AUTHORS.csv @@ -0,0 +1,12 @@ +Full Name, Surname First, Name Reference +Ernest W. Durbin III, "Durbin, Ernest W., III", Durbin +Inada Naoki, "Inada, Naoki", Inada +Guido van Rossum, "van Rossum, Guido (GvR)", GvR +Just van Rossum, "van Rossum, Just (JvR)", JvR +The Python core team and community, The Python core team and community, python-dev +P.J. Eby, "Eby, Phillip J.", Eby +Greg Ewing, "Ewing, Gregory", Ewing +Jim Jewett, "Jewett, Jim J.", Jewett +Nathaniel Smith, "Smith, Nathaniel J.", Smith +Martin v. Löwis, "von Löwis, Martin", von Löwis + diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py b/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py new file mode 100644 index 00000000000..e33ec1269f9 --- /dev/null +++ b/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py @@ -0,0 +1,70 @@ +"""Automatically create PEP 0 (the PEP index), + +This file generates and writes the PEP index to disk, ready for later +processing by Sphinx. Firstly, we parse the individual PEP files, getting the +RFC2822 header, and parsing and then validating that metadata. + +After collecting and validating all the PEP data, the creation of the index +itself is in three steps: + + 1. Output static text. + 2. Format an entry for the PEP. + 3. Output the PEP (both by the category and numerical index). + +We then add the newly created PEP 0 file to two Sphinx environment variables +to allow it to be processed as normal. + +""" +from __future__ import annotations + +import csv +import re +from pathlib import Path +from typing import TYPE_CHECKING + +from pep_sphinx_extensions.pep_zero_generator import pep_0_parser +from pep_sphinx_extensions.pep_zero_generator import pep_0_writer + +if TYPE_CHECKING: + from sphinx.application import Sphinx + from sphinx.environment import BuildEnvironment + + +def create_pep_zero(_: Sphinx, env: BuildEnvironment, docnames: list[str]) -> None: + # Sphinx app object is unneeded by this function + + # Read from root directory + path = Path(".") + + pep_zero_filename = "pep-0000" + peps: list[pep_0_parser.PEP] = [] + pep_pat = re.compile(r"pep-\d{4}") # Path.match() doesn't support regular expressions + title_length = pep_0_writer.title_length + + # AUTHORS.csv is an exception file for PEP0 name parsing + with open("AUTHORS.csv", "r", encoding="utf-8") as f: + read = csv.DictReader(f, quotechar='"', skipinitialspace=True) + author_exception_data = {} + for line in read: + full_name = line.pop("Full Name").strip() + details = {k.strip(): v.strip() for k, v in line.items()} + author_exception_data[full_name] = details + + for file_path in path.iterdir(): + if not file_path.is_file(): + continue # Skip directories etc. + if file_path.match("pep-0000*"): + continue # Skip pre-existing PEP 0 files + if pep_pat.match(str(file_path)) and file_path.suffix in {".txt", ".rst"}: + pep = pep_0_parser.PEP(path.joinpath(file_path).absolute(), author_exception_data, title_length) + peps.append(pep) + peps.sort(key=lambda pep: pep.number) + + pep_writer = pep_0_writer.PEPZeroWriter() + pep0_text = pep_writer.write_pep0(peps) + Path(f"{pep_zero_filename}.rst").write_text(pep0_text, encoding="utf-8") + + # Add to files for builder + docnames.insert(1, pep_zero_filename) + # Add to files for writer + env.found_docs.add(pep_zero_filename) From 85ae140d4003d983a2045f631d9e612518c1c7f0 Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Tue, 20 Apr 2021 07:41:56 +0100 Subject: [PATCH 04/37] Add/update build and run --- build.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/build.py b/build.py index f5e26a7dcac..172e20bbaed 100644 --- a/build.py +++ b/build.py @@ -2,6 +2,7 @@ import argparse from pathlib import Path +import shutil from sphinx.application import Sphinx @@ -22,6 +23,16 @@ def create_parser(): return parser.parse_args() +def create_index_file(html_content: Path): + pep_zero_html = html_content / "pep-0000.html" + pep_zero_dir = html_content / "pep-0000" / "index.html" + + if pep_zero_html.is_file(): + shutil.copy(pep_zero_html, html_content / "index.html") + elif pep_zero_dir.is_file(): + shutil.copy(pep_zero_dir, html_content / "index.html") + + if __name__ == "__main__": args = create_parser() @@ -52,3 +63,6 @@ def create_parser(): ) app.builder.copysource = False # Prevent unneeded source copying - we link direct to GitHub app.build() + + if args.index_file: + create_index_file(build_directory) From 835adfc32171284ed185c264171e2be9d2cb188c Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Fri, 7 May 2021 16:04:53 +0100 Subject: [PATCH 05/37] Simplify `create_index_file` --- build.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/build.py b/build.py index 172e20bbaed..0423c3c0760 100644 --- a/build.py +++ b/build.py @@ -23,14 +23,11 @@ def create_parser(): return parser.parse_args() -def create_index_file(html_content: Path): - pep_zero_html = html_content / "pep-0000.html" - pep_zero_dir = html_content / "pep-0000" / "index.html" - - if pep_zero_html.is_file(): - shutil.copy(pep_zero_html, html_content / "index.html") - elif pep_zero_dir.is_file(): - shutil.copy(pep_zero_dir, html_content / "index.html") +def create_index_file(html_root: Path): + """Copies PEP 0 to the root index.html so that /peps/ works.""" + pep_zero_path = html_root / "pep-0000" / "index.html" + if pep_zero_path.is_file(): + shutil.copy(pep_zero_path, html_root / "index.html") if __name__ == "__main__": From 530ca9a4a57a4e9f58c76ae38d08a49a56f323c2 Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 01:58:34 +0100 Subject: [PATCH 06/37] Special status handling --- .../pep_zero_generator/pep_0_parser.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py b/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py index 308090c4cf3..cb26d5d5535 100644 --- a/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py +++ b/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py @@ -164,6 +164,9 @@ class PEP: "Accepted", "Provisional", "Rejected", "Withdrawn", "Deferred", "Final", "Active", "Draft", "Superseded", } + special_statuses = { + "April Fool!": "Rejected", # See PEP 401 :) + } def raise_pep_error(self, msg: str, pep_num: bool = False) -> None: pep_number = self.number if pep_num else None @@ -205,11 +208,10 @@ def __init__(self, filename: Path, author_lookup: dict, title_length: int): # Status status = metadata["Status"] + if status in self.special_statuses: + status = self.special_statuses[status] if status not in self.status_values: - if status == "April Fool!": # See PEP 401 :) - status = "Rejected" - else: - self.raise_pep_error(f"{status} is not a valid Status value", pep_num=True) + self.raise_pep_error(f"{status} is not a valid Status value", pep_num=True) # Special case for Active PEPs. if status == "Active" and self.pep_type not in {"Process", "Informational"}: From 2578fe2123d9c27ce8aa0e85117d577d699ecd3a Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 02:07:52 +0100 Subject: [PATCH 07/37] Add constants for PEP related magic strings --- .../pep_zero_generator/pep_0_parser.py | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py b/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py index cb26d5d5535..0999446c865 100644 --- a/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py +++ b/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py @@ -141,6 +141,20 @@ def author_sort_by(author: Author) -> str: return unicodedata.normalize("NFKD", surname.lower()) +STATUS_ACCEPTED = "Accepted" +STATUS_PROVISIONAL = "Provisional" +STATUS_REJECTED = "Rejected" +STATUS_WITHDRAWN = "Withdrawn" +STATUS_DEFERRED = "Deferred" +STATUS_FINAL = "Final" +STATUS_ACTIVE = "Active" +STATUS_DRAFT = "Draft" +STATUS_SUPERSEDED = "Superseded" +TYPE_STANDARDS = "Standards Track" +TYPE_INFO = "Informational" +TYPE_PROCESS = "Process" + + class PEP: """Representation of PEPs. @@ -157,16 +171,17 @@ class PEP: required_headers = {"PEP", "Title", "Author", "Status", "Type", "Created"} # Valid values for the Type header. - type_values = {"Standards Track", "Informational", "Process"} + type_values = {TYPE_STANDARDS, TYPE_INFO, TYPE_PROCESS} # Valid values for the Status header. # Active PEPs can only be for Informational or Process PEPs. status_values = { - "Accepted", "Provisional", "Rejected", "Withdrawn", - "Deferred", "Final", "Active", "Draft", "Superseded", + STATUS_ACCEPTED, STATUS_PROVISIONAL, STATUS_REJECTED, STATUS_WITHDRAWN, + STATUS_DEFERRED, STATUS_FINAL, STATUS_ACTIVE, STATUS_DRAFT, STATUS_SUPERSEDED, } special_statuses = { - "April Fool!": "Rejected", # See PEP 401 :) + "April Fool!": STATUS_REJECTED, # See PEP 401 :) } + active_allowed = {TYPE_PROCESS, TYPE_INFO} def raise_pep_error(self, msg: str, pep_num: bool = False) -> None: pep_number = self.number if pep_num else None From c839d513912da77315d5a9f79233defa28f89e56 Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 02:08:24 +0100 Subject: [PATCH 08/37] Prefer checking on class --- .../pep_zero_generator/pep_0_parser.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py b/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py index 0999446c865..7b8807bab17 100644 --- a/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py +++ b/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py @@ -199,7 +199,7 @@ def __init__(self, filename: Path, author_lookup: dict, title_length: int): # Parse the headers. pep_text = filename.read_text("UTF8") metadata = HeaderParser().parsestr(pep_text) - required_header_misses = self.required_headers - set(metadata.keys()) + required_header_misses = PEP.required_headers - set(metadata.keys()) if required_header_misses: msg = f"PEP is missing required headers ({', '.join(required_header_misses)})" self.raise_pep_error(msg) @@ -218,14 +218,14 @@ def __init__(self, filename: Path, author_lookup: dict, title_length: int): # Type self.pep_type: str = metadata["Type"] - if self.pep_type not in self.type_values: + if self.pep_type not in PEP.type_values: self.raise_pep_error(f"{self.pep_type} is not a valid Type value", pep_num=True) # Status status = metadata["Status"] - if status in self.special_statuses: - status = self.special_statuses[status] - if status not in self.status_values: + if status in PEP.special_statuses: + status = PEP.special_statuses[status] + if status not in PEP.status_values: self.raise_pep_error(f"{status} is not a valid Status value", pep_num=True) # Special case for Active PEPs. From a9b0559635025b1a41e0903d3e90f96a2e8350d3 Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 02:10:07 +0100 Subject: [PATCH 09/37] Add PEP.hide_status, use constants --- pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py b/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py index 7b8807bab17..ecf14bf29f7 100644 --- a/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py +++ b/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py @@ -182,6 +182,7 @@ class PEP: "April Fool!": STATUS_REJECTED, # See PEP 401 :) } active_allowed = {TYPE_PROCESS, TYPE_INFO} + hide_status = {STATUS_DRAFT, STATUS_ACTIVE} def raise_pep_error(self, msg: str, pep_num: bool = False) -> None: pep_number = self.number if pep_num else None @@ -229,12 +230,12 @@ def __init__(self, filename: Path, author_lookup: dict, title_length: int): self.raise_pep_error(f"{status} is not a valid Status value", pep_num=True) # Special case for Active PEPs. - if status == "Active" and self.pep_type not in {"Process", "Informational"}: + if status == STATUS_ACTIVE and self.pep_type not in PEP.active_allowed: msg = "Only Process and Informational PEPs may have an Active status" self.raise_pep_error(msg, pep_num=True) # Special case for Provisional PEPs. - if status == "Provisional" and self.pep_type != "Standards Track": + if status == STATUS_PROVISIONAL and self.pep_type != TYPE_STANDARDS: msg = "Only Standards Track PEPs may have a Provisional status" self.raise_pep_error(msg, pep_num=True) self.status: str = status @@ -299,7 +300,7 @@ def pep(self) -> dict[str, str | int]: "number": self.number, "title": self.title_abbr, # how the status should be represented in the index - "status": self.status[0].upper() if self.status not in {"Draft", "Active"} else " ", + "status": self.status[0].upper() if self.status not in PEP.hide_status else " ", # the author list as a comma-separated with only last names "authors": ", ".join(x.nick for x in self.authors), } From 77c5492efb5de236504f97791d8cdbf51ed64929 Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 02:12:36 +0100 Subject: [PATCH 10/37] Remove comment from 2008 (current method works fine) --- pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py b/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py index ecf14bf29f7..5ab12be7763 100644 --- a/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py +++ b/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py @@ -257,8 +257,6 @@ def parse_authors(self, author_header: str, author_lookup: dict) -> list[Author] @staticmethod def _parse_author(data: str) -> list[tuple[str, str]]: """Return a list of author names and emails.""" - # XXX Consider using email.utils.parseaddr (doesn't work with names - # lacking an email address. author_list = [] for regex in (PEP.angled, PEP.paren, PEP.simple): From f6f7b65c86eac91d748699ad319aefa5efb6c1e5 Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 02:18:27 +0100 Subject: [PATCH 11/37] Clarify intent of for-else loop --- .../pep_zero_generator/pep_0_parser.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py b/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py index 5ab12be7763..84edd9c073d 100644 --- a/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py +++ b/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py @@ -274,11 +274,11 @@ def _parse_author(data: str) -> list[tuple[str, str]]: else: email = match_dict["email"] author_list.append((author, email)) - else: - # If authors were found then stop searching as only expect one - # style of author citation. - if author_list: - break + + # If authors were found then stop searching as only expect one + # style of author citation. + if author_list: + break return author_list @property From d0513e29ee04d64ca13ec0c3c8425953d42c1c85 Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 12:39:49 +0100 Subject: [PATCH 12/37] Hook in to Sphinx (oops, missed when splitting out this PR) --- pep_sphinx_extensions/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pep_sphinx_extensions/__init__.py b/pep_sphinx_extensions/__init__.py index cac99c62665..82f13b48410 100644 --- a/pep_sphinx_extensions/__init__.py +++ b/pep_sphinx_extensions/__init__.py @@ -10,6 +10,7 @@ from pep_sphinx_extensions.pep_processor.html import pep_html_translator from pep_sphinx_extensions.pep_processor.parsing import pep_parser from pep_sphinx_extensions.pep_processor.parsing import pep_role +from pep_sphinx_extensions.pep_zero_generator.pep_index_generator import create_pep_zero if TYPE_CHECKING: from sphinx.application import Sphinx @@ -37,6 +38,7 @@ def setup(app: Sphinx) -> dict[str, bool]: app.add_source_parser(pep_parser.PEPParser) # Add PEP transforms app.add_role("pep", pep_role.PEPRole(), override=True) # Transform PEP references to links app.set_translator("html", pep_html_translator.PEPTranslator) # Docutils Node Visitor overrides + app.connect("env-before-read-docs", create_pep_zero) # PEP 0 hook # Mathematics rendering inline_maths = HTMLTranslator.visit_math, _depart_maths From b8d9eff9b36905423be97d28b4ca7a0cdffc4624 Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 12:47:26 +0100 Subject: [PATCH 13/37] Rename AUTHORS.csv for clarity --- AUTHORS.csv => AUTHOR_OVERRIDES.csv | 3 +-- .../pep_zero_generator/pep_index_generator.py | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) rename AUTHORS.csv => AUTHOR_OVERRIDES.csv (90%) diff --git a/AUTHORS.csv b/AUTHOR_OVERRIDES.csv similarity index 90% rename from AUTHORS.csv rename to AUTHOR_OVERRIDES.csv index c17ad10c73d..deb522eaa4c 100644 --- a/AUTHORS.csv +++ b/AUTHOR_OVERRIDES.csv @@ -1,4 +1,4 @@ -Full Name, Surname First, Name Reference +Overridden Name, Surname First, Name Reference Ernest W. Durbin III, "Durbin, Ernest W., III", Durbin Inada Naoki, "Inada, Naoki", Inada Guido van Rossum, "van Rossum, Guido (GvR)", GvR @@ -9,4 +9,3 @@ Greg Ewing, "Ewing, Gregory", Ewing Jim Jewett, "Jewett, Jim J.", Jewett Nathaniel Smith, "Smith, Nathaniel J.", Smith Martin v. Löwis, "von Löwis, Martin", von Löwis - diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py b/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py index e33ec1269f9..6c5a58f1c2b 100644 --- a/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py +++ b/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py @@ -41,12 +41,12 @@ def create_pep_zero(_: Sphinx, env: BuildEnvironment, docnames: list[str]) -> No pep_pat = re.compile(r"pep-\d{4}") # Path.match() doesn't support regular expressions title_length = pep_0_writer.title_length - # AUTHORS.csv is an exception file for PEP0 name parsing - with open("AUTHORS.csv", "r", encoding="utf-8") as f: + # AUTHOR_OVERRIDES.csv is an exception file for PEP0 name parsing + with open("AUTHOR_OVERRIDES.csv", "r", encoding="utf-8") as f: read = csv.DictReader(f, quotechar='"', skipinitialspace=True) author_exception_data = {} for line in read: - full_name = line.pop("Full Name").strip() + full_name = line.pop("Overridden Name").strip() details = {k.strip(): v.strip() for k, v in line.items()} author_exception_data[full_name] = details From 4b0d0427eaeb943c5c23a6ce46450e215652eedf Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 12:59:15 +0100 Subject: [PATCH 14/37] Sort and strip spaces --- AUTHOR_OVERRIDES.csv | 22 +++++++++---------- .../pep_zero_generator/pep_index_generator.py | 8 +++---- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/AUTHOR_OVERRIDES.csv b/AUTHOR_OVERRIDES.csv index deb522eaa4c..c97178f0c7d 100644 --- a/AUTHOR_OVERRIDES.csv +++ b/AUTHOR_OVERRIDES.csv @@ -1,11 +1,11 @@ -Overridden Name, Surname First, Name Reference -Ernest W. Durbin III, "Durbin, Ernest W., III", Durbin -Inada Naoki, "Inada, Naoki", Inada -Guido van Rossum, "van Rossum, Guido (GvR)", GvR -Just van Rossum, "van Rossum, Just (JvR)", JvR -The Python core team and community, The Python core team and community, python-dev -P.J. Eby, "Eby, Phillip J.", Eby -Greg Ewing, "Ewing, Gregory", Ewing -Jim Jewett, "Jewett, Jim J.", Jewett -Nathaniel Smith, "Smith, Nathaniel J.", Smith -Martin v. Löwis, "von Löwis, Martin", von Löwis +Overridden Name,Surname First,Name Reference +The Python core team and community,The Python core team and community,python-dev +Ernest W. Durbin III,"Durbin, Ernest W., III",Durbin +Greg Ewing,"Ewing, Gregory",Ewing +Guido van Rossum,"van Rossum, Guido (GvR)",GvR +Inada Naoki,"Inada, Naoki",Inada +Jim Jewett,"Jewett, Jim J.",Jewett +Just van Rossum,"van Rossum, Just (JvR)",JvR +Martin v. Löwis,"von Löwis, Martin",von Löwis +Nathaniel Smith,"Smith, Nathaniel J.",Smith +P.J. Eby,"Eby, Phillip J.",Eby diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py b/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py index 6c5a58f1c2b..d101e130340 100644 --- a/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py +++ b/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py @@ -43,12 +43,10 @@ def create_pep_zero(_: Sphinx, env: BuildEnvironment, docnames: list[str]) -> No # AUTHOR_OVERRIDES.csv is an exception file for PEP0 name parsing with open("AUTHOR_OVERRIDES.csv", "r", encoding="utf-8") as f: - read = csv.DictReader(f, quotechar='"', skipinitialspace=True) author_exception_data = {} - for line in read: - full_name = line.pop("Overridden Name").strip() - details = {k.strip(): v.strip() for k, v in line.items()} - author_exception_data[full_name] = details + for line in csv.DictReader(f): + full_name = line.pop("Overridden Name") + author_exception_data[full_name] = line for file_path in path.iterdir(): if not file_path.is_file(): From a993eed1a02d2b7173beefa511a54a00306a6094 Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 13:19:42 +0100 Subject: [PATCH 15/37] Prefer `authors_overrides` name --- .../pep_zero_generator/pep_0_parser.py | 14 +++++++------- .../pep_zero_generator/pep_index_generator.py | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py b/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py index 84edd9c073d..252788a25ff 100644 --- a/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py +++ b/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py @@ -42,7 +42,7 @@ class Author: """ __slots__ = "last_first", "nick", "email", "_first_last" - def __init__(self, author_email_tuple: tuple[str, str], authors_exceptions: dict[str, dict[str, str]]): + def __init__(self, author_email_tuple: tuple[str, str], authors_overrides: dict[str, dict[str, str]]): """Parse the name and email address of an author.""" name, email = author_email_tuple self._first_last: str = name.strip() @@ -51,8 +51,8 @@ def __init__(self, author_email_tuple: tuple[str, str], authors_exceptions: dict self.last_first: str = "" self.nick: str = "" - if self._first_last in authors_exceptions: - name_dict = authors_exceptions[self._first_last] + if self._first_last in authors_overrides: + name_dict = authors_overrides[self._first_last] self.last_first = name_dict["Surname First"] self.nick = name_dict["Name Reference"] else: @@ -188,7 +188,7 @@ def raise_pep_error(self, msg: str, pep_num: bool = False) -> None: pep_number = self.number if pep_num else None raise PEPError(msg, self.filename, pep_number=pep_number) - def __init__(self, filename: Path, author_lookup: dict, title_length: int): + def __init__(self, filename: Path, authors_overrides: dict, title_length: int): """Init object from an open PEP file object. pep_file is full text of the PEP file, filename is path of the PEP file, author_lookup is author exceptions file @@ -241,14 +241,14 @@ def __init__(self, filename: Path, author_lookup: dict, title_length: int): self.status: str = status # Parse PEP authors - self.authors: list[Author] = self.parse_authors(metadata["Author"], author_lookup) + self.authors: list[Author] = self.parse_authors(metadata["Author"], authors_overrides) - def parse_authors(self, author_header: str, author_lookup: dict) -> list[Author]: + def parse_authors(self, author_header: str, authors_overrides: dict) -> list[Author]: """Parse Author header line""" authors_and_emails = self._parse_author(author_header) if not authors_and_emails: raise self.raise_pep_error("no authors found", pep_num=True) - return [Author(author_tuple, author_lookup) for author_tuple in authors_and_emails] + return [Author(author_tuple, authors_overrides) for author_tuple in authors_and_emails] angled = re.compile(r"(?P.+?) <(?P.+?)>(,\s*)?") paren = re.compile(r"(?P.+?) \((?P.+?)\)(,\s*)?") diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py b/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py index d101e130340..5da38d25041 100644 --- a/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py +++ b/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py @@ -43,10 +43,10 @@ def create_pep_zero(_: Sphinx, env: BuildEnvironment, docnames: list[str]) -> No # AUTHOR_OVERRIDES.csv is an exception file for PEP0 name parsing with open("AUTHOR_OVERRIDES.csv", "r", encoding="utf-8") as f: - author_exception_data = {} + authors_overrides = {} for line in csv.DictReader(f): full_name = line.pop("Overridden Name") - author_exception_data[full_name] = line + authors_overrides[full_name] = line for file_path in path.iterdir(): if not file_path.is_file(): @@ -54,7 +54,7 @@ def create_pep_zero(_: Sphinx, env: BuildEnvironment, docnames: list[str]) -> No if file_path.match("pep-0000*"): continue # Skip pre-existing PEP 0 files if pep_pat.match(str(file_path)) and file_path.suffix in {".txt", ".rst"}: - pep = pep_0_parser.PEP(path.joinpath(file_path).absolute(), author_exception_data, title_length) + pep = pep_0_parser.PEP(path.joinpath(file_path).absolute(), authors_overrides, title_length) peps.append(pep) peps.sort(key=lambda pep: pep.number) From 92fe1fb0a895b6177c08f5ac2db5734213a10f7e Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 13:28:14 +0100 Subject: [PATCH 16/37] Add pep_0_errors.py --- .../pep_zero_generator/pep_0_errors.py | 14 ++++++++++++++ .../pep_zero_generator/pep_0_parser.py | 13 +------------ .../pep_zero_generator/pep_0_writer.py | 3 ++- 3 files changed, 17 insertions(+), 13 deletions(-) create mode 100644 pep_sphinx_extensions/pep_zero_generator/pep_0_errors.py diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_0_errors.py b/pep_sphinx_extensions/pep_zero_generator/pep_0_errors.py new file mode 100644 index 00000000000..cdde4f3810e --- /dev/null +++ b/pep_sphinx_extensions/pep_zero_generator/pep_0_errors.py @@ -0,0 +1,14 @@ +from pathlib import Path + + +class PEPError(Exception): + def __init__(self, error: str, pep_file: Path, pep_number: int | None = None): + super().__init__(error) + self.filename = pep_file + self.number = pep_number + + def __str__(self): + error_msg = super(PEPError, self).__str__() + error_msg = f"({self.filename}): {error_msg}" + pep_str = f"PEP {self.number}" + return f"{pep_str} {error_msg}" if self.number is not None else error_msg diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py b/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py index 252788a25ff..d44edd2f4b3 100644 --- a/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py +++ b/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py @@ -9,18 +9,7 @@ from typing import NamedTuple import unicodedata - -class PEPError(Exception): - def __init__(self, error: str, pep_file: Path, pep_number: int | None = None): - super().__init__(error) - self.filename = pep_file - self.number = pep_number - - def __str__(self): - error_msg = super(PEPError, self).__str__() - error_msg = f"({self.filename}): {error_msg}" - pep_str = f"PEP {self.number}" - return f"{pep_str} {error_msg}" if self.number is not None else error_msg +from pep_sphinx_extensions.pep_zero_generator.pep_0_errors import PEPError class Name(NamedTuple): diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_0_writer.py b/pep_sphinx_extensions/pep_zero_generator/pep_0_writer.py index b5e2289ed90..2bd24d0ee4b 100644 --- a/pep_sphinx_extensions/pep_zero_generator/pep_0_writer.py +++ b/pep_sphinx_extensions/pep_zero_generator/pep_0_writer.py @@ -4,6 +4,7 @@ import functools from pep_sphinx_extensions.pep_zero_generator import pep_0_parser +from pep_sphinx_extensions.pep_zero_generator.pep_0_errors import PEPError title_length = 55 author_length = 40 @@ -126,7 +127,7 @@ def sort_peps(peps: list[pep_0_parser.PEP]) -> tuple[list[pep_0_parser.PEP], ... elif pep.status == "Final": finished.append(pep) else: - raise pep_0_parser.PEPError(f"unsorted ({pep.pep_type}/{pep.status})", pep.filename, pep.number) + raise PEPError(f"unsorted ({pep.pep_type}/{pep.status})", pep.filename, pep.number) return meta, info, provisional, accepted, open_, finished, historical, deferred, dead @staticmethod From 3f695ab2ded41ee132e58662b27fb01756db289e Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 13:29:20 +0100 Subject: [PATCH 17/37] Move author_sort_by to writer --- .../pep_zero_generator/pep_0_parser.py | 14 +--------- .../pep_zero_generator/pep_0_writer.py | 27 ++++++++++++++++--- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py b/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py index d44edd2f4b3..ba9e4dfc5b5 100644 --- a/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py +++ b/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py @@ -118,18 +118,6 @@ def _parse_name(full_name: str) -> Name: return Name(forename=forename, surname=name_parts[-1], suffix=suffix) -def author_sort_by(author: Author) -> str: - """Skip lower-cased words in surname when sorting.""" - surname, *_ = author.last_first.split(",") - surname_parts = surname.split() - for i, part in enumerate(surname_parts): - if part[0].isupper(): - base = " ".join(surname_parts[i:]).lower() - return unicodedata.normalize("NFKD", base) - # If no capitals, use the whole string - return unicodedata.normalize("NFKD", surname.lower()) - - STATUS_ACCEPTED = "Accepted" STATUS_PROVISIONAL = "Provisional" STATUS_REJECTED = "Rejected" @@ -162,7 +150,6 @@ class PEP: # Valid values for the Type header. type_values = {TYPE_STANDARDS, TYPE_INFO, TYPE_PROCESS} # Valid values for the Status header. - # Active PEPs can only be for Informational or Process PEPs. status_values = { STATUS_ACCEPTED, STATUS_PROVISIONAL, STATUS_REJECTED, STATUS_WITHDRAWN, STATUS_DEFERRED, STATUS_FINAL, STATUS_ACTIVE, STATUS_DRAFT, STATUS_SUPERSEDED, @@ -170,6 +157,7 @@ class PEP: special_statuses = { "April Fool!": STATUS_REJECTED, # See PEP 401 :) } + # Active PEPs can only be for Informational or Process PEPs. active_allowed = {TYPE_PROCESS, TYPE_INFO} hide_status = {STATUS_DRAFT, STATUS_ACTIVE} diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_0_writer.py b/pep_sphinx_extensions/pep_zero_generator/pep_0_writer.py index 2bd24d0ee4b..9fb598445b3 100644 --- a/pep_sphinx_extensions/pep_zero_generator/pep_0_writer.py +++ b/pep_sphinx_extensions/pep_zero_generator/pep_0_writer.py @@ -1,11 +1,18 @@ """Code to handle the output of PEP 0.""" +from __future__ import annotations + import datetime import functools +from typing import TYPE_CHECKING +import unicodedata -from pep_sphinx_extensions.pep_zero_generator import pep_0_parser +from pep_sphinx_extensions.pep_zero_generator.pep_0_parser import PEP from pep_sphinx_extensions.pep_zero_generator.pep_0_errors import PEPError +if TYPE_CHECKING: + from pep_sphinx_extensions.pep_zero_generator import pep_0_parser + title_length = 55 author_length = 40 table_separator = "== ==== " + "="*title_length + " " + "="*author_length @@ -165,7 +172,7 @@ def verify_email_addresses(peps: list[pep_0_parser.PEP]) -> dict[pep_0_parser.Au @staticmethod def sort_authors(authors_dict: dict[pep_0_parser.Author, str]) -> list[pep_0_parser.Author]: - return sorted(authors_dict.keys(), key=pep_0_parser.author_sort_by) + return sorted(authors_dict.keys(), key=_author_sort_by) def emit_title(self, text: str, anchor: str, *, symbol: str = "=") -> None: self.output(f".. _{anchor}:\n") @@ -244,7 +251,7 @@ def write_pep0(self, peps: list[pep_0_parser.PEP]): # PEP types key self.emit_title("PEP Types Key", "type-key") - for type_ in sorted(pep_0_parser.PEP.type_values): + for type_ in sorted(PEP.type_values): self.output(f" {type_[0]} - {type_} PEP") self.emit_newline() @@ -252,7 +259,7 @@ def write_pep0(self, peps: list[pep_0_parser.PEP]): # PEP status key self.emit_title("PEP Status Key", "status-key") - for status in sorted(pep_0_parser.PEP.status_values): + for status in sorted(PEP.status_values): # Draft PEPs have no status displayed, Active shares a key with Accepted if status in {"Active", "Draft"}: continue @@ -286,3 +293,15 @@ def write_pep0(self, peps: list[pep_0_parser.PEP]): pep0_string = "\n".join([str(s) for s in self._output]) return pep0_string + + +def _author_sort_by(author: pep_0_parser.Author) -> str: + """Skip lower-cased words in surname when sorting.""" + surname, *_ = author.last_first.split(",") + surname_parts = surname.split() + for i, part in enumerate(surname_parts): + if part[0].isupper(): + base = " ".join(surname_parts[i:]).lower() + return unicodedata.normalize("NFKD", base) + # If no capitals, use the whole string + return unicodedata.normalize("NFKD", surname.lower()) From 327fd1b53dc4075c8737edd3a75ad7fefecf117c Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 13:31:16 +0100 Subject: [PATCH 18/37] PEP init misc --- pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py b/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py index ba9e4dfc5b5..22f5333d814 100644 --- a/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py +++ b/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py @@ -175,15 +175,14 @@ def __init__(self, filename: Path, authors_overrides: dict, title_length: int): self.title_length: int = title_length # Parse the headers. - pep_text = filename.read_text("UTF8") + pep_text = filename.read_text(encoding="utf-8") metadata = HeaderParser().parsestr(pep_text) required_header_misses = PEP.required_headers - set(metadata.keys()) if required_header_misses: - msg = f"PEP is missing required headers ({', '.join(required_header_misses)})" - self.raise_pep_error(msg) + self.raise_pep_error(f"PEP is missing required headers {required_header_misses}") try: - self.number: int = int(metadata["PEP"]) + self.number = int(metadata["PEP"]) except ValueError: self.raise_pep_error("PEP number isn't an integer") From 403bff3ff29c680c324709f40258ca902480b6ed Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 13:33:05 +0100 Subject: [PATCH 19/37] Split out Author --- .../pep_zero_generator/author.py | 110 ++++++++++++++++++ .../pep_zero_generator/pep_0_parser.py | 110 +----------------- 2 files changed, 111 insertions(+), 109 deletions(-) create mode 100644 pep_sphinx_extensions/pep_zero_generator/author.py diff --git a/pep_sphinx_extensions/pep_zero_generator/author.py b/pep_sphinx_extensions/pep_zero_generator/author.py new file mode 100644 index 00000000000..5969d028e98 --- /dev/null +++ b/pep_sphinx_extensions/pep_zero_generator/author.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +from typing import NamedTuple +import unicodedata + + +class Name(NamedTuple): + name: str = None # mononym + forename: str = None + surname: str = None + suffix: str = None + + +class Author: + """Represent PEP authors. + + Attributes: + last_first: The author's name in Surname, Forename, Suffix order. + nick: Author's nickname for PEP tables. Defaults to surname. + email: The author's email address. + _first_last: The author's full name, unchanged + + """ + __slots__ = "last_first", "nick", "email", "_first_last" + + def __init__(self, author_email_tuple: tuple[str, str], authors_overrides: dict[str, dict[str, str]]): + """Parse the name and email address of an author.""" + name, email = author_email_tuple + self._first_last: str = name.strip() + self.email: str = email.lower() + + self.last_first: str = "" + self.nick: str = "" + + if self._first_last in authors_overrides: + name_dict = authors_overrides[self._first_last] + self.last_first = name_dict["Surname First"] + self.nick = name_dict["Name Reference"] + else: + name_parts = self._parse_name(self._first_last) + if name_parts.name is not None: + self.last_first = self.nick = name_parts.name + else: + if name_parts.surname[1] == ".": + # Add an escape to avoid docutils turning `v.` into `22.`. + name_parts.surname = f"\\{name_parts.surname}" + self.last_first = f"{name_parts.surname}, {name_parts.forename}" + self.nick = name_parts.surname + + if name_parts.suffix is not None: + self.last_first += f", {name_parts.suffix}" + + def __hash__(self): + return hash(self.last_first) + + def __eq__(self, other): + if not isinstance(other, Author): + return NotImplemented + return self.last_first == other.last_first + + def __len__(self): + return len(unicodedata.normalize("NFC", self.last_first)) + + @staticmethod + def _parse_name(full_name: str) -> Name: + """Decompose a full name into parts. + + If a mononym (e.g, 'Aahz') then return the full name. If there are + suffixes in the name (e.g. ', Jr.' or 'III'), then find and extract + them. If there is a middle initial followed by a full stop, then + combine the following words into a surname (e.g. N. Vander Weele). If + there is a leading, lowercase portion to the last name (e.g. 'van' or + 'von') then include it in the surname. + + """ + possible_suffixes = {"Jr", "Jr.", "II", "III"} + + pre_suffix, _, raw_suffix = full_name.partition(",") + name_parts = pre_suffix.strip().split(" ") + num_parts = len(name_parts) + suffix = raw_suffix.strip() or None + + if num_parts == 0: + raise ValueError("Name is empty!") + elif num_parts == 1: + return Name(name=name_parts[0], suffix=suffix) + elif num_parts == 2: + return Name(forename=name_parts[0].strip(), surname=name_parts[1], suffix=suffix) + + # handles rogue uncaught suffixes + if name_parts[-1] in possible_suffixes: + suffix = f"{name_parts.pop(-1)} {suffix}".strip() + + # handles von, van, v. etc. + if name_parts[-2].islower(): + forename = " ".join(name_parts[:-2]).strip() + surname = " ".join(name_parts[-2:]) + return Name(forename=forename, surname=surname, suffix=suffix) + + # handles double surnames after a middle initial (e.g. N. Vander Weele) + elif any(s.endswith(".") for s in name_parts): + split_position = [i for i, x in enumerate(name_parts) if x.endswith(".")][-1] + 1 + forename = " ".join(name_parts[:split_position]).strip() + surname = " ".join(name_parts[split_position:]) + return Name(forename=forename, surname=surname, suffix=suffix) + + # default to using the last item as the surname + else: + forename = " ".join(name_parts[:-1]).strip() + return Name(forename=forename, surname=name_parts[-1], suffix=suffix) diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py b/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py index 22f5333d814..52092417469 100644 --- a/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py +++ b/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py @@ -6,118 +6,10 @@ from pathlib import Path import re import textwrap -from typing import NamedTuple -import unicodedata +from pep_sphinx_extensions.pep_zero_generator.author import Author from pep_sphinx_extensions.pep_zero_generator.pep_0_errors import PEPError - -class Name(NamedTuple): - name: str = None # mononym - forename: str = None - surname: str = None - suffix: str = None - - -class Author: - """Represent PEP authors. - - Attributes: - last_first: The author's name in Surname, Forename, Suffix order. - nick: Author's nickname for PEP tables. Defaults to surname. - email: The author's email address. - _first_last: The author's full name, unchanged - - """ - __slots__ = "last_first", "nick", "email", "_first_last" - - def __init__(self, author_email_tuple: tuple[str, str], authors_overrides: dict[str, dict[str, str]]): - """Parse the name and email address of an author.""" - name, email = author_email_tuple - self._first_last: str = name.strip() - self.email: str = email.lower() - - self.last_first: str = "" - self.nick: str = "" - - if self._first_last in authors_overrides: - name_dict = authors_overrides[self._first_last] - self.last_first = name_dict["Surname First"] - self.nick = name_dict["Name Reference"] - else: - name_parts = self._parse_name(self._first_last) - if name_parts.name is not None: - self.last_first = self.nick = name_parts.name - else: - if name_parts.surname[1] == ".": - # Add an escape to avoid docutils turning `v.` into `22.`. - name_parts.surname = f"\\{name_parts.surname}" - self.last_first = f"{name_parts.surname}, {name_parts.forename}" - self.nick = name_parts.surname - - if name_parts.suffix is not None: - self.last_first += f", {name_parts.suffix}" - - def __hash__(self): - return hash(self.last_first) - - def __eq__(self, other): - if not isinstance(other, Author): - return NotImplemented - return self.last_first == other.last_first - - def __len__(self): - return len(unicodedata.normalize("NFC", self.last_first)) - - @staticmethod - def _parse_name(full_name: str) -> Name: - """Decompose a full name into parts. - - If a mononym (e.g, 'Aahz') then return the full name. If there are - suffixes in the name (e.g. ', Jr.' or 'III'), then find and extract - them. If there is a middle initial followed by a full stop, then - combine the following words into a surname (e.g. N. Vander Weele). If - there is a leading, lowercase portion to the last name (e.g. 'van' or - 'von') then include it in the surname. - - """ - possible_suffixes = {"Jr", "Jr.", "II", "III"} - - pre_suffix, _, raw_suffix = full_name.partition(",") - name_parts = pre_suffix.strip().split(" ") - num_parts = len(name_parts) - suffix = raw_suffix.strip() or None - - if num_parts == 0: - raise ValueError("Name is empty!") - elif num_parts == 1: - return Name(name=name_parts[0], suffix=suffix) - elif num_parts == 2: - return Name(forename=name_parts[0].strip(), surname=name_parts[1], suffix=suffix) - - # handles rogue uncaught suffixes - if name_parts[-1] in possible_suffixes: - suffix = f"{name_parts.pop(-1)} {suffix}".strip() - - # handles von, van, v. etc. - if name_parts[-2].islower(): - forename = " ".join(name_parts[:-2]).strip() - surname = " ".join(name_parts[-2:]) - return Name(forename=forename, surname=surname, suffix=suffix) - - # handles double surnames after a middle initial (e.g. N. Vander Weele) - elif any(s.endswith(".") for s in name_parts): - split_position = [i for i, x in enumerate(name_parts) if x.endswith(".")][-1] + 1 - forename = " ".join(name_parts[:split_position]).strip() - surname = " ".join(name_parts[split_position:]) - return Name(forename=forename, surname=surname, suffix=suffix) - - # default to using the last item as the surname - else: - forename = " ".join(name_parts[:-1]).strip() - return Name(forename=forename, surname=name_parts[-1], suffix=suffix) - - STATUS_ACCEPTED = "Accepted" STATUS_PROVISIONAL = "Provisional" STATUS_REJECTED = "Rejected" From 0d9bf61e10eed9ab0bd7ea3cb72c0efac2075bb1 Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 13:35:38 +0100 Subject: [PATCH 20/37] Drop pep_0 prefix --- .../{pep_0_errors.py => errors.py} | 0 .../{pep_0_parser.py => parser.py} | 2 +- .../pep_zero_generator/pep_index_generator.py | 12 +++++------ .../{pep_0_writer.py => writer.py} | 20 +++++++++---------- 4 files changed, 17 insertions(+), 17 deletions(-) rename pep_sphinx_extensions/pep_zero_generator/{pep_0_errors.py => errors.py} (100%) rename pep_sphinx_extensions/pep_zero_generator/{pep_0_parser.py => parser.py} (98%) rename pep_sphinx_extensions/pep_zero_generator/{pep_0_writer.py => writer.py} (94%) diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_0_errors.py b/pep_sphinx_extensions/pep_zero_generator/errors.py similarity index 100% rename from pep_sphinx_extensions/pep_zero_generator/pep_0_errors.py rename to pep_sphinx_extensions/pep_zero_generator/errors.py diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py b/pep_sphinx_extensions/pep_zero_generator/parser.py similarity index 98% rename from pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py rename to pep_sphinx_extensions/pep_zero_generator/parser.py index 52092417469..c175c8f14c6 100644 --- a/pep_sphinx_extensions/pep_zero_generator/pep_0_parser.py +++ b/pep_sphinx_extensions/pep_zero_generator/parser.py @@ -8,7 +8,7 @@ import textwrap from pep_sphinx_extensions.pep_zero_generator.author import Author -from pep_sphinx_extensions.pep_zero_generator.pep_0_errors import PEPError +from pep_sphinx_extensions.pep_zero_generator.errors import PEPError STATUS_ACCEPTED = "Accepted" STATUS_PROVISIONAL = "Provisional" diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py b/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py index 5da38d25041..c9fffa486ca 100644 --- a/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py +++ b/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py @@ -22,8 +22,8 @@ from pathlib import Path from typing import TYPE_CHECKING -from pep_sphinx_extensions.pep_zero_generator import pep_0_parser -from pep_sphinx_extensions.pep_zero_generator import pep_0_writer +from pep_sphinx_extensions.pep_zero_generator import parser +from pep_sphinx_extensions.pep_zero_generator import writer if TYPE_CHECKING: from sphinx.application import Sphinx @@ -37,9 +37,9 @@ def create_pep_zero(_: Sphinx, env: BuildEnvironment, docnames: list[str]) -> No path = Path(".") pep_zero_filename = "pep-0000" - peps: list[pep_0_parser.PEP] = [] + peps: list[parser.PEP] = [] pep_pat = re.compile(r"pep-\d{4}") # Path.match() doesn't support regular expressions - title_length = pep_0_writer.title_length + title_length = writer.title_length # AUTHOR_OVERRIDES.csv is an exception file for PEP0 name parsing with open("AUTHOR_OVERRIDES.csv", "r", encoding="utf-8") as f: @@ -54,11 +54,11 @@ def create_pep_zero(_: Sphinx, env: BuildEnvironment, docnames: list[str]) -> No if file_path.match("pep-0000*"): continue # Skip pre-existing PEP 0 files if pep_pat.match(str(file_path)) and file_path.suffix in {".txt", ".rst"}: - pep = pep_0_parser.PEP(path.joinpath(file_path).absolute(), authors_overrides, title_length) + pep = parser.PEP(path.joinpath(file_path).absolute(), authors_overrides, title_length) peps.append(pep) peps.sort(key=lambda pep: pep.number) - pep_writer = pep_0_writer.PEPZeroWriter() + pep_writer = writer.PEPZeroWriter() pep0_text = pep_writer.write_pep0(peps) Path(f"{pep_zero_filename}.rst").write_text(pep0_text, encoding="utf-8") diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_0_writer.py b/pep_sphinx_extensions/pep_zero_generator/writer.py similarity index 94% rename from pep_sphinx_extensions/pep_zero_generator/pep_0_writer.py rename to pep_sphinx_extensions/pep_zero_generator/writer.py index 9fb598445b3..d2e6cb373f2 100644 --- a/pep_sphinx_extensions/pep_zero_generator/pep_0_writer.py +++ b/pep_sphinx_extensions/pep_zero_generator/writer.py @@ -7,11 +7,11 @@ from typing import TYPE_CHECKING import unicodedata -from pep_sphinx_extensions.pep_zero_generator.pep_0_parser import PEP -from pep_sphinx_extensions.pep_zero_generator.pep_0_errors import PEPError +from pep_sphinx_extensions.pep_zero_generator.parser import PEP +from pep_sphinx_extensions.pep_zero_generator.errors import PEPError if TYPE_CHECKING: - from pep_sphinx_extensions.pep_zero_generator import pep_0_parser + from pep_sphinx_extensions.pep_zero_generator import parser title_length = 55 author_length = 40 @@ -91,7 +91,7 @@ def emit_column_headers(self) -> None: self.emit_table_separator() @staticmethod - def sort_peps(peps: list[pep_0_parser.PEP]) -> tuple[list[pep_0_parser.PEP], ...]: + def sort_peps(peps: list[parser.PEP]) -> tuple[list[parser.PEP], ...]: """Sort PEPs into meta, informational, accepted, open, finished, and essentially dead.""" meta = [] @@ -138,8 +138,8 @@ def sort_peps(peps: list[pep_0_parser.PEP]) -> tuple[list[pep_0_parser.PEP], ... return meta, info, provisional, accepted, open_, finished, historical, deferred, dead @staticmethod - def verify_email_addresses(peps: list[pep_0_parser.PEP]) -> dict[pep_0_parser.Author, str]: - authors_dict: dict[pep_0_parser.Author, set[str]] = {} + def verify_email_addresses(peps: list[parser.PEP]) -> dict[parser.Author, str]: + authors_dict: dict[parser.Author, set[str]] = {} for pep in peps: for author in pep.authors: # If this is the first time we have come across an author, add them. @@ -171,7 +171,7 @@ def verify_email_addresses(peps: list[pep_0_parser.PEP]) -> dict[pep_0_parser.Au return valid_authors_dict @staticmethod - def sort_authors(authors_dict: dict[pep_0_parser.Author, str]) -> list[pep_0_parser.Author]: + def sort_authors(authors_dict: dict[parser.Author, str]) -> list[parser.Author]: return sorted(authors_dict.keys(), key=_author_sort_by) def emit_title(self, text: str, anchor: str, *, symbol: str = "=") -> None: @@ -183,7 +183,7 @@ def emit_title(self, text: str, anchor: str, *, symbol: str = "=") -> None: def emit_subtitle(self, text: str, anchor: str) -> None: self.emit_title(text, anchor, symbol="-") - def emit_pep_category(self, category: str, anchor: str, peps: list[pep_0_parser.PEP]) -> None: + def emit_pep_category(self, category: str, anchor: str, peps: list[parser.PEP]) -> None: self.emit_subtitle(category, anchor) self.emit_column_headers() for pep in peps: @@ -191,7 +191,7 @@ def emit_pep_category(self, category: str, anchor: str, peps: list[pep_0_parser. self.emit_table_separator() self.emit_newline() - def write_pep0(self, peps: list[pep_0_parser.PEP]): + def write_pep0(self, peps: list[parser.PEP]): # PEP metadata self.output(header) @@ -295,7 +295,7 @@ def write_pep0(self, peps: list[pep_0_parser.PEP]): return pep0_string -def _author_sort_by(author: pep_0_parser.Author) -> str: +def _author_sort_by(author: parser.Author) -> str: """Skip lower-cased words in surname when sorting.""" surname, *_ = author.last_first.split(",") surname_parts = surname.split() From dedb043dc01254ce1751473ea444872ac06cfc12 Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 13:48:27 +0100 Subject: [PATCH 21/37] Pass title length as an argument --- .../pep_zero_generator/parser.py | 15 ++++++--------- .../pep_zero_generator/pep_index_generator.py | 2 +- .../pep_zero_generator/writer.py | 4 ++-- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/pep_sphinx_extensions/pep_zero_generator/parser.py b/pep_sphinx_extensions/pep_zero_generator/parser.py index c175c8f14c6..d47f834e66f 100644 --- a/pep_sphinx_extensions/pep_zero_generator/parser.py +++ b/pep_sphinx_extensions/pep_zero_generator/parser.py @@ -57,14 +57,13 @@ def raise_pep_error(self, msg: str, pep_num: bool = False) -> None: pep_number = self.number if pep_num else None raise PEPError(msg, self.filename, pep_number=pep_number) - def __init__(self, filename: Path, authors_overrides: dict, title_length: int): + def __init__(self, filename: Path, authors_overrides: dict): """Init object from an open PEP file object. pep_file is full text of the PEP file, filename is path of the PEP file, author_lookup is author exceptions file """ self.filename: Path = filename - self.title_length: int = title_length # Parse the headers. pep_text = filename.read_text(encoding="utf-8") @@ -149,22 +148,20 @@ def _parse_author(data: str) -> list[tuple[str, str]]: break return author_list - @property - def title_abbr(self) -> str: + def title_abbr(self, title_length) -> str: """Shorten the title to be no longer than the max title length.""" - if len(self.title) <= self.title_length: + if len(self.title) <= title_length: return self.title - wrapped_title, *_excess = textwrap.wrap(self.title, self.title_length - 4) + wrapped_title, *_excess = textwrap.wrap(self.title, title_length - 4) return f"{wrapped_title} ..." - @property - def pep(self) -> dict[str, str | int]: + def pep(self, *, title_length) -> dict[str, str | int]: """Return the line entry for the PEP.""" return { # how the type is to be represented in the index "type": self.pep_type[0].upper(), "number": self.number, - "title": self.title_abbr, + "title": self.title_abbr(title_length), # how the status should be represented in the index "status": self.status[0].upper() if self.status not in PEP.hide_status else " ", # the author list as a comma-separated with only last names diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py b/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py index c9fffa486ca..a68a68c360c 100644 --- a/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py +++ b/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py @@ -54,7 +54,7 @@ def create_pep_zero(_: Sphinx, env: BuildEnvironment, docnames: list[str]) -> No if file_path.match("pep-0000*"): continue # Skip pre-existing PEP 0 files if pep_pat.match(str(file_path)) and file_path.suffix in {".txt", ".rst"}: - pep = parser.PEP(path.joinpath(file_path).absolute(), authors_overrides, title_length) + pep = parser.PEP(path.joinpath(file_path).absolute(), authors_overrides) peps.append(pep) peps.sort(key=lambda pep: pep.number) diff --git a/pep_sphinx_extensions/pep_zero_generator/writer.py b/pep_sphinx_extensions/pep_zero_generator/writer.py index d2e6cb373f2..8488ad327f2 100644 --- a/pep_sphinx_extensions/pep_zero_generator/writer.py +++ b/pep_sphinx_extensions/pep_zero_generator/writer.py @@ -187,7 +187,7 @@ def emit_pep_category(self, category: str, anchor: str, peps: list[parser.PEP]) self.emit_subtitle(category, anchor) self.emit_column_headers() for pep in peps: - self.output(column_format(**pep.pep)) + self.output(column_format(**pep.pep(title_length=title_length))) self.emit_table_separator() self.emit_newline() @@ -228,7 +228,7 @@ def write_pep0(self, peps: list[parser.PEP]): for pep in peps: if pep.number - prev_pep > 1: self.emit_newline() - self.output(column_format(**pep.pep)) + self.output(column_format(**pep.pep(title_length=title_length))) prev_pep = pep.number self.emit_table_separator() From 84518a3e84a74c51b99b5449470b84f6d727dbe4 Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 13:59:22 +0100 Subject: [PATCH 22/37] Add constants.py to hold global type / status values --- .../pep_zero_generator/constants.py | 30 ++++++++++++ .../pep_zero_generator/parser.py | 47 ++++++------------- .../pep_zero_generator/pep_index_generator.py | 1 - .../pep_zero_generator/writer.py | 27 ++++++----- 4 files changed, 59 insertions(+), 46 deletions(-) create mode 100644 pep_sphinx_extensions/pep_zero_generator/constants.py diff --git a/pep_sphinx_extensions/pep_zero_generator/constants.py b/pep_sphinx_extensions/pep_zero_generator/constants.py new file mode 100644 index 00000000000..3e854f6f8ff --- /dev/null +++ b/pep_sphinx_extensions/pep_zero_generator/constants.py @@ -0,0 +1,30 @@ +STATUS_ACCEPTED = "Accepted" +STATUS_PROVISIONAL = "Provisional" +STATUS_REJECTED = "Rejected" +STATUS_WITHDRAWN = "Withdrawn" +STATUS_DEFERRED = "Deferred" +STATUS_FINAL = "Final" +STATUS_ACTIVE = "Active" +STATUS_DRAFT = "Draft" +STATUS_SUPERSEDED = "Superseded" + +# Valid values for the Status header. +status_values = { + STATUS_ACCEPTED, STATUS_PROVISIONAL, STATUS_REJECTED, STATUS_WITHDRAWN, + STATUS_DEFERRED, STATUS_FINAL, STATUS_ACTIVE, STATUS_DRAFT, STATUS_SUPERSEDED, +} +# Map of invalid/special statuses to their valid counterparts +special_statuses = { + "April Fool!": STATUS_REJECTED, # See PEP 401 :) +} +# Draft PEPs have no status displayed, Active shares a key with Accepted +hide_status = {STATUS_DRAFT, STATUS_ACTIVE} + +TYPE_STANDARDS = "Standards Track" +TYPE_INFO = "Informational" +TYPE_PROCESS = "Process" + +# Valid values for the Type header. +type_values = {TYPE_STANDARDS, TYPE_INFO, TYPE_PROCESS} +# Active PEPs can only be for Informational or Process PEPs. +active_allowed = {TYPE_PROCESS, TYPE_INFO} diff --git a/pep_sphinx_extensions/pep_zero_generator/parser.py b/pep_sphinx_extensions/pep_zero_generator/parser.py index d47f834e66f..6f79e28a701 100644 --- a/pep_sphinx_extensions/pep_zero_generator/parser.py +++ b/pep_sphinx_extensions/pep_zero_generator/parser.py @@ -8,21 +8,16 @@ import textwrap from pep_sphinx_extensions.pep_zero_generator.author import Author +from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_PROVISIONAL +from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_ACTIVE +from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_STANDARDS +from pep_sphinx_extensions.pep_zero_generator.constants import type_values +from pep_sphinx_extensions.pep_zero_generator.constants import status_values +from pep_sphinx_extensions.pep_zero_generator.constants import special_statuses +from pep_sphinx_extensions.pep_zero_generator.constants import active_allowed +from pep_sphinx_extensions.pep_zero_generator.constants import hide_status from pep_sphinx_extensions.pep_zero_generator.errors import PEPError -STATUS_ACCEPTED = "Accepted" -STATUS_PROVISIONAL = "Provisional" -STATUS_REJECTED = "Rejected" -STATUS_WITHDRAWN = "Withdrawn" -STATUS_DEFERRED = "Deferred" -STATUS_FINAL = "Final" -STATUS_ACTIVE = "Active" -STATUS_DRAFT = "Draft" -STATUS_SUPERSEDED = "Superseded" -TYPE_STANDARDS = "Standards Track" -TYPE_INFO = "Informational" -TYPE_PROCESS = "Process" - class PEP: """Representation of PEPs. @@ -39,20 +34,6 @@ class PEP: # The required RFC 822 headers for all PEPs. required_headers = {"PEP", "Title", "Author", "Status", "Type", "Created"} - # Valid values for the Type header. - type_values = {TYPE_STANDARDS, TYPE_INFO, TYPE_PROCESS} - # Valid values for the Status header. - status_values = { - STATUS_ACCEPTED, STATUS_PROVISIONAL, STATUS_REJECTED, STATUS_WITHDRAWN, - STATUS_DEFERRED, STATUS_FINAL, STATUS_ACTIVE, STATUS_DRAFT, STATUS_SUPERSEDED, - } - special_statuses = { - "April Fool!": STATUS_REJECTED, # See PEP 401 :) - } - # Active PEPs can only be for Informational or Process PEPs. - active_allowed = {TYPE_PROCESS, TYPE_INFO} - hide_status = {STATUS_DRAFT, STATUS_ACTIVE} - def raise_pep_error(self, msg: str, pep_num: bool = False) -> None: pep_number = self.number if pep_num else None raise PEPError(msg, self.filename, pep_number=pep_number) @@ -86,18 +67,18 @@ def __init__(self, filename: Path, authors_overrides: dict): # Type self.pep_type: str = metadata["Type"] - if self.pep_type not in PEP.type_values: + if self.pep_type not in type_values: self.raise_pep_error(f"{self.pep_type} is not a valid Type value", pep_num=True) # Status status = metadata["Status"] - if status in PEP.special_statuses: - status = PEP.special_statuses[status] - if status not in PEP.status_values: + if status in special_statuses: + status = special_statuses[status] + if status not in status_values: self.raise_pep_error(f"{status} is not a valid Status value", pep_num=True) # Special case for Active PEPs. - if status == STATUS_ACTIVE and self.pep_type not in PEP.active_allowed: + if status == STATUS_ACTIVE and self.pep_type not in active_allowed: msg = "Only Process and Informational PEPs may have an Active status" self.raise_pep_error(msg, pep_num=True) @@ -163,7 +144,7 @@ def pep(self, *, title_length) -> dict[str, str | int]: "number": self.number, "title": self.title_abbr(title_length), # how the status should be represented in the index - "status": self.status[0].upper() if self.status not in PEP.hide_status else " ", + "status": self.status[0].upper() if self.status not in hide_status else " ", # the author list as a comma-separated with only last names "authors": ", ".join(x.nick for x in self.authors), } diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py b/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py index a68a68c360c..a391241d25f 100644 --- a/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py +++ b/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py @@ -39,7 +39,6 @@ def create_pep_zero(_: Sphinx, env: BuildEnvironment, docnames: list[str]) -> No pep_zero_filename = "pep-0000" peps: list[parser.PEP] = [] pep_pat = re.compile(r"pep-\d{4}") # Path.match() doesn't support regular expressions - title_length = writer.title_length # AUTHOR_OVERRIDES.csv is an exception file for PEP0 name parsing with open("AUTHOR_OVERRIDES.csv", "r", encoding="utf-8") as f: diff --git a/pep_sphinx_extensions/pep_zero_generator/writer.py b/pep_sphinx_extensions/pep_zero_generator/writer.py index 8488ad327f2..4e64b88955d 100644 --- a/pep_sphinx_extensions/pep_zero_generator/writer.py +++ b/pep_sphinx_extensions/pep_zero_generator/writer.py @@ -7,11 +7,14 @@ from typing import TYPE_CHECKING import unicodedata -from pep_sphinx_extensions.pep_zero_generator.parser import PEP +from pep_sphinx_extensions.pep_zero_generator.constants import type_values +from pep_sphinx_extensions.pep_zero_generator.constants import status_values +from pep_sphinx_extensions.pep_zero_generator.constants import hide_status from pep_sphinx_extensions.pep_zero_generator.errors import PEPError if TYPE_CHECKING: - from pep_sphinx_extensions.pep_zero_generator import parser + from pep_sphinx_extensions.pep_zero_generator.parser import PEP + from pep_sphinx_extensions.pep_zero_generator.author import Author title_length = 55 author_length = 40 @@ -91,7 +94,7 @@ def emit_column_headers(self) -> None: self.emit_table_separator() @staticmethod - def sort_peps(peps: list[parser.PEP]) -> tuple[list[parser.PEP], ...]: + def sort_peps(peps: list[PEP]) -> tuple[list[PEP], ...]: """Sort PEPs into meta, informational, accepted, open, finished, and essentially dead.""" meta = [] @@ -138,8 +141,8 @@ def sort_peps(peps: list[parser.PEP]) -> tuple[list[parser.PEP], ...]: return meta, info, provisional, accepted, open_, finished, historical, deferred, dead @staticmethod - def verify_email_addresses(peps: list[parser.PEP]) -> dict[parser.Author, str]: - authors_dict: dict[parser.Author, set[str]] = {} + def verify_email_addresses(peps: list[PEP]) -> dict[Author, str]: + authors_dict: dict[Author, set[str]] = {} for pep in peps: for author in pep.authors: # If this is the first time we have come across an author, add them. @@ -171,7 +174,7 @@ def verify_email_addresses(peps: list[parser.PEP]) -> dict[parser.Author, str]: return valid_authors_dict @staticmethod - def sort_authors(authors_dict: dict[parser.Author, str]) -> list[parser.Author]: + def sort_authors(authors_dict: dict[Author, str]) -> list[Author]: return sorted(authors_dict.keys(), key=_author_sort_by) def emit_title(self, text: str, anchor: str, *, symbol: str = "=") -> None: @@ -183,7 +186,7 @@ def emit_title(self, text: str, anchor: str, *, symbol: str = "=") -> None: def emit_subtitle(self, text: str, anchor: str) -> None: self.emit_title(text, anchor, symbol="-") - def emit_pep_category(self, category: str, anchor: str, peps: list[parser.PEP]) -> None: + def emit_pep_category(self, category: str, anchor: str, peps: list[PEP]) -> None: self.emit_subtitle(category, anchor) self.emit_column_headers() for pep in peps: @@ -191,7 +194,7 @@ def emit_pep_category(self, category: str, anchor: str, peps: list[parser.PEP]) self.emit_table_separator() self.emit_newline() - def write_pep0(self, peps: list[parser.PEP]): + def write_pep0(self, peps: list[PEP]): # PEP metadata self.output(header) @@ -251,7 +254,7 @@ def write_pep0(self, peps: list[parser.PEP]): # PEP types key self.emit_title("PEP Types Key", "type-key") - for type_ in sorted(PEP.type_values): + for type_ in sorted(type_values): self.output(f" {type_[0]} - {type_} PEP") self.emit_newline() @@ -259,9 +262,9 @@ def write_pep0(self, peps: list[parser.PEP]): # PEP status key self.emit_title("PEP Status Key", "status-key") - for status in sorted(PEP.status_values): + for status in sorted(status_values): # Draft PEPs have no status displayed, Active shares a key with Accepted - if status in {"Active", "Draft"}: + if status in hide_status: continue if status == "Accepted": msg = " A - Accepted (Standards Track only) or Active proposal" @@ -295,7 +298,7 @@ def write_pep0(self, peps: list[parser.PEP]): return pep0_string -def _author_sort_by(author: parser.Author) -> str: +def _author_sort_by(author: Author) -> str: """Skip lower-cased words in surname when sorting.""" surname, *_ = author.last_first.split(",") surname_parts = surname.split() From 51645710d7cb208c9507e1ed99c17fca6cd9c247 Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 15:35:20 +0100 Subject: [PATCH 23/37] Capitalise constants --- .../pep_zero_generator/constants.py | 12 +++++---- .../pep_zero_generator/parser.py | 26 +++++++++---------- .../pep_zero_generator/writer.py | 12 ++++----- 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/pep_sphinx_extensions/pep_zero_generator/constants.py b/pep_sphinx_extensions/pep_zero_generator/constants.py index 3e854f6f8ff..a23dd60ede1 100644 --- a/pep_sphinx_extensions/pep_zero_generator/constants.py +++ b/pep_sphinx_extensions/pep_zero_generator/constants.py @@ -1,3 +1,5 @@ +"""Holds type and status constants for PEP 0 generation.""" + STATUS_ACCEPTED = "Accepted" STATUS_PROVISIONAL = "Provisional" STATUS_REJECTED = "Rejected" @@ -9,22 +11,22 @@ STATUS_SUPERSEDED = "Superseded" # Valid values for the Status header. -status_values = { +STATUS_VALUES = { STATUS_ACCEPTED, STATUS_PROVISIONAL, STATUS_REJECTED, STATUS_WITHDRAWN, STATUS_DEFERRED, STATUS_FINAL, STATUS_ACTIVE, STATUS_DRAFT, STATUS_SUPERSEDED, } # Map of invalid/special statuses to their valid counterparts -special_statuses = { +SPECIAL_STATUSES = { "April Fool!": STATUS_REJECTED, # See PEP 401 :) } # Draft PEPs have no status displayed, Active shares a key with Accepted -hide_status = {STATUS_DRAFT, STATUS_ACTIVE} +HIDE_STATUS = {STATUS_DRAFT, STATUS_ACTIVE} TYPE_STANDARDS = "Standards Track" TYPE_INFO = "Informational" TYPE_PROCESS = "Process" # Valid values for the Type header. -type_values = {TYPE_STANDARDS, TYPE_INFO, TYPE_PROCESS} +TYPE_VALUES = {TYPE_STANDARDS, TYPE_INFO, TYPE_PROCESS} # Active PEPs can only be for Informational or Process PEPs. -active_allowed = {TYPE_PROCESS, TYPE_INFO} +ACTIVE_ALLOWED = {TYPE_PROCESS, TYPE_INFO} diff --git a/pep_sphinx_extensions/pep_zero_generator/parser.py b/pep_sphinx_extensions/pep_zero_generator/parser.py index 6f79e28a701..5ca20ff8184 100644 --- a/pep_sphinx_extensions/pep_zero_generator/parser.py +++ b/pep_sphinx_extensions/pep_zero_generator/parser.py @@ -11,11 +11,11 @@ from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_PROVISIONAL from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_ACTIVE from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_STANDARDS -from pep_sphinx_extensions.pep_zero_generator.constants import type_values -from pep_sphinx_extensions.pep_zero_generator.constants import status_values -from pep_sphinx_extensions.pep_zero_generator.constants import special_statuses -from pep_sphinx_extensions.pep_zero_generator.constants import active_allowed -from pep_sphinx_extensions.pep_zero_generator.constants import hide_status +from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_VALUES +from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_VALUES +from pep_sphinx_extensions.pep_zero_generator.constants import SPECIAL_STATUSES +from pep_sphinx_extensions.pep_zero_generator.constants import ACTIVE_ALLOWED +from pep_sphinx_extensions.pep_zero_generator.constants import HIDE_STATUS from pep_sphinx_extensions.pep_zero_generator.errors import PEPError @@ -25,8 +25,8 @@ class PEP: Attributes: number : PEP number. title : PEP title. - pep_type : The type of PEP. Can only be one of the values from PEP.type_values. - status : The PEP's status. Value must be found in PEP.status_values. + pep_type : The type of PEP. Can only be one of the values from TYPE_VALUES. + status : The PEP's status. Value must be found in STATUS_VALUES. authors : A list of the authors. """ @@ -67,18 +67,18 @@ def __init__(self, filename: Path, authors_overrides: dict): # Type self.pep_type: str = metadata["Type"] - if self.pep_type not in type_values: + if self.pep_type not in TYPE_VALUES: self.raise_pep_error(f"{self.pep_type} is not a valid Type value", pep_num=True) # Status status = metadata["Status"] - if status in special_statuses: - status = special_statuses[status] - if status not in status_values: + if status in SPECIAL_STATUSES: + status = SPECIAL_STATUSES[status] + if status not in STATUS_VALUES: self.raise_pep_error(f"{status} is not a valid Status value", pep_num=True) # Special case for Active PEPs. - if status == STATUS_ACTIVE and self.pep_type not in active_allowed: + if status == STATUS_ACTIVE and self.pep_type not in ACTIVE_ALLOWED: msg = "Only Process and Informational PEPs may have an Active status" self.raise_pep_error(msg, pep_num=True) @@ -144,7 +144,7 @@ def pep(self, *, title_length) -> dict[str, str | int]: "number": self.number, "title": self.title_abbr(title_length), # how the status should be represented in the index - "status": self.status[0].upper() if self.status not in hide_status else " ", + "status": self.status[0].upper() if self.status not in HIDE_STATUS else " ", # the author list as a comma-separated with only last names "authors": ", ".join(x.nick for x in self.authors), } diff --git a/pep_sphinx_extensions/pep_zero_generator/writer.py b/pep_sphinx_extensions/pep_zero_generator/writer.py index 4e64b88955d..884257bce25 100644 --- a/pep_sphinx_extensions/pep_zero_generator/writer.py +++ b/pep_sphinx_extensions/pep_zero_generator/writer.py @@ -7,9 +7,9 @@ from typing import TYPE_CHECKING import unicodedata -from pep_sphinx_extensions.pep_zero_generator.constants import type_values -from pep_sphinx_extensions.pep_zero_generator.constants import status_values -from pep_sphinx_extensions.pep_zero_generator.constants import hide_status +from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_VALUES +from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_VALUES +from pep_sphinx_extensions.pep_zero_generator.constants import HIDE_STATUS from pep_sphinx_extensions.pep_zero_generator.errors import PEPError if TYPE_CHECKING: @@ -254,7 +254,7 @@ def write_pep0(self, peps: list[PEP]): # PEP types key self.emit_title("PEP Types Key", "type-key") - for type_ in sorted(type_values): + for type_ in sorted(TYPE_VALUES): self.output(f" {type_[0]} - {type_} PEP") self.emit_newline() @@ -262,9 +262,9 @@ def write_pep0(self, peps: list[PEP]): # PEP status key self.emit_title("PEP Status Key", "status-key") - for status in sorted(status_values): + for status in sorted(STATUS_VALUES): # Draft PEPs have no status displayed, Active shares a key with Accepted - if status in hide_status: + if status in HIDE_STATUS: continue if status == "Accepted": msg = " A - Accepted (Standards Track only) or Active proposal" From 29738c543c9148a439fbc7df4570a06ee0dd4700 Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 15:37:16 +0100 Subject: [PATCH 24/37] Capitalise constants --- pep_sphinx_extensions/pep_zero_generator/errors.py | 2 ++ pep_sphinx_extensions/pep_zero_generator/parser.py | 6 ++++++ .../pep_zero_generator/pep_index_generator.py | 3 +-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pep_sphinx_extensions/pep_zero_generator/errors.py b/pep_sphinx_extensions/pep_zero_generator/errors.py index cdde4f3810e..deb12021ac9 100644 --- a/pep_sphinx_extensions/pep_zero_generator/errors.py +++ b/pep_sphinx_extensions/pep_zero_generator/errors.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pathlib import Path diff --git a/pep_sphinx_extensions/pep_zero_generator/parser.py b/pep_sphinx_extensions/pep_zero_generator/parser.py index 5ca20ff8184..464be1bf610 100644 --- a/pep_sphinx_extensions/pep_zero_generator/parser.py +++ b/pep_sphinx_extensions/pep_zero_generator/parser.py @@ -91,6 +91,12 @@ def __init__(self, filename: Path, authors_overrides: dict): # Parse PEP authors self.authors: list[Author] = self.parse_authors(metadata["Author"], authors_overrides) + def __repr__(self) -> str: + return f"4} - {self.title}>" + + def __lt__(self, other: PEP) -> bool: + return self.number < other.number + def parse_authors(self, author_header: str, authors_overrides: dict) -> list[Author]: """Parse Author header line""" authors_and_emails = self._parse_author(author_header) diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py b/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py index a391241d25f..9fd46a06e93 100644 --- a/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py +++ b/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py @@ -55,10 +55,9 @@ def create_pep_zero(_: Sphinx, env: BuildEnvironment, docnames: list[str]) -> No if pep_pat.match(str(file_path)) and file_path.suffix in {".txt", ".rst"}: pep = parser.PEP(path.joinpath(file_path).absolute(), authors_overrides) peps.append(pep) - peps.sort(key=lambda pep: pep.number) pep_writer = writer.PEPZeroWriter() - pep0_text = pep_writer.write_pep0(peps) + pep0_text = pep_writer.write_pep0(sorted(peps)) Path(f"{pep_zero_filename}.rst").write_text(pep0_text, encoding="utf-8") # Add to files for builder From 918a4b9564aeb64eda5df1daab811fe3f6d06b3b Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 16:00:21 +0100 Subject: [PATCH 25/37] Update PEP classification algorithm --- .../pep_zero_generator/writer.py | 87 ++++++++++--------- 1 file changed, 46 insertions(+), 41 deletions(-) diff --git a/pep_sphinx_extensions/pep_zero_generator/writer.py b/pep_sphinx_extensions/pep_zero_generator/writer.py index 884257bce25..8509bf93ba0 100644 --- a/pep_sphinx_extensions/pep_zero_generator/writer.py +++ b/pep_sphinx_extensions/pep_zero_generator/writer.py @@ -97,47 +97,52 @@ def emit_column_headers(self) -> None: def sort_peps(peps: list[PEP]) -> tuple[list[PEP], ...]: """Sort PEPs into meta, informational, accepted, open, finished, and essentially dead.""" - meta = [] - info = [] - provisional = [] - accepted = [] - open_ = [] - finished = [] - historical = [] - deferred = [] - dead = [] - for pep in peps: - # Order of 'if' statement important. Key Status values take precedence - # over Type value, and vice-versa. - if pep.status == "Draft": - open_.append(pep) - elif pep.status == "Deferred": - deferred.append(pep) - elif pep.pep_type == "Process": - if pep.status == "Active": - meta.append(pep) - elif pep.status in {"Withdrawn", "Rejected"}: - dead.append(pep) - else: - historical.append(pep) - elif pep.status in {"Rejected", "Withdrawn", "Incomplete", "Superseded"}: - dead.append(pep) - elif pep.pep_type == "Informational": - # Hack until the conflict between the use of "Final" - # for both API definition PEPs and other (actually - # obsolete) PEPs is addressed - if pep.status == "Active" or "Release Schedule" not in pep.title: - info.append(pep) - else: - historical.append(pep) - elif pep.status == "Provisional": - provisional.append(pep) - elif pep.status in {"Accepted", "Active"}: - accepted.append(pep) - elif pep.status == "Final": - finished.append(pep) - else: - raise PEPError(f"unsorted ({pep.pep_type}/{pep.status})", pep.filename, pep.number) + remaining = set(peps) + + # The order of the comprehensions below is important. Key status values + # take precedence over type value, and vice-versa. + open_ = sorted(pep for pep in remaining if pep.status == "Draft") + remaining -= {pep for pep in open_} + + deferred = sorted(pep for pep in remaining if pep.status == "Deferred") + remaining -= {pep for pep in deferred} + + meta = sorted(pep for pep in remaining if pep.pep_type == "Process" and pep.status == "Active") + remaining -= {pep for pep in meta} + + dead = sorted(pep for pep in remaining if pep.pep_type == "Process" and pep.status in {"Withdrawn", "Rejected"}) + remaining -= {pep for pep in dead} + + historical = sorted(pep for pep in remaining if pep.pep_type == "Process") + remaining -= {pep for pep in historical} + + dead += sorted(pep for pep in remaining if pep.status in {"Rejected", "Withdrawn", "Incomplete", "Superseded"}) + remaining -= {pep for pep in dead} + + # Hack until the conflict between the use of "Final" + # for both API definition PEPs and other (actually + # obsolete) PEPs is addressed + info = sorted( + pep for pep in remaining + if pep.pep_type == "Informational" and (pep.status == "Active" or "Release Schedule" not in pep.title) + ) + remaining -= {pep for pep in info} + + historical += sorted(pep for pep in remaining if pep.pep_type == "Informational") + remaining -= {pep for pep in historical} + + provisional = sorted(pep for pep in remaining if pep.status == "Provisional") + remaining -= {pep for pep in provisional} + + accepted = sorted(pep for pep in remaining if pep.status in {"Accepted", "Active"}) + remaining -= {pep for pep in accepted} + + finished = sorted(pep for pep in remaining if pep.status == "Final") + remaining -= {pep for pep in finished} + + for pep in remaining: + raise PEPError(f"unsorted ({pep.pep_type}/{pep.status})", pep.filename, pep.number) + return meta, info, provisional, accepted, open_, finished, historical, deferred, dead @staticmethod From 70011e0e69dfb4168e8ce542c94c1d6aabc01126 Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 16:05:01 +0100 Subject: [PATCH 26/37] Extract static methods to module level --- .../pep_zero_generator/writer.py | 184 +++++++++--------- 1 file changed, 92 insertions(+), 92 deletions(-) diff --git a/pep_sphinx_extensions/pep_zero_generator/writer.py b/pep_sphinx_extensions/pep_zero_generator/writer.py index 8509bf93ba0..a0da7e10c41 100644 --- a/pep_sphinx_extensions/pep_zero_generator/writer.py +++ b/pep_sphinx_extensions/pep_zero_generator/writer.py @@ -93,95 +93,6 @@ def emit_column_headers(self) -> None: )) self.emit_table_separator() - @staticmethod - def sort_peps(peps: list[PEP]) -> tuple[list[PEP], ...]: - """Sort PEPs into meta, informational, accepted, open, finished, - and essentially dead.""" - remaining = set(peps) - - # The order of the comprehensions below is important. Key status values - # take precedence over type value, and vice-versa. - open_ = sorted(pep for pep in remaining if pep.status == "Draft") - remaining -= {pep for pep in open_} - - deferred = sorted(pep for pep in remaining if pep.status == "Deferred") - remaining -= {pep for pep in deferred} - - meta = sorted(pep for pep in remaining if pep.pep_type == "Process" and pep.status == "Active") - remaining -= {pep for pep in meta} - - dead = sorted(pep for pep in remaining if pep.pep_type == "Process" and pep.status in {"Withdrawn", "Rejected"}) - remaining -= {pep for pep in dead} - - historical = sorted(pep for pep in remaining if pep.pep_type == "Process") - remaining -= {pep for pep in historical} - - dead += sorted(pep for pep in remaining if pep.status in {"Rejected", "Withdrawn", "Incomplete", "Superseded"}) - remaining -= {pep for pep in dead} - - # Hack until the conflict between the use of "Final" - # for both API definition PEPs and other (actually - # obsolete) PEPs is addressed - info = sorted( - pep for pep in remaining - if pep.pep_type == "Informational" and (pep.status == "Active" or "Release Schedule" not in pep.title) - ) - remaining -= {pep for pep in info} - - historical += sorted(pep for pep in remaining if pep.pep_type == "Informational") - remaining -= {pep for pep in historical} - - provisional = sorted(pep for pep in remaining if pep.status == "Provisional") - remaining -= {pep for pep in provisional} - - accepted = sorted(pep for pep in remaining if pep.status in {"Accepted", "Active"}) - remaining -= {pep for pep in accepted} - - finished = sorted(pep for pep in remaining if pep.status == "Final") - remaining -= {pep for pep in finished} - - for pep in remaining: - raise PEPError(f"unsorted ({pep.pep_type}/{pep.status})", pep.filename, pep.number) - - return meta, info, provisional, accepted, open_, finished, historical, deferred, dead - - @staticmethod - def verify_email_addresses(peps: list[PEP]) -> dict[Author, str]: - authors_dict: dict[Author, set[str]] = {} - for pep in peps: - for author in pep.authors: - # If this is the first time we have come across an author, add them. - if author not in authors_dict: - authors_dict[author] = {author.email} if author.email else set() - else: - # If the new email is an empty string, move on. - if not author.email: - continue - # If the email has not been seen, add it to the list. - authors_dict[author].add(author.email) - - valid_authors_dict = {} - too_many_emails = [] - for author, emails in authors_dict.items(): - if len(emails) > 1: - too_many_emails.append((author.last_first, emails)) - else: - valid_authors_dict[author] = next(iter(emails), "") - if too_many_emails: - err_output = [] - for author, emails in too_many_emails: - err_output.append(" " * 4 + f"{author}: {emails}") - raise ValueError( - "some authors have more than one email address listed:\n" - + "\n".join(err_output) - ) - - return valid_authors_dict - - @staticmethod - def sort_authors(authors_dict: dict[Author, str]) -> list[Author]: - return sorted(authors_dict.keys(), key=_author_sort_by) - def emit_title(self, text: str, anchor: str, *, symbol: str = "=") -> None: self.output(f".. _{anchor}:\n") self.output(text) @@ -212,7 +123,7 @@ def write_pep0(self, peps: list[PEP]): # PEPs by category self.emit_title("Index by Category", "by-category") - meta, info, provisional, accepted, open_, finished, historical, deferred, dead = self.sort_peps(peps) + meta, info, provisional, accepted, open_, finished, historical, deferred, dead = _classify_peps(peps) pep_categories = [ ("Meta-PEPs (PEPs about PEPs or Processes)", "by-category-meta", meta), ("Other Informational PEPs", "by-category-other-info", info), @@ -281,13 +192,13 @@ def write_pep0(self, peps: list[PEP]): self.emit_newline() # PEP owners - authors_dict = self.verify_email_addresses(peps) + authors_dict = _verify_email_addresses(peps) max_name_len = max(len(author) for author in authors_dict.keys()) self.emit_title("Authors/Owners", "authors") self.emit_author_table_separator(max_name_len) self.output(f"{'Name':{max_name_len}} Email Address") self.emit_author_table_separator(max_name_len) - for author in self.sort_authors(authors_dict): + for author in _sort_authors(authors_dict): # Use the email from authors_dict instead of the one from "author" as # the author instance may have an empty email. self.output(f"{author.last_first:{max_name_len}} {authors_dict[author]}") @@ -303,6 +214,95 @@ def write_pep0(self, peps: list[PEP]): return pep0_string +def _classify_peps(peps: list[PEP]) -> tuple[list[PEP], ...]: + """Sort PEPs into meta, informational, accepted, open, finished, + and essentially dead.""" + remaining = set(peps) + + # The order of the comprehensions below is important. Key status values + # take precedence over type value, and vice-versa. + open_ = sorted(pep for pep in remaining if pep.status == "Draft") + remaining -= {pep for pep in open_} + + deferred = sorted(pep for pep in remaining if pep.status == "Deferred") + remaining -= {pep for pep in deferred} + + meta = sorted(pep for pep in remaining if pep.pep_type == "Process" and pep.status == "Active") + remaining -= {pep for pep in meta} + + dead = sorted(pep for pep in remaining if pep.pep_type == "Process" and pep.status in {"Withdrawn", "Rejected"}) + remaining -= {pep for pep in dead} + + historical = sorted(pep for pep in remaining if pep.pep_type == "Process") + remaining -= {pep for pep in historical} + + dead += sorted(pep for pep in remaining if pep.status in {"Rejected", "Withdrawn", "Incomplete", "Superseded"}) + remaining -= {pep for pep in dead} + + # Hack until the conflict between the use of "Final" + # for both API definition PEPs and other (actually + # obsolete) PEPs is addressed + info = sorted( + pep for pep in remaining + if pep.pep_type == "Informational" and (pep.status == "Active" or "Release Schedule" not in pep.title) + ) + remaining -= {pep for pep in info} + + historical += sorted(pep for pep in remaining if pep.pep_type == "Informational") + remaining -= {pep for pep in historical} + + provisional = sorted(pep for pep in remaining if pep.status == "Provisional") + remaining -= {pep for pep in provisional} + + accepted = sorted(pep for pep in remaining if pep.status in {"Accepted", "Active"}) + remaining -= {pep for pep in accepted} + + finished = sorted(pep for pep in remaining if pep.status == "Final") + remaining -= {pep for pep in finished} + + for pep in remaining: + raise PEPError(f"unsorted ({pep.pep_type}/{pep.status})", pep.filename, pep.number) + + return meta, info, provisional, accepted, open_, finished, historical, deferred, dead + + +def _verify_email_addresses(peps: list[PEP]) -> dict[Author, str]: + authors_dict: dict[Author, set[str]] = {} + for pep in peps: + for author in pep.authors: + # If this is the first time we have come across an author, add them. + if author not in authors_dict: + authors_dict[author] = {author.email} if author.email else set() + else: + # If the new email is an empty string, move on. + if not author.email: + continue + # If the email has not been seen, add it to the list. + authors_dict[author].add(author.email) + + valid_authors_dict = {} + too_many_emails = [] + for author, emails in authors_dict.items(): + if len(emails) > 1: + too_many_emails.append((author.last_first, emails)) + else: + valid_authors_dict[author] = next(iter(emails), "") + if too_many_emails: + err_output = [] + for author, emails in too_many_emails: + err_output.append(" " * 4 + f"{author}: {emails}") + raise ValueError( + "some authors have more than one email address listed:\n" + + "\n".join(err_output) + ) + + return valid_authors_dict + + +def _sort_authors(authors_dict: dict[Author, str]) -> list[Author]: + return sorted(authors_dict.keys(), key=_author_sort_by) + + def _author_sort_by(author: Author) -> str: """Skip lower-cased words in surname when sorting.""" surname, *_ = author.last_first.split(",") From e72bed11e6fc2ff9c794022d752db623db4cfded Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 16:12:36 +0100 Subject: [PATCH 27/37] Add emit_text, emit_pep_row --- .../pep_zero_generator/writer.py | 59 ++++++++----------- 1 file changed, 25 insertions(+), 34 deletions(-) diff --git a/pep_sphinx_extensions/pep_zero_generator/writer.py b/pep_sphinx_extensions/pep_zero_generator/writer.py index a0da7e10c41..99fdd1fa361 100644 --- a/pep_sphinx_extensions/pep_zero_generator/writer.py +++ b/pep_sphinx_extensions/pep_zero_generator/writer.py @@ -65,38 +65,35 @@ class PEPZeroWriter: } def __init__(self): - self._output: list[str] = [] + self.output: list[str] = [] - def output(self, content: str) -> None: - # Appends content argument to the _output list - self._output.append(content) + def emit_text(self, content: str) -> None: + # Appends content argument to the output list + self.output.append(content) def emit_newline(self) -> None: - self.output("") + self.output.append("") def emit_table_separator(self) -> None: - self.output(table_separator) + self.output.append(table_separator) def emit_author_table_separator(self, max_name_len: int) -> None: author_table_separator = "=" * max_name_len + " " + "=" * len("email address") - self.output(author_table_separator) + self.output.append(author_table_separator) + + def emit_pep_row(self, pep_details: dict[str, int | str]) -> None: + self.emit_text(column_format(**pep_details)) def emit_column_headers(self) -> None: """Output the column headers for the PEP indices.""" self.emit_table_separator() - self.output(column_format( - status=".", - type=".", - number="PEP", - title="PEP Title", - authors="PEP Author(s)", - )) + self.emit_pep_row({"status": ".", "type": ".", "number": "PEP", "title": "PEP Title", "authors": "PEP Author(s)"}) self.emit_table_separator() def emit_title(self, text: str, anchor: str, *, symbol: str = "=") -> None: - self.output(f".. _{anchor}:\n") - self.output(text) - self.output(symbol * len(text)) + self.output.append(f".. _{anchor}:\n") + self.output.append(text) + self.output.append(symbol * len(text)) self.emit_newline() def emit_subtitle(self, text: str, anchor: str) -> None: @@ -106,19 +103,19 @@ def emit_pep_category(self, category: str, anchor: str, peps: list[PEP]) -> None self.emit_subtitle(category, anchor) self.emit_column_headers() for pep in peps: - self.output(column_format(**pep.pep(title_length=title_length))) + self.output.append(column_format(**pep.pep(title_length=title_length))) self.emit_table_separator() self.emit_newline() def write_pep0(self, peps: list[PEP]): # PEP metadata - self.output(header) + self.emit_text(header) self.emit_newline() # Introduction self.emit_title("Introduction", "intro") - self.output(intro) + self.emit_text(intro) self.emit_newline() # PEPs by category @@ -147,7 +144,7 @@ def write_pep0(self, peps: list[PEP]): for pep in peps: if pep.number - prev_pep > 1: self.emit_newline() - self.output(column_format(**pep.pep(title_length=title_length))) + self.emit_pep_row(pep.pep(title_length=title_length)) prev_pep = pep.number self.emit_table_separator() @@ -157,13 +154,7 @@ def write_pep0(self, peps: list[PEP]): self.emit_title("Reserved PEP Numbers", "reserved") self.emit_column_headers() for number, claimants in sorted(self.RESERVED.items()): - self.output(column_format( - type=".", - status=".", - number=number, - title="RESERVED", - authors=claimants, - )) + self.emit_pep_row({"type": ".", "status": ".", "number": number, "title": "RESERVED", "authors": claimants}) self.emit_table_separator() self.emit_newline() @@ -171,7 +162,7 @@ def write_pep0(self, peps: list[PEP]): # PEP types key self.emit_title("PEP Types Key", "type-key") for type_ in sorted(TYPE_VALUES): - self.output(f" {type_[0]} - {type_} PEP") + self.emit_text(f" {type_[0]} - {type_} PEP") self.emit_newline() self.emit_newline() @@ -186,7 +177,7 @@ def write_pep0(self, peps: list[PEP]): msg = " A - Accepted (Standards Track only) or Active proposal" else: msg = f" {status[0]} - {status} proposal" - self.output(msg) + self.emit_text(msg) self.emit_newline() self.emit_newline() @@ -196,21 +187,21 @@ def write_pep0(self, peps: list[PEP]): max_name_len = max(len(author) for author in authors_dict.keys()) self.emit_title("Authors/Owners", "authors") self.emit_author_table_separator(max_name_len) - self.output(f"{'Name':{max_name_len}} Email Address") + self.emit_text(f"{'Name':{max_name_len}} Email Address") self.emit_author_table_separator(max_name_len) for author in _sort_authors(authors_dict): # Use the email from authors_dict instead of the one from "author" as # the author instance may have an empty email. - self.output(f"{author.last_first:{max_name_len}} {authors_dict[author]}") + self.emit_text(f"{author.last_first:{max_name_len}} {authors_dict[author]}") self.emit_author_table_separator(max_name_len) self.emit_newline() self.emit_newline() # References for introduction footnotes self.emit_title("References", "references") - self.output(references) + self.emit_text(references) - pep0_string = "\n".join([str(s) for s in self._output]) + pep0_string = "\n".join([str(s) for s in self.output]) return pep0_string From 32454c84848d774cba696d598d568c1c34f584c7 Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 16:30:38 +0100 Subject: [PATCH 28/37] Use constants in writer.py --- .../pep_zero_generator/constants.py | 3 ++ .../pep_zero_generator/writer.py | 37 ++++++++++++------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/pep_sphinx_extensions/pep_zero_generator/constants.py b/pep_sphinx_extensions/pep_zero_generator/constants.py index a23dd60ede1..8e72685227c 100644 --- a/pep_sphinx_extensions/pep_zero_generator/constants.py +++ b/pep_sphinx_extensions/pep_zero_generator/constants.py @@ -9,6 +9,7 @@ STATUS_ACTIVE = "Active" STATUS_DRAFT = "Draft" STATUS_SUPERSEDED = "Superseded" +STATUS_INCOMPLETE = "Incomplete" # Valid values for the Status header. STATUS_VALUES = { @@ -21,6 +22,8 @@ } # Draft PEPs have no status displayed, Active shares a key with Accepted HIDE_STATUS = {STATUS_DRAFT, STATUS_ACTIVE} +# Dead PEP statuses +DEAD_STATUSES = {STATUS_REJECTED, STATUS_WITHDRAWN, STATUS_INCOMPLETE, STATUS_SUPERSEDED} TYPE_STANDARDS = "Standards Track" TYPE_INFO = "Informational" diff --git a/pep_sphinx_extensions/pep_zero_generator/writer.py b/pep_sphinx_extensions/pep_zero_generator/writer.py index 99fdd1fa361..d90a32039b8 100644 --- a/pep_sphinx_extensions/pep_zero_generator/writer.py +++ b/pep_sphinx_extensions/pep_zero_generator/writer.py @@ -10,6 +10,17 @@ from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_VALUES from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_VALUES from pep_sphinx_extensions.pep_zero_generator.constants import HIDE_STATUS +from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_ACCEPTED +from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_PROVISIONAL +from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_REJECTED +from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_WITHDRAWN +from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_DEFERRED +from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_FINAL +from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_ACTIVE +from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_DRAFT +from pep_sphinx_extensions.pep_zero_generator.constants import DEAD_STATUSES +from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_INFO +from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_PROCESS from pep_sphinx_extensions.pep_zero_generator.errors import PEPError if TYPE_CHECKING: @@ -173,7 +184,7 @@ def write_pep0(self, peps: list[PEP]): # Draft PEPs have no status displayed, Active shares a key with Accepted if status in HIDE_STATUS: continue - if status == "Accepted": + if status == STATUS_ACCEPTED: msg = " A - Accepted (Standards Track only) or Active proposal" else: msg = f" {status[0]} - {status} proposal" @@ -212,43 +223,43 @@ def _classify_peps(peps: list[PEP]) -> tuple[list[PEP], ...]: # The order of the comprehensions below is important. Key status values # take precedence over type value, and vice-versa. - open_ = sorted(pep for pep in remaining if pep.status == "Draft") + open_ = sorted(pep for pep in remaining if pep.status == STATUS_DRAFT) remaining -= {pep for pep in open_} - deferred = sorted(pep for pep in remaining if pep.status == "Deferred") + deferred = sorted(pep for pep in remaining if pep.status == STATUS_DEFERRED) remaining -= {pep for pep in deferred} - meta = sorted(pep for pep in remaining if pep.pep_type == "Process" and pep.status == "Active") + meta = sorted(pep for pep in remaining if pep.pep_type == TYPE_PROCESS and pep.status == STATUS_ACTIVE) remaining -= {pep for pep in meta} - dead = sorted(pep for pep in remaining if pep.pep_type == "Process" and pep.status in {"Withdrawn", "Rejected"}) + dead = sorted(pep for pep in remaining if pep.pep_type == TYPE_PROCESS and pep.status in {STATUS_WITHDRAWN, STATUS_REJECTED}) remaining -= {pep for pep in dead} - historical = sorted(pep for pep in remaining if pep.pep_type == "Process") + historical = sorted(pep for pep in remaining if pep.pep_type == TYPE_PROCESS) remaining -= {pep for pep in historical} - dead += sorted(pep for pep in remaining if pep.status in {"Rejected", "Withdrawn", "Incomplete", "Superseded"}) + dead += sorted(pep for pep in remaining if pep.status in DEAD_STATUSES) remaining -= {pep for pep in dead} - # Hack until the conflict between the use of "Final" + # Hack until the conflict between the use of `Final` # for both API definition PEPs and other (actually # obsolete) PEPs is addressed info = sorted( pep for pep in remaining - if pep.pep_type == "Informational" and (pep.status == "Active" or "Release Schedule" not in pep.title) + if pep.pep_type == TYPE_INFO and (pep.status == STATUS_ACTIVE or "Release Schedule" not in pep.title) ) remaining -= {pep for pep in info} - historical += sorted(pep for pep in remaining if pep.pep_type == "Informational") + historical += sorted(pep for pep in remaining if pep.pep_type == TYPE_INFO) remaining -= {pep for pep in historical} - provisional = sorted(pep for pep in remaining if pep.status == "Provisional") + provisional = sorted(pep for pep in remaining if pep.status == STATUS_PROVISIONAL) remaining -= {pep for pep in provisional} - accepted = sorted(pep for pep in remaining if pep.status in {"Accepted", "Active"}) + accepted = sorted(pep for pep in remaining if pep.status in {STATUS_ACCEPTED, STATUS_ACTIVE}) remaining -= {pep for pep in accepted} - finished = sorted(pep for pep in remaining if pep.status == "Final") + finished = sorted(pep for pep in remaining if pep.status == STATUS_FINAL) remaining -= {pep for pep in finished} for pep in remaining: From e42938a52db5b464c4c8c8f39f02bd7b4bac123d Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 16:33:01 +0100 Subject: [PATCH 29/37] Sort imports --- pep_sphinx_extensions/__init__.py | 2 +- .../pep_processor/parsing/pep_parser.py | 4 ++-- .../pep_processor/transforms/pep_footer.py | 2 +- .../pep_processor/transforms/pep_headers.py | 4 ++-- .../pep_zero_generator/parser.py | 10 +++++----- .../pep_zero_generator/pep_index_generator.py | 2 +- .../pep_zero_generator/writer.py | 18 +++++++++--------- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/pep_sphinx_extensions/__init__.py b/pep_sphinx_extensions/__init__.py index 82f13b48410..326521eabea 100644 --- a/pep_sphinx_extensions/__init__.py +++ b/pep_sphinx_extensions/__init__.py @@ -4,8 +4,8 @@ from typing import TYPE_CHECKING -from sphinx.environment import default_settings from docutils.writers.html5_polyglot import HTMLTranslator +from sphinx.environment import default_settings from pep_sphinx_extensions.pep_processor.html import pep_html_translator from pep_sphinx_extensions.pep_processor.parsing import pep_parser diff --git a/pep_sphinx_extensions/pep_processor/parsing/pep_parser.py b/pep_sphinx_extensions/pep_processor/parsing/pep_parser.py index 550d2ce719c..2ccbd6cb857 100644 --- a/pep_sphinx_extensions/pep_processor/parsing/pep_parser.py +++ b/pep_sphinx_extensions/pep_processor/parsing/pep_parser.py @@ -4,10 +4,10 @@ from sphinx import parsers -from pep_sphinx_extensions.pep_processor.transforms import pep_headers -from pep_sphinx_extensions.pep_processor.transforms import pep_title from pep_sphinx_extensions.pep_processor.transforms import pep_contents from pep_sphinx_extensions.pep_processor.transforms import pep_footer +from pep_sphinx_extensions.pep_processor.transforms import pep_headers +from pep_sphinx_extensions.pep_processor.transforms import pep_title if TYPE_CHECKING: from docutils import transforms diff --git a/pep_sphinx_extensions/pep_processor/transforms/pep_footer.py b/pep_sphinx_extensions/pep_processor/transforms/pep_footer.py index d959a6bcf44..9f25df2ae5a 100644 --- a/pep_sphinx_extensions/pep_processor/transforms/pep_footer.py +++ b/pep_sphinx_extensions/pep_processor/transforms/pep_footer.py @@ -1,6 +1,6 @@ import datetime -import subprocess from pathlib import Path +import subprocess from docutils import nodes from docutils import transforms diff --git a/pep_sphinx_extensions/pep_processor/transforms/pep_headers.py b/pep_sphinx_extensions/pep_processor/transforms/pep_headers.py index 259966c4afa..5e5a4bc18b2 100644 --- a/pep_sphinx_extensions/pep_processor/transforms/pep_headers.py +++ b/pep_sphinx_extensions/pep_processor/transforms/pep_headers.py @@ -1,13 +1,13 @@ -import re from pathlib import Path +import re from docutils import nodes from docutils import transforms from docutils.transforms import peps from sphinx import errors -from pep_sphinx_extensions.pep_processor.transforms import pep_zero from pep_sphinx_extensions.config import pep_url +from pep_sphinx_extensions.pep_processor.transforms import pep_zero class PEPParsingError(errors.SphinxError): diff --git a/pep_sphinx_extensions/pep_zero_generator/parser.py b/pep_sphinx_extensions/pep_zero_generator/parser.py index 464be1bf610..688b0932665 100644 --- a/pep_sphinx_extensions/pep_zero_generator/parser.py +++ b/pep_sphinx_extensions/pep_zero_generator/parser.py @@ -8,14 +8,14 @@ import textwrap from pep_sphinx_extensions.pep_zero_generator.author import Author -from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_PROVISIONAL +from pep_sphinx_extensions.pep_zero_generator.constants import ACTIVE_ALLOWED +from pep_sphinx_extensions.pep_zero_generator.constants import HIDE_STATUS +from pep_sphinx_extensions.pep_zero_generator.constants import SPECIAL_STATUSES from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_ACTIVE +from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_PROVISIONAL +from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_VALUES from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_STANDARDS from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_VALUES -from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_VALUES -from pep_sphinx_extensions.pep_zero_generator.constants import SPECIAL_STATUSES -from pep_sphinx_extensions.pep_zero_generator.constants import ACTIVE_ALLOWED -from pep_sphinx_extensions.pep_zero_generator.constants import HIDE_STATUS from pep_sphinx_extensions.pep_zero_generator.errors import PEPError diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py b/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py index 9fd46a06e93..c653b19add8 100644 --- a/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py +++ b/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py @@ -18,8 +18,8 @@ from __future__ import annotations import csv -import re from pathlib import Path +import re from typing import TYPE_CHECKING from pep_sphinx_extensions.pep_zero_generator import parser diff --git a/pep_sphinx_extensions/pep_zero_generator/writer.py b/pep_sphinx_extensions/pep_zero_generator/writer.py index d90a32039b8..0c3a821c8d5 100644 --- a/pep_sphinx_extensions/pep_zero_generator/writer.py +++ b/pep_sphinx_extensions/pep_zero_generator/writer.py @@ -7,25 +7,25 @@ from typing import TYPE_CHECKING import unicodedata -from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_VALUES -from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_VALUES +from pep_sphinx_extensions.pep_zero_generator.constants import DEAD_STATUSES from pep_sphinx_extensions.pep_zero_generator.constants import HIDE_STATUS from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_ACCEPTED +from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_ACTIVE +from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_DEFERRED +from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_DRAFT +from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_FINAL from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_PROVISIONAL from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_REJECTED +from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_VALUES from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_WITHDRAWN -from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_DEFERRED -from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_FINAL -from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_ACTIVE -from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_DRAFT -from pep_sphinx_extensions.pep_zero_generator.constants import DEAD_STATUSES from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_INFO from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_PROCESS +from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_VALUES from pep_sphinx_extensions.pep_zero_generator.errors import PEPError if TYPE_CHECKING: - from pep_sphinx_extensions.pep_zero_generator.parser import PEP - from pep_sphinx_extensions.pep_zero_generator.author import Author + from pep_sphinx_extensions.pep_zero_generator.author import Author + from pep_sphinx_extensions.pep_zero_generator.parser import PEP title_length = 55 author_length = 40 From d4447abcdc7f16625effa0b7aada368862f14bc8 Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 16:36:29 +0100 Subject: [PATCH 30/37] Sort constants --- .../pep_zero_generator/constants.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/pep_sphinx_extensions/pep_zero_generator/constants.py b/pep_sphinx_extensions/pep_zero_generator/constants.py index 8e72685227c..5b3ea5f6f91 100644 --- a/pep_sphinx_extensions/pep_zero_generator/constants.py +++ b/pep_sphinx_extensions/pep_zero_generator/constants.py @@ -1,15 +1,14 @@ """Holds type and status constants for PEP 0 generation.""" STATUS_ACCEPTED = "Accepted" -STATUS_PROVISIONAL = "Provisional" -STATUS_REJECTED = "Rejected" -STATUS_WITHDRAWN = "Withdrawn" -STATUS_DEFERRED = "Deferred" -STATUS_FINAL = "Final" STATUS_ACTIVE = "Active" +STATUS_DEFERRED = "Deferred" STATUS_DRAFT = "Draft" +STATUS_FINAL = "Final" +STATUS_PROVISIONAL = "Provisional" +STATUS_REJECTED = "Rejected" STATUS_SUPERSEDED = "Superseded" -STATUS_INCOMPLETE = "Incomplete" +STATUS_WITHDRAWN = "Withdrawn" # Valid values for the Status header. STATUS_VALUES = { @@ -23,11 +22,11 @@ # Draft PEPs have no status displayed, Active shares a key with Accepted HIDE_STATUS = {STATUS_DRAFT, STATUS_ACTIVE} # Dead PEP statuses -DEAD_STATUSES = {STATUS_REJECTED, STATUS_WITHDRAWN, STATUS_INCOMPLETE, STATUS_SUPERSEDED} +DEAD_STATUSES = {STATUS_REJECTED, STATUS_WITHDRAWN, STATUS_SUPERSEDED} -TYPE_STANDARDS = "Standards Track" TYPE_INFO = "Informational" TYPE_PROCESS = "Process" +TYPE_STANDARDS = "Standards Track" # Valid values for the Type header. TYPE_VALUES = {TYPE_STANDARDS, TYPE_INFO, TYPE_PROCESS} From 5ebcb9dbc547ca9024e345c8f7e278b22ab96dee Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 16:44:44 +0100 Subject: [PATCH 31/37] Fix sorting in historical and dead PEPs --- pep_sphinx_extensions/pep_zero_generator/writer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pep_sphinx_extensions/pep_zero_generator/writer.py b/pep_sphinx_extensions/pep_zero_generator/writer.py index 0c3a821c8d5..98ab60e8723 100644 --- a/pep_sphinx_extensions/pep_zero_generator/writer.py +++ b/pep_sphinx_extensions/pep_zero_generator/writer.py @@ -232,13 +232,13 @@ def _classify_peps(peps: list[PEP]) -> tuple[list[PEP], ...]: meta = sorted(pep for pep in remaining if pep.pep_type == TYPE_PROCESS and pep.status == STATUS_ACTIVE) remaining -= {pep for pep in meta} - dead = sorted(pep for pep in remaining if pep.pep_type == TYPE_PROCESS and pep.status in {STATUS_WITHDRAWN, STATUS_REJECTED}) + dead = [pep for pep in remaining if pep.pep_type == TYPE_PROCESS and pep.status in {STATUS_WITHDRAWN, STATUS_REJECTED}] remaining -= {pep for pep in dead} - historical = sorted(pep for pep in remaining if pep.pep_type == TYPE_PROCESS) + historical = [pep for pep in remaining if pep.pep_type == TYPE_PROCESS] remaining -= {pep for pep in historical} - dead += sorted(pep for pep in remaining if pep.status in DEAD_STATUSES) + dead = sorted(dead + [pep for pep in remaining if pep.status in DEAD_STATUSES]) remaining -= {pep for pep in dead} # Hack until the conflict between the use of `Final` @@ -250,7 +250,7 @@ def _classify_peps(peps: list[PEP]) -> tuple[list[PEP], ...]: ) remaining -= {pep for pep in info} - historical += sorted(pep for pep in remaining if pep.pep_type == TYPE_INFO) + historical = sorted(historical + [pep for pep in remaining if pep.pep_type == TYPE_INFO]) remaining -= {pep for pep in historical} provisional = sorted(pep for pep in remaining if pep.status == STATUS_PROVISIONAL) From a4a4f50513ddbfeb40362d14b4b9dc63a47d9a8f Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 16:53:19 +0100 Subject: [PATCH 32/37] Extract static methods to module level --- .../pep_zero_generator/author.py | 102 +++++++++--------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/pep_sphinx_extensions/pep_zero_generator/author.py b/pep_sphinx_extensions/pep_zero_generator/author.py index 5969d028e98..8a3bb31c3e2 100644 --- a/pep_sphinx_extensions/pep_zero_generator/author.py +++ b/pep_sphinx_extensions/pep_zero_generator/author.py @@ -5,7 +5,7 @@ class Name(NamedTuple): - name: str = None # mononym + mononym: str = None forename: str = None surname: str = None suffix: str = None @@ -37,9 +37,9 @@ def __init__(self, author_email_tuple: tuple[str, str], authors_overrides: dict[ self.last_first = name_dict["Surname First"] self.nick = name_dict["Name Reference"] else: - name_parts = self._parse_name(self._first_last) - if name_parts.name is not None: - self.last_first = self.nick = name_parts.name + name_parts = _parse_name(self._first_last) + if name_parts.mononym is not None: + self.last_first = self.nick = name_parts.mononym else: if name_parts.surname[1] == ".": # Add an escape to avoid docutils turning `v.` into `22.`. @@ -61,50 +61,50 @@ def __eq__(self, other): def __len__(self): return len(unicodedata.normalize("NFC", self.last_first)) - @staticmethod - def _parse_name(full_name: str) -> Name: - """Decompose a full name into parts. - - If a mononym (e.g, 'Aahz') then return the full name. If there are - suffixes in the name (e.g. ', Jr.' or 'III'), then find and extract - them. If there is a middle initial followed by a full stop, then - combine the following words into a surname (e.g. N. Vander Weele). If - there is a leading, lowercase portion to the last name (e.g. 'van' or - 'von') then include it in the surname. - - """ - possible_suffixes = {"Jr", "Jr.", "II", "III"} - - pre_suffix, _, raw_suffix = full_name.partition(",") - name_parts = pre_suffix.strip().split(" ") - num_parts = len(name_parts) - suffix = raw_suffix.strip() or None - - if num_parts == 0: - raise ValueError("Name is empty!") - elif num_parts == 1: - return Name(name=name_parts[0], suffix=suffix) - elif num_parts == 2: - return Name(forename=name_parts[0].strip(), surname=name_parts[1], suffix=suffix) - - # handles rogue uncaught suffixes - if name_parts[-1] in possible_suffixes: - suffix = f"{name_parts.pop(-1)} {suffix}".strip() - - # handles von, van, v. etc. - if name_parts[-2].islower(): - forename = " ".join(name_parts[:-2]).strip() - surname = " ".join(name_parts[-2:]) - return Name(forename=forename, surname=surname, suffix=suffix) - - # handles double surnames after a middle initial (e.g. N. Vander Weele) - elif any(s.endswith(".") for s in name_parts): - split_position = [i for i, x in enumerate(name_parts) if x.endswith(".")][-1] + 1 - forename = " ".join(name_parts[:split_position]).strip() - surname = " ".join(name_parts[split_position:]) - return Name(forename=forename, surname=surname, suffix=suffix) - - # default to using the last item as the surname - else: - forename = " ".join(name_parts[:-1]).strip() - return Name(forename=forename, surname=name_parts[-1], suffix=suffix) + +def _parse_name(full_name: str) -> Name: + """Decompose a full name into parts. + + If a mononym (e.g, 'Aahz') then return the full name. If there are + suffixes in the name (e.g. ', Jr.' or 'II'), then find and extract + them. If there is a middle initial followed by a full stop, then + combine the following words into a surname (e.g. N. Vander Weele). If + there is a leading, lowercase portion to the last name (e.g. 'van' or + 'von') then include it in the surname. + + """ + possible_suffixes = {"Jr", "Jr.", "II", "III"} + + pre_suffix, _, raw_suffix = full_name.partition(",") + name_parts = pre_suffix.strip().split(" ") + num_parts = len(name_parts) + suffix = raw_suffix.strip() or None + + if num_parts == 0: + raise ValueError("Name is empty!") + elif num_parts == 1: + return Name(mononym=name_parts[0], suffix=suffix) + elif num_parts == 2: + return Name(forename=name_parts[0].strip(), surname=name_parts[1], suffix=suffix) + + # handles rogue uncaught suffixes + if name_parts[-1] in possible_suffixes: + suffix = f"{name_parts.pop(-1)} {suffix}".strip() + + # handles von, van, v. etc. + if name_parts[-2].islower(): + forename = " ".join(name_parts[:-2]).strip() + surname = " ".join(name_parts[-2:]) + return Name(forename=forename, surname=surname, suffix=suffix) + + # handles double surnames after a middle initial (e.g. N. Vander Weele) + elif any(s.endswith(".") for s in name_parts): + split_position = [i for i, x in enumerate(name_parts) if x.endswith(".")][-1] + 1 + forename = " ".join(name_parts[:split_position]).strip() + surname = " ".join(name_parts[split_position:]) + return Name(forename=forename, surname=surname, suffix=suffix) + + # default to using the last item as the surname + else: + forename = " ".join(name_parts[:-1]).strip() + return Name(forename=forename, surname=name_parts[-1], suffix=suffix) From 1ec8438f27ea43f504bf9bf8de2ba22e1f106ee0 Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 17:07:46 +0100 Subject: [PATCH 33/37] Extract static methods to module level (parser.py --- .../pep_zero_generator/parser.py | 125 +++++++++--------- .../pep_zero_generator/writer.py | 4 +- 2 files changed, 67 insertions(+), 62 deletions(-) diff --git a/pep_sphinx_extensions/pep_zero_generator/parser.py b/pep_sphinx_extensions/pep_zero_generator/parser.py index 688b0932665..977bc3d96b1 100644 --- a/pep_sphinx_extensions/pep_zero_generator/parser.py +++ b/pep_sphinx_extensions/pep_zero_generator/parser.py @@ -34,10 +34,6 @@ class PEP: # The required RFC 822 headers for all PEPs. required_headers = {"PEP", "Title", "Author", "Status", "Type", "Created"} - def raise_pep_error(self, msg: str, pep_num: bool = False) -> None: - pep_number = self.number if pep_num else None - raise PEPError(msg, self.filename, pep_number=pep_number) - def __init__(self, filename: Path, authors_overrides: dict): """Init object from an open PEP file object. @@ -51,16 +47,16 @@ def __init__(self, filename: Path, authors_overrides: dict): metadata = HeaderParser().parsestr(pep_text) required_header_misses = PEP.required_headers - set(metadata.keys()) if required_header_misses: - self.raise_pep_error(f"PEP is missing required headers {required_header_misses}") + _raise_pep_error(self, f"PEP is missing required headers {required_header_misses}") try: self.number = int(metadata["PEP"]) except ValueError: - self.raise_pep_error("PEP number isn't an integer") + _raise_pep_error(self, "PEP number isn't an integer") # Check PEP number matches filename if self.number != int(filename.stem[4:]): - self.raise_pep_error(f"PEP number does not match file name ({filename})", pep_num=True) + _raise_pep_error(self, f"PEP number does not match file name ({filename})", pep_num=True) # Title self.title: str = metadata["Title"] @@ -68,28 +64,28 @@ def __init__(self, filename: Path, authors_overrides: dict): # Type self.pep_type: str = metadata["Type"] if self.pep_type not in TYPE_VALUES: - self.raise_pep_error(f"{self.pep_type} is not a valid Type value", pep_num=True) + _raise_pep_error(self, f"{self.pep_type} is not a valid Type value", pep_num=True) # Status status = metadata["Status"] if status in SPECIAL_STATUSES: status = SPECIAL_STATUSES[status] if status not in STATUS_VALUES: - self.raise_pep_error(f"{status} is not a valid Status value", pep_num=True) + _raise_pep_error(self, f"{status} is not a valid Status value", pep_num=True) # Special case for Active PEPs. if status == STATUS_ACTIVE and self.pep_type not in ACTIVE_ALLOWED: msg = "Only Process and Informational PEPs may have an Active status" - self.raise_pep_error(msg, pep_num=True) + _raise_pep_error(self, msg, pep_num=True) # Special case for Provisional PEPs. if status == STATUS_PROVISIONAL and self.pep_type != TYPE_STANDARDS: msg = "Only Standards Track PEPs may have a Provisional status" - self.raise_pep_error(msg, pep_num=True) + _raise_pep_error(self, msg, pep_num=True) self.status: str = status # Parse PEP authors - self.authors: list[Author] = self.parse_authors(metadata["Author"], authors_overrides) + self.authors: list[Author] = _parse_authors(self, metadata["Author"], authors_overrides) def __repr__(self) -> str: return f"4} - {self.title}>" @@ -97,60 +93,69 @@ def __repr__(self) -> str: def __lt__(self, other: PEP) -> bool: return self.number < other.number - def parse_authors(self, author_header: str, authors_overrides: dict) -> list[Author]: - """Parse Author header line""" - authors_and_emails = self._parse_author(author_header) - if not authors_and_emails: - raise self.raise_pep_error("no authors found", pep_num=True) - return [Author(author_tuple, authors_overrides) for author_tuple in authors_and_emails] - - angled = re.compile(r"(?P.+?) <(?P.+?)>(,\s*)?") - paren = re.compile(r"(?P.+?) \((?P.+?)\)(,\s*)?") - simple = re.compile(r"(?P[^,]+)(,\s*)?") - - @staticmethod - def _parse_author(data: str) -> list[tuple[str, str]]: - """Return a list of author names and emails.""" - - author_list = [] - for regex in (PEP.angled, PEP.paren, PEP.simple): - for match in regex.finditer(data): - # Watch out for suffixes like 'Jr.' when they are comma-separated - # from the name and thus cause issues when *all* names are only - # separated by commas. - match_dict = match.groupdict() - author = match_dict["author"] - if not author.partition(" ")[1] and author.endswith("."): - prev_author = author_list.pop() - author = ", ".join([prev_author, author]) - if "email" not in match_dict: - email = "" - else: - email = match_dict["email"] - author_list.append((author, email)) - - # If authors were found then stop searching as only expect one - # style of author citation. - if author_list: - break - return author_list - - def title_abbr(self, title_length) -> str: - """Shorten the title to be no longer than the max title length.""" - if len(self.title) <= title_length: - return self.title - wrapped_title, *_excess = textwrap.wrap(self.title, title_length - 4) - return f"{wrapped_title} ..." - - def pep(self, *, title_length) -> dict[str, str | int]: + def details(self, *, title_length) -> dict[str, str | int]: """Return the line entry for the PEP.""" return { # how the type is to be represented in the index "type": self.pep_type[0].upper(), "number": self.number, - "title": self.title_abbr(title_length), + "title": _title_abbr(self.title, title_length), # how the status should be represented in the index - "status": self.status[0].upper() if self.status not in HIDE_STATUS else " ", + "status": " " if self.status in HIDE_STATUS else self.status[0].upper(), # the author list as a comma-separated with only last names "authors": ", ".join(x.nick for x in self.authors), } + + +def _raise_pep_error(pep: PEP, msg: str, pep_num: bool = False) -> None: + if pep_num: + raise PEPError(msg, pep.filename, pep_number=pep.number) + raise PEPError(msg, pep.filename) + + +def _parse_authors(pep: PEP, author_header: str, authors_overrides: dict) -> list[Author]: + """Parse Author header line""" + authors_and_emails = _parse_author(author_header) + if not authors_and_emails: + raise _raise_pep_error(pep, "no authors found", pep_num=True) + return [Author(author_tuple, authors_overrides) for author_tuple in authors_and_emails] + + +author_angled = re.compile(r"(?P.+?) <(?P.+?)>(,\s*)?") +author_paren = re.compile(r"(?P.+?) \((?P.+?)\)(,\s*)?") +author_simple = re.compile(r"(?P[^,]+)(,\s*)?") + + +def _parse_author(data: str) -> list[tuple[str, str]]: + """Return a list of author names and emails.""" + + author_list = [] + for regex in (author_angled, author_paren, author_simple): + for match in regex.finditer(data): + # Watch out for suffixes like 'Jr.' when they are comma-separated + # from the name and thus cause issues when *all* names are only + # separated by commas. + match_dict = match.groupdict() + author = match_dict["author"] + if not author.partition(" ")[1] and author.endswith("."): + prev_author = author_list.pop() + author = ", ".join([prev_author, author]) + if "email" not in match_dict: + email = "" + else: + email = match_dict["email"] + author_list.append((author, email)) + + # If authors were found then stop searching as only expect one + # style of author citation. + if author_list: + break + return author_list + + +def _title_abbr(title, title_length) -> str: + """Shorten the title to be no longer than the max title length.""" + if len(title) <= title_length: + return title + wrapped_title, *_excess = textwrap.wrap(title, title_length - 4) + return f"{wrapped_title} ..." diff --git a/pep_sphinx_extensions/pep_zero_generator/writer.py b/pep_sphinx_extensions/pep_zero_generator/writer.py index 98ab60e8723..e07ee48e05f 100644 --- a/pep_sphinx_extensions/pep_zero_generator/writer.py +++ b/pep_sphinx_extensions/pep_zero_generator/writer.py @@ -114,7 +114,7 @@ def emit_pep_category(self, category: str, anchor: str, peps: list[PEP]) -> None self.emit_subtitle(category, anchor) self.emit_column_headers() for pep in peps: - self.output.append(column_format(**pep.pep(title_length=title_length))) + self.output.append(column_format(**pep.details(title_length=title_length))) self.emit_table_separator() self.emit_newline() @@ -155,7 +155,7 @@ def write_pep0(self, peps: list[PEP]): for pep in peps: if pep.number - prev_pep > 1: self.emit_newline() - self.emit_pep_row(pep.pep(title_length=title_length)) + self.emit_pep_row(pep.details(title_length=title_length)) prev_pep = pep.number self.emit_table_separator() From de9ab25dd24565fc0315eeb795b6c2f1e511bf53 Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 17:39:29 +0100 Subject: [PATCH 34/37] Make Author a NamedTuple --- .../pep_zero_generator/author.py | 99 ++++++++----------- .../pep_zero_generator/parser.py | 8 +- .../pep_zero_generator/pep_index_generator.py | 3 +- .../pep_zero_generator/writer.py | 2 +- 4 files changed, 49 insertions(+), 63 deletions(-) diff --git a/pep_sphinx_extensions/pep_zero_generator/author.py b/pep_sphinx_extensions/pep_zero_generator/author.py index 8a3bb31c3e2..032a60b40e9 100644 --- a/pep_sphinx_extensions/pep_zero_generator/author.py +++ b/pep_sphinx_extensions/pep_zero_generator/author.py @@ -1,68 +1,51 @@ from __future__ import annotations from typing import NamedTuple -import unicodedata -class Name(NamedTuple): +class _Name(NamedTuple): mononym: str = None forename: str = None surname: str = None suffix: str = None -class Author: - """Represent PEP authors. +class Author(NamedTuple): + """Represent PEP authors.""" + last_first: str # The author's name in Surname, Forename, Suffix order. + nick: str # Author's nickname for PEP tables. Defaults to surname. + email: str # The author's email address. - Attributes: - last_first: The author's name in Surname, Forename, Suffix order. - nick: Author's nickname for PEP tables. Defaults to surname. - email: The author's email address. - _first_last: The author's full name, unchanged - """ - __slots__ = "last_first", "nick", "email", "_first_last" - - def __init__(self, author_email_tuple: tuple[str, str], authors_overrides: dict[str, dict[str, str]]): - """Parse the name and email address of an author.""" - name, email = author_email_tuple - self._first_last: str = name.strip() - self.email: str = email.lower() - - self.last_first: str = "" - self.nick: str = "" - - if self._first_last in authors_overrides: - name_dict = authors_overrides[self._first_last] - self.last_first = name_dict["Surname First"] - self.nick = name_dict["Name Reference"] - else: - name_parts = _parse_name(self._first_last) - if name_parts.mononym is not None: - self.last_first = self.nick = name_parts.mononym - else: - if name_parts.surname[1] == ".": - # Add an escape to avoid docutils turning `v.` into `22.`. - name_parts.surname = f"\\{name_parts.surname}" - self.last_first = f"{name_parts.surname}, {name_parts.forename}" - self.nick = name_parts.surname - - if name_parts.suffix is not None: - self.last_first += f", {name_parts.suffix}" - - def __hash__(self): - return hash(self.last_first) - - def __eq__(self, other): - if not isinstance(other, Author): - return NotImplemented - return self.last_first == other.last_first - - def __len__(self): - return len(unicodedata.normalize("NFC", self.last_first)) - - -def _parse_name(full_name: str) -> Name: +def parse_author_email(author_email_tuple: tuple[str, str], authors_overrides: dict[str, dict[str, str]]) -> Author: + """Parse the name and email address of an author.""" + name, email = author_email_tuple + _first_last = name.strip() + email = email.lower() + + if _first_last in authors_overrides: + name_dict = authors_overrides[_first_last] + last_first = name_dict["Surname First"] + nick = name_dict["Name Reference"] + return Author(last_first, nick, email) + + name_parts = _parse_name(_first_last) + if name_parts.mononym is not None: + return Author(name_parts.mononym, name_parts.mononym, email) + + if name_parts.surname[1] == ".": + # Add an escape to avoid docutils turning `v.` into `22.`. + name_parts.surname = f"\\{name_parts.surname}" + + if name_parts.suffix is not None: + last_first = f"{name_parts.surname}, {name_parts.forename}, {name_parts.suffix}" + return Author(last_first, name_parts.surname, email) + + last_first = f"{name_parts.surname}, {name_parts.forename}" + return Author(last_first, name_parts.surname, email) + + +def _parse_name(full_name: str) -> _Name: """Decompose a full name into parts. If a mononym (e.g, 'Aahz') then return the full name. If there are @@ -78,14 +61,14 @@ def _parse_name(full_name: str) -> Name: pre_suffix, _, raw_suffix = full_name.partition(",") name_parts = pre_suffix.strip().split(" ") num_parts = len(name_parts) - suffix = raw_suffix.strip() or None + suffix = raw_suffix.strip() if num_parts == 0: raise ValueError("Name is empty!") elif num_parts == 1: - return Name(mononym=name_parts[0], suffix=suffix) + return _Name(mononym=name_parts[0], suffix=suffix) elif num_parts == 2: - return Name(forename=name_parts[0].strip(), surname=name_parts[1], suffix=suffix) + return _Name(forename=name_parts[0].strip(), surname=name_parts[1], suffix=suffix) # handles rogue uncaught suffixes if name_parts[-1] in possible_suffixes: @@ -95,16 +78,16 @@ def _parse_name(full_name: str) -> Name: if name_parts[-2].islower(): forename = " ".join(name_parts[:-2]).strip() surname = " ".join(name_parts[-2:]) - return Name(forename=forename, surname=surname, suffix=suffix) + return _Name(forename=forename, surname=surname, suffix=suffix) # handles double surnames after a middle initial (e.g. N. Vander Weele) elif any(s.endswith(".") for s in name_parts): split_position = [i for i, x in enumerate(name_parts) if x.endswith(".")][-1] + 1 forename = " ".join(name_parts[:split_position]).strip() surname = " ".join(name_parts[split_position:]) - return Name(forename=forename, surname=surname, suffix=suffix) + return _Name(forename=forename, surname=surname, suffix=suffix) # default to using the last item as the surname else: forename = " ".join(name_parts[:-1]).strip() - return Name(forename=forename, surname=name_parts[-1], suffix=suffix) + return _Name(forename=forename, surname=name_parts[-1], suffix=suffix) diff --git a/pep_sphinx_extensions/pep_zero_generator/parser.py b/pep_sphinx_extensions/pep_zero_generator/parser.py index 977bc3d96b1..6218df6d7d9 100644 --- a/pep_sphinx_extensions/pep_zero_generator/parser.py +++ b/pep_sphinx_extensions/pep_zero_generator/parser.py @@ -6,8 +6,9 @@ from pathlib import Path import re import textwrap +from typing import TYPE_CHECKING -from pep_sphinx_extensions.pep_zero_generator.author import Author +from pep_sphinx_extensions.pep_zero_generator.author import parse_author_email from pep_sphinx_extensions.pep_zero_generator.constants import ACTIVE_ALLOWED from pep_sphinx_extensions.pep_zero_generator.constants import HIDE_STATUS from pep_sphinx_extensions.pep_zero_generator.constants import SPECIAL_STATUSES @@ -18,6 +19,9 @@ from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_VALUES from pep_sphinx_extensions.pep_zero_generator.errors import PEPError +if TYPE_CHECKING: + from pep_sphinx_extensions.pep_zero_generator.author import Author + class PEP: """Representation of PEPs. @@ -118,7 +122,7 @@ def _parse_authors(pep: PEP, author_header: str, authors_overrides: dict) -> lis authors_and_emails = _parse_author(author_header) if not authors_and_emails: raise _raise_pep_error(pep, "no authors found", pep_num=True) - return [Author(author_tuple, authors_overrides) for author_tuple in authors_and_emails] + return [parse_author_email(author_tuple, authors_overrides) for author_tuple in authors_and_emails] author_angled = re.compile(r"(?P.+?) <(?P.+?)>(,\s*)?") diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py b/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py index c653b19add8..898ab2b0b0f 100644 --- a/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py +++ b/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py @@ -56,8 +56,7 @@ def create_pep_zero(_: Sphinx, env: BuildEnvironment, docnames: list[str]) -> No pep = parser.PEP(path.joinpath(file_path).absolute(), authors_overrides) peps.append(pep) - pep_writer = writer.PEPZeroWriter() - pep0_text = pep_writer.write_pep0(sorted(peps)) + pep0_text = writer.PEPZeroWriter().write_pep0(sorted(peps)) Path(f"{pep_zero_filename}.rst").write_text(pep0_text, encoding="utf-8") # Add to files for builder diff --git a/pep_sphinx_extensions/pep_zero_generator/writer.py b/pep_sphinx_extensions/pep_zero_generator/writer.py index e07ee48e05f..cd26921cc00 100644 --- a/pep_sphinx_extensions/pep_zero_generator/writer.py +++ b/pep_sphinx_extensions/pep_zero_generator/writer.py @@ -195,7 +195,7 @@ def write_pep0(self, peps: list[PEP]): # PEP owners authors_dict = _verify_email_addresses(peps) - max_name_len = max(len(author) for author in authors_dict.keys()) + max_name_len = max(len(author.last_first) for author in authors_dict.keys()) self.emit_title("Authors/Owners", "authors") self.emit_author_table_separator(max_name_len) self.emit_text(f"{'Name':{max_name_len}} Email Address") From 4cb6e8c4c573819d32fef9db060b957faf73cc66 Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 9 Jun 2021 18:07:48 +0100 Subject: [PATCH 35/37] Fix author duplication bug with NamedTuples --- .../pep_zero_generator/author.py | 2 +- .../pep_zero_generator/parser.py | 2 +- .../pep_zero_generator/writer.py | 49 +++++++++---------- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/pep_sphinx_extensions/pep_zero_generator/author.py b/pep_sphinx_extensions/pep_zero_generator/author.py index 032a60b40e9..22299b056af 100644 --- a/pep_sphinx_extensions/pep_zero_generator/author.py +++ b/pep_sphinx_extensions/pep_zero_generator/author.py @@ -37,7 +37,7 @@ def parse_author_email(author_email_tuple: tuple[str, str], authors_overrides: d # Add an escape to avoid docutils turning `v.` into `22.`. name_parts.surname = f"\\{name_parts.surname}" - if name_parts.suffix is not None: + if name_parts.suffix: last_first = f"{name_parts.surname}, {name_parts.forename}, {name_parts.suffix}" return Author(last_first, name_parts.surname, email) diff --git a/pep_sphinx_extensions/pep_zero_generator/parser.py b/pep_sphinx_extensions/pep_zero_generator/parser.py index 6218df6d7d9..c4441fda3cd 100644 --- a/pep_sphinx_extensions/pep_zero_generator/parser.py +++ b/pep_sphinx_extensions/pep_zero_generator/parser.py @@ -107,7 +107,7 @@ def details(self, *, title_length) -> dict[str, str | int]: # how the status should be represented in the index "status": " " if self.status in HIDE_STATUS else self.status[0].upper(), # the author list as a comma-separated with only last names - "authors": ", ".join(x.nick for x in self.authors), + "authors": ", ".join(author.nick for author in self.authors), } diff --git a/pep_sphinx_extensions/pep_zero_generator/writer.py b/pep_sphinx_extensions/pep_zero_generator/writer.py index cd26921cc00..508333c504b 100644 --- a/pep_sphinx_extensions/pep_zero_generator/writer.py +++ b/pep_sphinx_extensions/pep_zero_generator/writer.py @@ -24,8 +24,7 @@ from pep_sphinx_extensions.pep_zero_generator.errors import PEPError if TYPE_CHECKING: - from pep_sphinx_extensions.pep_zero_generator.author import Author - from pep_sphinx_extensions.pep_zero_generator.parser import PEP + from pep_sphinx_extensions.pep_zero_generator.parser import PEP title_length = 55 author_length = 40 @@ -195,15 +194,15 @@ def write_pep0(self, peps: list[PEP]): # PEP owners authors_dict = _verify_email_addresses(peps) - max_name_len = max(len(author.last_first) for author in authors_dict.keys()) + max_name_len = max(len(author_name) for author_name in authors_dict) self.emit_title("Authors/Owners", "authors") self.emit_author_table_separator(max_name_len) self.emit_text(f"{'Name':{max_name_len}} Email Address") self.emit_author_table_separator(max_name_len) - for author in _sort_authors(authors_dict): + for author_name in _sort_authors(authors_dict): # Use the email from authors_dict instead of the one from "author" as # the author instance may have an empty email. - self.emit_text(f"{author.last_first:{max_name_len}} {authors_dict[author]}") + self.emit_text(f"{author_name:{max_name_len}} {authors_dict[author_name]}") self.emit_author_table_separator(max_name_len) self.emit_newline() self.emit_newline() @@ -268,27 +267,27 @@ def _classify_peps(peps: list[PEP]) -> tuple[list[PEP], ...]: return meta, info, provisional, accepted, open_, finished, historical, deferred, dead -def _verify_email_addresses(peps: list[PEP]) -> dict[Author, str]: - authors_dict: dict[Author, set[str]] = {} +def _verify_email_addresses(peps: list[PEP]) -> dict[str, str]: + authors_dict: dict[str, set[str]] = {} for pep in peps: for author in pep.authors: # If this is the first time we have come across an author, add them. - if author not in authors_dict: - authors_dict[author] = {author.email} if author.email else set() - else: - # If the new email is an empty string, move on. - if not author.email: - continue - # If the email has not been seen, add it to the list. - authors_dict[author].add(author.email) - - valid_authors_dict = {} - too_many_emails = [] - for author, emails in authors_dict.items(): + if author.last_first not in authors_dict: + authors_dict[author.last_first] = set() + + # If the new email is an empty string, move on. + if not author.email: + continue + # If the email has not been seen, add it to the list. + authors_dict[author.last_first].add(author.email) + + valid_authors_dict: dict[str, str] = {} + too_many_emails: list[tuple[str, set[str]]] = [] + for last_first, emails in authors_dict.items(): if len(emails) > 1: - too_many_emails.append((author.last_first, emails)) + too_many_emails.append((last_first, emails)) else: - valid_authors_dict[author] = next(iter(emails), "") + valid_authors_dict[last_first] = next(iter(emails), "") if too_many_emails: err_output = [] for author, emails in too_many_emails: @@ -301,13 +300,13 @@ def _verify_email_addresses(peps: list[PEP]) -> dict[Author, str]: return valid_authors_dict -def _sort_authors(authors_dict: dict[Author, str]) -> list[Author]: - return sorted(authors_dict.keys(), key=_author_sort_by) +def _sort_authors(authors_dict: dict[str, str]) -> list[str]: + return sorted(authors_dict, key=_author_sort_by) -def _author_sort_by(author: Author) -> str: +def _author_sort_by(author_name: str) -> str: """Skip lower-cased words in surname when sorting.""" - surname, *_ = author.last_first.split(",") + surname, *_ = author_name.split(",") surname_parts = surname.split() for i, part in enumerate(surname_parts): if part[0].isupper(): From 1e62868e7705d9a719500c931f51f86fc094831e Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Fri, 11 Jun 2021 00:12:43 +0100 Subject: [PATCH 36/37] Revert to old PEP classification algorithm --- .../pep_zero_generator/writer.py | 87 +++++++++---------- 1 file changed, 41 insertions(+), 46 deletions(-) diff --git a/pep_sphinx_extensions/pep_zero_generator/writer.py b/pep_sphinx_extensions/pep_zero_generator/writer.py index 508333c504b..61f98f091ca 100644 --- a/pep_sphinx_extensions/pep_zero_generator/writer.py +++ b/pep_sphinx_extensions/pep_zero_generator/writer.py @@ -218,52 +218,47 @@ def write_pep0(self, peps: list[PEP]): def _classify_peps(peps: list[PEP]) -> tuple[list[PEP], ...]: """Sort PEPs into meta, informational, accepted, open, finished, and essentially dead.""" - remaining = set(peps) - - # The order of the comprehensions below is important. Key status values - # take precedence over type value, and vice-versa. - open_ = sorted(pep for pep in remaining if pep.status == STATUS_DRAFT) - remaining -= {pep for pep in open_} - - deferred = sorted(pep for pep in remaining if pep.status == STATUS_DEFERRED) - remaining -= {pep for pep in deferred} - - meta = sorted(pep for pep in remaining if pep.pep_type == TYPE_PROCESS and pep.status == STATUS_ACTIVE) - remaining -= {pep for pep in meta} - - dead = [pep for pep in remaining if pep.pep_type == TYPE_PROCESS and pep.status in {STATUS_WITHDRAWN, STATUS_REJECTED}] - remaining -= {pep for pep in dead} - - historical = [pep for pep in remaining if pep.pep_type == TYPE_PROCESS] - remaining -= {pep for pep in historical} - - dead = sorted(dead + [pep for pep in remaining if pep.status in DEAD_STATUSES]) - remaining -= {pep for pep in dead} - - # Hack until the conflict between the use of `Final` - # for both API definition PEPs and other (actually - # obsolete) PEPs is addressed - info = sorted( - pep for pep in remaining - if pep.pep_type == TYPE_INFO and (pep.status == STATUS_ACTIVE or "Release Schedule" not in pep.title) - ) - remaining -= {pep for pep in info} - - historical = sorted(historical + [pep for pep in remaining if pep.pep_type == TYPE_INFO]) - remaining -= {pep for pep in historical} - - provisional = sorted(pep for pep in remaining if pep.status == STATUS_PROVISIONAL) - remaining -= {pep for pep in provisional} - - accepted = sorted(pep for pep in remaining if pep.status in {STATUS_ACCEPTED, STATUS_ACTIVE}) - remaining -= {pep for pep in accepted} - - finished = sorted(pep for pep in remaining if pep.status == STATUS_FINAL) - remaining -= {pep for pep in finished} - - for pep in remaining: - raise PEPError(f"unsorted ({pep.pep_type}/{pep.status})", pep.filename, pep.number) - + meta = [] + info = [] + provisional = [] + accepted = [] + open_ = [] + finished = [] + historical = [] + deferred = [] + dead = [] + for pep in peps: + # Order of 'if' statement important. Key Status values take precedence + # over Type value, and vice-versa. + if pep.status == STATUS_DRAFT: + open_.append(pep) + elif pep.status == STATUS_DEFERRED: + deferred.append(pep) + elif pep.pep_type == TYPE_PROCESS: + if pep.status == STATUS_ACTIVE: + meta.append(pep) + elif pep.status in {STATUS_WITHDRAWN, STATUS_REJECTED}: + dead.append(pep) + else: + historical.append(pep) + elif pep.status in DEAD_STATUSES: + dead.append(pep) + elif pep.pep_type == TYPE_INFO: + # Hack until the conflict between the use of "Final" + # for both API definition PEPs and other (actually + # obsolete) PEPs is addressed + if pep.status == STATUS_ACTIVE or "Release Schedule" not in pep.title: + info.append(pep) + else: + historical.append(pep) + elif pep.status == STATUS_PROVISIONAL: + provisional.append(pep) + elif pep.status in {STATUS_ACCEPTED, STATUS_ACTIVE}: + accepted.append(pep) + elif pep.status == STATUS_FINAL: + finished.append(pep) + else: + raise PEPError(f"Unsorted ({pep.pep_type}/{pep.status})", pep.filename, pep.number) return meta, info, provisional, accepted, open_, finished, historical, deferred, dead From 48b72c27efed18c8e8fdcfb1d9e0979d1615590f Mon Sep 17 00:00:00 2001 From: AA Turner <9087854+AA-Turner@users.noreply.github.com> Date: Fri, 11 Jun 2021 00:13:48 +0100 Subject: [PATCH 37/37] Define PEP equality --- pep_sphinx_extensions/pep_zero_generator/parser.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pep_sphinx_extensions/pep_zero_generator/parser.py b/pep_sphinx_extensions/pep_zero_generator/parser.py index c4441fda3cd..e5cec9ebba8 100644 --- a/pep_sphinx_extensions/pep_zero_generator/parser.py +++ b/pep_sphinx_extensions/pep_zero_generator/parser.py @@ -97,6 +97,9 @@ def __repr__(self) -> str: def __lt__(self, other: PEP) -> bool: return self.number < other.number + def __eq__(self, other): + return self.number == other.number + def details(self, *, title_length) -> dict[str, str | int]: """Return the line entry for the PEP.""" return {