Using cattrs to structure data and I want to omit x1 string field.
I want to perform a trivial cleanup on strings that have been passed in except for the password field.
I can get it to work on all strings
from attrs import define
from cattrs import Converter
MYDATA = {
"hostname": "MYhostNAme ",
"port": 389,
"adminuser": " cn=admin, dc=acme, dc=com",
"adminpass": " ADmin "
}
@define
class _LDAP:
hostname: str
port: int
adminuser: str
adminpass: str
def tidystr(text):
return text.lower().translate(str.maketrans("", "", " \n\t\r"))
class _Vars:
converter = Converter()
converter.register_structure_hook(str, lambda x, cls: tidystr(x))
ldap = converter.structure(MYDATA, _LDAP)
app = _Vars()
assert app.ldap.hostname == "myhostname" # True
assert app.ldap.adminpass == "admin" # True !!Not what I want!!
I can fool cattrs by passing in the adminpass field as Any
@define
class _LDAP:
adminpass: Any
but this is a bit clunky.
The docs show how to omit individual fields - but I can't figure out how then I would call the tidystr function. Following the docs closely I would do
class Vars:
converter = Converter()
hook = make_dict_structure_fn(_LDAP, converter, adminpass=override(omit=True))
converter.register_structure_hook(_LDAP, hook)
ldap = converter.structure(MYDATA, _LDAP)
which obviously won't work because tidystr() isn't being called.
I've tried various ways and am lost. The docs also show something like what I'm trying to do but the example is changing the keys not the values.
I'm the author of cattrs. Let's see how we can solve this.
First we need a way to recognize which fields you want to tidy up and which you don't. Looks like you'd like to apply tidying to all strings by default and opt-out for some fields.
NewType
Use a NewType for the password field.
from typing import NewType
from attrs import define
from cattrs import Converter
NonTidyStr = NewType("NonTidyStr", str)
MYDATA = {
"hostname": "MYhostNAme ",
"port": 389,
"adminuser": " cn=admin, dc=acme, dc=com",
"adminpass": " ADmin ",
}
@define
class _LDAP:
hostname: str
port: int
adminuser: str
adminpass: NonTidyStr
def tidystr(text):
return text.lower().translate(str.maketrans("", "", " \n\t\r"))
class _Vars:
converter = Converter()
converter.register_structure_hook(str, lambda x, cls: tidystr(x))
converter.register_structure_hook(NonTidyStr, lambda x, _: str(x))
ldap = converter.structure(MYDATA, _LDAP)
app = _Vars()
assert app.ldap.hostname == "myhostname" # True
assert app.ldap.adminpass == " ADmin "
(Any function taking a string will happily take a NewType based on it instead, and it'll actually be a string at runtime.)
Annotated
We can use typing.Annotated
to build our own little mini system for structuring.
The class becomes:
@define
class _LDAP:
hostname: str
port: int
adminuser: str
adminpass: Annotated[str, "notidy"]
Then, we need to create the appropriate hook:
from cattrs._compat import is_annotated
def is_no_tidy(t: Any) -> bool:
return is_annotated(t) and "notidy" in t.__metadata__
converter.register_structure_hook_func(is_no_tidy, lambda x, _: str(x))
The implementation is a little gnarly since Python doesn't have super good type inspection capabilities, but we can use an internal cattrs function instead.
This approach is, in general, very powerful.
Option 3: wait for 22.3.0
I'm adding an option to override the structure and unstructure functions for individual fields in the next version. Then, you'll be able to do something like:
hook = make_dict_structure_fn(_LDAP, converter, adminpass=override(structure_fn=str))
converter.register_structure_hook(_LDAP, hook)
Can't promise a release date though ;)