Last active
June 23, 2023 21:17
-
-
Save benkehoe/3fd5c2ca04093618f486c42d87e9cb1c to your computer and use it in GitHub Desktop.
Demo of the two new methods of string.Template in Python 3.11
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3.11 | |
# MIT No Attribution | |
# | |
# Copyright 2023 Ben Kehoe | |
# | |
# Permission is hereby granted, free of charge, to any person obtaining a copy of this | |
# software and associated documentation files (the "Software"), to deal in the Software | |
# without restriction, including without limitation the rights to use, copy, modify, | |
# merge, publish, distribute, sublicense, and/or sell copies of the Software, and to | |
# permit persons to whom the Software is furnished to do so. | |
# | |
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, | |
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A | |
# PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT | |
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | |
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE | |
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
""" | |
This program demonstrates the two new methods of string.Template in Python 3.11. | |
https://docs.python.org/3/library/string.html#template-strings | |
The two methods are get_identifiers() and is_valid(). | |
get_identifiers() returns a list of the valid identifiers in the template, | |
in the order they first appear, ignoring any invalid identifiers. | |
is_valid() complements this by indicating if the template has invalid placeholders | |
that will cause substitute() to raise ValueError. | |
You can use these methods to prep for a call to substitute(). In this program, | |
that means interactively prompting for all the identifiers, after first determining | |
if they are all valid. Then it can substitute them all at once, knowing that won't fail. | |
Another use case is if you've got a possibly-incomplete mapping to dump into a template, | |
just calling substitute() will only give you the first missing key. With get_identifiers() | |
you can find all missing keys and raise a better error message. | |
usage: string_template_demo.py [-h] (--file JSON_FILE | --value JSON_VALUE) [--output-file OUTPUT_FILE] | |
""" | |
import json | |
from string import Template | |
import argparse | |
import sys | |
if sys.version_info[0] != 3 or sys.version_info[1] < 11: | |
print("Requires python >= 3.11", file=sys.stderr) | |
sys.exit(1) | |
class InvalidTemplateError(Exception): | |
pass | |
def _add_all(list1, list2): | |
"""Append new items from list2 to list1""" | |
for v in list2: | |
if v not in list1: | |
list1.append(v) | |
return list1 | |
def get_identifiers(obj): | |
"""Returns a list of identifiers in the object, in the order they appear""" | |
# note sets don't iterate in insertion order, so we have to use a list | |
# could use dict, I guess | |
if isinstance(obj, str): | |
t = Template(obj) | |
if not t.is_valid(): | |
raise InvalidTemplateError(f"String value {obj!r} is an invalid template") | |
return t.get_identifiers() | |
if isinstance(obj, list): | |
ids = [] | |
for item in obj: | |
_add_all(ids, get_identifiers(item)) | |
return ids | |
elif isinstance(obj, dict): | |
ids = [] | |
for key, value in obj.items(): | |
_add_all(ids, get_identifiers(key)) | |
_add_all(ids, get_identifiers(value)) | |
return ids | |
return [] | |
def substitute(obj, values): | |
"""Populate the JSON with the given values""" | |
if isinstance(obj, str): | |
t = Template(obj) | |
return t.substitute(values) | |
if isinstance(obj, list): | |
return [substitute(v, values) for v in obj] | |
if isinstance(obj, dict): | |
return {substitute(k, values): substitute(v, values) for k, v in obj.items()} | |
return obj | |
parser = argparse.ArgumentParser(description="Demo for interactive string interpolation in JSON using string.Template") | |
group = parser.add_mutually_exclusive_group(required=True) | |
group.add_argument("--file", type=argparse.FileType(), metavar="JSON_FILE") | |
group.add_argument("--value", metavar="JSON_VALUE") | |
parser.add_argument("--output-file", type=argparse.FileType("w")) | |
args = parser.parse_args() | |
if args.file: | |
input_data = json.load(args.file) | |
else: | |
input_data = json.loads(args.value) | |
try: | |
ids = get_identifiers(input_data) | |
except InvalidTemplateError as e: | |
print(e, file=sys.stderr) | |
sys.exit(2) | |
values = {} | |
for id in ids: | |
values[id] = input(f"Enter a value for {id}: ") | |
print("") | |
output_data = substitute(input_data, values) | |
if args.output_file: | |
json.dump(output_data, args.output_file) | |
else: | |
print(json.dumps(output_data, indent=2)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"key1": "$value1", | |
"key2": ["$list_value1", "literal", "${list_value2}"], | |
"duplicate_value": "$value1", | |
"$key3": "prefix${embedded_value}suffix", | |
"key4": "literal_dollar_sign_$$" | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"key1": "$value1", | |
"key2": ["$list_value1", "literal", "${list_value2}"], | |
"duplicate_value": "$value1", | |
"$key3": "prefix${embedded_value}suffix", | |
"key4": "literal_dollar_sign_$$", | |
"invalid": "${unclosed" | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment