mirror of
				https://github.com/gristlabs/grist-core.git
				synced 2025-06-13 20:53:59 +00:00 
			
		
		
		
	(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
This commit is contained in:
		
							parent
							
								
									1af99e9567
								
							
						
					
					
						commit
						305b133c59
					
				@ -639,7 +639,7 @@ async function handleSandboxError<T>(tableId: string, colNames: string[], p: Pro
 | 
			
		||||
      if (match) {
 | 
			
		||||
        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[1] === tableId) {
 | 
			
		||||
          throw new ApiError(`Table not found "${tableId}"`, 404);
 | 
			
		||||
 | 
			
		||||
@ -78,7 +78,7 @@ export class NSandbox implements ISandbox {
 | 
			
		||||
  public readonly childProc: ChildProcess;
 | 
			
		||||
  private _logTimes: boolean;
 | 
			
		||||
  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 });
 | 
			
		||||
 | 
			
		||||
  // 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.
 | 
			
		||||
  for rule_rec in docmodel.aclRules.all:
 | 
			
		||||
    if rule_rec.aclFormula:
 | 
			
		||||
      # Positions are obtained from unicode version of formulas, so that's what we must patch
 | 
			
		||||
      formula = rule_rec.aclFormula.decode('utf8')
 | 
			
		||||
      formula = rule_rec.aclFormula
 | 
			
		||||
      patches = []
 | 
			
		||||
 | 
			
		||||
      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)
 | 
			
		||||
 | 
			
		||||
      replacer = textbuilder.Replacer(textbuilder.Text(formula), patches)
 | 
			
		||||
      txt = replacer.get_text().encode('utf8')
 | 
			
		||||
      txt = replacer.get_text()
 | 
			
		||||
      rule_updates.append((rule_rec, {'aclFormula': txt,
 | 
			
		||||
                                      'aclFormulaParsed': parse_acl_formula_json(txt)}))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,8 @@ import tokenize
 | 
			
		||||
from collections import namedtuple
 | 
			
		||||
 | 
			
		||||
import asttokens
 | 
			
		||||
import six
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def parse_acl_formula(acl_formula):
 | 
			
		||||
  """
 | 
			
		||||
@ -26,10 +28,12 @@ def parse_acl_formula(acl_formula):
 | 
			
		||||
    Attr                    node, attr_name
 | 
			
		||||
    Comment                 node, comment
 | 
			
		||||
  """
 | 
			
		||||
  if isinstance(acl_formula, six.binary_type):
 | 
			
		||||
    acl_formula = acl_formula.decode('utf8')
 | 
			
		||||
  try:
 | 
			
		||||
    tree = ast.parse(acl_formula, mode='eval')
 | 
			
		||||
    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('#'):
 | 
			
		||||
        result = ['Comment', result, part[1][1:].strip()]
 | 
			
		||||
        break
 | 
			
		||||
 | 
			
		||||
@ -116,7 +116,7 @@ def action_from_repr(doc_action):
 | 
			
		||||
  try:
 | 
			
		||||
    return decode_objects(action_type(*doc_action[1:]))
 | 
			
		||||
  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):
 | 
			
		||||
 | 
			
		||||
@ -70,6 +70,9 @@ class AutocompleteContext(object):
 | 
			
		||||
    return self._context
 | 
			
		||||
 | 
			
		||||
  def process_result(self, result):
 | 
			
		||||
    # 'for' suggests the autocompletion 'for ' in python 3
 | 
			
		||||
    result = result.rstrip()
 | 
			
		||||
 | 
			
		||||
    # Callables are returned by rlcompleter with a trailing "(".
 | 
			
		||||
    if result.endswith('('):
 | 
			
		||||
      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)
 | 
			
		||||
  input_offset = builder.map_back_offset(output_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()),
 | 
			
		||||
    type(err).__name__, err.args[0], line, col + 1)
 | 
			
		||||
    type(err).__name__, message)
 | 
			
		||||
 | 
			
		||||
#----------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -268,11 +268,43 @@ class DateTimeColumn(NumericColumn):
 | 
			
		||||
  def sample_value(self):
 | 
			
		||||
    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):
 | 
			
		||||
  def __init__(self, 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.
 | 
			
		||||
    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):
 | 
			
		||||
    self._sorted_rows.discard(row_id)
 | 
			
		||||
@ -282,7 +314,8 @@ class PositionColumn(NumericColumn):
 | 
			
		||||
 | 
			
		||||
  def copy_from_column(self, 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):
 | 
			
		||||
    # 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
 | 
			
		||||
    # this value is set, the old position will be removed and the new one added.
 | 
			
		||||
    if ignore_data:
 | 
			
		||||
      rows = SortedListWithKey([], key=self.raw_get)
 | 
			
		||||
      rows = []
 | 
			
		||||
    else:
 | 
			
		||||
      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)
 | 
			
		||||
    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 "Dependency graph (%d edges):" % len(self._all_edges)
 | 
			
		||||
    print("Dependency graph (%d edges):" % len(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):
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
@ -34,12 +34,13 @@ import table as table_module
 | 
			
		||||
import useractions
 | 
			
		||||
import column
 | 
			
		||||
import repl
 | 
			
		||||
import urllib_patch  # noqa imported for side effect
 | 
			
		||||
 | 
			
		||||
log = logger.Logger(__name__, logger.INFO)
 | 
			
		||||
 | 
			
		||||
if six.PY2:
 | 
			
		||||
  reload(sys)
 | 
			
		||||
  sys.setdefaultencoding('utf8')
 | 
			
		||||
  sys.setdefaultencoding('utf8')  # noqa
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OrderError(Exception):
 | 
			
		||||
@ -1125,8 +1126,14 @@ class Engine(object):
 | 
			
		||||
      except Exception:
 | 
			
		||||
        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).
 | 
			
		||||
      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
 | 
			
		||||
 | 
			
		||||
    # Note that recalculations and auto-removals get included after processing all useractions.
 | 
			
		||||
    self._bring_all_up_to_date()
 | 
			
		||||
@ -1183,8 +1190,15 @@ class Engine(object):
 | 
			
		||||
          self.rebuild_usercode()
 | 
			
		||||
        except Exception:
 | 
			
		||||
          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
 | 
			
		||||
    # 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)`.
 | 
			
		||||
 | 
			
		||||
  >>> SELF_HYPERLINK()
 | 
			
		||||
  'https://docs.getgrist.com/sbaltsirg/Example'
 | 
			
		||||
  u'https://docs.getgrist.com/sbaltsirg/Example'
 | 
			
		||||
  >>> SELF_HYPERLINK(label='doc')
 | 
			
		||||
  'doc https://docs.getgrist.com/sbaltsirg/Example'
 | 
			
		||||
  u'doc https://docs.getgrist.com/sbaltsirg/Example'
 | 
			
		||||
  >>> 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')
 | 
			
		||||
  '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')
 | 
			
		||||
  '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')
 | 
			
		||||
  Traceback (most recent call last):
 | 
			
		||||
  ...
 | 
			
		||||
@ -110,6 +110,7 @@ def SELF_HYPERLINK(label=None, page=None, **kwargs):
 | 
			
		||||
  txt = os.environ.get('DOC_URL')
 | 
			
		||||
  if not txt:
 | 
			
		||||
    return None
 | 
			
		||||
  txt = six.text_type(txt)
 | 
			
		||||
  if page:
 | 
			
		||||
    txt += "/p/{}".format(page)
 | 
			
		||||
  if kwargs:
 | 
			
		||||
@ -124,7 +125,7 @@ def SELF_HYPERLINK(label=None, page=None, **kwargs):
 | 
			
		||||
    parts[4] = urllib_parse.urlencode(query)
 | 
			
		||||
    txt = urllib_parse.urlunparse(parts)
 | 
			
		||||
  if label:
 | 
			
		||||
    txt = "{} {}".format(label, txt)
 | 
			
		||||
    txt = u"{} {}".format(label, txt)
 | 
			
		||||
  return txt
 | 
			
		||||
 | 
			
		||||
def VLOOKUP(table, **field_value_pairs):
 | 
			
		||||
 | 
			
		||||
@ -1,16 +1,17 @@
 | 
			
		||||
# -*- coding: UTF-8 -*-
 | 
			
		||||
 | 
			
		||||
import datetime
 | 
			
		||||
import dateutil.parser
 | 
			
		||||
import numbers
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
import dateutil.parser
 | 
			
		||||
import six
 | 
			
		||||
from six import unichr
 | 
			
		||||
from six.moves import xrange
 | 
			
		||||
 | 
			
		||||
from usertypes import AltText  # pylint: disable=import-error
 | 
			
		||||
from .unimplemented import unimplemented
 | 
			
		||||
