Skip to content

Add fake f-strings to blurb. Works with 3.5! #288

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

Merged
merged 1 commit into from
Sep 11, 2018
Merged
Changes from all commits
Commits
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
133 changes: 80 additions & 53 deletions blurb/blurb.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,32 @@
sections.append(section.strip())


def f(s):
"""
Basic support for 3.6's f-strings, in 3.5!

Formats "s" using appropriate globals and locals
dictionaries. This f-string:
f"hello a is {a}"
simply becomes
f("hello a is {a}")
In other words, just throw parentheses around the
string, and you're done!

Implemented internally using str.format_map().
This means it doesn't support expressions:
f("two minus three is {2-3}")
And it doesn't support function calls:
f("how many elements? {len(my_list)}")
But most other f-string features work.
"""
frame = sys._getframe(1)
d = dict(builtins.__dict__)
d.update(frame.f_globals)
d.update(frame.f_locals)
return s.format_map(d)


def sanitize_section(section):
"""
Cleans up a section string, making it viable as a directory name.
Expand Down Expand Up @@ -224,10 +250,10 @@ def sortable_datetime():


def prompt(prompt):
return input("[{}> ".format(prompt))
return input(f("[{prompt}> "))

def require_ok(prompt):
prompt = "[{}> ".format(prompt)
prompt = f("[{prompt}> ")
while True:
s = input(prompt).strip()
if s == 'ok':
Expand Down Expand Up @@ -457,7 +483,7 @@ def parse(self, text, *, metadata=None, filename="input"):
line_number = None

def throw(s):
raise BlurbError("Error in {}:{}:\n{}".format(filename, line_number, s))
raise BlurbError(f("Error in {filename}:{line_number}:\n{s}"))

def finish_entry():
nonlocal body
Expand Down Expand Up @@ -522,8 +548,8 @@ def load(self, filename, *, metadata=None):

Broadly equivalent to blurb.parse(open(filename).read()).
"""
with open(filename, "rt", encoding="utf-8") as f:
text = f.read()
with open(filename, "rt", encoding="utf-8") as file:
text = file.read()
self.parse(text, metadata=metadata, filename=filename)

def __str__(self):
Expand All @@ -537,7 +563,7 @@ def __str__(self):
add_separator = True
if metadata:
for name, value in sorted(metadata.items()):
add(".. {}: {}\n".format(name, value))
add(f(".. {name}: {value}\n"))
add("\n")
add(textwrap_body(body))
return "".join(output)
Expand All @@ -547,8 +573,8 @@ def save(self, path):
safe_mkdir(dirname)

text = str(self)
with open(path, "wt", encoding="utf-8") as f:
f.write(text)
with open(path, "wt", encoding="utf-8") as file:
file.write(text)

@staticmethod
def _parse_next_filename(filename):
Expand All @@ -559,10 +585,10 @@ def _parse_next_filename(filename):
components = filename.split(os.sep)
section, filename = components[-2:]
section = unsanitize_section(section)
assert section in sections, "Unknown section {}".format(section)
assert section in sections, f("Unknown section {section}")

fields = [x.strip() for x in filename.split(".")]
assert len(fields) >= 4, "Can't parse 'next' filename! filename {!r} fields {}".format(filename, fields)
assert len(fields) >= 4, f("Can't parse 'next' filename! filename {filename!r} fields {fields}")
assert fields[-1] == "rst"

metadata = {"date": fields[0], "nonce": fields[-2], "section": section}
Expand Down Expand Up @@ -656,8 +682,8 @@ def filename_test(self, filename):
b.load(filename)
self.assertTrue(b)
if os.path.exists(filename + '.res'):
with open(filename + '.res', encoding='utf-8') as f:
expected = f.read()
with open(filename + '.res', encoding='utf-8') as file:
expected = file.read()
self.assertEqual(str(b), expected)

