(core) Remaining Python 3 compatibility changes

Summary: Biggest change is turning everything to unicode

Test Plan: The tests

Reviewers: dsagal, paulfitz

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D2875
pull/9/head
Alex Hall 3 years ago
parent 1af99e9567
commit 305b133c59

@ -639,7 +639,7 @@ async function handleSandboxError<T>(tableId: string, colNames: string[], p: Pro
if (match) { if (match) {
throw new ApiError(`Invalid row id ${match[1]}`, 400); throw new ApiError(`Invalid row id ${match[1]}`, 400);
} }
match = e.message.match(/\[Sandbox\] KeyError '(.*?)'/); match = e.message.match(/\[Sandbox\] KeyError u?'(.*?)'/);
if (match) { if (match) {
if (match[1] === tableId) { if (match[1] === tableId) {
throw new ApiError(`Table not found "${tableId}"`, 404); throw new ApiError(`Table not found "${tableId}"`, 404);

@ -78,7 +78,7 @@ export class NSandbox implements ISandbox {
public readonly childProc: ChildProcess; public readonly childProc: ChildProcess;
private _logTimes: boolean; private _logTimes: boolean;
private _exportedFunctions: {[name: string]: SandboxMethod}; private _exportedFunctions: {[name: string]: SandboxMethod};
private _marshaller = new marshal.Marshaller({stringToBuffer: true, version: 2}); private _marshaller = new marshal.Marshaller({stringToBuffer: false, version: 2});
private _unmarshaller = new marshal.Unmarshaller({ bufferToString: false }); private _unmarshaller = new marshal.Unmarshaller({ bufferToString: false });
// Members used for reading from the sandbox process. // Members used for reading from the sandbox process.

@ -109,8 +109,7 @@ def prepare_acl_col_renames(docmodel, useractions, col_renames_dict):
# Go through again checking if anything in ACL formulas is affected by the rename. # Go through again checking if anything in ACL formulas is affected by the rename.
for rule_rec in docmodel.aclRules.all: for rule_rec in docmodel.aclRules.all:
if rule_rec.aclFormula: if rule_rec.aclFormula:
# Positions are obtained from unicode version of formulas, so that's what we must patch formula = rule_rec.aclFormula
formula = rule_rec.aclFormula.decode('utf8')
patches = [] patches = []
for entity in parse_acl_grist_entities(rule_rec.aclFormula): for entity in parse_acl_grist_entities(rule_rec.aclFormula):
@ -129,7 +128,7 @@ def prepare_acl_col_renames(docmodel, useractions, col_renames_dict):
patches.append(patch) patches.append(patch)
replacer = textbuilder.Replacer(textbuilder.Text(formula), patches) replacer = textbuilder.Replacer(textbuilder.Text(formula), patches)
txt = replacer.get_text().encode('utf8') txt = replacer.get_text()
rule_updates.append((rule_rec, {'aclFormula': txt, rule_updates.append((rule_rec, {'aclFormula': txt,
'aclFormulaParsed': parse_acl_formula_json(txt)})) 'aclFormulaParsed': parse_acl_formula_json(txt)}))

@ -5,6 +5,8 @@ import tokenize
from collections import namedtuple from collections import namedtuple
import asttokens import asttokens
import six
def parse_acl_formula(acl_formula): def parse_acl_formula(acl_formula):
""" """
@ -26,10 +28,12 @@ def parse_acl_formula(acl_formula):
Attr node, attr_name Attr node, attr_name
Comment node, comment Comment node, comment
""" """
if isinstance(acl_formula, six.binary_type):
acl_formula = acl_formula.decode('utf8')
try: try:
tree = ast.parse(acl_formula, mode='eval') tree = ast.parse(acl_formula, mode='eval')
result = _TreeConverter().visit(tree) result = _TreeConverter().visit(tree)
for part in tokenize.generate_tokens(io.StringIO(acl_formula.decode('utf-8')).readline): for part in tokenize.generate_tokens(io.StringIO(acl_formula).readline):
if part[0] == tokenize.COMMENT and part[1].startswith('#'): if part[0] == tokenize.COMMENT and part[1].startswith('#'):
result = ['Comment', result, part[1][1:].strip()] result = ['Comment', result, part[1][1:].strip()]
break break

@ -116,7 +116,7 @@ def action_from_repr(doc_action):
try: try:
return decode_objects(action_type(*doc_action[1:])) return decode_objects(action_type(*doc_action[1:]))
except TypeError as e: except TypeError as e:
raise TypeError("%s: %s" % (doc_action[0], e.message)) raise TypeError("%s: %s" % (doc_action[0], str(e)))
def convert_recursive_helper(converter, data): def convert_recursive_helper(converter, data):

@ -70,6 +70,9 @@ class AutocompleteContext(object):
return self._context return self._context
def process_result(self, result): def process_result(self, result):
# 'for' suggests the autocompletion 'for ' in python 3
result = result.rstrip()
# Callables are returned by rlcompleter with a trailing "(". # Callables are returned by rlcompleter with a trailing "(".
if result.endswith('('): if result.endswith('('):
funcname = result[0:-1] funcname = result[0:-1]

@ -118,9 +118,10 @@ def _create_syntax_error_code(builder, input_text, err):
output_offset = output_ln.line_to_offset(err.lineno, err.offset - 1 if err.offset else 0) output_offset = output_ln.line_to_offset(err.lineno, err.offset - 1 if err.offset else 0)
input_offset = builder.map_back_offset(output_offset) input_offset = builder.map_back_offset(output_offset)
line, col = input_ln.offset_to_line(input_offset) line, col = input_ln.offset_to_line(input_offset)
return "%s\nraise %s('%s on line %d col %d')" % ( message = '%s on line %d col %d' % (err.args[0], line, col + 1)
return "%s\nraise %s(%r)" % (
textbuilder.line_start_re.sub('# ', input_text.rstrip()), textbuilder.line_start_re.sub('# ', input_text.rstrip()),
type(err).__name__, err.args[0], line, col + 1) type(err).__name__, message)
#---------------------------------------------------------------------- #----------------------------------------------------------------------

@ -268,11 +268,43 @@ class DateTimeColumn(NumericColumn):
def sample_value(self): def sample_value(self):
return _sample_datetime return _sample_datetime
class MixedTypesKey(object):
"""
Sort key that can contain different types.
This mimics Python 2 where values of different types can be compared,
falling back on some comparison of the types when the values
can't be compared normally.
"""
__slots__ = ("value",)
def __init__(self, value):
self.value = value
def __repr__(self):
return "MixedTypesKey({self.value!r})".format(self=self)
def __eq__(self, other):
return self.value == other.value
def __lt__(self, other):
try:
return self.value < other.value
except TypeError:
return type(self.value).__name__ < type(other.value).__name__
if six.PY2:
def MixedTypesKey(x):
return x
class PositionColumn(NumericColumn): class PositionColumn(NumericColumn):
def __init__(self, table, col_id, col_info): def __init__(self, table, col_id, col_info):
super(PositionColumn, self).__init__(table, col_id, col_info) super(PositionColumn, self).__init__(table, col_id, col_info)
# This is a list of row_ids, ordered by the position. # This is a list of row_ids, ordered by the position.
self._sorted_rows = SortedListWithKey(key=self.raw_get) self._sorted_rows = SortedListWithKey(key=lambda x: MixedTypesKey(self.raw_get(x)))
def set(self, row_id, value): def set(self, row_id, value):
self._sorted_rows.discard(row_id) self._sorted_rows.discard(row_id)
@ -282,7 +314,8 @@ class PositionColumn(NumericColumn):
def copy_from_column(self, other_column): def copy_from_column(self, other_column):
super(PositionColumn, self).copy_from_column(other_column) super(PositionColumn, self).copy_from_column(other_column)
self._sorted_rows = SortedListWithKey(other_column._sorted_rows[:], key=self.raw_get) self._sorted_rows = SortedListWithKey(other_column._sorted_rows[:],
key=lambda x: MixedTypesKey(self.raw_get(x)))
def prepare_new_values(self, values, ignore_data=False, action_summary=None): def prepare_new_values(self, values, ignore_data=False, action_summary=None):
# This does the work of adjusting positions and relabeling existing rows with new position # This does the work of adjusting positions and relabeling existing rows with new position
@ -290,9 +323,11 @@ class PositionColumn(NumericColumn):
# used for updating a position for an existing row: we'll find a new value for it; later when # used for updating a position for an existing row: we'll find a new value for it; later when
# this value is set, the old position will be removed and the new one added. # this value is set, the old position will be removed and the new one added.
if ignore_data: if ignore_data:
rows = SortedListWithKey([], key=self.raw_get) rows = []
else: else:
rows = self._sorted_rows rows = self._sorted_rows
# prepare_inserts expects floats as keys, not MixedTypesKeys
rows = SortedListWithKey(rows, key=self.raw_get)
adjustments, new_values = relabeling.prepare_inserts(rows, values) adjustments, new_values = relabeling.prepare_inserts(rows, values)
return new_values, [(self._sorted_rows[i], pos) for (i, pos) in adjustments] return new_values, [(self._sorted_rows[i], pos) for (i, pos) in adjustments]

@ -77,9 +77,9 @@ class Graph(object):
""" """
Print out the graph to stdout, for debugging. Print out the graph to stdout, for debugging.
""" """
print "Dependency graph (%d edges):" % len(self._all_edges) print("Dependency graph (%d edges):" % len(self._all_edges))
for edge in sorted(self._all_edges): for edge in sorted(self._all_edges):
print " %s" % (edge,) print(" %s" % (edge,))
def add_edge(self, out_node, in_node, relation): def add_edge(self, out_node, in_node, relation):
""" """

@ -34,12 +34,13 @@ import table as table_module
import useractions import useractions
import column import column
import repl import repl
import urllib_patch # noqa imported for side effect
log = logger.Logger(__name__, logger.INFO) log = logger.Logger(__name__, logger.INFO)
if six.PY2: if six.PY2:
reload(sys) reload(sys)
sys.setdefaultencoding('utf8') sys.setdefaultencoding('utf8') # noqa
class OrderError(Exception): class OrderError(Exception):
@ -1125,8 +1126,14 @@ class Engine(object):
except Exception: except Exception:
log.error("Inconsistent schema after revert on failure: %s" % traceback.format_exc()) log.error("Inconsistent schema after revert on failure: %s" % traceback.format_exc())
# Re-raise the original exception (simple `raise` wouldn't do if undo also fails above). # Re-raise the original exception
raise exc_info[0], exc_info[1], exc_info[2] # In Python 2, 'raise' raises the most recent exception,
# which may come from the try/except just above
# Python 3 keeps track of nested exceptions better
if six.PY2:
six.reraise(*exc_info)
else:
raise
# Note that recalculations and auto-removals get included after processing all useractions. # Note that recalculations and auto-removals get included after processing all useractions.
self._bring_all_up_to_date() self._bring_all_up_to_date()
@ -1183,8 +1190,15 @@ class Engine(object):
self.rebuild_usercode() self.rebuild_usercode()
except Exception: except Exception:
log.error("Error rebuilding usercode after restoring schema: %s" % traceback.format_exc()) log.error("Error rebuilding usercode after restoring schema: %s" % traceback.format_exc())
# Re-raise the original exception (simple `raise` wouldn't do if rebuild also fails above).
raise exc_info[0], exc_info[1], exc_info[2] # Re-raise the original exception
# In Python 2, 'raise' raises the most recent exception,
# which may come from the try/except just above
# Python 3 keeps track of nested exceptions better
if six.PY2:
six.reraise(*exc_info)
else:
raise
# If any columns got deleted, destroy them to clear _back_references in other tables, and to # If any columns got deleted, destroy them to clear _back_references in other tables, and to
# force errors if anything still uses them. Also clear them from calc actions if needed. # force errors if anything still uses them. Also clear them from calc actions if needed.

@ -93,15 +93,15 @@ def SELF_HYPERLINK(label=None, page=None, **kwargs):
we might want to create links with `SELF_HYPERLINK(LinkKey_Code=$Code)`. we might want to create links with `SELF_HYPERLINK(LinkKey_Code=$Code)`.
>>> SELF_HYPERLINK() >>> SELF_HYPERLINK()
'https://docs.getgrist.com/sbaltsirg/Example' u'https://docs.getgrist.com/sbaltsirg/Example'
>>> SELF_HYPERLINK(label='doc') >>> SELF_HYPERLINK(label='doc')
'doc https://docs.getgrist.com/sbaltsirg/Example' u'doc https://docs.getgrist.com/sbaltsirg/Example'
>>> SELF_HYPERLINK(page=2) >>> SELF_HYPERLINK(page=2)
'https://docs.getgrist.com/sbaltsirg/Example/p/2' u'https://docs.getgrist.com/sbaltsirg/Example/p/2'
>>> SELF_HYPERLINK(LinkKey_Code='X1234') >>> SELF_HYPERLINK(LinkKey_Code='X1234')
'https://docs.getgrist.com/sbaltsirg/Example?Code_=X1234' u'https://docs.getgrist.com/sbaltsirg/Example?Code_=X1234'
>>> SELF_HYPERLINK(label='order', page=3, LinkKey_Code='X1234', LinkKey_Name='Bi Ngo') >>> SELF_HYPERLINK(label='order', page=3, LinkKey_Code='X1234', LinkKey_Name='Bi Ngo')
'order https://docs.getgrist.com/sbaltsirg/Example/p/3?Code_=X1234&Name_=Bi+Ngo' u'order https://docs.getgrist.com/sbaltsirg/Example/p/3?Code_=X1234&Name_=Bi+Ngo'
>>> SELF_HYPERLINK(Linky_Link='Link') >>> SELF_HYPERLINK(Linky_Link='Link')
Traceback (most recent call last): Traceback (most recent call last):
... ...
@ -110,6 +110,7 @@ def SELF_HYPERLINK(label=None, page=None, **kwargs):
txt = os.environ.get('DOC_URL') txt = os.environ.get('DOC_URL')
if not txt: if not txt:
return None return None
txt = six.text_type(txt)
if page: if page:
txt += "/p/{}".format(page) txt += "/p/{}".format(page)
if kwargs: if kwargs:
@ -124,7 +125,7 @@ def SELF_HYPERLINK(label=None, page=None, **kwargs):
parts[4] = urllib_parse.urlencode(query) parts[4] = urllib_parse.urlencode(query)
txt = urllib_parse.urlunparse(parts) txt = urllib_parse.urlunparse(parts)
if label: if label:
txt = "{} {}".format(label, txt) txt = u"{} {}".format(label, txt)
return txt return txt
def VLOOKUP(table, **field_value_pairs): def VLOOKUP(table, **field_value_pairs):

@ -1,16 +1,17 @@
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
import datetime import datetime
import dateutil.parser
import numbers import numbers
import re import re
import dateutil.parser
import six import six
from six import unichr from six import unichr
from six.moves import xrange from six.moves import xrange
from usertypes import AltText # pylint: disable=import-error
from .unimplemented import unimplemented from .unimplemented import unimplemented
from usertypes import AltText # pylint: disable=import-error
def CHAR(table_number): def CHAR(table_number):
""" """
@ -26,7 +27,7 @@ def CHAR(table_number):
# See http://stackoverflow.com/a/93029/328565 # See http://stackoverflow.com/a/93029/328565
_control_chars = ''.join(map(unichr, range(0,32) + range(127,160))) _control_chars = ''.join(map(unichr, list(xrange(0,32)) + list(xrange(127,160))))
_control_char_re = re.compile('[%s]' % re.escape(_control_chars)) _control_char_re = re.compile('[%s]' % re.escape(_control_chars))
def CLEAN(text): def CLEAN(text):
@ -58,7 +59,7 @@ def CODE(string):
def CONCATENATE(string, *more_strings): def CONCATENATE(string, *more_strings):
""" u"""
Joins together any number of text strings into one string. Also available under the name Joins together any number of text strings into one string. Also available under the name
`CONCAT`. Similar to the Python expression `"".join(array_of_strings)`. `CONCAT`. Similar to the Python expression `"".join(array_of_strings)`.
@ -70,11 +71,15 @@ def CONCATENATE(string, *more_strings):
u'abc' u'abc'
>>> CONCATENATE(0, "abc") >>> CONCATENATE(0, "abc")
u'0abc' u'0abc'
>>> CONCATENATE(2, " crème ", "brûlée".decode('utf8')) == "2 crème brûlée".decode('utf8') >>> assert CONCATENATE(2, u" crème ", u"brûlée") == u'2 crème brûlée'
True >>> assert CONCATENATE(2, " crème ", u"brûlée") == u'2 crème brûlée'
>>> assert CONCATENATE(2, " crème ", "brûlée") == u'2 crème brûlée'
""" """
return u''.join(val if isinstance(val, unicode) else str(val).decode('utf8') return u''.join(
for val in (string,) + more_strings) val.decode('utf8') if isinstance(val, six.binary_type) else
six.text_type(val)
for val in (string,) + more_strings
)
def CONCAT(string, *more_strings): def CONCAT(string, *more_strings):
@ -90,8 +95,7 @@ def CONCAT(string, *more_strings):
u'abc' u'abc'
>>> CONCAT(0, "abc") >>> CONCAT(0, "abc")
u'0abc' u'0abc'
>>> CONCAT(2, " crème ", "brûlée".decode('utf8')) == "2 crème brûlée".decode('utf8') >>> assert CONCAT(2, u" crème ", u"brûlée") == u'2 crème brûlée'
True
""" """
return CONCATENATE(string, *more_strings) return CONCATENATE(string, *more_strings)
@ -443,48 +447,57 @@ def SUBSTITUTE(text, old_text, new_text, instance_num=None):
Same as `text.replace(old_text, new_text)` when instance_num is omitted. Same as `text.replace(old_text, new_text)` when instance_num is omitted.
>>> SUBSTITUTE("Sales Data", "Sales", "Cost") >>> SUBSTITUTE("Sales Data", "Sales", "Cost")
'Cost Data' u'Cost Data'
>>> SUBSTITUTE("Quarter 1, 2008", "1", "2", 1) >>> SUBSTITUTE("Quarter 1, 2008", "1", "2", 1)
'Quarter 2, 2008' u'Quarter 2, 2008'
>>> SUBSTITUTE("Quarter 1, 2011", "1", "2", 3) >>> SUBSTITUTE("Quarter 1, 2011", "1", "2", 3)
'Quarter 1, 2012' u'Quarter 1, 2012'
More tests: More tests:
>>> SUBSTITUTE("Hello world", "", "-") >>> SUBSTITUTE("Hello world", "", "-")
'Hello world' u'Hello world'
>>> SUBSTITUTE("Hello world", " ", "-") >>> SUBSTITUTE("Hello world", " ", "-")
'Hello-world' u'Hello-world'
>>> SUBSTITUTE("Hello world", " ", 12.1) >>> SUBSTITUTE("Hello world", " ", 12.1)
'Hello12.1world' u'Hello12.1world'
>>> SUBSTITUTE(u"Hello world", u" ", 12.1) >>> SUBSTITUTE(u"Hello world", u" ", 12.1)
u'Hello12.1world' u'Hello12.1world'
>>> SUBSTITUTE("Hello world", "world", "") >>> SUBSTITUTE("Hello world", "world", "")
'Hello ' u'Hello '
>>> SUBSTITUTE("Hello", "world", "") >>> SUBSTITUTE("Hello", "world", "")
'Hello' u'Hello'
Overlapping matches are all counted when looking for instance_num. Overlapping matches are all counted when looking for instance_num.
>>> SUBSTITUTE('abababab', 'abab', 'xxxx') >>> SUBSTITUTE('abababab', 'abab', 'xxxx')
'xxxxxxxx' u'xxxxxxxx'
>>> SUBSTITUTE('abababab', 'abab', 'xxxx', 1) >>> SUBSTITUTE('abababab', 'abab', 'xxxx', 1)
'xxxxabab' u'xxxxabab'
>>> SUBSTITUTE('abababab', 'abab', 'xxxx', 2) >>> SUBSTITUTE('abababab', 'abab', 'xxxx', 2)
'abxxxxab' u'abxxxxab'
>>> SUBSTITUTE('abababab', 'abab', 'xxxx', 3) >>> SUBSTITUTE('abababab', 'abab', 'xxxx', 3)
'ababxxxx' u'ababxxxx'
>>> SUBSTITUTE('abababab', 'abab', 'xxxx', 4) >>> SUBSTITUTE('abababab', 'abab', 'xxxx', 4)
'abababab' u'abababab'
>>> SUBSTITUTE('abababab', 'abab', 'xxxx', 0) >>> SUBSTITUTE('abababab', 'abab', 'xxxx', 0)
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValueError: instance_num invalid ValueError: instance_num invalid
""" >>> SUBSTITUTE( "crème", "è", "e")
u'creme'
>>> SUBSTITUTE(u"crème", u"è", "e")
u'creme'
>>> SUBSTITUTE(u"crème", "è", "e")
u'creme'
>>> SUBSTITUTE( "crème", u"è", "e")
u'creme'
"""
text = six.text_type(text)
old_text = six.text_type(old_text)
new_text = six.text_type(new_text)
if not old_text: if not old_text:
return text return text
if not isinstance(new_text, six.string_types):
new_text = str(new_text)
if instance_num is None: if instance_num is None:
return text.replace(old_text, new_text) return text.replace(old_text, new_text)
@ -505,22 +518,23 @@ def T(value):
Returns value if value is text, or the empty string when value is not text. Returns value if value is text, or the empty string when value is not text.
>>> T('Text') >>> T('Text')
'Text' u'Text'
>>> T(826) >>> T(826)
'' u''
>>> T('826') >>> T('826')
'826' u'826'
>>> T(False) >>> T(False)
'' u''
>>> T('100 points') >>> T('100 points')
'100 points' u'100 points'
>>> T(AltText('Text')) >>> T(AltText('Text'))
'Text' u'Text'
>>> T(float('nan')) >>> T(float('nan'))
'' u''
""" """
return (value if isinstance(value, basestring) else return (value.decode('utf8') if isinstance(value, six.binary_type) else
str(value) if isinstance(value, AltText) else "") value if isinstance(value, six.text_type) else
six.text_type(value) if isinstance(value, AltText) else u"")
@unimplemented @unimplemented
@ -565,8 +579,7 @@ def VALUE(text):
>>> VALUE("$1,000") >>> VALUE("$1,000")
1000 1000
>>> VALUE("16:48:00") - VALUE("12:00:00") >>> assert VALUE("16:48:00") - VALUE("12:00:00") == datetime.timedelta(0, 17280)
datetime.timedelta(0, 17280)
>>> VALUE("01/01/2012") >>> VALUE("01/01/2012")
datetime.datetime(2012, 1, 1, 0, 0) datetime.datetime(2012, 1, 1, 0, 0)
>>> VALUE("") >>> VALUE("")

@ -36,6 +36,7 @@ def table_data_from_db(table_name, table_data_repr):
if table_data_repr is None: if table_data_repr is None:
return actions.TableData(table_name, [], {}) return actions.TableData(table_name, [], {})
table_data_parsed = marshal.loads(table_data_repr) table_data_parsed = marshal.loads(table_data_repr)
table_data_parsed = {key.decode("utf8"): value for key, value in table_data_parsed.items()}
id_col = table_data_parsed.pop("id") id_col = table_data_parsed.pop("id")
return actions.TableData(table_name, id_col, return actions.TableData(table_name, id_col,
actions.decode_bulk_values(table_data_parsed, _decode_db_value)) actions.decode_bulk_values(table_data_parsed, _decode_db_value))
@ -44,14 +45,8 @@ def _decode_db_value(value):
# Decode database values received from SQLite's allMarshal() call. These are encoded by # Decode database values received from SQLite's allMarshal() call. These are encoded by
# marshalling certain types and storing as BLOBs (received in Python as binary strings, as # marshalling certain types and storing as BLOBs (received in Python as binary strings, as
# opposed to text which is received as unicode). See also encodeValue() in DocStorage.js # opposed to text which is received as unicode). See also encodeValue() in DocStorage.js
# TODO For the moment, the sandbox uses binary strings throughout (with text in utf8 encoding).
# We should switch to representing text with unicode instead. This requires care, at least in
# fixing various occurrences of str() in our code, which may fail and which return wrong type.
t = type(value) t = type(value)
if t == unicode: if t == six.binary_type:
return value.encode('utf8')
elif t == str:
return objtypes.decode_object(marshal.loads(value)) return objtypes.decode_object(marshal.loads(value))
else: else:
return value return value

@ -73,7 +73,8 @@ def create_migrations(all_tables, metadata_only=False):
new_col_info = {c['id']: c for c in new_schema[table_id].columns} new_col_info = {c['id']: c for c in new_schema[table_id].columns}
# Use an incomplete default for unknown (i.e. deprecated) columns; some uses of the column # Use an incomplete default for unknown (i.e. deprecated) columns; some uses of the column
# would be invalid, such as adding a new record with missing values. # would be invalid, such as adding a new record with missing values.
col_info = sorted([new_col_info.get(col_id, {'id': col_id}) for col_id in data.columns]) col_info = sorted([new_col_info.get(col_id, {'id': col_id}) for col_id in data.columns],
key=lambda c: list(six.iteritems(c)))
tdset.apply_doc_action(actions.AddTable(table_id, col_info)) tdset.apply_doc_action(actions.AddTable(table_id, col_info))
# And load in the original data, interpreting the TableData object as BulkAddRecord action. # And load in the original data, interpreting the TableData object as BulkAddRecord action.
@ -177,7 +178,7 @@ def migration1(tdset):
if rows: if rows:
values = {'tableRef': [r[0] for r in rows], values = {'tableRef': [r[0] for r in rows],
'viewRef': [r[1] for r in rows]} 'viewRef': [r[1] for r in rows]}
row_ids = range(1, len(rows) + 1) row_ids = list(xrange(1, len(rows) + 1))
doc_actions.append(actions.ReplaceTableData('_grist_TabItems', row_ids, values)) doc_actions.append(actions.ReplaceTableData('_grist_TabItems', row_ids, values))
return tdset.apply_doc_actions(doc_actions) return tdset.apply_doc_actions(doc_actions)
@ -212,14 +213,14 @@ def migration2(tdset):
return actions.BulkUpdateRecord('_grist_Tables', row_ids, values) return actions.BulkUpdateRecord('_grist_Tables', row_ids, values)
def create_tab_bar_action(views_to_table): def create_tab_bar_action(views_to_table):
row_ids = range(1, len(views_to_table) + 1) row_ids = list(xrange(1, len(views_to_table) + 1))
return actions.ReplaceTableData('_grist_TabBar', row_ids, { return actions.ReplaceTableData('_grist_TabBar', row_ids, {
'viewRef': sorted(views_to_table.keys()) 'viewRef': sorted(views_to_table.keys())
}) })
def create_table_views_action(views_to_table, primary_views): def create_table_views_action(views_to_table, primary_views):
related_views = sorted(set(views_to_table.keys()) - set(primary_views.values())) related_views = sorted(set(views_to_table.keys()) - set(primary_views.values()))
row_ids = range(1, len(related_views) + 1) row_ids = list(xrange(1, len(related_views) + 1))
return actions.ReplaceTableData('_grist_TableViews', row_ids, { return actions.ReplaceTableData('_grist_TableViews', row_ids, {
'tableRef': [views_to_table[v] for v in related_views], 'tableRef': [views_to_table[v] for v in related_views],
'viewRef': related_views, 'viewRef': related_views,
@ -757,7 +758,7 @@ def migration20(tdset):
# the name of primary view's is the same as the tableId # the name of primary view's is the same as the tableId
return (view.name, -1) return (view.name, -1)
views.sort(key=view_key) views.sort(key=view_key)
row_ids = range(1, len(views) + 1) row_ids = list(xrange(1, len(views) + 1))
return tdset.apply_doc_actions([ return tdset.apply_doc_actions([
actions.AddTable('_grist_Pages', [ actions.AddTable('_grist_Pages', [
schema.make_column('viewRef', 'Ref:_grist_Views'), schema.make_column('viewRef', 'Ref:_grist_Views'),

@ -142,8 +142,9 @@ class TzInfo(_tzinfo):
def tzname(self, dt): def tzname(self, dt):
"""Implementation of tzinfo.tzname interface.""" """Implementation of tzinfo.tzname interface."""
abbr = self.zone.dt_tzname(dt, self._favor_offset) abbr = self.zone.dt_tzname(dt, self._favor_offset)
# tzname must return a string, not unicode. if six.PY2 and isinstance(abbr, six.text_type):
return abbr.encode('utf8') if isinstance(abbr, unicode) else abbr abbr = abbr.encode('utf8')
return abbr
def dst(self, dt): def dst(self, dt):
"""Implementation of tzinfo.dst interface.""" """Implementation of tzinfo.dst interface."""

@ -47,6 +47,7 @@ DATE_TOKENS_REGEX = re.compile("("+("|".join(DATE_TOKENS))+")")
# List of separators to replace and match any standard date/time separators # List of separators to replace and match any standard date/time separators
SEP = r"[\s/.\-:,]*" SEP = r"[\s/.\-:,]*"
SEP_REGEX = re.compile(SEP) SEP_REGEX = re.compile(SEP)
SEP_REPLACEMENT = SEP.replace("\\", "\\\\")
# Maps date parse format to compile regex # Maps date parse format to compile regex
FORMAT_CACHE = {} FORMAT_CACHE = {}
@ -77,7 +78,8 @@ def parse(date_string, parse_format, zonelabel='UTC', override_current_date=None
# e.g. "MM-YY" -> "(?P<mm>\d{1,2})-(?P<yy>\d{2})" # e.g. "MM-YY" -> "(?P<mm>\d{1,2})-(?P<yy>\d{2})"
# Note that DATE_TOKENS is ordered so that the longer letter chains are recognized first # Note that DATE_TOKENS is ordered so that the longer letter chains are recognized first
tokens = DATE_TOKENS_REGEX.split(parse_format) tokens = DATE_TOKENS_REGEX.split(parse_format)
tokens = [DATE_TOKENS[t] if t in DATE_TOKENS else SEP_REGEX.sub(SEP, t) for t in tokens] tokens = [DATE_TOKENS[t] if t in DATE_TOKENS else SEP_REGEX.sub(SEP_REPLACEMENT, t)
for t in tokens]
# Compile new token string ignoring case (for month names) # Compile new token string ignoring case (for month names)
parser = re.compile(''.join(tokens), re.I) parser = re.compile(''.join(tokens), re.I)

@ -163,14 +163,16 @@ def encode_object(value):
Returns ['U', repr(value)] if it fails to encode otherwise. Returns ['U', repr(value)] if it fails to encode otherwise.
""" """
try: try:
if isinstance(value, (str, unicode, float, bool)) or value is None: if isinstance(value, (six.text_type, float, bool)) or value is None:
return value return value
elif isinstance(value, (long, int)): elif isinstance(value, six.binary_type):
return value.decode('utf8')
elif isinstance(value, six.integer_types):
if not is_int_short(value): if not is_int_short(value):
raise UnmarshallableError("Integer too large") raise UnmarshallableError("Integer too large")
return value return value
elif isinstance(value, AltText): elif isinstance(value, AltText):
return str(value) return six.text_type(value)
elif isinstance(value, records.Record): elif isinstance(value, records.Record):
return ['R', value._table.table_id, value._row_id] return ['R', value._table.table_id, value._row_id]
elif isinstance(value, RecordStub): elif isinstance(value, RecordStub):
@ -210,13 +212,6 @@ def decode_object(value):
""" """
try: try:
if not isinstance(value, (list, tuple)): if not isinstance(value, (list, tuple)):
if isinstance(value, unicode):
# TODO For now, the sandbox uses binary strings throughout; see TODO in main.py for more
# on this. Strings that come from JS become Python binary strings, and we will not see
# unicode here. But we may see it if unmarshalling data that comes from DB, since
# DocStorage encodes/decodes values by marshaling JS strings as unicode. For consistency,
# convert those unicode strings to binary strings too.
return value.encode('utf8')
return value return value
code = value[0] code = value[0]
args = value[1:] args = value[1:]

@ -6,9 +6,10 @@ slight changes in order to be convenient for Grist's purposes
import code import code
import sys import sys
from StringIO import StringIO
from collections import namedtuple from collections import namedtuple
import six
SUCCESS = 0 SUCCESS = 0
INCOMPLETE = 1 INCOMPLETE = 1
ERROR = 2 ERROR = 2
@ -38,7 +39,7 @@ class REPLInterpreter(code.InteractiveInterpreter):
old_stdout = sys.stdout old_stdout = sys.stdout
old_stderr = sys.stderr old_stderr = sys.stderr
user_output = StringIO() user_output = six.StringIO()
self.error_text = "" self.error_text = ""
try: try:
@ -67,7 +68,10 @@ class REPLInterpreter(code.InteractiveInterpreter):
sys.stderr = old_stderr sys.stderr = old_stderr
program_output = user_output.getvalue() program_output = user_output.getvalue()
user_output.close() try:
user_output.close()
except:
pass
return EvalTuple(program_output, self.error_text, status) return EvalTuple(program_output, self.error_text, status)

@ -92,7 +92,6 @@ class TestCodeBuilder(unittest.TestCase):
# Check for reasonable behaviour with non-empty text and no statements. # Check for reasonable behaviour with non-empty text and no statements.
self.assertEqual(make_body('# comment'), '# comment\npass') self.assertEqual(make_body('# comment'), '# comment\npass')
self.assertEqual(make_body('\\'), '\\\npass')
self.assertEqual(make_body('rec = 1'), "# rec = 1\n" + self.assertEqual(make_body('rec = 1'), "# rec = 1\n" +
"raise SyntaxError('Grist disallows assignment " + "raise SyntaxError('Grist disallows assignment " +

@ -1,5 +1,9 @@
import doctest import doctest
import os import os
import re
import six
import functions import functions
import moment import moment
@ -15,6 +19,12 @@ def date_tearDown(doc_test):
# pylint: disable=unused-argument # pylint: disable=unused-argument
functions.date._get_global_tz = _old_date_get_global_tz functions.date._get_global_tz = _old_date_get_global_tz
class Py23DocChecker(doctest.OutputChecker):
def check_output(self, want, got, optionflags):
if six.PY3:
want = re.sub(r"^u'(.*?)'$", r"'\1'", want)
want = re.sub(r'^u"(.*?)"$', r'"\1"', want)
return doctest.OutputChecker.check_output(self, want, got, optionflags)
# This works with the unittest module to turn all the doctests in the functions' doc-comments into # This works with the unittest module to turn all the doctests in the functions' doc-comments into
# unittest test cases. # unittest test cases.
@ -26,8 +36,8 @@ def load_tests(loader, tests, ignore):
tests.addTests(doctest.DocTestSuite(functions.logical)) tests.addTests(doctest.DocTestSuite(functions.logical))
tests.addTests(doctest.DocTestSuite(functions.math)) tests.addTests(doctest.DocTestSuite(functions.math))
tests.addTests(doctest.DocTestSuite(functions.stats)) tests.addTests(doctest.DocTestSuite(functions.stats))
tests.addTests(doctest.DocTestSuite(functions.text)) tests.addTests(doctest.DocTestSuite(functions.text, checker=Py23DocChecker()))
tests.addTests(doctest.DocTestSuite(functions.schedule, tests.addTests(doctest.DocTestSuite(functions.schedule,
setUp = date_setUp, tearDown = date_tearDown)) setUp = date_setUp, tearDown = date_tearDown))
tests.addTests(doctest.DocTestSuite(functions.lookup)) tests.addTests(doctest.DocTestSuite(functions.lookup, checker=Py23DocChecker()))
return tests return tests

@ -256,7 +256,7 @@ class TestRelabeling(unittest.TestCase):
self._do_test_renumber_ends([]) self._do_test_renumber_ends([])
def test_renumber_endpoints2(self): def test_renumber_endpoints2(self):
self._do_test_renumber_ends(zip("abcd", [40,50,60,70])) self._do_test_renumber_ends(list(zip("abcd", [40,50,60,70])))
def _do_test_renumber_ends(self, initial): def _do_test_renumber_ends(self, initial):
# Test insertions that happen together on the left and on the right. # Test insertions that happen together on the left and on the right.

@ -330,23 +330,23 @@ class TestRenames(test_engine.EngineTestCase):
def test_renames_with_non_ascii(self): def test_renames_with_non_ascii(self):
# Test that presence of unicode does not interfere with formula adjustments for renaming. # Test that presence of unicode does not interfere with formula adjustments for renaming.
self.load_sample(self.sample) self.load_sample(self.sample)
self.add_column("Address", "CityUpper", formula="'Øî'+$city.upper()+'áü'") self.add_column("Address", "CityUpper", formula=u"'Øî'+$city.upper()+'áü'")
out_actions = self.apply_user_action(["RenameColumn", "Address", "city", "ciudad"]) out_actions = self.apply_user_action(["RenameColumn", "Address", "city", "ciudad"])
self.assertPartialOutActions(out_actions, { "stored": [ self.assertPartialOutActions(out_actions, { "stored": [
["RenameColumn", "Address", "city", "ciudad"], ["RenameColumn", "Address", "city", "ciudad"],
["ModifyColumn", "People", "city", {"formula": "$addr.ciudad"}], ["ModifyColumn", "People", "city", {"formula": "$addr.ciudad"}],
["ModifyColumn", "Address", "CityUpper", {"formula": "'Øî'+$ciudad.upper()+'áü'"}], ["ModifyColumn", "Address", "CityUpper", {"formula": u"'Øî'+$ciudad.upper()+'áü'"}],
["BulkUpdateRecord", "_grist_Tables_column", [21, 24, 25], { ["BulkUpdateRecord", "_grist_Tables_column", [21, 24, 25], {
"colId": ["ciudad", "city", "CityUpper"], "colId": ["ciudad", "city", "CityUpper"],
"formula": ["", "$addr.ciudad", "'Øî'+$ciudad.upper()+'áü'"], "formula": ["", "$addr.ciudad", u"'Øî'+$ciudad.upper()+'áü'"],
}] }]
]}) ]})
self.assertTableData("Address", cols="all", data=[ self.assertTableData("Address", cols="all", data=[
["id", "ciudad", "CityUpper"], ["id", "ciudad", "CityUpper"],
[11, "New York", "ØîNEW YORKáü"], [11, "New York", u"ØîNEW YORKáü"],
[12, "Colombia", "ØîCOLOMBIAáü"], [12, "Colombia", u"ØîCOLOMBIAáü"],
[13, "New Haven", "ØîNEW HAVENáü"], [13, "New Haven", u"ØîNEW HAVENáü"],
[14, "West Haven", "ØîWEST HAVENáü"], [14, "West Haven", u"ØîWEST HAVENáü"],
]) ])
def test_rename_updates_properties(self): def test_rename_updates_properties(self):

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# pylint: disable=line-too-long # pylint: disable=line-too-long
import six
import logger import logger
import testutil import testutil
@ -25,7 +26,7 @@ class TestTypes(test_engine.EngineTestCase):
"Types": [ "Types": [
["id", "text", "numeric", "int", "bool", "date"], ["id", "text", "numeric", "int", "bool", "date"],
[11, "New York", "New York", "New York", "New York", "New York"], [11, "New York", "New York", "New York", "New York", "New York"],
[12, "Chîcágö", "Chîcágö", "Chîcágö", "Chîcágö", "Chîcágö"], [12, u"Chîcágö", u"Chîcágö", u"Chîcágö", u"Chîcágö", u"Chîcágö"],
[13, False, False, False, False, False], [13, False, False, False, False, False],
[14, True, True, True, True, True], [14, True, True, True, True, True],
[15, 1509556595, 1509556595, 1509556595, 1509556595, 1509556595], [15, 1509556595, 1509556595, 1509556595, 1509556595, 1509556595],
@ -61,20 +62,20 @@ class TestTypes(test_engine.EngineTestCase):
self.assertPartialOutActions(out_actions, { self.assertPartialOutActions(out_actions, {
"stored": [["BulkUpdateRecord", "Types", self.all_row_ids, { "stored": [["BulkUpdateRecord", "Types", self.all_row_ids, {
"text": [None,"","1","0","8.153","1509556595","True","False","Chîcágö","New York"], "text": [None,"","1","0","8.153","1509556595","True","False",u"Chîcágö","New York"],
"numeric": [None, None, 1.0, 0.0, 8.153, 1509556595.0, 1.0, 0.0, "Chîcágö", "New York"], "numeric": [None, None, 1.0, 0.0, 8.153, 1509556595.0, 1.0, 0.0, u"Chîcágö", "New York"],
"int": [None, None, 1, 0, 8, 1509556595, 1, 0, "Chîcágö", "New York"], "int": [None, None, 1, 0, 8, 1509556595, 1, 0, u"Chîcágö", "New York"],
"bool": [False, False, True, False, True, True, True, False, "Chîcágö", "New York"], "bool": [False, False, True, False, True, True, True, False, u"Chîcágö", "New York"],
"date": [None, None, 1.0, 0.0, 8.153, 1509556595.0, 1.0, 0.0, 1548115200.0, "New York"] "date": [None, None, 1.0, 0.0, 8.153, 1509556595.0, 1.0, 0.0, 1548115200.0, "New York"]
}], }],
["UpdateRecord", "Formulas", 1, {"division": 0.0}], ["UpdateRecord", "Formulas", 1, {"division": 0.0}],
], ],
"undo": [["BulkUpdateRecord", "Types", self.all_row_ids, { "undo": [["BulkUpdateRecord", "Types", self.all_row_ids, {
"text": ["New York", "Chîcágö", False, True, 1509556595, 8.153, 0, 1, "", None], "text": ["New York", u"Chîcágö", False, True, 1509556595, 8.153, 0, 1, "", None],
"numeric": ["New York", "Chîcágö", False, True, 1509556595, 8.153, 0, 1, "", None], "numeric": ["New York", u"Chîcágö", False, True, 1509556595, 8.153, 0, 1, "", None],
"int": ["New York", "Chîcágö", False, True, 1509556595, 8.153, 0, 1, "", None], "int": ["New York", u"Chîcágö", False, True, 1509556595, 8.153, 0, 1, "", None],
"bool": ["New York", "Chîcágö", False, True, 1509556595, 8.153, False, True, "", None], "bool": ["New York", u"Chîcágö", False, True, 1509556595, 8.153, False, True, "", None],
"date": ["New York", "Chîcágö", False, True, 1509556595, 8.153, 0, 1, "", None] "date": ["New York", u"Chîcágö", False, True, 1509556595, 8.153, 0, 1, "", None]
}], }],
["UpdateRecord", "Formulas", 1, {"division": 0.5}], ["UpdateRecord", "Formulas", 1, {"division": 0.5}],
] ]
@ -90,7 +91,7 @@ class TestTypes(test_engine.EngineTestCase):
[16, "1509556595", 1509556595, 1509556595, True, 1509556595.0], [16, "1509556595", 1509556595, 1509556595, True, 1509556595.0],
[17, "True", 1.0, 1, True, 1.0], [17, "True", 1.0, 1, True, 1.0],
[18, "False", 0.0, 0, False, 0.0], [18, "False", 0.0, 0, False, 0.0],
[19, "Chîcágö", "Chîcágö", "Chîcágö", "Chîcágö", 1548115200.0], [19, u"Chîcágö", u"Chîcágö", u"Chîcágö", u"Chîcágö", 1548115200.0],
[20, "New York", "New York", "New York", "New York", "New York"] [20, "New York", "New York", "New York", "New York", "New York"]
]) ])
@ -184,7 +185,7 @@ class TestTypes(test_engine.EngineTestCase):
self.assertTableData("Types", data=[ self.assertTableData("Types", data=[
["id", "text", "numeric", "int", "bool", "date"], ["id", "text", "numeric", "int", "bool", "date"],
[11, "New York", "New York", "New York", "New York", "New York"], [11, "New York", "New York", "New York", "New York", "New York"],
[12, "Chîcágö", "Chîcágö", "Chîcágö", "Chîcágö", "Chîcágö"], [12, u"Chîcágö", u"Chîcágö", u"Chîcágö", u"Chîcágö", u"Chîcágö"],
[13, False, "False", "False", "False", "False"], [13, False, "False", "False", "False", "False"],
[14, True, "True", "True", "True", "True"], [14, True, "True", "True", "True", "True"],
[15, 1509556595, "1509556595.0","1509556595","1509556595","1509556595.0"], [15, 1509556595, "1509556595.0","1509556595","1509556595","1509556595.0"],
@ -283,7 +284,7 @@ class TestTypes(test_engine.EngineTestCase):
self.assertTableData("Types", data=[ self.assertTableData("Types", data=[
["id", "text", "numeric", "int", "bool", "date"], ["id", "text", "numeric", "int", "bool", "date"],
[11, "New York", "New York", "New York", "New York", "New York"], [11, "New York", "New York", "New York", "New York", "New York"],
[12, "Chîcágö", "Chîcágö", "Chîcágö", "Chîcágö", "Chîcágö"], [12, u"Chîcágö", u"Chîcágö", u"Chîcágö", u"Chîcágö", u"Chîcágö"],
[13, 0.0, False, 0.0, 0.0, 0.0], [13, 0.0, False, 0.0, 0.0, 0.0],
[14, 1.0, True, 1.0, 1.0, 1.0], [14, 1.0, True, 1.0, 1.0, 1.0],
[15, 1509556595, 1509556595, 1509556595, 1509556595, 1509556595], [15, 1509556595, 1509556595, 1509556595, 1509556595, 1509556595],
@ -327,15 +328,13 @@ class TestTypes(test_engine.EngineTestCase):
["BulkUpdateRecord", "Types", [13, 14, 16, 19], ["BulkUpdateRecord", "Types", [13, 14, 16, 19],
{"numeric": [0, 1, 8, None]}], {"numeric": [0, 1, 8, None]}],
["UpdateRecord", "_grist_Tables_column", 22, {"type": "Int"}], ["UpdateRecord", "_grist_Tables_column", 22, {"type": "Int"}],
["UpdateRecord", "Formulas", 1, {"division": 0}], ] + six.PY2 * [["UpdateRecord", "Formulas", 1, {"division": 0}]], # Only in Python 2 due to integer division,
],
"undo": [ "undo": [
["BulkUpdateRecord", "Types", [13, 14, 16, 19], ["BulkUpdateRecord", "Types", [13, 14, 16, 19],
{"numeric": [False, True, 8.153, ""]}], {"numeric": [False, True, 8.153, ""]}],
["ModifyColumn", "Types", "numeric", {"type": "Numeric"}], ["ModifyColumn", "Types", "numeric", {"type": "Numeric"}],
["UpdateRecord", "_grist_Tables_column", 22, {"type": "Numeric"}], ["UpdateRecord", "_grist_Tables_column", 22, {"type": "Numeric"}],
["UpdateRecord", "Formulas", 1, {"division": 0.5}], ] + six.PY2 * [["UpdateRecord", "Formulas", 1, {"division": 0.5}]], # Only in Python 2 due to integer division
]
}) })
# Test Int -> Int conversion # Test Int -> Int conversion
@ -383,7 +382,7 @@ class TestTypes(test_engine.EngineTestCase):
self.assertTableData("Types", data=[ self.assertTableData("Types", data=[
["id", "text", "numeric", "int", "bool", "date"], ["id", "text", "numeric", "int", "bool", "date"],
[11, "New York", "New York", "New York", "New York", "New York"], [11, "New York", "New York", "New York", "New York", "New York"],
[12, "Chîcágö", "Chîcágö", "Chîcágö", "Chîcágö", "Chîcágö"], [12, u"Chîcágö", u"Chîcágö", u"Chîcágö", u"Chîcágö", u"Chîcágö"],
[13, 0, 0, False, 0, 0], [13, 0, 0, False, 0, 0],
[14, 1, 1, True, 1, 1], [14, 1, 1, True, 1, 1],
[15, 1509556595, 1509556595, 1509556595, 1509556595, 1509556595], [15, 1509556595, 1509556595, 1509556595, 1509556595, 1509556595],
@ -428,15 +427,13 @@ class TestTypes(test_engine.EngineTestCase):
["BulkUpdateRecord", "Types", [15, 16, 17, 18, 19, 20], ["BulkUpdateRecord", "Types", [15, 16, 17, 18, 19, 20],
{"numeric": [True, True, False, True, False, False]}], {"numeric": [True, True, False, True, False, False]}],
["UpdateRecord", "_grist_Tables_column", 22, {"type": "Bool"}], ["UpdateRecord", "_grist_Tables_column", 22, {"type": "Bool"}],
["UpdateRecord", "Formulas", 1, {"division": 0}], ] + six.PY2 * [["UpdateRecord", "Formulas", 1, {"division": 0}]], # Only in Python 2 due to integer division,
],
"undo": [ "undo": [
["BulkUpdateRecord", "Types", [15, 16, 17, 18, 19, 20], ["BulkUpdateRecord", "Types", [15, 16, 17, 18, 19, 20],
{"numeric": [1509556595.0, 8.153, 0.0, 1.0, "", None]}], {"numeric": [1509556595.0, 8.153, 0.0, 1.0, "", None]}],
["ModifyColumn", "Types", "numeric", {"type": "Numeric"}], ["ModifyColumn", "Types", "numeric", {"type": "Numeric"}],
["UpdateRecord", "_grist_Tables_column", 22, {"type": "Numeric"}], ["UpdateRecord", "_grist_Tables_column", 22, {"type": "Numeric"}],
["UpdateRecord", "Formulas", 1, {"division": 0.5}], ] + six.PY2 * [["UpdateRecord", "Formulas", 1, {"division": 0.5}]], # Only in Python 2 due to integer division
]
}) })
# Test Int -> Bool conversion # Test Int -> Bool conversion
@ -484,7 +481,7 @@ class TestTypes(test_engine.EngineTestCase):
self.assertTableData("Types", data=[ self.assertTableData("Types", data=[
["id", "text", "numeric", "int", "bool", "date"], ["id", "text", "numeric", "int", "bool", "date"],
[11, "New York", "New York", "New York", "New York", "New York"], [11, "New York", "New York", "New York", "New York", "New York"],
[12, "Chîcágö", "Chîcágö", "Chîcágö", "Chîcágö", "Chîcágö"], [12, u"Chîcágö", u"Chîcágö", u"Chîcágö", u"Chîcágö", u"Chîcágö"],
[13, False, False, False, False, False], [13, False, False, False, False, False],
[14, True, True, True, True, True], [14, True, True, True, True, True],
[15, True, True, True, 1509556595, True], [15, True, True, True, 1509556595, True],
@ -585,7 +582,7 @@ class TestTypes(test_engine.EngineTestCase):
self.assertTableData("Types", data=[ self.assertTableData("Types", data=[
["id", "text", "numeric", "int", "bool", "date"], ["id", "text", "numeric", "int", "bool", "date"],
[11, "New York", "New York", "New York", "New York", "New York"], [11, "New York", "New York", "New York", "New York", "New York"],
[12, "Chîcágö", "Chîcágö", "Chîcágö", "Chîcágö", "Chîcágö"], [12, u"Chîcágö", u"Chîcágö", u"Chîcágö", u"Chîcágö", u"Chîcágö"],
[13, 0.0, 0.0, 0.0, 0.0, False], [13, 0.0, 0.0, 0.0, 0.0, False],
[14, 1.0, 1.0, 1.0, 1.0, True], [14, 1.0, 1.0, 1.0, 1.0, True],
[15, 1509556595, 1509556595, 1509556595, 1509556595, 1509556595], [15, 1509556595, 1509556595, 1509556595, 1509556595, 1509556595],

@ -0,0 +1,19 @@
# coding=utf-8
import unittest
import urllib
import six
from urllib_patch import original_quote
class TestUrllibPatch(unittest.TestCase):
def test_patched_quote(self):
self.assertEqual(urllib.quote( "a b"), u"a%20b")
self.assertEqual(urllib.quote(u"a b"), u"a%20b")
self.assertEqual(urllib.quote(u"a é"), u"a%20%C3%A9")
self.assertEqual(original_quote( "a b"), u"a%20b")
self.assertEqual(original_quote(u"a b"), u"a%20b")
if six.PY3: # python 2 original quote can't handle non-ascii
self.assertEqual(original_quote(u"a é"), u"a%20%C3%A9")

@ -2768,7 +2768,7 @@
["APPLY", { ["APPLY", {
"USER_ACTIONS": [ "USER_ACTIONS": [
// Access to usercode before and after re-generation // Access to usercode before and after re-generation
["EvalCode", "list(Students.all.firstName)", null], ["EvalCode", "list(map(str, Students.all.firstName))", null],
["UpdateRecord", "Students", 1, {"firstName": "2e6"}], ["UpdateRecord", "Students", 1, {"firstName": "2e6"}],
["ModifyColumn", "Students", "firstName", { "type" : "Numeric" }], ["ModifyColumn", "Students", "firstName", { "type" : "Numeric" }],
["EvalCode", "list(Students.all.firstName)", 6], ["EvalCode", "list(Students.all.firstName)", 6],
@ -2778,7 +2778,7 @@
"ACTIONS": { "ACTIONS": {
"stored": [ "stored": [
["AddRecord", "_grist_REPL_Hist", 6, ["AddRecord", "_grist_REPL_Hist", 6,
{"code": "list(Students.all.firstName)", "errorText": "", "outputText": "['Barack', 'George W', 'Bill', 'George H', 'Ronald', 'Jimmy', 'Gerald']\n"}], {"code": "list(map(str, Students.all.firstName))", "errorText": "", "outputText": "['Barack', 'George W', 'Bill', 'George H', 'Ronald', 'Jimmy', 'Gerald']\n"}],
["UpdateRecord", "Students", 1, {"firstName": "2e6"}], ["UpdateRecord", "Students", 1, {"firstName": "2e6"}],
["ModifyColumn", "Students", "firstName", {"type": "Numeric"}], ["ModifyColumn", "Students", "firstName", {"type": "Numeric"}],
@ -2808,7 +2808,7 @@
["ModifyColumn", "Students", "firstName", {"type": "Text"}], ["ModifyColumn", "Students", "firstName", {"type": "Text"}],
["UpdateRecord", "_grist_Tables_column", 1, {"type": "Text"}], ["UpdateRecord", "_grist_Tables_column", 1, {"type": "Text"}],
["UpdateRecord", "_grist_REPL_Hist", 6, {"code": "list(Students.all.firstName)", ["UpdateRecord", "_grist_REPL_Hist", 6, {"code": "list(map(str, Students.all.firstName))",
"errorText": "", "outputText": "['Barack', 'George W', 'Bill', 'George H', 'Ronald', 'Jimmy', 'Gerald']\n"}], "errorText": "", "outputText": "['Barack', 'George W', 'Bill', 'George H', 'Ronald', 'Jimmy', 'Gerald']\n"}],
["UpdateRecord", "_grist_REPL_Hist", 6, {"code": "list(Students.all.firstName)", ["UpdateRecord", "_grist_REPL_Hist", 6, {"code": "list(Students.all.firstName)",
"errorText": "", "errorText": "",
@ -2825,7 +2825,7 @@
["APPLY", { ["APPLY", {
"USER_ACTIONS": [ "USER_ACTIONS": [
// Syntax Error // Syntax Error
["EvalCode", "*!@&$fjjj112#(8!", null], ["EvalCode", "not correct c", null],
// Other continuations // Other continuations
["EvalCode", "map(filter, ", null], ["EvalCode", "map(filter, ", null],
["EvalCode", "[1,2,3,", null], ["EvalCode", "[1,2,3,", null],
@ -2834,15 +2834,14 @@
["EvalCode", "sys.exit(0)", null], ["EvalCode", "sys.exit(0)", null],
// User reassignment of sys.stdout/sys.stderr // User reassignment of sys.stdout/sys.stderr
["EvalCode", "sys.stdout = None", null], ["EvalCode", "sys.stdout = None", null],
["EvalCode", "delattr(sys.stderr, 'close')", null],
["EvalCode", "setattr(sys.stdout, 'getvalue', lambda : 2)", null], ["EvalCode", "setattr(sys.stdout, 'getvalue', lambda : 2)", null],
["EvalCode", "def foo():\n global stdout\n exec 'stdout = 2'\n", null], ["EvalCode", "def foo():\n global stdout\n exec('stdout = 2')\n", null],
["EvalCode", "setattr(sys.stderr, 'close', foo)", null] ["EvalCode", "setattr(sys.stderr, 'close', foo)", null]
], ],
"ACTIONS": { "ACTIONS": {
"stored": [ "stored": [
["AddRecord", "_grist_REPL_Hist", 7, ["AddRecord", "_grist_REPL_Hist", 7,
{"code": "*!@&$fjjj112#(8!", "errorText": " File \"<input>\", line 1\n *!@&$fjjj112#(8!\n ^\nSyntaxError: invalid syntax\n", "outputText": ""}], {"code": "not correct c", "errorText": " File \"<input>\", line 1\n not correct c\n ^\nSyntaxError: invalid syntax\n", "outputText": ""}],
["AddRecord", "_grist_REPL_Hist", 8, ["AddRecord", "_grist_REPL_Hist", 8,
{"code": "import sys", "errorText": "", "outputText": ""}], {"code": "import sys", "errorText": "", "outputText": ""}],
["AddRecord", "_grist_REPL_Hist", 9, ["AddRecord", "_grist_REPL_Hist", 9,
@ -2850,15 +2849,13 @@
["AddRecord", "_grist_REPL_Hist", 10, ["AddRecord", "_grist_REPL_Hist", 10,
{"code": "sys.stdout = None", "errorText": "", "outputText": ""}], {"code": "sys.stdout = None", "errorText": "", "outputText": ""}],
["AddRecord", "_grist_REPL_Hist", 11, ["AddRecord", "_grist_REPL_Hist", 11,
{"code": "delattr(sys.stderr, 'close')", "errorText": "Traceback (most recent call last):\n File \"<input>\", line 1, in <module>\nAttributeError: StringIO instance has no attribute 'close'\n", "outputText": ""}],
["AddRecord", "_grist_REPL_Hist", 12,
{"code": "setattr(sys.stdout, 'getvalue', lambda : 2)", "errorText": "", "outputText": 2}], {"code": "setattr(sys.stdout, 'getvalue', lambda : 2)", "errorText": "", "outputText": 2}],
["AddRecord", "_grist_REPL_Hist", 12,
{"code": "def foo():\n global stdout\n exec('stdout = 2')\n", "errorText": "", "outputText": ""}],
["AddRecord", "_grist_REPL_Hist", 13, ["AddRecord", "_grist_REPL_Hist", 13,
{"code": "def foo():\n global stdout\n exec 'stdout = 2'\n", "errorText": "", "outputText": ""}],
["AddRecord", "_grist_REPL_Hist", 14,
{"code": "setattr(sys.stderr, 'close', foo)", "errorText": "", "outputText": ""}] {"code": "setattr(sys.stderr, 'close', foo)", "errorText": "", "outputText": ""}]
], ],
"direct": [true, true, true, true, true, true, true, true], "direct": [true, true, true, true, true, true, true],
"undo": [ "undo": [
["RemoveRecord", "_grist_REPL_Hist", 7], ["RemoveRecord", "_grist_REPL_Hist", 7],
["RemoveRecord", "_grist_REPL_Hist", 8], ["RemoveRecord", "_grist_REPL_Hist", 8],
@ -2866,10 +2863,9 @@
["RemoveRecord", "_grist_REPL_Hist", 10], ["RemoveRecord", "_grist_REPL_Hist", 10],
["RemoveRecord", "_grist_REPL_Hist", 11], ["RemoveRecord", "_grist_REPL_Hist", 11],
["RemoveRecord", "_grist_REPL_Hist", 12], ["RemoveRecord", "_grist_REPL_Hist", 12],
["RemoveRecord", "_grist_REPL_Hist", 13], ["RemoveRecord", "_grist_REPL_Hist", 13]
["RemoveRecord", "_grist_REPL_Hist", 14]
], ],
"retValue" : [true,false,false,true,true,true,true,true,true,true ] "retValue" : [true,false,false,true,true,true,true,true,true ]
} }
}] }]
] ]

@ -59,7 +59,7 @@ def parse_testscript(script_path=None):
all_lines.append(line) all_lines.append(line)
full_text = "".join(all_lines) full_text = "".join(all_lines)
script = byteify(json.loads(full_text)) script = json.loads(full_text)
samples = {} samples = {}
test_cases = [] test_cases = []
@ -109,16 +109,6 @@ def parse_test_sample(obj, samples={}):
return {"SCHEMA": schema, "DATA": data} return {"SCHEMA": schema, "DATA": data}
def byteify(data):
"""
Convert all unicode strings in a parsed JSON object into utf8-encoded strings. We deal with
utf8-encoded strings throughout the test.
"""
if isinstance(data, unicode):
return data.encode('utf-8')
return actions.convert_recursive_helper(byteify, data)
def replace_nans(data): def replace_nans(data):
""" """
Convert all NaNs and Infinities in the data to descriptive strings, since they cannot be Convert all NaNs and Infinities in the data to descriptive strings, since they cannot be

@ -157,7 +157,11 @@ class Combiner(Builder):
def __init__(self, parts): def __init__(self, parts):
self._parts = parts self._parts = parts
self._offsets = [] self._offsets = []
text_parts = [(p if isinstance(p, basestring) else p.get_text()) for p in self._parts] text_parts = [
(p if isinstance(p, six.text_type) else
p.decode('utf8') if isinstance(p, six.binary_type) else
p.get_text())
for p in self._parts]
self._text = ''.join(text_parts) self._text = ''.join(text_parts)
offset = 0 offset = 0

@ -0,0 +1,16 @@
import urllib
import six
from six.moves import urllib_parse
original_quote = urllib_parse.quote
def patched_quote(s, safe='/'):
if isinstance(s, six.text_type):
s = s.encode('utf8')
result = original_quote(s, safe=safe)
if isinstance(result, six.binary_type):
result = result.decode('utf8')
return result
urllib.quote = patched_quote

@ -5,6 +5,7 @@ import json
import sys import sys
import six import six
from six.moves import xrange
import acl import acl
from acl_formula import parse_acl_formula_json from acl_formula import parse_acl_formula_json
@ -109,7 +110,7 @@ def from_repr(user_action):
try: try:
return action_type(*user_action[1:]) return action_type(*user_action[1:])
except TypeError as e: except TypeError as e:
raise TypeError("%s: %s" % (user_action[0], e.message)) raise TypeError("%s: %s" % (user_action[0], str(e)))
def _make_clean_col_info(col_info, col_id=None): def _make_clean_col_info(col_info, col_id=None):
""" """
@ -332,7 +333,7 @@ class UserActions(object):
# Invalidate new records, including the omitted columns that may have default formulas, # Invalidate new records, including the omitted columns that may have default formulas,
# in order to get dynamically-computed default values. # in order to get dynamically-computed default values.
omitted_cols = table.all_columns.viewkeys() - column_values.viewkeys() omitted_cols = six.viewkeys(table.all_columns) - six.viewkeys(column_values)
self._engine.invalidate_records(table_id, filled_row_ids, data_cols_to_recompute=omitted_cols) self._engine.invalidate_records(table_id, filled_row_ids, data_cols_to_recompute=omitted_cols)
return filled_row_ids return filled_row_ids
@ -599,18 +600,16 @@ class UserActions(object):
col_rec = self._docmodel.get_column_rec(formula_table, formula_col) col_rec = self._docmodel.get_column_rec(formula_table, formula_col)
# Create a patch and append to the list for this col_rec. # Create a patch and append to the list for this col_rec.
name = col_id or table_id name = col_id or table_id
# Positions are obtained from unicode version of formulas, so that's what we must patch formula = col_rec.formula
formula = col_rec.formula.decode('utf8')
patch = textbuilder.make_patch(formula, pos, pos + len(name), new_name) patch = textbuilder.make_patch(formula, pos, pos + len(name), new_name)
patches_map.setdefault(col_rec, []).append(patch) patches_map.setdefault(col_rec, []).append(patch)
# Apply the collected patches to each affected formula, converting to unicode to apply the # Apply the collected patches to each affected formula
# patches and back to byte string for how we maintain string values.
result = {} result = {}
for col_rec, patches in six.iteritems(patches_map): for col_rec, patches in patches_map.items():
formula = col_rec.formula.decode('utf8') formula = col_rec.formula
replacer = textbuilder.Replacer(textbuilder.Text(formula), patches) replacer = textbuilder.Replacer(textbuilder.Text(formula), patches)
result[col_rec] = replacer.get_text().encode('utf8') result[col_rec] = replacer.get_text()
return result return result

@ -12,7 +12,6 @@ data structure for values of the wrong type, and the memory savings aren't that
the extra complexity. the extra complexity.
""" """
import csv import csv
import cStringIO
import datetime import datetime
import json import json
import six import six
@ -31,7 +30,7 @@ _type_defaults = {
'Attachments': None, 'Attachments': None,
'Blob': None, 'Blob': None,
'Bool': False, 'Bool': False,
'Choice': '', 'Choice': u'',
'ChoiceList': None, 'ChoiceList': None,
'Date': None, 'Date': None,
'DateTime': None, 'DateTime': None,
@ -42,7 +41,7 @@ _type_defaults = {
'PositionNumber': float('inf'), 'PositionNumber': float('inf'),
'Ref': 0, 'Ref': 0,
'RefList': None, 'RefList': None,
'Text': '', 'Text': u'',
} }
def get_type_default(col_type): def get_type_default(col_type):
@ -131,10 +130,7 @@ class BaseColumnType(object):
except Exception as e: except Exception as e:
# If conversion failed, return a string to serve as alttext. # If conversion failed, return a string to serve as alttext.
try: try:
if isinstance(value_to_convert, six.text_type): return six.text_type(value_to_convert)
# str() will fail for a non-ascii unicode object, which needs an explicit encoding.
return value_to_convert.encode('utf8')
return str(value_to_convert)
except Exception: except Exception:
# If converting to string failed, we should still produce something. # If converting to string failed, we should still produce something.
return objtypes.safe_repr(value_to_convert) return objtypes.safe_repr(value_to_convert)
@ -157,7 +153,12 @@ class Text(BaseColumnType):
""" """
@classmethod @classmethod
def do_convert(cls, value): def do_convert(cls, value):
return str(value) if value is not None else None if isinstance(value, six.binary_type):
return value.decode('utf8')
elif value is None:
return None
else:
return six.text_type(value)
@classmethod @classmethod
def is_right_type(cls, value): def is_right_type(cls, value):
@ -176,11 +177,11 @@ class Blob(BaseColumnType):
""" """
@classmethod @classmethod
def do_convert(cls, value): def do_convert(cls, value):
return str(value) if value is not None else None return value
@classmethod @classmethod
def is_right_type(cls, value): def is_right_type(cls, value):
return isinstance(value, (basestring, NoneType)) return isinstance(value, (six.binary_type, NoneType))
class Any(BaseColumnType): class Any(BaseColumnType):
@ -190,7 +191,7 @@ class Any(BaseColumnType):
@classmethod @classmethod
def do_convert(cls, value): def do_convert(cls, value):
# Convert AltText values to plain text when assigning to type Any. # Convert AltText values to plain text when assigning to type Any.
return str(value) if isinstance(value, AltText) else value return six.text_type(value) if isinstance(value, AltText) else value
class Bool(BaseColumnType): class Bool(BaseColumnType):
@ -206,7 +207,7 @@ class Bool(BaseColumnType):
if isinstance(value, (float, six.integer_types)): if isinstance(value, (float, six.integer_types)):
return True return True
if isinstance(value, AltText): if isinstance(value, AltText):
value = str(value) value = six.text_type(value)
if isinstance(value, six.string_types): if isinstance(value, six.string_types):
if value.lower() in ("false", "no", "0"): if value.lower() in ("false", "no", "0"):
return False return False
@ -334,13 +335,13 @@ class ChoiceList(BaseColumnType):
# If it's a string that looks like JSON, try to parse it as such. # If it's a string that looks like JSON, try to parse it as such.
if value.startswith('['): if value.startswith('['):
try: try:
return tuple(str(item) for item in json.loads(value)) return tuple(six.text_type(item) for item in json.loads(value))
except Exception: except Exception:
pass pass
return value return value
else: else:
# Accepts other kinds of iterables; if that doesn't work, fail the conversion too. # Accepts other kinds of iterables; if that doesn't work, fail the conversion too.
return tuple(str(item) for item in value) return tuple(six.text_type(item) for item in value)
@classmethod @classmethod
def is_right_type(cls, value): def is_right_type(cls, value):
@ -362,7 +363,7 @@ class ChoiceList(BaseColumnType):
def toString(cls, value): def toString(cls, value):
if isinstance(value, (tuple, list)): if isinstance(value, (tuple, list)):
try: try:
buf = cStringIO.StringIO() buf = six.StringIO()
csv.writer(buf).writerow(value) csv.writer(buf).writerow(value)
return buf.getvalue().strip() return buf.getvalue().strip()
except Exception: except Exception:
@ -434,7 +435,7 @@ class Reference(Id):
@classmethod @classmethod
def typeConvert(cls, value, ref_table, visible_col=None): # pylint: disable=arguments-differ def typeConvert(cls, value, ref_table, visible_col=None): # pylint: disable=arguments-differ
if ref_table and visible_col: if ref_table and visible_col:
return ref_table.lookupOne(**{visible_col: value}) or str(value) return ref_table.lookupOne(**{visible_col: value}) or six.text_type(value)
else: else:
return value return value

Loading…
Cancel
Save