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:
commit
386b579405
6 changed files with 130 additions and 33 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -228,6 +228,7 @@ VALID_UTILS = (
|
|||
"getdate",
|
||||
"get_datetime",
|
||||
"to_timedelta",
|
||||
"get_timedelta",
|
||||
"add_to_date",
|
||||
"add_days",
|
||||
"add_months",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue