Merge pull request #13989 from gavindsouza/get-single-value-oof

fix(frappe.utils.data): Deprecate `cast_fieldtype` to use `cast` for consistent return types
This commit is contained in:
mergify[bot] 2021-09-01 08:57:49 +00:00 committed by GitHub
commit 386b579405
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 130 additions and 33 deletions

View file

@ -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)

View file

@ -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_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

View file

@ -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:

View file

@ -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,45 @@ class TestDataManipulation(unittest.TestCase):
self.assertTrue('style="background-image: url(\'{0}/assets/frappe/bg.jpg\') !important"'.format(url) in html)
self.assertTrue('<a href="mailto:test@example.com">email</a>' 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:
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 +245,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 +268,4 @@ class TestPythonExpressions(unittest.TestCase):
"oops = forgot_equals",
]
for expr in invalid_expressions:
self.assertRaises(frappe.ValidationError, validate_python_code, expr)
self.assertRaises(frappe.ValidationError, validate_python_code, expr)

View file

@ -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
@ -8,6 +9,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"
@ -16,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: Optional[str] = None):
"""
Converts string date (yyyy-mm-dd) to datetime.date object.
If no input is provided, current date is returned.
@ -67,6 +69,31 @@ def get_datetime(datetime_str=None):
except ValueError:
return parser.parse(datetime_str)
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
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
@ -505,7 +532,14 @@ 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, 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)
@ -527,6 +561,46 @@ def cast_fieldtype(fieldtype, 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.
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",)
* datetime.date => ("Date",)
* datetime.time => ("Time",)
"""
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":
if value:
value = getdate(value)
else:
value = datetime.datetime(1, 1, 1).date()
elif fieldtype == "Datetime":
if value:
value = get_datetime(value)
else:
value = datetime.datetime(1, 1, 1)
elif fieldtype == "Time":
value = get_timedelta(value)
return value
def flt(s, precision=None):
"""Convert to float (ignoring commas in string)
@ -1202,7 +1276,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)

View file

@ -228,6 +228,7 @@ VALID_UTILS = (
"getdate",
"get_datetime",
"to_timedelta",
"get_timedelta",
"add_to_date",
"add_days",
"add_months",