Skip to content

Commit a44b234

Browse files
Add fake f-strings to blurb. Works with 3.5! (#288)
1 parent 8242138 commit a44b234

File tree

1 file changed

+80
-53
lines changed

1 file changed

+80
-53
lines changed

blurb/blurb.py

Lines changed: 80 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,32 @@
112112
sections.append(section.strip())
113113

114114

115+
def f(s):
116+
"""
117+
Basic support for 3.6's f-strings, in 3.5!
118+
119+
Formats "s" using appropriate globals and locals
120+
dictionaries. This f-string:
121+
f"hello a is {a}"
122+
simply becomes
123+
f("hello a is {a}")
124+
In other words, just throw parentheses around the
125+
string, and you're done!
126+
127+
Implemented internally using str.format_map().
128+
This means it doesn't support expressions:
129+
f("two minus three is {2-3}")
130+
And it doesn't support function calls:
131+
f("how many elements? {len(my_list)}")
132+
But most other f-string features work.
133+
"""
134+
frame = sys._getframe(1)
135+
d = dict(builtins.__dict__)
136+
d.update(frame.f_globals)
137+
d.update(frame.f_locals)
138+
return s.format_map(d)
139+
140+
115141
def sanitize_section(section):
116142
"""
117143
Cleans up a section string, making it viable as a directory name.
@@ -224,10 +250,10 @@ def sortable_datetime():
224250

225251

226252
def prompt(prompt):
227-
return input("[{}> ".format(prompt))
253+
return input(f("[{prompt}> "))
228254

229255
def require_ok(prompt):
230-
prompt = "[{}> ".format(prompt)
256+
prompt = f("[{prompt}> ")
231257
while True:
232258
s = input(prompt).strip()
233259
if s == 'ok':
@@ -457,7 +483,7 @@ def parse(self, text, *, metadata=None, filename="input"):
457483
line_number = None
458484

459485
def throw(s):
460-
raise BlurbError("Error in {}:{}:\n{}".format(filename, line_number, s))
486+
raise BlurbError(f("Error in {filename}:{line_number}:\n{s}"))
461487

462488
def finish_entry():
463489
nonlocal body
@@ -522,8 +548,8 @@ def load(self, filename, *, metadata=None):
522548
523549
Broadly equivalent to blurb.parse(open(filename).read()).
524550
"""
525-
with open(filename, "rt", encoding="utf-8") as f:
526-
text = f.read()
551+
with open(filename, "rt", encoding="utf-8") as file:
552+
text = file.read()
527553
self.parse(text, metadata=metadata, filename=filename)
528554

529555
def __str__(self):
@@ -537,7 +563,7 @@ def __str__(self):
537563
add_separator = True
538564
if metadata:
539565
for name, value in sorted(metadata.items()):
540-
add(".. {}: {}\n".format(name, value))
566+
add(f(".. {name}: {value}\n"))
541567
add("\n")
542568
add(textwrap_body(body))
543569
return "".join(output)
@@ -547,8 +573,8 @@ def save(self, path):
547573
safe_mkdir(dirname)
548574

549575
text = str(self)
550-
with open(path, "wt", encoding="utf-8") as f:
551-
f.write(text)
576+
with open(path, "wt", encoding="utf-8") as file:
577+
file.write(text)
552578

553579
@staticmethod
554580
def _parse_next_filename(filename):
@@ -559,10 +585,10 @@ def _parse_next_filename(filename):
559585
components = filename.split(os.sep)
560586
section, filename = components[-2:]
561587
section = unsanitize_section(section)
562-
assert section in sections, "Unknown section {}".format(section)
588+
assert section in sections, f("Unknown section {section}")
563589

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

568594
metadata = {"date": fields[0], "nonce": fields[-2], "section": section}
@@ -656,8 +682,8 @@ def filename_test(self, filename):
656682
b.load(filename)
657683
self.assertTrue(b)
658684
if os.path.exists(filename + '.res'):
659-
with open(filename + '.res', encoding='utf-8') as f:
660-
expected = f.read()
685+
with open(filename + '.res', encoding='utf-8') as file:
686+
expected = file.read()
661687
self.assertEqual(str(b), expected)
662688

663689
def test_files(self):
@@ -712,8 +738,8 @@ def chdir_to_repo_root():
712738
def test_first_line(filename, test):
713739
if not os.path.exists(filename):
714740
return False
715-
with open(filename, "rt") as f:
716-
lines = f.read().split('\n')
741+
with open(filename, "rt") as file:
742+
lines = file.read().split('\n')
717743
if not (lines and test(lines[0])):
718744
return False
719745
return True
@@ -751,7 +777,7 @@ def subcommand(fn):
751777
def get_subcommand(subcommand):
752778
fn = subcommands.get(subcommand)
753779
if not fn:
754-
error("Unknown subcommand: {}\nRun 'blurb help' for help.".format(subcommand))
780+
error(f("Unknown subcommand: {subcommand}\nRun 'blurb help' for help."))
755781
return fn
756782

757783

@@ -813,19 +839,19 @@ def help(subcommand=None):
813839
for name, p in inspect.signature(fn).parameters.items():
814840
if p.kind == inspect.Parameter.KEYWORD_ONLY:
815841
short_option = name[0]
816-
options.append(" [-{}|--{}]".format(short_option, name))
842+
options.append(f(" [-{short_option}|--{name}]"))
817843
elif p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
818844
positionals.append(" ")
819845
has_default = (p.default != inspect._empty)
820846
if has_default:
821847
positionals.append("[")
822848
nesting += 1
823-
positionals.append("<{}>".format(name))
849+
positionals.append(f("<{name}>"))
824850
positionals.append("]" * nesting)
825851

826852

827853
parameters = "".join(options + positionals)
828-
print("blurb {}{}".format(subcommand, parameters))
854+
print(f("blurb {subcommand}{parameters}"))
829855
print()
830856
print(doc)
831857
sys.exit(0)
@@ -888,7 +914,7 @@ def add():
888914
atexit.register(lambda : os.unlink(tmp_path))
889915

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

907933
init_tmp_with_template()
908934

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

922948
while True:
@@ -936,7 +962,7 @@ def init_tmp_with_template():
936962

937963
if failure:
938964
print()
939-
print("Error: {}".format(failure))
965+
print(f("Error: {failure}"))
940966
print()
941967
try:
942968
prompt("Hit return to retry (or Ctrl-C to abort)")
@@ -970,20 +996,20 @@ def release(version):
970996
if existing_filenames:
971997
error("Sorry, can't handle appending 'next' files to an existing version (yet).")
972998

973-
output = "Misc/NEWS.d/{}.rst".format(version)
999+
output = f("Misc/NEWS.d/{version}.rst")
9741000
filenames = glob_blurbs("next")
9751001
blurbs = Blurbs()
9761002
date = current_date()
9771003

9781004
if not filenames:
979-
print("No blurbs found. Setting {} as having no changes.".format(version))
980-
body = "There were no new changes in version {}.\n".format(version)
1005+
print(f("No blurbs found. Setting {version} as having no changes."))
1006+
body = f("There were no new changes in version {version}.\n")
9811007
metadata = {"no changes": "True", "bpo": "0", "section": "Library", "date": date, "nonce": nonceify(body)}
9821008
blurbs.append((metadata, body))
9831009
else:
9841010
no_changes = None
9851011
count = len(filenames)
986-
print('Merging {} blurbs to "{}".'.format(count, output))
1012+
print(f('Merging {count} blurbs to "{output}".'))
9871013

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

1002-
print("Removing {} 'next' files from git.".format(len(filenames)))
1028+
how_many = len(filenames)
1029+
print(f("Removing {how_many} 'next' files from git."))
10031030
git_rm_files.extend(filenames)
10041031
flush_git_rm_files()
10051032

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

10111038
print()
10121039
print("Ready for commit.")
@@ -1077,7 +1104,7 @@ def print(*a, sep=" "):
10771104
metadata, body = blurbs[0]
10781105
release_date = metadata["release date"]
10791106

1080-
print("*Release date: {}*".format(release_date))
1107+
print(f("*Release date: {release_date}*"))
10811108
print()
10821109

10831110
if "no changes" in metadata:
@@ -1145,11 +1172,11 @@ def populate():
11451172

11461173
for section in sections:
11471174
dir_name = sanitize_section(section)
1148-
dir_path = "NEWS.d/next/{}".format(dir_name)
1175+
dir_path = f("NEWS.d/next/{dir_name}")
11491176
safe_mkdir(dir_path)
1150-
readme_path = "NEWS.d/next/{}/README.rst".format(dir_name)
1151-
with open(readme_path, "wt", encoding="utf-8") as f:
1152-
f.write("Put news entry ``blurb`` files for the *{}* section in this directory.\n".format(section))
1177+
readme_path = f("NEWS.d/next/{dir_name}/README.rst")
1178+
with open(readme_path, "wt", encoding="utf-8") as readme:
1179+
readme.write(f("Put news entry ``blurb`` files for the *{section}* section in this directory.\n"))
11531180
git_add_files.append(dir_path)
11541181
git_add_files.append(readme_path)
11551182
flush_git_add_files()
@@ -1170,7 +1197,7 @@ def export():
11701197
# """
11711198
# Test function for blurb command-line processing.
11721199
# """
1173-
# print("arg: boolean {} option {}".format(boolean, option))
1200+
# print(f("arg: boolean {boolean} option {option}"))
11741201

11751202

11761203
@subcommand
@@ -1255,7 +1282,7 @@ def flush_blurb():
12551282
fields.append(field)
12561283
see_also = ", ".join(fields)
12571284
# print("see_also: ", repr(see_also))
1258-
accumulator.append("(See also: {})".format(see_also))
1285+
accumulator.append(f("(See also: {see_also})"))
12591286
see_also = None
12601287
if not accumulator:
12611288
return
@@ -1301,8 +1328,8 @@ def flush_version():
13011328
if version is None:
13021329
assert not blurbs, "version should only be None initially, we shouldn't have blurbs yet"
13031330
return
1304-
assert blurbs, "No blurbs defined when flushing version {}!".format(version)
1305-
output = "NEWS.d/{}.rst".format(version)
1331+
assert blurbs, f("No blurbs defined when flushing version {version}!")
1332+
output = f("NEWS.d/{version}.rst")
13061333

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

1321-
with open("NEWS", "rt", encoding="utf-8") as f:
1322-
for line_number, line in enumerate(f):
1348+
with open("NEWS", "rt", encoding="utf-8") as file:
1349+
for line_number, line in enumerate(file):
13231350
line = line.rstrip()
13241351

13251352
if line.startswith("\ufeff"):
@@ -1420,11 +1447,11 @@ def flush_version():
14201447
elif line.startswith("- Issue #9516: Issue #9516: avoid errors in sysconfig when MACOSX_DEPLOYMENT_TARGET"):
14211448
line = "- Issue #9516 and Issue #9516: avoid errors in sysconfig when MACOSX_DEPLOYMENT_TARGET"
14221449
elif line.title().startswith(("- Request #", "- Bug #", "- Patch #", "- Patches #")):
1423-
# print("FIXING LINE {}: {!r}".format(line_number), line)
1450+
# print(f("FIXING LINE {line_number}: {line!r}"))
14241451
line = "- Issue #" + line.partition('#')[2]
1425-
# print("FIXED LINE {!r}".format(line))
1452+
# print(f("FIXED LINE {line_number}: {line!r}"))
14261453
# else:
1427-
# print("NOT FIXING LINE {}: {!r}".format(line_number, line))
1454+
# print(f("NOT FIXING LINE {line_number}: {line!r}"))
14281455

14291456

14301457
# 4. determine the actual content of the line
@@ -1489,7 +1516,7 @@ def flush_version():
14891516
line = line[4:]
14901517
parse_bpo = True
14911518
else:
1492-
# print("[[{:8} no bpo]] {}".format(line_number, line))
1519+
# print(f("[[{line_number:8} no bpo]] {line}"))
14931520
parse_bpo = False
14941521
if parse_bpo:
14951522
# GAAAH
@@ -1529,9 +1556,9 @@ def flush_version():
15291556
try:
15301557
int(bpo) # this will throw if it's not a legal int
15311558
except ValueError:
1532-
sys.exit("Couldn't convert bpo number to int on line {}! ".format(line_number) + repr(bpo))
1559+
sys.exit(f("Couldn't convert bpo number to int on line {line_number}! {bpo!r}"))
15331560
if see_also == "partially":
1534-
sys.exit("What the hell on line {}! ".format(line_number) + repr(bpo))
1561+
sys.exit(f("What the hell on line {line_number}! {bpo!r}"))
15351562

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

1555-
print("Wrote {} news items across {} versions.".format(blurb_count, version_count))
1582+
print(f("Wrote {blurb_count} news items across {version_count} versions."))
15561583
print()
15571584
print("Ready for commit.")
15581585

@@ -1601,10 +1628,10 @@ def main():
16011628
def handle_option(s, dict):
16021629
name = dict.get(s, None)
16031630
if not name:
1604-
sys.exit('blurb: Unknown option for {}: "{}"'.format(subcommand, s))
1631+
sys.exit(f('blurb: Unknown option for {subcommand}: "{s}"'))
16051632
kwargs[name] = not kwargs[name]
16061633

1607-
# print("short_options {} long_options {}".format(short_options, long_options))
1634+
# print(f("short_options {short_options} long_options {long_options}"))
16081635
for a in args:
16091636
if done_with_options:
16101637
filtered_args.append(a)
@@ -1638,7 +1665,7 @@ def handle_option(s, dict):
16381665
# whoops, must be a real type error, reraise
16391666
raise e
16401667

1641-
how_many = "{} argument".format(specified)
1668+
how_many = f("{specified} argument")
16421669
if specified != 1:
16431670
how_many += "s"
16441671

@@ -1649,12 +1676,12 @@ def handle_option(s, dict):
16491676
middle = "requires"
16501677
else:
16511678
plural = "" if required == 1 else "s"
1652-
middle = "requires at least {} argument{} and at most".format(required, plural)
1653-
middle += " {} argument".format(total)
1679+
middle = f("requires at least {required} argument{plural} and at most")
1680+
middle += f(" {total} argument")
16541681
if total != 1:
16551682
middle += "s"
16561683

1657-
print('Error: Wrong number of arguments!\n\nblurb {} {},\nand you specified {}.'.format(subcommand, middle, how_many))
1684+
print(f('Error: Wrong number of arguments!\n\nblurb {subcommand} {middle},\nand you specified {how_many}.'))
16581685
print()
16591686
print("usage: ", end="")
16601687
help(subcommand)

0 commit comments

Comments
 (0)