From fee0aa539328e4f6a5c108ddc10e45b087d525ec Mon Sep 17 00:00:00 2001 From: Larry Hastings Date: Mon, 10 Sep 2018 16:26:19 -0700 Subject: [PATCH] Add fake f-strings to blurb. Works with 3.5! --- blurb/blurb.py | 133 +++++++++++++++++++++++++++++-------------------- 1 file changed, 80 insertions(+), 53 deletions(-) diff --git a/blurb/blurb.py b/blurb/blurb.py index 10dac4d..b4188bd 100755 --- a/blurb/blurb.py +++ b/blurb/blurb.py @@ -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. @@ -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': @@ -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 @@ -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): @@ -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) @@ -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): @@ -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} @@ -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): @@ -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 @@ -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 @@ -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) @@ -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 @@ -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() @@ -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: @@ -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)") @@ -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"): @@ -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.") @@ -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: @@ -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() @@ -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 @@ -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 @@ -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 @@ -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"): @@ -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}")) # 4. determine the actual content of the line @@ -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 @@ -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(" "): @@ -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) @@ -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.") @@ -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}"')) 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) @@ -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" @@ -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)