mirror of
https://github.com/torproject/torspec.git
synced 2024-11-27 11:50:32 +00:00
201 lines
6.3 KiB
Python
Executable File
201 lines
6.3 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
import sys
|
|
if sys.version_info[0] < 3:
|
|
print("No support for Python 2.")
|
|
sys.exit(1)
|
|
|
|
import codecs, re, os
|
|
class Error(Exception): pass
|
|
|
|
STATUSES = """DRAFT NEEDS-REVISION NEEDS-RESEARCH OPEN ACCEPTED META FINISHED
|
|
CLOSED SUPERSEDED DEAD REJECTED OBSOLETE RESERVE INFORMATIONAL""".split()
|
|
REQUIRED_FIELDS = [ "Filename", "Status", "Title"]
|
|
CONDITIONAL_FIELDS = { "OPEN" : [ "Target", "Ticket" ],
|
|
"ACCEPTED" : [ "Target", "Ticket" ],
|
|
"CLOSED" : [ "Implemented-In", "Ticket" ],
|
|
"FINISHED" : [ "Implemented-In", "Ticket" ] }
|
|
FNAME_RE = re.compile(r'^(\d\d\d)-.*[^\~]$')
|
|
DIR = "."
|
|
OUTFILE_TXT = "000-index.txt"
|
|
TMPFILE_TXT = OUTFILE_TXT+".tmp"
|
|
|
|
TEMPFILES = [TMPFILE_TXT]
|
|
|
|
def unlink_if_present(fname):
|
|
try:
|
|
os.unlink(fname)
|
|
except OSError:
|
|
pass
|
|
|
|
def indexed(seq):
|
|
n = 0
|
|
for i in seq:
|
|
yield n, i
|
|
n += 1
|
|
|
|
def readProposal(fn):
|
|
fields = { }
|
|
f = codecs.open(fn, 'r', encoding='utf-8')
|
|
lastField = None
|
|
try:
|
|
for lineno, line in indexed(f):
|
|
line = line.rstrip()
|
|
if not line:
|
|
return fields
|
|
if line[0].isspace():
|
|
fields[lastField] += " %s"%(line.strip())
|
|
elif line == "```":
|
|
pass
|
|
else:
|
|
parts = line.split(":", 1)
|
|
if len(parts) != 2:
|
|
raise Error("%s:%s: Neither field, continuation, nor ```."%
|
|
(fn,lineno))
|
|
else:
|
|
fields[parts[0]] = parts[1].strip()
|
|
lastField = parts[0]
|
|
|
|
return fields
|
|
finally:
|
|
f.close()
|
|
|
|
def getProposalNumber(fn):
|
|
"""Get the proposal's assigned number from its filename `fn`."""
|
|
parts = fn.split('-', 1)
|
|
|
|
assert len(parts) == 2, \
|
|
"Filename must have a proposal number and title separated by a '-'"
|
|
|
|
return int(parts[0])
|
|
|
|
def checkProposal(fn, fields):
|
|
status = fields.get("Status")
|
|
need_fields = REQUIRED_FIELDS + CONDITIONAL_FIELDS.get(status, [])
|
|
|
|
number = getProposalNumber(fn)
|
|
# Since prop#288 was the newest when we began requiring the 'Ticket:'
|
|
# field, we don't require the field for it or any older proposal.
|
|
# (Although you're encouraged to add it to your proposal, and add it for
|
|
# older proposals where you know the correct ticket, as it greatly helps
|
|
# newcomers find more information on the implementation.)
|
|
if number <= 288:
|
|
if "Ticket" in need_fields:
|
|
need_fields.remove("Ticket")
|
|
|
|
for f in need_fields:
|
|
if f not in fields:
|
|
raise Error("%s has no %s field"%(fn, f))
|
|
if fn != fields['Filename']:
|
|
raise Error("Mismatched Filename field in %s"%fn)
|
|
if fields['Title'][-1] == '.':
|
|
fields['Title'] = fields['Title'][:-1]
|
|
|
|
status = fields['Status'] = status.upper()
|
|
if status not in STATUSES:
|
|
raise Error("I've never heard of status %s in %s"%(status,fn))
|
|
if status in [ "SUPERSEDED", "DEAD" ]:
|
|
for f in [ 'Implemented-In', 'Target' ]:
|
|
if f in fields: del fields[f]
|
|
|
|
def readProposals():
|
|
res = []
|
|
for fn in os.listdir(DIR):
|
|
m = FNAME_RE.match(fn)
|
|
if not m: continue
|
|
if fn.endswith(".tmp"):
|
|
continue
|
|
if not (fn.endswith(".txt") or fn.endswith(".md")):
|
|
raise Error("%s doesn't end with .txt or .md"%fn)
|
|
num = m.group(1)
|
|
fields = readProposal(fn)
|
|
checkProposal(fn, fields)
|
|
fields['num'] = num
|
|
res.append(fields)
|
|
return res
|
|
|
|
def writeTextIndexFile(proposals):
|
|
proposals.sort(key=lambda f:f['num'])
|
|
seenStatuses = set()
|
|
for p in proposals:
|
|
seenStatuses.add(p['Status'])
|
|
|
|
out = open(TMPFILE_TXT, 'w')
|
|
inf = open(OUTFILE_TXT, 'r')
|
|
for line in inf:
|
|
out.write(line)
|
|
if line.startswith("====="): break
|
|
inf.close()
|
|
|
|
out.write("Proposals by number:\n\n")
|
|
for prop in proposals:
|
|
out.write("%(num)s %(Title)s [%(Status)s]\n"%prop)
|
|
out.write("\n\nProposals by status:\n\n")
|
|
for s in STATUSES:
|
|
if s not in seenStatuses: continue
|
|
out.write(" %s:\n"%s)
|
|
for prop in proposals:
|
|
if s == prop['Status']:
|
|
out.write(" %(num)s %(Title)s"%prop)
|
|
if "Target" in prop:
|
|
out.write(" [for %(Target)s]"%prop)
|
|
if "Implemented-In" in prop:
|
|
out.write(" [in %(Implemented-In)s]"%prop)
|
|
out.write("\n")
|
|
out.close()
|
|
os.rename(TMPFILE_TXT, OUTFILE_TXT)
|
|
|
|
def formatMarkdownEntry(prop, withStatus=False):
|
|
if withStatus:
|
|
fmt = "* [`{Filename}`](/proposals/{Filename}): {Title} [{Status}]\n"
|
|
else:
|
|
fmt = "* [`{Filename}`](/proposals/{Filename}): {Title}\n"
|
|
return fmt.format(**prop)
|
|
|
|
def writeMarkdownFile(prefix, format_inputs):
|
|
template = prefix+"_template.md"
|
|
output = prefix+".md"
|
|
t = open(template).read()
|
|
content = t.format(**format_inputs)
|
|
with open(output, 'w') as f:
|
|
f.write(content)
|
|
|
|
def writeMarkdownIndexFiles(proposals):
|
|
markdown_files = [ "README", "BY_INDEX" ]
|
|
format_inputs = {}
|
|
|
|
format_inputs['warning'] = "<!-- DO NOT EDIT THIS FILE -->"
|
|
|
|
entries = []
|
|
for prop in proposals:
|
|
entries.append(formatMarkdownEntry(prop, withStatus=True))
|
|
format_inputs["BY_INDEX"] = "".join(entries)
|
|
|
|
for s in STATUSES:
|
|
entries = []
|
|
for prop in proposals:
|
|
if s == prop['Status']:
|
|
entries.append(formatMarkdownEntry(prop))
|
|
if entries:
|
|
format_inputs[s] = "".join(entries)
|
|
else:
|
|
format_inputs[s] = "(There are no proposals in this category)\n"
|
|
|
|
entries = []
|
|
for prop in proposals:
|
|
if prop['Status'] in ('DEAD', 'REJECTED', 'OBSOLETE'):
|
|
entries.append(formatMarkdownEntry(prop, withStatus=True))
|
|
format_inputs['DEAD_REJECTED_OBSOLETE'] = "".join(entries)
|
|
|
|
for prefix in markdown_files:
|
|
writeMarkdownFile(prefix, format_inputs)
|
|
|
|
if __name__ == '__main__':
|
|
proposals = readProposals()
|
|
try:
|
|
writeTextIndexFile(proposals)
|
|
writeMarkdownIndexFiles(proposals)
|
|
finally:
|
|
for tempfile in TEMPFILES:
|
|
unlink_if_present(tempfile)
|