I want to silently ignore KeyErrors and instead replace them with placeholders if values are not found. For example:
class Name:
def __init__(self, name):
self.name = name
self.capitalized = name.capitalize()
def __str__(self):
return self.name
"hello, {name}!".format(name=Name("bob")) # hello, bob!
"greetings, {name.capitalized}!".format(name=Name("bob")) # greetings, Bob!
# but, if no name kwarg is given...
"hello, {name}!".format(age=34) # hello, {name}!
"greetings, {name.capitalized}!".format(age=34) # greetings, {name.capitalized}!
My goal with this is that I'm trying to create a custom localization package for personal projects (I couldn't find existing ones that did everything I wanted to). Messages would be user-customizable, but I want users to have a flawless experience, so for example, if they make a typo and insert {nmae}
instead of {name}
, I don't want users to have to deal with errors, but I want to instead signal to them that they made a typo by giving them the placeholder value.
I found several solutions on stackoverflow, but none of them can handle attributes. My first solution was this:
class Default(dict):
"""A dictionary that returns the key itself wrapped in curly braces if the key is not found."""
def __missing__(self, key: str) -> str:
return f"{{{key}}}"
But this results in an error when trying to use it with attributes: AttributeError: 'str' object has no attribute 'capitalized'
, it does print "hello, {name}!"
with no issues. Same goes for my second solution using string.Formatter
:
class CustomFormatter(string.Formatter):
def get_value(self, key, args, kwargs):
try:
value = super().get_value(key, args, kwargs)
except KeyError:
value = f'{{{key}}}'
except AttributeError:
value = f'{{{key}}}'
return value
formatter.format("hello, {name}!", name=Name("bob")) # hello, bob!
formatter.format("greetings, {name.capitalized}!", name=Name("bob")) # greetings, Bob!
formatter.format("hello, {name}!", age=42) # hello, {name}!
formatter.format("greetings, {name.capitalized}!", age=42) # AttributeError: 'str' object has no attribute 'capitalized'
So how could I achieve something like this?
"hello, {name}!".format(name=Name("bob")) # hello, bob!
"greetings, {name.capitalized}!".format(name=Name("bob")) # greetings, Bob!
# but, if no name kwarg is given...
"hello, {name}!".format(age=34) # hello, {name}!
"greetings, {name.capitalized}!".format(age=34) # greetings, {name.capitalized}!
The best solution is to override get_field
instead of get_value
in CustomFormatter
:
class CustomFormatter(string.Formatter):
def get_field(self, field_name, args, kwargs):
try:
return super().get_field(field_name, args, kwargs)
except (AttributeError, KeyError):
return f"{{{field_name}}}", None
Kuddos to @blhsing for suggesting this solution.
The issue is that the AttributeError
gets raised when formatter.get_field()
is called, not in get_value()
, so you also need to override get_field()
.
By adding this function to your CustomFormatter
class, I was able to get the behaviour you want with {name.capitalized}
shown when you pass name="bob"
or name=34
instead of name=Name("bob")
:
def get_field(self, field_name, args, kwargs):
try:
return super().get_field(field_name, args, kwargs)
except AttributeError:
return f"{{{field_name}}}", None
The return value is a tuple, to respect get_field
's return value: a tuple with the result, and the key used.
>>> formatter = CustomFormatter()
>>> formatter.format("greetings, {name.capitalized}!", name="bob")
'greetings, {name.capitalized}!'
>>> formatter.format("greetings, {name.capitalized}!", name=34)
'greetings, {name.capitalized}!'
>>> formatter.format("greetings, {name.capitalized}!", name=Name("bob"))
'greetings, Bob!'
>>> formatter.format("{name.capitalized}, you are {age} years old.", name=Name("bob"))
'Bob, you are {age} years old.'
When I added some debugging print statements, namely:
class CustomFormatter(string.Formatter):
def get_value(self, key, args, kwargs):
print(f"get_value({key=}, {args=}, {kwargs=}")
...
def get_field(self, field_name, args, kwargs):
print(f"get_field({field_name=}, {args=}, {kwargs=}")
...
I could see this log when using name=Name("bob")
:
get_field(field_name='name.capitalized', args=(), kwargs={'name': <__main__.Name object at 0x000002818AA8E8D0>}
get_value(key='name', args=(), kwargs={'name': <__main__.Name object at 0x000002818AA8E8D0>}
and this log with for age=34
and leaving out name
:
get_field(field_name='name', args=(), kwargs={'age': 34}
get_value(key='name', args=(), kwargs={'age': 34}
so you see it's your overriden get_value
that handles the wrong key, and my overriden get_field
that handles the missing attribute.
As @blhsing pointed out, if you also catch the KeyError
in get_field
, then you don't need to override get_value
at all, leading to the final solution in the TL;DR above.