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("", "", " ntr")) 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.
Advertisement
Answer
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.
Option #1: 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("", "", " ntr")) 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.)
Option 2: 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 ;)