From 623998c787314dfd0eb70b41d2bb26a418de3624 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 20 Aug 2021 11:03:06 +0530 Subject: [PATCH 01/12] fix(db): Cast single dt field only if value is truthy --- frappe/database/database.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/database/database.py b/frappe/database/database.py index d6ecf0795d..d4fc6c2219 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -557,7 +557,10 @@ class Database(object): if not df: frappe.throw(_('Invalid field name: {0}').format(frappe.bold(fieldname)), self.InvalidColumnName) - val = cast_fieldtype(df.fieldtype, val) + # cast only if value is "set" or is truthy? + # cast_fieldtype returns currnt TS value for Datetime, Date fields + if val: + val = cast_fieldtype(df.fieldtype, val) self.value_cache[doctype][fieldname] = val From cb034e4c52e2213cdbe5f5a053e8a1bec169ebd9 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 20 Aug 2021 12:08:39 +0530 Subject: [PATCH 02/12] fix: Consistent return types in cast_fieldtype Note: BREAKING CHANGE --- frappe/database/database.py | 5 +---- frappe/utils/data.py | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/frappe/database/database.py b/frappe/database/database.py index d4fc6c2219..d6ecf0795d 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -557,10 +557,7 @@ class Database(object): if not df: frappe.throw(_('Invalid field name: {0}').format(frappe.bold(fieldname)), self.InvalidColumnName) - # cast only if value is "set" or is truthy? - # cast_fieldtype returns currnt TS value for Datetime, Date fields - if val: - val = cast_fieldtype(df.fieldtype, val) + val = cast_fieldtype(df.fieldtype, val) self.value_cache[doctype][fieldname] = val diff --git a/frappe/utils/data.py b/frappe/utils/data.py index f2c553211d..d89cda1519 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -505,7 +505,17 @@ def has_common(l1, l2): """Returns truthy value if there are common elements in lists l1 and l2""" return set(l1) & set(l2) -def cast_fieldtype(fieldtype, value): +def cast_fieldtype(fieldtype, value=None): + """Cast the value to the Python native object of the Frappe fieldtype provided. + If value is None, the first/lowest value of the `fieldtype` will be returned. + + Mapping of Python types => Frappe types: + * float => ("Currency", "Float", "Percent") + * int => ("Int", "Check") + * datetime.datetime => ("Datetime",) + * datetime.date => ("Date",) + * datetime.time => ("Time",) + """ if fieldtype in ("Currency", "Float", "Percent"): value = flt(value) @@ -517,12 +527,18 @@ def cast_fieldtype(fieldtype, value): value = cstr(value) elif fieldtype == "Date": + if value is None: + value = datetime.datetime(1, 1, 1).date() value = getdate(value) elif fieldtype == "Datetime": + if value is None: + value = datetime.datetime(1, 1, 1) value = get_datetime(value) elif fieldtype == "Time": + if value is None: + value = "0:0:0" value = to_timedelta(value) return value From a2cb9be7a4cd89e563888006e91ba03f353c251c Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 20 Aug 2021 12:35:24 +0530 Subject: [PATCH 03/12] feat: frappe.utils.data.cast Cast the value to the Python native object of the Frappe fieldtype provided. If value is None, the first/lowest value of the `fieldtype` will be returned. Mapping of Python types => Frappe types: * float => ("Currency", "Float", "Percent") * int => ("Int", "Check") * datetime.datetime => ("Datetime",) * datetime.date => ("Date",) * datetime.time => ("Time",) Deprecate frappe.utils.data.cast_fieldtype in favour of new util cast which handles types "better" --- frappe/utils/data.py | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index d89cda1519..4a25ad997a 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -8,6 +8,7 @@ import re, datetime, math, time from code import compile_command from urllib.parse import quote, urljoin from frappe.desk.utils import slug +from click import secho DATE_FORMAT = "%Y-%m-%d" TIME_FORMAT = "%H:%M:%S.%f" @@ -505,7 +506,36 @@ def has_common(l1, l2): """Returns truthy value if there are common elements in lists l1 and l2""" return set(l1) & set(l2) -def cast_fieldtype(fieldtype, value=None): +def cast_fieldtype(fieldtype, value): + # TODO: Add DeprecationWarning for this util + message = ( + "Function `frappe.utils.data.cast` has been deprecated in favour" + " of `frappe.utils.data.cast`. Use the newer util for safer type casting. " + ) + secho(message, fg="yellow") + + if fieldtype in ("Currency", "Float", "Percent"): + value = flt(value) + + elif fieldtype in ("Int", "Check"): + value = cint(value) + + elif fieldtype in ("Data", "Text", "Small Text", "Long Text", + "Text Editor", "Select", "Link", "Dynamic Link"): + value = cstr(value) + + elif fieldtype == "Date": + value = getdate(value) + + elif fieldtype == "Datetime": + value = get_datetime(value) + + elif fieldtype == "Time": + value = to_timedelta(value) + + return value + +def cast(fieldtype, value=None): """Cast the value to the Python native object of the Frappe fieldtype provided. If value is None, the first/lowest value of the `fieldtype` will be returned. @@ -1218,7 +1248,7 @@ def evaluate_filters(doc, filters): def compare(val1, condition, val2, fieldtype=None): ret = False if fieldtype: - val2 = cast_fieldtype(fieldtype, val2) + val2 = cast(fieldtype, val2) if condition in operator_map: ret = operator_map[condition](val1, val2) From ed6533f73705a483853d5b5ae8bf8cc0fca9156a Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 20 Aug 2021 12:37:15 +0530 Subject: [PATCH 04/12] fix: Use cast in favour of cast_fieldtype Use newly introduced casting util for Python-Frappe types mapping --- frappe/database/database.py | 21 ++------------------- frappe/model/base_document.py | 4 ++-- frappe/model/meta.py | 12 ++++++------ 3 files changed, 10 insertions(+), 27 deletions(-) diff --git a/frappe/database/database.py b/frappe/database/database.py index d6ecf0795d..9fab8e116f 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -14,7 +14,7 @@ import frappe.model.meta from frappe import _ from time import time -from frappe.utils import now, getdate, cast_fieldtype, get_datetime, get_table_name +from frappe.utils import now, getdate, cast, get_datetime, get_table_name from frappe.model.utils.link_count import flush_local_link_count @@ -516,7 +516,6 @@ class Database(object): FROM `tabSingles` WHERE doctype = %s """, doctype) - # result = _cast_result(doctype, result) dict_ = frappe._dict(result) @@ -557,7 +556,7 @@ class Database(object): if not df: frappe.throw(_('Invalid field name: {0}').format(frappe.bold(fieldname)), self.InvalidColumnName) - val = cast_fieldtype(df.fieldtype, val) + val = cast(df.fieldtype, val) self.value_cache[doctype][fieldname] = val @@ -1052,19 +1051,3 @@ def enqueue_jobs_after_commit(): q.enqueue_call(execute_job, timeout=job.get("timeout"), kwargs=job.get("queue_args")) frappe.flags.enqueue_after_commit = [] - -# Helpers -def _cast_result(doctype, result): - batch = [ ] - - try: - for field, value in result: - df = frappe.get_meta(doctype).get_field(field) - if df: - value = cast_fieldtype(df.fieldtype, value) - - batch.append(tuple([field, value])) - except frappe.exceptions.DoesNotExistError: - return result - - return tuple(batch) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 752543f46a..815dd27002 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -9,7 +9,7 @@ from frappe.model.utils.link_count import notify_link_count from frappe.modules import load_doctype_module from frappe.model import display_fieldtypes from frappe.utils import (cint, flt, now, cstr, strip_html, - sanitize_html, sanitize_email, cast_fieldtype) + sanitize_html, sanitize_email, cast) from frappe.utils.html_utils import unescape_html max_positive_value = { @@ -969,7 +969,7 @@ class BaseDocument(object): return self.cast(val, df) def cast(self, value, df): - return cast_fieldtype(df.fieldtype, value) + return cast(df.fieldtype, value) def _extract_images_from_text_editor(self): from frappe.core.doctype.file.file import extract_images_from_doc diff --git a/frappe/model/meta.py b/frappe/model/meta.py index de794ba77f..f89163e092 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -16,7 +16,7 @@ Example: ''' from datetime import datetime import frappe, json, os -from frappe.utils import cstr, cint, cast_fieldtype +from frappe.utils import cstr, cint, cast from frappe.model import default_fields, no_value_fields, optional_fields, data_fieldtypes, table_fields from frappe.model.document import Document from frappe.model.base_document import BaseDocument @@ -322,24 +322,24 @@ class Meta(Document): for ps in property_setters: if ps.doctype_or_field=='DocType': - self.set(ps.property, cast_fieldtype(ps.property_type, ps.value)) + self.set(ps.property, cast(ps.property_type, ps.value)) elif ps.doctype_or_field=='DocField': for d in self.fields: if d.fieldname == ps.field_name: - d.set(ps.property, cast_fieldtype(ps.property_type, ps.value)) + d.set(ps.property, cast(ps.property_type, ps.value)) break elif ps.doctype_or_field=='DocType Link': for d in self.links: if d.name == ps.row_name: - d.set(ps.property, cast_fieldtype(ps.property_type, ps.value)) + d.set(ps.property, cast(ps.property_type, ps.value)) break elif ps.doctype_or_field=='DocType Action': for d in self.actions: if d.name == ps.row_name: - d.set(ps.property, cast_fieldtype(ps.property_type, ps.value)) + d.set(ps.property, cast(ps.property_type, ps.value)) break def add_custom_links_and_actions(self): @@ -532,7 +532,7 @@ class Meta(Document): label = link.group, items = [link.parent_doctype or link.link_doctype] )) - + if not link.is_child_table: if link.link_fieldname != data.fieldname: if data.fieldname: From b8c51b13e2f89e231c19bc94b32fb7e8251557ca Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Mon, 30 Aug 2021 13:45:30 +0530 Subject: [PATCH 05/12] fix: Revert to using cast_fieldtype in BaseDocument.cast * reference: revert Breaking Change - https://github.com/frappe/frappe/pull/13989#discussion_r695624003 * Show deprecation warning unless `show_warning` is unset --- frappe/model/base_document.py | 4 ++-- frappe/utils/data.py | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 815dd27002..1e3ef53fbd 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -9,7 +9,7 @@ from frappe.model.utils.link_count import notify_link_count from frappe.modules import load_doctype_module from frappe.model import display_fieldtypes from frappe.utils import (cint, flt, now, cstr, strip_html, - sanitize_html, sanitize_email, cast) + sanitize_html, sanitize_email, cast_fieldtype) from frappe.utils.html_utils import unescape_html max_positive_value = { @@ -969,7 +969,7 @@ class BaseDocument(object): return self.cast(val, df) def cast(self, value, df): - return cast(df.fieldtype, value) + return cast_fieldtype(df.fieldtype, value, show_warning=False) def _extract_images_from_text_editor(self): from frappe.core.doctype.file.file import extract_images_from_doc diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 4a25ad997a..4e972c0189 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -506,13 +506,13 @@ def has_common(l1, l2): """Returns truthy value if there are common elements in lists l1 and l2""" return set(l1) & set(l2) -def cast_fieldtype(fieldtype, value): - # TODO: Add DeprecationWarning for this util - message = ( - "Function `frappe.utils.data.cast` has been deprecated in favour" - " of `frappe.utils.data.cast`. Use the newer util for safer type casting. " - ) - secho(message, fg="yellow") +def cast_fieldtype(fieldtype, value, show_warning=True): + if show_warning: + message = ( + "Function `frappe.utils.data.cast` has been deprecated in favour" + " of `frappe.utils.data.cast`. Use the newer util for safer type casting." + ) + secho(message, fg="yellow") if fieldtype in ("Currency", "Float", "Percent"): value = flt(value) From be72397bcad856a1acbf1ac0fa2202966504afae Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Mon, 30 Aug 2021 18:51:36 +0530 Subject: [PATCH 06/12] test: Add tests for frappe.utils.data.cast --- frappe/tests/test_utils.py | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index 95ba763482..0d41f285be 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -6,12 +6,13 @@ import frappe from frappe.utils import evaluate_filters, money_in_words, scrub_urls, get_url from frappe.utils import validate_url, validate_email_address from frappe.utils import ceil, floor -from frappe.utils.data import validate_python_code +from frappe.utils.data import cast, validate_python_code from PIL import Image from frappe.utils.image import strip_exif_data, optimize_image import io from mimetypes import guess_type +from datetime import datetime, timedelta, date class TestFilters(unittest.TestCase): def test_simple_dict(self): @@ -93,6 +94,34 @@ class TestDataManipulation(unittest.TestCase): self.assertTrue('style="background-image: url(\'{0}/assets/frappe/bg.jpg\') !important"'.format(url) in html) self.assertTrue('email' in html) +class TestFieldCasting(unittest.TestCase): + def test_float_types(self): + FLOAT_TYPES = ("Currency", "Float", "Percent") + for fieldtype in FLOAT_TYPES: + self.assertIsInstance(cast(fieldtype, value=None), float) + self.assertIsInstance(cast(fieldtype, value=1.12), float) + self.assertIsInstance(cast(fieldtype, value=112), float) + + def test_int_types(self): + INT_TYPES = ("Int", "Check") + + for fieldtype in INT_TYPES: + self.assertIsInstance(cast(fieldtype, value=None), int) + self.assertIsInstance(cast(fieldtype, value=1.12), int) + self.assertIsInstance(cast(fieldtype, value=112), int) + + def test_datetime_types(self): + self.assertIsInstance(cast("Datetime", value=None), datetime) + self.assertIsInstance(cast("Datetime", value="12-2-22"), datetime) + + def test_date_types(self): + self.assertIsInstance(cast("Date", value=None), date) + self.assertIsInstance(cast("Date", value="12-12-2021"), date) + + def test_time_types(self): + self.assertIsInstance(cast("Time", value=None), timedelta) + self.assertIsInstance(cast("Time", value="12:03:34"), timedelta) + class TestMathUtils(unittest.TestCase): def test_floor(self): from decimal import Decimal @@ -205,7 +234,6 @@ class TestImage(unittest.TestCase): self.assertLess(len(optimized_content), len(original_content)) class TestPythonExpressions(unittest.TestCase): - def test_validation_for_good_python_expression(self): valid_expressions = [ "foo == bar", @@ -229,4 +257,4 @@ class TestPythonExpressions(unittest.TestCase): "oops = forgot_equals", ] for expr in invalid_expressions: - self.assertRaises(frappe.ValidationError, validate_python_code, expr) \ No newline at end of file + self.assertRaises(frappe.ValidationError, validate_python_code, expr) From 8e96d8d52294d371d9dacd533e844f18356204e2 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 31 Aug 2021 12:55:31 +0530 Subject: [PATCH 07/12] test: Add tests for str fieldstypes cast --- frappe/tests/test_utils.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index 0d41f285be..3033673224 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -95,6 +95,17 @@ class TestDataManipulation(unittest.TestCase): self.assertTrue('email' in html) class TestFieldCasting(unittest.TestCase): + def test_str_types(self): + STR_TYPES = ( + "Data", "Text", "Small Text", "Long Text", "Text Editor", "Select", "Link", "Dynamic Link" + ) + for fieldtype in STR_TYPES: + self.assertIsInstance(cast(fieldtype, value=None), str) + self.assertIsInstance(cast(fieldtype, value="12-12-2021"), str) + self.assertIsInstance(cast(fieldtype, value=""), str) + self.assertIsInstance(cast(fieldtype, value=[]), str) + self.assertIsInstance(cast(fieldtype, value=set()), str) + def test_float_types(self): FLOAT_TYPES = ("Currency", "Float", "Percent") for fieldtype in FLOAT_TYPES: From 3858e95e80073fde1e88e2cc21b71bf6ca02b4f6 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 31 Aug 2021 12:57:05 +0530 Subject: [PATCH 08/12] feat(utils): Add util get_timedelta get_timedelta returns None in case of invalid or imparsable inputs. This behaviour is consistent wrt other utils. The util, to_timedelta tries to convert to timedelta objects only if str object is passed. It returns the same object if not string, which is absurd...given its called `to_timedelta`. --- frappe/utils/data.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 4e972c0189..9ff8deb517 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -1,6 +1,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt +from typing import Optional import frappe import operator import json @@ -68,6 +69,31 @@ def get_datetime(datetime_str=None): except ValueError: return parser.parse(datetime_str) +def get_timedelta(time: str = None) -> Optional[datetime.timedelta]: + """Return `datetime.timedelta` object from string value of a + valid time format. Returns None if `time` is not a valid format + + Args: + time (str): A valid time representation. This string is parsed + using `dateutil.parser.parse`. Examples of valid inputs are: + '0:0:0', '17:21:00', '2012-01-19 17:21:00'. Checkout + https://dateutil.readthedocs.io/en/stable/parser.html#dateutil.parser.parse + + Returns: + datetime.timedelta: Timedelta object equivalent of the passed `time` string + """ + from dateutil import parser + + time = time or "0:0:0" + + try: + t = parser.parse(time) + return datetime.timedelta( + hours=t.hour, minutes=t.minute, seconds=t.second, microseconds=t.microsecond + ) + except Exception: + return None + def to_timedelta(time_str): from dateutil import parser From 8622142d7dd539b36a15b7dfaa98e5e22b6cfe1c Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 31 Aug 2021 13:04:32 +0530 Subject: [PATCH 09/12] fix: Use get_timedelta in cast, add to safe_exec list * Return date and datetime objects for Date and Datetime field types respectively if Falsy output is set --- frappe/utils/data.py | 14 +++++++------- frappe/utils/safe_exec.py | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 9ff8deb517..62eb6790e0 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -583,19 +583,19 @@ def cast(fieldtype, value=None): value = cstr(value) elif fieldtype == "Date": - if value is None: + if value: + value = getdate(value) + else: value = datetime.datetime(1, 1, 1).date() - value = getdate(value) elif fieldtype == "Datetime": - if value is None: + if value: + value = get_datetime(value) + else: value = datetime.datetime(1, 1, 1) - value = get_datetime(value) elif fieldtype == "Time": - if value is None: - value = "0:0:0" - value = to_timedelta(value) + value = get_timedelta(value) return value diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index 2e27859faa..4de685e53e 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -228,6 +228,7 @@ VALID_UTILS = ( "getdate", "get_datetime", "to_timedelta", +"get_timedelta", "add_to_date", "add_days", "add_months", From 176d6d2d069b918d959a3240c17d153cc4312f54 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 31 Aug 2021 13:07:22 +0530 Subject: [PATCH 10/12] fix: Check if input is str is_invalid_date_string This is sort of a breaking change? Because if an int/dict/list/tuple was passed instead of a str, object doesnt have .startswith (AttributeError) would be raised instead of just returning None. --- frappe/utils/data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 62eb6790e0..5cd41226da 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -18,10 +18,10 @@ DATETIME_FORMAT = DATE_FORMAT + " " + TIME_FORMAT def is_invalid_date_string(date_string): # dateutil parser does not agree with dates like "0001-01-01" or "0000-00-00" - return (not date_string) or (date_string or "").startswith(("0001-01-01", "0000-00-00")) + return not isinstance(date_string, str) or ((not date_string) or (date_string or "").startswith(("0001-01-01", "0000-00-00"))) # datetime functions -def getdate(string_date=None): +def getdate(string_date: str = None): """ Converts string date (yyyy-mm-dd) to datetime.date object. If no input is provided, current date is returned. From 9a32513b472e214aa7c7d2c62f19d5075b8dd3cb Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Mon, 30 Aug 2021 02:12:19 +0530 Subject: [PATCH 11/12] chore: Update frappe.utils.data.cast docstring --- frappe/utils/data.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 5cd41226da..2e70658dbd 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -564,8 +564,10 @@ def cast_fieldtype(fieldtype, value, show_warning=True): def cast(fieldtype, value=None): """Cast the value to the Python native object of the Frappe fieldtype provided. If value is None, the first/lowest value of the `fieldtype` will be returned. + If value can't be cast as fieldtype due to an invalid input, None will be returned. Mapping of Python types => Frappe types: + * str => ("Data", "Text", "Small Text", "Long Text", "Text Editor", "Select", "Link", "Dynamic Link") * float => ("Currency", "Float", "Percent") * int => ("Int", "Check") * datetime.datetime => ("Datetime",) From 8394bbeb4c7578084a21c5cdf841a9b1119340d2 Mon Sep 17 00:00:00 2001 From: gavin Date: Tue, 31 Aug 2021 21:50:07 +0530 Subject: [PATCH 12/12] chore(utils): Add type hints for get_timedelta, getdate Co-authored-by: Ankush Menat --- frappe/utils/data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 2e70658dbd..5a7328b07e 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -21,7 +21,7 @@ def is_invalid_date_string(date_string): return not isinstance(date_string, str) or ((not date_string) or (date_string or "").startswith(("0001-01-01", "0000-00-00"))) # datetime functions -def getdate(string_date: str = None): +def getdate(string_date: Optional[str] = None): """ Converts string date (yyyy-mm-dd) to datetime.date object. If no input is provided, current date is returned. @@ -69,7 +69,7 @@ def get_datetime(datetime_str=None): except ValueError: return parser.parse(datetime_str) -def get_timedelta(time: str = None) -> Optional[datetime.timedelta]: +def get_timedelta(time: Optional[str] = None) -> Optional[datetime.timedelta]: """Return `datetime.timedelta` object from string value of a valid time format. Returns None if `time` is not a valid format