One could create a decorator that does this. Here, we just blindly attempt to convert each argument into a date/time. If it works, great, we use the date/time, otherwise just use the object.
import functools
import dateutil.parser
def dt_convert(obj):
if not isinstance(obj, str):
return obj
try:
return dateutil.parser.parse(obj)
except TypeError:
return obj
def parse_dates(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
new_args = map(dt_convert, args)
new_kwargs = {kw: dt_convert(value) for kw, value in kwargs.items()}
return func(*new_args, **new_kwargs)
return wrapper
Which allows you to simply add the decorator to the existing functions like so
@parse_dates
def days_until(until_date, from_date=None)
# ... the original function logic
>>> christmas = dateutil.parser.parse('12/25')
>>> day_until(christmas, from_date='12/20')
5
This works in this particular case. However, in some cases you may have arguments that should actually be strings, but would be converted into datetimes erroneously, if the string happened to be a valid datetime as well.
Take for example the following
@parse_dates
def tricky_case(dt: datetime.datetime, tricky_string: str):
print(dt, tricky_string)
The result could potentially be unexpected
>>> tricky_case('12/25', '24')
2018-12-25 00:00:00 2018-03-24 00:00:00
As a workaround for this, we can have a decorator whose parameters are the names of the arguments we want to convert in the decorated function. This solution cheats a little by using inspect
to work with the signature of the decorated function. However, it allows us to bind the signature, appropriately handling positional and keyword arguments.
def parse_dates(*argnames):
def decorator(func):
sig = inspect.signature(func)
@functools.wraps(func)
def wrapper(*args, **kwargs):
ba = sig.bind(*args, **kwargs)
for argname in argnames:
if argname in ba.arguments and isinstance(ba.arguments[argname], str):
ba.arguments[argname] = dateutil.parser.parse(ba.arguments[argname])
return func(*ba.args, **ba.kwargs)
return wrapper
return decorator
Then the problem encountered in the ambiguous case can be avoided by specifically specifying that only dt
should be converted.
@parse_dates('dt')
def tricky_case(dt: datetime.datetime, tricky_string: str)
print(dt, tricky_string)
Then the result is no longer unexpected
>>> tricky_case('12/25', '24')
2018-12-25 00:00:00 24
A downside compared to the naive approach is, in this case, you still need to visit each function and identify the datetime argument names. Also, this hack uses a feature of inspect
that is not available in Python2.x -- in which case you would need to either rework this using inspect.get_arg_spec
, or use a third party module that provides the backport for the legacy version of Python.