Package: warned-dataclasses
warned-dataclassesThis package adds functionality to Python'sdataclassesfeature to
emit a warning or raise an exception if an explicit value for an
attribute was used to initialize a dataclass but some user-specified
condition that that attribute logically relies on was not met.MotivationThe primary use case for this package is for tools (such as 🤗
Transformers) that usedataclassesfor command-line parsing, where
different command-line parameters make sense in different scenarios.In the simple case, a programmer can just emit a warning or raise an
exception if an explicit value was passed to one of these parameters in
a context where it is not appropriate. However, the programmer may also
want to set sensible defaults for such parameters when they are
appropriate.One approach to this problem is to compare the runtime value against the
default value, and assume an explicit value was passed if they do not
match. However, this approach presents two concerns:complex and
difficult-to-introspectdefault_factoryobjects may be used in the
dataclass's fieldsthe programmer may want to warn the user even if they explicitly pass
the default valueThis package presents a solution to both of these problems.InstallationThis package is on PyPI for Python >=3.8:pipinstallwarned-dataclassesUsageThere are two simple usage paradigms for this package.
The following (contrived) examples should illustrate them.1.warn_for_conditionWith this approach, warnings are emitted as unmet conditions are
discovered. This could be useful for emitting relevant warnings amidst
other logging statements describing program flow, or (when using error=True)
if an unmet condition in a critical point in the program should terminate
the program immediately.importjsonfromdataclassesimportdataclass,fieldfromwarned_dataclassesimportWarned,warned,warn_for_condition@warned@dataclassclassUser:id:intadmin_level:Warned[int,'admin_only']=field(default=1)defcheck_admin(user:User):withopen('admins.json','r')asadmins_fd:admins=json.load(admins_fd)ifuser.idnotinadmins:# uh-oh, user is not an adminwarn_for_condition(user,'admin_only')if__name__=='__main__':user=User(123,admin_level=2)check_admin(user)2.satisfyandwarn_allWith this approach, warnings are suppressed as conditions are judged to
be met and marked as satisfied, and all warnings for a dataclass are
emitted at once whenwarn_allis called.importjsonfromdataclassesimportdataclass,fieldfromwarned_dataclassesimportWarned,warned,warn_all,satisfy@warned@dataclassclassUser:id:intadmin_level:Warned[int,'admin_only']=field(default=1)fileshare_home_dir:Warned[str,'db_access']=field(default='~')defcheck_admin(user:User):withopen('admins.json','r')asadmins_fd:admins=json.load(admins_fd)ifuser.idinadmins:# user is an admin; no warningsatisfy(user,'admin_only')defcheck_db_access(user:User):withopen('db_users.json','r')asdb_users_fd:db_users=json.load(db_users_fd)ifuser.idindb_users:# user is a db user; no warningsatisfy(user,'db_access')if__name__=='__main__':user=User(123,admin_level=2,fileshare_home_dir='/')check_admin(user)check_db_access(user)# now emit all unsatisfied errorswarn_all(user)Advanced Usage1. Decorator optionsBy default, a warned dataclass will emit a warning to the currentlogginglogger. To raise an exception instead, use@warned(error=True).By default, a warned dataclass will only emit a warning once for each
condition; future calls towarn_for_conditionorwarn_allwill
treat that condition as satisfied. To disable this behavior and emit
a warning every time it is invoked, use@warned(satisfy_on_warn=False).By default, a warned dataclass will emit a warning if a value equal to
the default value for a field is passed explicitly. Sometimes this is
undesirable, and explicitly-passed default values should be ignored. To
disable warnings for explicit default values, use@warned(warn_on_default=False).A plain@warnedcan be used with or without parentheses.2. Multiple warned dataclasses andConditionSetIf your code uses multiple warned dataclasses that share some conditions,
the methods illustrated above become clunky, as you end up having to callwarn_for_condition(obj, condition)for every warned dataclass object
that usescondition. Instead, you can collect conditions immediately
after instantiation usingConditionSet, then call any of the usual functions,
either with theConditionSetobject as the first parameter or as a
method onConditionSet:fromdataclassesimportdataclass,fieldfromwarned_dataclassesimportwarned,Warned,ConditionSet,warn_for_condition,warn_all,satisfy@warned@dataclassclassOne:...some_attr:Warned[int,'abc']=field(default=32)other_attr:Warned[str,'shared_condition']=field(default='')...@warned@dataclassclassTwo:...some_attr:Warned[int,'shared_condition']=field(default=5)other_attr:Warned[float,'123']=field(default=0.0)...defmain():...one=One(...)two=Two(...)conditions=ConditionSet(one,two)...conditions.warn_for_condition("shared_condition")conditions.satisfy("123")conditions.warn_all()# or:warn_for_condition(conditions,"shared_condition")satisfy(conditions,"123")warn_all(conditions)