def test_files(self):
Expand Down Expand Up @@ -712,8 +738,8 @@ def chdir_to_repo_root():
def test_first_line(filename, test):
if not os.path.exists(filename):
return False
with open(filename, "rt") as f:
lines = f.read().split('\n')
with open(filename, "rt") as file:
lines = file.read().split('\n')
if not (lines and test(lines[0])):
return False
return True
Expand Down Expand Up @@ -751,7 +777,7 @@ def subcommand(fn):
def get_subcommand(subcommand):
fn = subcommands.get(subcommand)
if not fn:
error("Unknown subcommand: {}\nRun 'blurb help' for help.".format(subcommand))
error(f("Unknown subcommand: {subcommand}\nRun 'blurb help' for help."))
return fn


Expand Down Expand Up @@ -813,19 +839,19 @@ def help(subcommand=None):
for name, p in inspect.signature(fn).parameters.items():
if p.kind == inspect.Parameter.KEYWORD_ONLY:
short_option = name[0]
options.append(" [-{}|--{}]".format(short_option, name))
options.append(f(" [-{short_option}|--{name}]"))
elif p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
positionals.append(" ")
has_default = (p.default != inspect._empty)
if has_default:
positionals.append("[")
nesting += 1
positionals.append("<{}>".format(name))
positionals.append(f("<{name}>"))
positionals.append("]" * nesting)


parameters = "".join(options + positionals)
print("blurb {}{}".format(subcommand, parameters))
print(f("blurb {subcommand}{parameters}"))
print()
print(doc)
sys.exit(0)
Expand Down Expand Up @@ -888,7 +914,7 @@ def add():
atexit.register(lambda : os.unlink(tmp_path))

def init_tmp_with_template():
with open(tmp_path, "wt", encoding="utf-8") as f:
with open(tmp_path, "wt", encoding="utf-8") as file:
# hack:
# my editor likes to strip trailing whitespace from lines.
# normally this is a good idea. but in the case of the template
Expand All @@ -902,7 +928,7 @@ def init_tmp_with_template():
if without_space not in text:
sys.exit("Can't find BPO line to ensure there's a space on the end!")
text = text.replace(without_space, with_space)
f.write(text)
file.write(text)

init_tmp_with_template()

Expand All @@ -916,7 +942,7 @@ def init_tmp_with_template():
else:
args = list(shlex.split(editor))
if not shutil.which(args[0]):
sys.exit("Invalid GIT_EDITOR / EDITOR value: {}".format(editor))
sys.exit(f("Invalid GIT_EDITOR / EDITOR value: {editor}"))
args.append(tmp_path)

while True:
Expand All @@ -936,7 +962,7 @@ def init_tmp_with_template():

if failure:
print()
print("Error: {}".format(failure))
print(f("Error: {failure}"))
print()
try:
prompt("Hit return to retry (or Ctrl-C to abort)")
Expand Down Expand Up @@ -970,20 +996,20 @@ def release(version):
if existing_filenames:
error("Sorry, can't handle appending 'next' files to an existing version (yet).")

output = "Misc/NEWS.d/{}.rst".format(version)
output = f("Misc/NEWS.d/{version}.rst")
filenames = glob_blurbs("next")
blurbs = Blurbs()
date = current_date()

if not filenames:
print("No blurbs found. Setting {} as having no changes.".format(version))
body = "There were no new changes in version {}.\n".format(version)
print(f("No blurbs found. Setting {version} as having no changes."))
body = f("There were no new changes in version {version}.\n")
metadata = {"no changes": "True", "bpo": "0", "section": "Library", "date": date, "nonce": nonceify(body)}
blurbs.append((metadata, body))
else:
no_changes = None
count = len(filenames)
print('Merging {} blurbs to "{}".'.format(count, output))
print(f('Merging {count} blurbs to "{output}".'))

for filename in filenames:
if not filename.endswith(".rst"):
Expand All @@ -999,14 +1025,15 @@ def release(version):
git_add_files.append(output)
flush_git_add_files()

print("Removing {} 'next' files from git.".format(len(filenames)))
how_many = len(filenames)
print(f("Removing {how_many} 'next' files from git."))
git_rm_files.extend(filenames)
flush_git_rm_files()

# sanity check: ensuring that saving/reloading the merged blurb file works.
blurbs2 = Blurbs()
blurbs2.load(output)
assert blurbs2 == blurbs, "Reloading {} isn't reproducible?!".format(output)
assert blurbs2 == blurbs, f("Reloading {output} isn't reproducible?!")

print()
print("Ready for commit.")
Expand Down Expand Up @@ -1077,7 +1104,7 @@ def print(*a, sep=" "):
metadata, body = blurbs[0]
release_date = metadata["release date"]

print("*Release date: {}*".format(release_date))
print(f("*Release date: {release_date}*"))
print()

if "no changes" in metadata:
Expand Down Expand Up @@ -1145,11 +1172,11 @@ def populate():

for section in sections:
dir_name = sanitize_section(section)
dir_path = "NEWS.d/next/{}".format(dir_name)
dir_path = f("NEWS.d/next/{dir_name}")
safe_mkdir(dir_path)
readme_path = "NEWS.d/next/{}/README.rst".format(dir_name)
with open(readme_path, "wt", encoding="utf-8") as f:
f.write("Put news entry ``blurb`` files for the *{}* section in this directory.\n".format(section))
readme_path = f("NEWS.d/next/{dir_name}/README.rst")
with open(readme_path, "wt", encoding="utf-8") as readme:
readme.write(f("Put news entry ``blurb`` files for the *{section}* section in this directory.\n"))
git_add_files.append(dir_path)
git_add_files.append(readme_path)
flush_git_add_files()
Expand All @@ -1170,7 +1197,7 @@ def export():
# """
# Test function for blurb command-line processing.
# """
# print("arg: boolean {} option {}".format(boolean, option))
# print(f("arg: boolean {boolean} option {option}"))


@subcommand
Expand Down Expand Up @@ -1255,7 +1282,7 @@ def flush_blurb():
fields.append(field)
see_also = ", ".join(fields)
# print("see_also: ", repr(see_also))
accumulator.append("(See also: {})".format(see_also))
accumulator.append(f("(See also: {see_also})"))
see_also = None
if not accumulator:
return
Expand Down Expand Up @@ -1301,8 +1328,8 @@ def flush_version():
if version is None:
assert not blurbs, "version should only be None initially, we shouldn't have blurbs yet"
return
assert blurbs, "No blurbs defined when flushing version {}!".format(version)
output = "NEWS.d/{}.rst".format(version)
assert blurbs, f("No blurbs defined when flushing version {version}!")
output = f("NEWS.d/{version}.rst")

if released:
# saving merged blurb file for version, e.g. Misc/NEWS.d/3.7.0a1.rst
Expand All @@ -1318,8 +1345,8 @@ def flush_version():
blurbs.clear()
version_count += 1

with open("NEWS", "rt", encoding="utf-8") as f:
for line_number, line in enumerate(f):
with open("NEWS", "rt", encoding="utf-8") as file:
for line_number, line in enumerate(file):
line = line.rstrip()

if line.startswith("\ufeff"):
Expand Down Expand Up @@ -1420,11 +1447,11 @@ def flush_version():
elif line.startswith("- Issue #9516: Issue #9516: avoid errors in sysconfig when MACOSX_DEPLOYMENT_TARGET"):
line = "- Issue #9516 and Issue #9516: avoid errors in sysconfig when MACOSX_DEPLOYMENT_TARGET"
elif line.title().startswith(("- Request #", "- Bug #", "- Patch #", "- Patches #")):
# print("FIXING LINE {}: {!r}".format(line_number), line)
# print(f("FIXING LINE {line_number}: {line!r}"))
line = "- Issue #" + line.partition('#')[2]
# print("FIXED LINE {!r}".format(line))
# print(f("FIXED LINE {line_number}: {line!r}"))
# else:
# print("NOT FIXING LINE {}: {!r}".format(line_number, line))
# print(f("NOT FIXING LINE {line_number}: {line!r}"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I admire your tenacity for even fixing the comments!



# 4. determine the actual content of the line
Expand Down Expand Up @@ -1489,7 +1516,7 @@ def flush_version():
line = line[4:]
parse_bpo = True
else:
# print("[[{:8} no bpo]] {}".format(line_number, line))
# print(f("[[{line_number:8} no bpo]] {line}"))
parse_bpo = False
if parse_bpo:
# GAAAH
Expand Down Expand Up @@ -1529,9 +1556,9 @@ def flush_version():
try:
int(bpo) # this will throw if it's not a legal int
except ValueError:
sys.exit("Couldn't convert bpo number to int on line {}! ".format(line_number) + repr(bpo))
sys.exit(f("Couldn't convert bpo number to int on line {line_number}! {bpo!r}"))
if see_also == "partially":
sys.exit("What the hell on line {}! ".format(line_number) + repr(bpo))
sys.exit(f("What the hell on line {line_number}! {bpo!r}"))

# 4.6.1 continuation of blurb
elif line.startswith(" "):
Expand All @@ -1540,7 +1567,7 @@ def flush_version():
elif line.startswith(" * "):
line = line[3:]
elif line:
sys.exit("Didn't recognize line {}! ".format(line_number) + repr(line))
sys.exit(f("Didn't recognize line {line_number}! {line!r}"))
# only add blank lines if we have an initial line in the accumulator
if line or accumulator:
accumulator.append(line)
Expand All @@ -1552,7 +1579,7 @@ def flush_version():
git_rm_files.append("NEWS")
flush_git_rm_files()

print("Wrote {} news items across {} versions.".format(blurb_count, version_count))
print(f("Wrote {blurb_count} news items across {version_count} versions."))
print()
print("Ready for commit.")

Expand Down Expand Up @@ -1601,10 +1628,10 @@ def main():
def handle_option(s, dict):
name = dict.get(s, None)
if not name:
sys.exit('blurb: Unknown option for {}: "{}"'.format(subcommand, s))
sys.exit(f('blurb: Unknown option for {subcommand}: "{s}"'))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would probably use {s!r} here instead of "{s}", but I realize it's outside the scope of your direct changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You would change the name of the variable from s to str?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I would use !r to add the quotes, instead of adding them manually. I realize it might change the type of quotes from double to single, based on what s contains.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh! Sorry. Tiny font made s!r look like str on my screen.

kwargs[name] = not kwargs[name]

# print("short_options {} long_options {}".format(short_options, long_options))
# print(f("short_options {short_options} long_options {long_options}"))
for a in args:
if done_with_options:
filtered_args.append(a)
Expand Down Expand Up @@ -1638,7 +1665,7 @@ def handle_option(s, dict):
# whoops, must be a real type error, reraise
raise e

how_many = "{} argument".format(specified)
how_many = f("{specified} argument")
if specified != 1:
how_many += "s"

Expand All @@ -1649,12 +1676,12 @@ def handle_option(s, dict):
middle = "requires"
else:
plural = "" if required == 1 else "s"
middle = "requires at least {} argument{} and at most".format(required, plural)
middle += " {} argument".format(total)
middle = f("requires at least {required} argument{plural} and at most")
middle += f(" {total} argument")
if total != 1:
middle += "s"

print('Error: Wrong number of arguments!\n\nblurb {} {},\nand you specified {}.'.format(subcommand, middle, how_many))
print(f('Error: Wrong number of arguments!\n\nblurb {subcommand} {middle},\nand you specified {how_many}.'))
print()
print("usage: ", end="")
help(subcommand)
Expand Down