Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sphinx - PEP 0 generation #1932

Merged
merged 37 commits into from
Jun 12, 2021
Merged
Changes from 1 commit
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
4a52b3c
Add PEP 0 parser
AA-Turner Apr 20, 2021
d3771d4
Add PEP 0 writer
AA-Turner Apr 20, 2021
c8268fb
Add PEP 0 generator and authors override
AA-Turner Apr 20, 2021
85ae140
Add/update build and run
AA-Turner Apr 20, 2021
835adfc
Simplify `create_index_file`
AA-Turner May 7, 2021
530ca9a
Special status handling
AA-Turner Jun 9, 2021
2578fe2
Add constants for PEP related magic strings
AA-Turner Jun 9, 2021
c839d51
Prefer checking on class
AA-Turner Jun 9, 2021
a9b0559
Add PEP.hide_status, use constants
AA-Turner Jun 9, 2021
77c5492
Remove comment from 2008 (current method works fine)
AA-Turner Jun 9, 2021
f6f7b65
Clarify intent of for-else loop
AA-Turner Jun 9, 2021
d0513e2
Hook in to Sphinx (oops, missed when splitting out this PR)
AA-Turner Jun 9, 2021
b8d9eff
Rename AUTHORS.csv for clarity
AA-Turner Jun 9, 2021
4b0d042
Sort and strip spaces
AA-Turner Jun 9, 2021
a993eed
Prefer `authors_overrides` name
AA-Turner Jun 9, 2021
92fe1fb
Add pep_0_errors.py
AA-Turner Jun 9, 2021
3f695ab
Move author_sort_by to writer
AA-Turner Jun 9, 2021
327fd1b
PEP init misc
AA-Turner Jun 9, 2021
403bff3
Split out Author
AA-Turner Jun 9, 2021
0d9bf61
Drop pep_0 prefix
AA-Turner Jun 9, 2021
dedb043
Pass title length as an argument
AA-Turner Jun 9, 2021
84518a3
Add constants.py to hold global type / status values
AA-Turner Jun 9, 2021
5164571
Capitalise constants
AA-Turner Jun 9, 2021
29738c5
Capitalise constants
AA-Turner Jun 9, 2021
918a4b9
Update PEP classification algorithm
AA-Turner Jun 9, 2021
70011e0
Extract static methods to module level
AA-Turner Jun 9, 2021
e72bed1
Add emit_text, emit_pep_row
AA-Turner Jun 9, 2021
32454c8
Use constants in writer.py
AA-Turner Jun 9, 2021
e42938a
Sort imports
AA-Turner Jun 9, 2021
d4447ab
Sort constants
AA-Turner Jun 9, 2021
5ebcb9d
Fix sorting in historical and dead PEPs
AA-Turner Jun 9, 2021
a4a4f50
Extract static methods to module level
AA-Turner Jun 9, 2021
1ec8438
Extract static methods to module level (parser.py
AA-Turner Jun 9, 2021
de9ab25
Make Author a NamedTuple
AA-Turner Jun 9, 2021
4cb6e8c
Fix author duplication bug with NamedTuples
AA-Turner Jun 9, 2021
1e62868
Revert to old PEP classification algorithm
AA-Turner Jun 10, 2021
48b72c2
Define PEP equality
AA-Turner Jun 10, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Extract static methods to module level (parser.py
AA-Turner committed Jun 9, 2021
commit 1ec8438f27ea43f504bf9bf8de2ba22e1f106ee0
125 changes: 65 additions & 60 deletions pep_sphinx_extensions/pep_zero_generator/parser.py
Original file line number Diff line number Diff line change
@@ -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,106 +47,115 @@ 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"]

# 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"<PEP {self.number:0>4} - {self.title}>"

def __lt__(self, other: PEP) -> bool:
pablogsal marked this conversation as resolved.
Show resolved Hide resolved
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<author>.+?) <(?P<email>.+?)>(,\s*)?")
paren = re.compile(r"(?P<email>.+?) \((?P<author>.+?)\)(,\s*)?")
simple = re.compile(r"(?P<author>[^,]+)(,\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<author>.+?) <(?P<email>.+?)>(,\s*)?")
author_paren = re.compile(r"(?P<email>.+?) \((?P<author>.+?)\)(,\s*)?")
author_simple = re.compile(r"(?P<author>[^,]+)(,\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} ..."
4 changes: 2 additions & 2 deletions pep_sphinx_extensions/pep_zero_generator/writer.py
Original file line number Diff line number Diff line change
@@ -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()