Skip to content

Instantly share code, notes, and snippets.

@benkehoe
Last active June 23, 2023 21:17
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save benkehoe/3fd5c2ca04093618f486c42d87e9cb1c to your computer and use it in GitHub Desktop.
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
#!/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))
{
"key1": "$value1",
"key2": ["$list_value1", "literal", "${list_value2}"],
"duplicate_value": "$value1",
"$key3": "prefix${embedded_value}suffix",
"key4": "literal_dollar_sign_$$"
}
{
"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