from usertypes import AltText   # pylint: disable=import-error
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def CHAR(table_number):
 | 
			
		||||
  """
 | 
			
		||||
@ -26,7 +27,7 @@ def CHAR(table_number):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# 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))
 | 
			
		||||
 | 
			
		||||
def CLEAN(text):
 | 
			
		||||
@ -58,7 +59,7 @@ def CODE(string):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def CONCATENATE(string, *more_strings):
 | 
			
		||||
  """
 | 
			
		||||
  u"""
 | 
			
		||||
  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)`.
 | 
			
		||||
 | 
			
		||||
@ -70,11 +71,15 @@ def CONCATENATE(string, *more_strings):
 | 
			
		||||
  u'abc'
 | 
			
		||||
  >>> CONCATENATE(0, "abc")
 | 
			
		||||
  u'0abc'
 | 
			
		||||
  >>> CONCATENATE(2, " crème ", "brûlée".decode('utf8')) == "2 crème brûlée".decode('utf8')
 | 
			
		||||
  True
 | 
			
		||||
  >>> assert CONCATENATE(2, u" crème ", u"brûlée") == u'2 crème brûlée'
 | 
			
		||||
  >>> 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')
 | 
			
		||||
      for val in (string,) + more_strings)
 | 
			
		||||
  return u''.join(
 | 
			
		||||
    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):
 | 
			
		||||
@ -90,8 +95,7 @@ def CONCAT(string, *more_strings):
 | 
			
		||||
  u'abc'
 | 
			
		||||
  >>> CONCAT(0, "abc")
 | 
			
		||||
  u'0abc'
 | 
			
		||||
  >>> CONCAT(2, " crème ", "brûlée".decode('utf8')) == "2 crème brûlée".decode('utf8')
 | 
			
		||||
  True
 | 
			
		||||
  >>> assert CONCAT(2, u" crème ", u"brûlée") == u'2 crème brûlée'
 | 
			
		||||
  """
 | 
			
		||||
  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.
 | 
			
		||||
 | 
			
		||||
  >>> SUBSTITUTE("Sales Data", "Sales", "Cost")
 | 
			
		||||
  'Cost Data'
 | 
			
		||||
  u'Cost Data'
 | 
			
		||||
  >>> SUBSTITUTE("Quarter 1, 2008", "1", "2", 1)
 | 
			
		||||
  'Quarter 2, 2008'
 | 
			
		||||
  u'Quarter 2, 2008'
 | 
			
		||||
  >>> SUBSTITUTE("Quarter 1, 2011", "1", "2", 3)
 | 
			
		||||
  'Quarter 1, 2012'
 | 
			
		||||
  u'Quarter 1, 2012'
 | 
			
		||||
 | 
			
		||||
  More tests:
 | 
			
		||||
  >>> SUBSTITUTE("Hello world", "", "-")
 | 
			
		||||
  'Hello world'
 | 
			
		||||
  u'Hello world'
 | 
			
		||||
  >>> SUBSTITUTE("Hello world", " ", "-")
 | 
			
		||||
  'Hello-world'
 | 
			
		||||
  u'Hello-world'
 | 
			
		||||
  >>> SUBSTITUTE("Hello world", " ", 12.1)
 | 
			
		||||
  'Hello12.1world'
 | 
			
		||||
  u'Hello12.1world'
 | 
			
		||||
  >>> SUBSTITUTE(u"Hello world", u" ", 12.1)
 | 
			
		||||
  u'Hello12.1world'
 | 
			
		||||
  >>> SUBSTITUTE("Hello world", "world", "")
 | 
			
		||||
  'Hello '
 | 
			
		||||
  u'Hello '
 | 
			
		||||
  >>> SUBSTITUTE("Hello", "world", "")
 | 
			
		||||
  'Hello'
 | 
			
		||||
  u'Hello'
 | 
			
		||||
 | 
			
		||||
  Overlapping matches are all counted when looking for instance_num.
 | 
			
		||||
  >>> SUBSTITUTE('abababab', 'abab', 'xxxx')
 | 
			
		||||
  'xxxxxxxx'
 | 
			
		||||
  u'xxxxxxxx'
 | 
			
		||||
  >>> SUBSTITUTE('abababab', 'abab', 'xxxx', 1)
 | 
			
		||||
  'xxxxabab'
 | 
			
		||||
  u'xxxxabab'
 | 
			
		||||
  >>> SUBSTITUTE('abababab', 'abab', 'xxxx', 2)
 | 
			
		||||
  'abxxxxab'
 | 
			
		||||
  u'abxxxxab'
 | 
			
		||||
  >>> SUBSTITUTE('abababab', 'abab', 'xxxx', 3)
 | 
			
		||||
  'ababxxxx'
 | 
			
		||||
  u'ababxxxx'
 | 
			
		||||
  >>> SUBSTITUTE('abababab', 'abab', 'xxxx', 4)
 | 
			
		||||
  'abababab'
 | 
			
		||||
  u'abababab'
 | 
			
		||||
  >>> SUBSTITUTE('abababab', 'abab', 'xxxx', 0)
 | 
			
		||||
  Traceback (most recent call last):
 | 
			
		||||
  ...
 | 
			
		||||
  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:
 | 
			
		||||
    return text
 | 
			
		||||
 | 
			
		||||
  if not isinstance(new_text, six.string_types):
 | 
			
		||||
    new_text = str(new_text)
 | 
			
		||||
 | 
			
		||||
  if instance_num is None:
 | 
			
		||||
    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.
 | 
			
		||||
 | 
			
		||||
  >>> T('Text')
 | 
			
		||||
  'Text'
 | 
			
		||||
  u'Text'
 | 
			
		||||
  >>> T(826)
 | 
			
		||||
  ''
 | 
			
		||||
  u''
 | 
			
		||||
  >>> T('826')
 | 
			
		||||
  '826'
 | 
			
		||||
  u'826'
 | 
			
		||||
  >>> T(False)
 | 
			
		||||
  ''
 | 
			
		||||
  u''
 | 
			
		||||
  >>> T('100 points')
 | 
			
		||||
  '100 points'
 | 
			
		||||
  u'100 points'
 | 
			
		||||
  >>> T(AltText('Text'))
 | 
			
		||||
  'Text'
 | 
			
		||||
  u'Text'
 | 
			
		||||
  >>> T(float('nan'))
 | 
			
		||||
  ''
 | 
			
		||||
  u''
 | 
			
		||||
  """
 | 
			
		||||
  return (value if isinstance(value, basestring) else
 | 
			
		||||
          str(value) if isinstance(value, AltText) else "")
 | 
			
		||||
  return (value.decode('utf8') if isinstance(value, six.binary_type) else
 | 
			
		||||
          value if isinstance(value, six.text_type) else
 | 
			
		||||
          six.text_type(value) if isinstance(value, AltText) else u"")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
@ -565,8 +579,7 @@ def VALUE(text):
 | 
			
		||||
 | 
			
		||||
  >>> VALUE("$1,000")
 | 
			
		||||
  1000
 | 
			
		||||
  >>> VALUE("16:48:00") - VALUE("12:00:00")
 | 
			
		||||
  datetime.timedelta(0, 17280)
 | 
			
		||||
  >>> assert VALUE("16:48:00") - VALUE("12:00:00") == datetime.timedelta(0, 17280)
 | 
			
		||||
  >>> VALUE("01/01/2012")
 | 
			
		||||
  datetime.datetime(2012, 1, 1, 0, 0)
 | 
			
		||||
  >>> VALUE("")
 | 
			
		||||
 | 
			
		||||
@ -36,6 +36,7 @@ def table_data_from_db(table_name, table_data_repr):
 | 
			
		||||
  if table_data_repr is None:
 | 
			
		||||
    return actions.TableData(table_name, [], {})
 | 
			
		||||
  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")
 | 
			
		||||
  return actions.TableData(table_name, id_col,
 | 
			
		||||
                           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
 | 
			
		||||
  # 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
 | 
			
		||||
 | 
			
		||||
  # 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)
 | 
			
		||||
  if t == unicode:
 | 
			
		||||
    return value.encode('utf8')
 | 
			
		||||
  elif t == str:
 | 
			
		||||
  if t == six.binary_type:
 | 
			
		||||
    return objtypes.decode_object(marshal.loads(value))
 | 
			
		||||
  else:
 | 
			
		||||
    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}
 | 
			
		||||
      # 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.
 | 
			
		||||
      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))
 | 
			
		||||
 | 
			
		||||
    # And load in the original data, interpreting the TableData object as BulkAddRecord action.
 | 
			
		||||
@ -177,7 +178,7 @@ def migration1(tdset):
 | 
			
		||||
  if rows:
 | 
			
		||||
    values = {'tableRef': [r[0] 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))
 | 
			
		||||
 | 
			
		||||
  return tdset.apply_doc_actions(doc_actions)
 | 
			
		||||
@ -212,14 +213,14 @@ def migration2(tdset):
 | 
			
		||||
    return actions.BulkUpdateRecord('_grist_Tables', row_ids, values)
 | 
			
		||||
 | 
			
		||||
  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, {
 | 
			
		||||
      'viewRef': sorted(views_to_table.keys())
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
  def create_table_views_action(views_to_table, primary_views):
 | 
			
		||||
    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, {
 | 
			
		||||
      'tableRef': [views_to_table[v] for v in related_views],
 | 
			
		||||
      'viewRef':  related_views,
 | 
			
		||||
@ -757,7 +758,7 @@ def migration20(tdset):
 | 
			
		||||
    # the name of primary view's is the same as the tableId
 | 
			
		||||
    return (view.name, -1)
 | 
			
		||||
  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([
 | 
			
		||||
    actions.AddTable('_grist_Pages', [
 | 
			
		||||
      schema.make_column('viewRef', 'Ref:_grist_Views'),
 | 
			
		||||
 | 
			
		||||
@ -142,8 +142,9 @@ class TzInfo(_tzinfo):
 | 
			
		||||
  def tzname(self, dt):
 | 
			
		||||
    """Implementation of tzinfo.tzname interface."""
 | 
			
		||||
    abbr = self.zone.dt_tzname(dt, self._favor_offset)
 | 
			
		||||
    # tzname must return a string, not unicode.
 | 
			
		||||
    return abbr.encode('utf8') if isinstance(abbr, unicode) else abbr
 | 
			
		||||
    if six.PY2 and isinstance(abbr, six.text_type):
 | 
			
		||||
      abbr = abbr.encode('utf8')
 | 
			
		||||
    return abbr
 | 
			
		||||
 | 
			
		||||
  def dst(self, dt):
 | 
			
		||||
    """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
 | 
			
		||||
SEP = r"[\s/.\-:,]*"
 | 
			
		||||
SEP_REGEX = re.compile(SEP)
 | 
			
		||||
SEP_REPLACEMENT = SEP.replace("\\", "\\\\")
 | 
			
		||||
 | 
			
		||||
# Maps date parse format to compile regex
 | 
			
		||||
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})"
 | 
			
		||||
    # 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[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)
 | 
			
		||||
    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.
 | 
			
		||||
  """
 | 
			
		||||
  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
 | 
			
		||||
    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):
 | 
			
		||||
        raise UnmarshallableError("Integer too large")
 | 
			
		||||
      return value
 | 
			
		||||
    elif isinstance(value, AltText):
 | 
			
		||||
      return str(value)
 | 
			
		||||
      return six.text_type(value)
 | 
			
		||||
    elif isinstance(value, records.Record):
 | 
			
		||||
      return ['R', value._table.table_id, value._row_id]
 | 
			
		||||
    elif isinstance(value, RecordStub):
 | 
			
		||||
@ -210,13 +212,6 @@ def decode_object(value):
 | 
			
		||||
  """
 | 
			
		||||
  try:
 | 
			
		||||
    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
 | 
			
		||||
    code = value[0]
 | 
			
		||||
    args = value[1:]
 | 
			
		||||
 | 
			
		||||
@ -6,9 +6,10 @@ slight changes in order to be convenient for Grist's purposes
 | 
			
		||||
 | 
			
		||||
import code
 | 
			
		||||
import sys
 | 
			
		||||
from StringIO import StringIO
 | 
			
		||||
from collections import namedtuple
 | 
			
		||||
 | 
			
		||||
import six
 | 
			
		||||
 | 
			
		||||
SUCCESS    = 0
 | 
			
		||||
INCOMPLETE = 1
 | 
			
		||||
ERROR      = 2
 | 
			
		||||
@ -38,7 +39,7 @@ class REPLInterpreter(code.InteractiveInterpreter):
 | 
			
		||||
    old_stdout = sys.stdout
 | 
			
		||||
    old_stderr = sys.stderr
 | 
			
		||||
 | 
			
		||||
    user_output = StringIO()
 | 
			
		||||
    user_output = six.StringIO()
 | 
			
		||||
 | 
			
		||||
    self.error_text = ""
 | 
			
		||||
    try:
 | 
			
		||||
@ -67,7 +68,10 @@ class REPLInterpreter(code.InteractiveInterpreter):
 | 
			
		||||
        sys.stderr = old_stderr
 | 
			
		||||
 | 
			
		||||
    program_output = user_output.getvalue()
 | 
			
		||||
    user_output.close()
 | 
			
		||||
    try:
 | 
			
		||||
      user_output.close()
 | 
			
		||||
    except:
 | 
			
		||||
      pass
 | 
			
		||||
 | 
			
		||||
    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.
 | 
			
		||||
    self.assertEqual(make_body('# comment'), '# comment\npass')
 | 
			
		||||
    self.assertEqual(make_body('\\'), '\\\npass')
 | 
			
		||||
 | 
			
		||||
    self.assertEqual(make_body('rec = 1'), "# rec = 1\n" +
 | 
			
		||||
                     "raise SyntaxError('Grist disallows assignment " +
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,9 @@
 | 
			
		||||
import doctest
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
import six
 | 
			
		||||
 | 
			
		||||
import functions
 | 
			
		||||
import moment
 | 
			
		||||
 | 
			
		||||
@ -15,6 +19,12 @@ def date_tearDown(doc_test):
 | 
			
		||||
  # pylint: disable=unused-argument
 | 
			
		||||
  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
 | 
			
		||||
# unittest test cases.
 | 
			
		||||
@ -26,8 +36,8 @@ def load_tests(loader, tests, ignore):
 | 
			
		||||
  tests.addTests(doctest.DocTestSuite(functions.logical))
 | 
			
		||||
  tests.addTests(doctest.DocTestSuite(functions.math))
 | 
			
		||||
  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,
 | 
			
		||||
                                      setUp = date_setUp, tearDown = date_tearDown))
 | 
			
		||||
  tests.addTests(doctest.DocTestSuite(functions.lookup))
 | 
			
		||||
  tests.addTests(doctest.DocTestSuite(functions.lookup, checker=Py23DocChecker()))
 | 
			
		||||
  return tests
 | 
			
		||||
 | 
			
		||||
@ -256,7 +256,7 @@ class TestRelabeling(unittest.TestCase):
 | 
			
		||||
    self._do_test_renumber_ends([])
 | 
			
		||||
 | 
			
		||||
  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):
 | 
			
		||||
    # 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):
 | 
			
		||||
    # Test that presence of unicode does not interfere with formula adjustments for renaming.
 | 
			
		||||
    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"])
 | 
			
		||||
    self.assertPartialOutActions(out_actions, { "stored": [
 | 
			
		||||
      ["RenameColumn", "Address", "city", "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], {
 | 
			
		||||
        "colId": ["ciudad", "city", "CityUpper"],
 | 
			
		||||
        "formula": ["", "$addr.ciudad", "'Øî'+$ciudad.upper()+'áü'"],
 | 
			
		||||
        "formula": ["", "$addr.ciudad", u"'Øî'+$ciudad.upper()+'áü'"],
 | 
			
		||||
      }]
 | 
			
		||||
    ]})
 | 
			
		||||
    self.assertTableData("Address", cols="all", data=[
 | 
			
		||||
      ["id",  "ciudad",     "CityUpper"],
 | 
			
		||||
      [11,    "New York",   "ØîNEW YORKáü"],
 | 
			
		||||
      [12,    "Colombia",   "ØîCOLOMBIAáü"],
 | 
			
		||||
      [13,    "New Haven",  "ØîNEW HAVENáü"],
 | 
			
		||||
      [14,    "West Haven", "ØîWEST HAVENáü"],
 | 
			
		||||
      [11,    "New York",   u"ØîNEW YORKáü"],
 | 
			
		||||
      [12,    "Colombia",   u"ØîCOLOMBIAáü"],
 | 
			
		||||
      [13,    "New Haven",  u"ØîNEW HAVENáü"],
 | 
			
		||||
      [14,    "West Haven", u"ØîWEST HAVENáü"],
 | 
			
		||||
    ])
 | 
			
		||||
 | 
			
		||||
  def test_rename_updates_properties(self):
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
# pylint: disable=line-too-long
 | 
			
		||||
import six
 | 
			
		||||
 | 
			
		||||
import logger
 | 
			
		||||
import testutil
 | 
			
		||||
@ -25,7 +26,7 @@ class TestTypes(test_engine.EngineTestCase):
 | 
			
		||||
      "Types": [
 | 
			
		||||
        ["id", "text",     "numeric",  "int",      "bool",     "date"],
 | 
			
		||||
        [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],
 | 
			
		||||
        [14,   True,       True,       True,       True,       True],
 | 
			
		||||
        [15,   1509556595, 1509556595, 1509556595, 1509556595, 1509556595],
 | 
			
		||||
@ -61,20 +62,20 @@ class TestTypes(test_engine.EngineTestCase):
 | 
			
		||||
 | 
			
		||||
    self.assertPartialOutActions(out_actions, {
 | 
			
		||||
      "stored": [["BulkUpdateRecord", "Types", self.all_row_ids, {
 | 
			
		||||
        "text":    [None,"","1","0","8.153","1509556595","True","False","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"],
 | 
			
		||||
        "int":     [None, None, 1, 0, 8, 1509556595, 1, 0, "Chîcágö", "New York"],
 | 
			
		||||
        "bool":    [False, False, True, False, True, True, 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, u"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, 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"]
 | 
			
		||||
        }],
 | 
			
		||||
        ["UpdateRecord", "Formulas", 1, {"division": 0.0}],
 | 
			
		||||
      ],
 | 
			
		||||
      "undo": [["BulkUpdateRecord", "Types", self.all_row_ids, {
 | 
			
		||||
        "text":    ["New York", "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],
 | 
			
		||||
        "int":     ["New York", "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],
 | 
			
		||||
        "date":    ["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", u"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", u"Chîcágö", False, True, 1509556595, 8.153, False, True, "", None],
 | 
			
		||||
        "date":    ["New York", u"Chîcágö", False, True, 1509556595, 8.153, 0, 1, "", None]
 | 
			
		||||
        }],
 | 
			
		||||
        ["UpdateRecord", "Formulas", 1, {"division": 0.5}],
 | 
			
		||||
      ]
 | 
			
		||||
@ -90,7 +91,7 @@ class TestTypes(test_engine.EngineTestCase):
 | 
			
		||||
      [16,   "1509556595", 1509556595, 1509556595, True,       1509556595.0],
 | 
			
		||||
      [17,   "True",       1.0,        1,          True,       1.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"]
 | 
			
		||||
    ])
 | 
			
		||||
 | 
			
		||||
@ -184,7 +185,7 @@ class TestTypes(test_engine.EngineTestCase):
 | 
			
		||||
    self.assertTableData("Types", data=[
 | 
			
		||||
      ["id", "text",      "numeric",   "int",       "bool",      "date"],
 | 
			
		||||
      [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"],
 | 
			
		||||
      [14,   True,        "True",      "True",      "True",      "True"],
 | 
			
		||||
      [15,   1509556595,  "1509556595.0","1509556595","1509556595","1509556595.0"],
 | 
			
		||||
@ -283,7 +284,7 @@ class TestTypes(test_engine.EngineTestCase):
 | 
			
		||||
    self.assertTableData("Types", data=[
 | 
			
		||||
      ["id", "text",     "numeric",  "int",      "bool",     "date"],
 | 
			
		||||
      [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],
 | 
			
		||||
      [14,   1.0,        True,       1.0,        1.0,        1.0],
 | 
			
		||||
      [15,   1509556595, 1509556595, 1509556595, 1509556595, 1509556595],
 | 
			
		||||
@ -327,15 +328,13 @@ class TestTypes(test_engine.EngineTestCase):
 | 
			
		||||
        ["BulkUpdateRecord", "Types", [13, 14, 16, 19],
 | 
			
		||||
         {"numeric": [0, 1, 8, None]}],
 | 
			
		||||
        ["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": [
 | 
			
		||||
        ["BulkUpdateRecord", "Types", [13, 14, 16, 19],
 | 
			
		||||
          {"numeric": [False, True, 8.153, ""]}],
 | 
			
		||||
        ["ModifyColumn", "Types", "numeric", {"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
 | 
			
		||||
@ -383,7 +382,7 @@ class TestTypes(test_engine.EngineTestCase):
 | 
			
		||||
    self.assertTableData("Types", data=[
 | 
			
		||||
      ["id", "text",     "numeric",  "int",      "bool",     "date"],
 | 
			
		||||
      [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],
 | 
			
		||||
      [14,   1,          1,          True,       1,          1],
 | 
			
		||||
      [15,   1509556595, 1509556595, 1509556595, 1509556595, 1509556595],
 | 
			
		||||
@ -428,15 +427,13 @@ class TestTypes(test_engine.EngineTestCase):
 | 
			
		||||
        ["BulkUpdateRecord", "Types", [15, 16, 17, 18, 19, 20],
 | 
			
		||||
          {"numeric": [True, True, False, True, False, False]}],
 | 
			
		||||
        ["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": [
 | 
			
		||||
        ["BulkUpdateRecord", "Types", [15, 16, 17, 18, 19, 20],
 | 
			
		||||
          {"numeric": [1509556595.0, 8.153, 0.0, 1.0, "", None]}],
 | 
			
		||||
        ["ModifyColumn", "Types", "numeric", {"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
 | 
			
		||||
@ -484,7 +481,7 @@ class TestTypes(test_engine.EngineTestCase):
 | 
			
		||||
    self.assertTableData("Types", data=[
 | 
			
		||||
      ["id", "text",     "numeric",  "int",      "bool",     "date"],
 | 
			
		||||
      [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],
 | 
			
		||||
      [14,   True,       True,       True,       True,       True],
 | 
			
		||||
      [15,   True,       True,       True,       1509556595, True],
 | 
			
		||||
@ -585,7 +582,7 @@ class TestTypes(test_engine.EngineTestCase):
 | 
			
		||||
    self.assertTableData("Types", data=[
 | 
			
		||||
      ["id", "text",     "numeric",  "int",      "bool",     "date"],
 | 
			
		||||
      [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],
 | 
			
		||||
      [14,   1.0,        1.0,        1.0,        1.0,        True],
 | 
			
		||||
      [15,   1509556595, 1509556595, 1509556595, 1509556595, 1509556595],
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										19
									
								
								sandbox/grist/test_urllib_patch.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								sandbox/grist/test_urllib_patch.py
									
									
									
									
									
										Normal file
									
								
							@ -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", {
 | 
			
		||||
      "USER_ACTIONS": [
 | 
			
		||||
        // 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"}],
 | 
			
		||||
        ["ModifyColumn", "Students", "firstName", { "type" : "Numeric" }],
 | 
			
		||||
        ["EvalCode", "list(Students.all.firstName)", 6],
 | 
			
		||||
@ -2778,7 +2778,7 @@
 | 
			
		||||
      "ACTIONS": {
 | 
			
		||||
        "stored": [
 | 
			
		||||
          ["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"}],
 | 
			
		||||
          ["ModifyColumn", "Students", "firstName", {"type": "Numeric"}],
 | 
			
		||||
@ -2808,7 +2808,7 @@
 | 
			
		||||
          ["ModifyColumn", "Students", "firstName", {"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"}],
 | 
			
		||||
          ["UpdateRecord", "_grist_REPL_Hist", 6, {"code": "list(Students.all.firstName)",
 | 
			
		||||
            "errorText": "",
 | 
			
		||||
@ -2825,7 +2825,7 @@
 | 
			
		||||
    ["APPLY", {
 | 
			
		||||
      "USER_ACTIONS": [
 | 
			
		||||
        // Syntax Error
 | 
			
		||||
        ["EvalCode", "*!@&$fjjj112#(8!", null],
 | 
			
		||||
        ["EvalCode", "not correct c", null],
 | 
			
		||||
        // Other continuations
 | 
			
		||||
        ["EvalCode", "map(filter, ", null],
 | 
			
		||||
        ["EvalCode", "[1,2,3,", null],
 | 
			
		||||
@ -2834,15 +2834,14 @@
 | 
			
		||||
        ["EvalCode", "sys.exit(0)", null],
 | 
			
		||||
        // User reassignment of sys.stdout/sys.stderr
 | 
			
		||||
        ["EvalCode", "sys.stdout = None", null],
 | 
			
		||||
        ["EvalCode", "delattr(sys.stderr, 'close')", 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]
 | 
			
		||||
      ],
 | 
			
		||||
      "ACTIONS": {
 | 
			
		||||
        "stored": [
 | 
			
		||||
          ["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,
 | 
			
		||||
            {"code": "import sys", "errorText": "", "outputText": ""}],
 | 
			
		||||
          ["AddRecord", "_grist_REPL_Hist", 9,
 | 
			
		||||
@ -2850,15 +2849,13 @@
 | 
			
		||||
          ["AddRecord", "_grist_REPL_Hist", 10,
 | 
			
		||||
            {"code": "sys.stdout = None", "errorText": "", "outputText": ""}],
 | 
			
		||||
          ["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}],
 | 
			
		||||
          ["AddRecord", "_grist_REPL_Hist", 12,
 | 
			
		||||
            {"code": "def foo():\n global stdout\n exec('stdout = 2')\n", "errorText": "", "outputText": ""}],
 | 
			
		||||
          ["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": ""}]
 | 
			
		||||
        ],
 | 
			
		||||
        "direct": [true, true, true, true, true, true, true, true],
 | 
			
		||||
        "direct": [true, true, true, true, true, true, true],
 | 
			
		||||
        "undo": [
 | 
			
		||||
          ["RemoveRecord", "_grist_REPL_Hist", 7],
 | 
			
		||||
          ["RemoveRecord", "_grist_REPL_Hist", 8],
 | 
			
		||||
@ -2866,10 +2863,9 @@
 | 
			
		||||
          ["RemoveRecord", "_grist_REPL_Hist", 10],
 | 
			
		||||
          ["RemoveRecord", "_grist_REPL_Hist", 11],
 | 
			
		||||
          ["RemoveRecord", "_grist_REPL_Hist", 12],
 | 
			
		||||
          ["RemoveRecord", "_grist_REPL_Hist", 13],
 | 
			
		||||
          ["RemoveRecord", "_grist_REPL_Hist", 14]
 | 
			
		||||
          ["RemoveRecord", "_grist_REPL_Hist", 13]
 | 
			
		||||
        ],
 | 
			
		||||
        "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)
 | 
			
		||||
  full_text = "".join(all_lines)
 | 
			
		||||
 | 
			
		||||
  script = byteify(json.loads(full_text))
 | 
			
		||||
  script = json.loads(full_text)
 | 
			
		||||
 | 
			
		||||
  samples = {}
 | 
			
		||||
  test_cases = []
 | 
			
		||||
@ -109,16 +109,6 @@ def parse_test_sample(obj, samples={}):
 | 
			
		||||
  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):
 | 
			
		||||
  """
 | 
			
		||||
  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):
 | 
			
		||||
    self._parts = parts
 | 
			
		||||
    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)
 | 
			
		||||
 | 
			
		||||
    offset = 0
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										16
									
								
								sandbox/grist/urllib_patch.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								sandbox/grist/urllib_patch.py
									
									
									
									
									
										Normal file
									
								
							@ -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 six
 | 
			
		||||
from six.moves import xrange
 | 
			
		||||
 | 
			
		||||
import acl
 | 
			
		||||
from acl_formula import parse_acl_formula_json
 | 
			
		||||
@ -109,7 +110,7 @@ def from_repr(user_action):
 | 
			
		||||
  try:
 | 
			
		||||
    return action_type(*user_action[1:])
 | 
			
		||||
  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):
 | 
			
		||||
  """
 | 
			
		||||
@ -332,7 +333,7 @@ class UserActions(object):
 | 
			
		||||
 | 
			
		||||
    # Invalidate new records, including the omitted columns that may have default formulas,
 | 
			
		||||
    # 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)
 | 
			
		||||
 | 
			
		||||
    return filled_row_ids
 | 
			
		||||
@ -599,18 +600,16 @@ class UserActions(object):
 | 
			
		||||
        col_rec = self._docmodel.get_column_rec(formula_table, formula_col)
 | 
			
		||||
        # Create a patch and append to the list for this col_rec.
 | 
			
		||||
        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.decode('utf8')
 | 
			
		||||
        formula = col_rec.formula
 | 
			
		||||
        patch = textbuilder.make_patch(formula, pos, pos + len(name), new_name)
 | 
			
		||||
        patches_map.setdefault(col_rec, []).append(patch)
 | 
			
		||||
 | 
			
		||||
    # Apply the collected patches to each affected formula, converting to unicode to apply the
 | 
			
		||||
    # patches and back to byte string for how we maintain string values.
 | 
			
		||||
    # Apply the collected patches to each affected formula
 | 
			
		||||
    result = {}
 | 
			
		||||
    for col_rec, patches in six.iteritems(patches_map):
 | 
			
		||||
      formula = col_rec.formula.decode('utf8')
 | 
			
		||||
    for col_rec, patches in patches_map.items():
 | 
			
		||||
      formula = col_rec.formula
 | 
			
		||||
      replacer = textbuilder.Replacer(textbuilder.Text(formula), patches)
 | 
			
		||||
      result[col_rec] = replacer.get_text().encode('utf8')
 | 
			
		||||
      result[col_rec] = replacer.get_text()
 | 
			
		||||
    return result
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,6 @@ data structure for values of the wrong type, and the memory savings aren't that
 | 
			
		||||
the extra complexity.
 | 
			
		||||
"""
 | 
			
		||||
import csv
 | 
			
		||||
import cStringIO
 | 
			
		||||
import datetime
 | 
			
		||||
import json
 | 
			
		||||
import six
 | 
			
		||||
@ -31,7 +30,7 @@ _type_defaults = {
 | 
			
		||||
  'Attachments':  None,
 | 
			
		||||
  'Blob':         None,
 | 
			
		||||
  'Bool':         False,
 | 
			
		||||
  'Choice':       '',
 | 
			
		||||
  'Choice':       u'',
 | 
			
		||||
  'ChoiceList':   None,
 | 
			
		||||
  'Date':         None,
 | 
			
		||||
  'DateTime':     None,
 | 
			
		||||
@ -42,7 +41,7 @@ _type_defaults = {
 | 
			
		||||
  'PositionNumber': float('inf'),
 | 
			
		||||
  'Ref':          0,
 | 
			
		||||
  'RefList':      None,
 | 
			
		||||
  'Text':         '',
 | 
			
		||||
  'Text':         u'',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
def get_type_default(col_type):
 | 
			
		||||
@ -131,10 +130,7 @@ class BaseColumnType(object):
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
      # If conversion failed, return a string to serve as alttext.
 | 
			
		||||
      try:
 | 
			
		||||
        if isinstance(value_to_convert, six.text_type):
 | 
			
		||||
          # 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)
 | 
			
		||||
        return six.text_type(value_to_convert)
 | 
			
		||||
      except Exception:
 | 
			
		||||
        # If converting to string failed, we should still produce something.
 | 
			
		||||
        return objtypes.safe_repr(value_to_convert)
 | 
			
		||||
@ -157,7 +153,12 @@ class Text(BaseColumnType):
 | 
			
		||||
  """
 | 
			
		||||
  @classmethod
 | 
			
		||||
  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
 | 
			
		||||
  def is_right_type(cls, value):
 | 
			
		||||
@ -176,11 +177,11 @@ class Blob(BaseColumnType):
 | 
			
		||||
  """
 | 
			
		||||
  @classmethod
 | 
			
		||||
  def do_convert(cls, value):
 | 
			
		||||
    return str(value) if value is not None else None
 | 
			
		||||
    return value
 | 
			
		||||
 | 
			
		||||
  @classmethod
 | 
			
		||||
  def is_right_type(cls, value):
 | 
			
		||||
    return isinstance(value, (basestring, NoneType))
 | 
			
		||||
    return isinstance(value, (six.binary_type, NoneType))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Any(BaseColumnType):
 | 
			
		||||
@ -190,7 +191,7 @@ class Any(BaseColumnType):
 | 
			
		||||
  @classmethod
 | 
			
		||||
  def do_convert(cls, value):
 | 
			
		||||
    # 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):
 | 
			
		||||
@ -206,7 +207,7 @@ class Bool(BaseColumnType):
 | 
			
		||||
    if isinstance(value, (float, six.integer_types)):
 | 
			
		||||
      return True
 | 
			
		||||
    if isinstance(value, AltText):
 | 
			
		||||
      value = str(value)
 | 
			
		||||
      value = six.text_type(value)
 | 
			
		||||
    if isinstance(value, six.string_types):
 | 
			
		||||
      if value.lower() in ("false", "no", "0"):
 | 
			
		||||
        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 value.startswith('['):
 | 
			
		||||
        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:
 | 
			
		||||
          pass
 | 
			
		||||
      return value
 | 
			
		||||
    else:
 | 
			
		||||
      # 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
 | 
			
		||||
  def is_right_type(cls, value):
 | 
			
		||||
@ -362,7 +363,7 @@ class ChoiceList(BaseColumnType):
 | 
			
		||||
  def toString(cls, value):
 | 
			
		||||
    if isinstance(value, (tuple, list)):
 | 
			
		||||
      try:
 | 
			
		||||
        buf = cStringIO.StringIO()
 | 
			
		||||
        buf = six.StringIO()
 | 
			
		||||
        csv.writer(buf).writerow(value)
 | 
			
		||||
        return buf.getvalue().strip()
 | 
			
		||||
      except Exception:
 | 
			
		||||
@ -434,7 +435,7 @@ class Reference(Id):
 | 
			
		||||
  @classmethod
 | 
			
		||||
  def typeConvert(cls, value, ref_table, visible_col=None):  # pylint: disable=arguments-differ
 | 
			
		||||
    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:
 | 
			
		||||
      return value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user