util.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762
  1. # sql/util.py
  2. # Copyright (C) 2005-2017 the SQLAlchemy authors and contributors
  3. # <see AUTHORS file>
  4. #
  5. # This module is part of SQLAlchemy and is released under
  6. # the MIT License: http://www.opensource.org/licenses/mit-license.php
  7. """High level utilities which build upon other modules here.
  8. """
  9. from .. import exc, util
  10. from .base import _from_objects, ColumnSet
  11. from . import operators, visitors
  12. from itertools import chain
  13. from collections import deque
  14. from .elements import BindParameter, ColumnClause, ColumnElement, \
  15. Null, UnaryExpression, literal_column, Label, _label_reference, \
  16. _textual_label_reference
  17. from .selectable import ScalarSelect, Join, FromClause, FromGrouping
  18. from .schema import Column
  19. join_condition = util.langhelpers.public_factory(
  20. Join._join_condition,
  21. ".sql.util.join_condition")
  22. # names that are still being imported from the outside
  23. from .annotation import _shallow_annotate, _deep_annotate, _deep_deannotate
  24. from .elements import _find_columns
  25. from .ddl import sort_tables
  26. def find_join_source(clauses, join_to):
  27. """Given a list of FROM clauses and a selectable,
  28. return the first index and element from the list of
  29. clauses which can be joined against the selectable. returns
  30. None, None if no match is found.
  31. e.g.::
  32. clause1 = table1.join(table2)
  33. clause2 = table4.join(table5)
  34. join_to = table2.join(table3)
  35. find_join_source([clause1, clause2], join_to) == clause1
  36. """
  37. selectables = list(_from_objects(join_to))
  38. for i, f in enumerate(clauses):
  39. for s in selectables:
  40. if f.is_derived_from(s):
  41. return i, f
  42. else:
  43. return None, None
  44. def visit_binary_product(fn, expr):
  45. """Produce a traversal of the given expression, delivering
  46. column comparisons to the given function.
  47. The function is of the form::
  48. def my_fn(binary, left, right)
  49. For each binary expression located which has a
  50. comparison operator, the product of "left" and
  51. "right" will be delivered to that function,
  52. in terms of that binary.
  53. Hence an expression like::
  54. and_(
  55. (a + b) == q + func.sum(e + f),
  56. j == r
  57. )
  58. would have the traversal::
  59. a <eq> q
  60. a <eq> e
  61. a <eq> f
  62. b <eq> q
  63. b <eq> e
  64. b <eq> f
  65. j <eq> r
  66. That is, every combination of "left" and
  67. "right" that doesn't further contain
  68. a binary comparison is passed as pairs.
  69. """
  70. stack = []
  71. def visit(element):
  72. if isinstance(element, ScalarSelect):
  73. # we don't want to dig into correlated subqueries,
  74. # those are just column elements by themselves
  75. yield element
  76. elif element.__visit_name__ == 'binary' and \
  77. operators.is_comparison(element.operator):
  78. stack.insert(0, element)
  79. for l in visit(element.left):
  80. for r in visit(element.right):
  81. fn(stack[0], l, r)
  82. stack.pop(0)
  83. for elem in element.get_children():
  84. visit(elem)
  85. else:
  86. if isinstance(element, ColumnClause):
  87. yield element
  88. for elem in element.get_children():
  89. for e in visit(elem):
  90. yield e
  91. list(visit(expr))
  92. def find_tables(clause, check_columns=False,
  93. include_aliases=False, include_joins=False,
  94. include_selects=False, include_crud=False):
  95. """locate Table objects within the given expression."""
  96. tables = []
  97. _visitors = {}
  98. if include_selects:
  99. _visitors['select'] = _visitors['compound_select'] = tables.append
  100. if include_joins:
  101. _visitors['join'] = tables.append
  102. if include_aliases:
  103. _visitors['alias'] = tables.append
  104. if include_crud:
  105. _visitors['insert'] = _visitors['update'] = \
  106. _visitors['delete'] = lambda ent: tables.append(ent.table)
  107. if check_columns:
  108. def visit_column(column):
  109. tables.append(column.table)
  110. _visitors['column'] = visit_column
  111. _visitors['table'] = tables.append
  112. visitors.traverse(clause, {'column_collections': False}, _visitors)
  113. return tables
  114. def unwrap_order_by(clause):
  115. """Break up an 'order by' expression into individual column-expressions,
  116. without DESC/ASC/NULLS FIRST/NULLS LAST"""
  117. cols = util.column_set()
  118. result = []
  119. stack = deque([clause])
  120. while stack:
  121. t = stack.popleft()
  122. if isinstance(t, ColumnElement) and \
  123. (
  124. not isinstance(t, UnaryExpression) or
  125. not operators.is_ordering_modifier(t.modifier)
  126. ):
  127. if isinstance(t, _label_reference):
  128. t = t.element
  129. if isinstance(t, (_textual_label_reference)):
  130. continue
  131. if t not in cols:
  132. cols.add(t)
  133. result.append(t)
  134. else:
  135. for c in t.get_children():
  136. stack.append(c)
  137. return result
  138. def unwrap_label_reference(element):
  139. def replace(elem):
  140. if isinstance(elem, (_label_reference, _textual_label_reference)):
  141. return elem.element
  142. return visitors.replacement_traverse(
  143. element, {}, replace
  144. )
  145. def expand_column_list_from_order_by(collist, order_by):
  146. """Given the columns clause and ORDER BY of a selectable,
  147. return a list of column expressions that can be added to the collist
  148. corresponding to the ORDER BY, without repeating those already
  149. in the collist.
  150. """
  151. cols_already_present = set([
  152. col.element if col._order_by_label_element is not None
  153. else col for col in collist
  154. ])
  155. return [
  156. col for col in
  157. chain(*[
  158. unwrap_order_by(o)
  159. for o in order_by
  160. ])
  161. if col not in cols_already_present
  162. ]
  163. def clause_is_present(clause, search):
  164. """Given a target clause and a second to search within, return True
  165. if the target is plainly present in the search without any
  166. subqueries or aliases involved.
  167. Basically descends through Joins.
  168. """
  169. for elem in surface_selectables(search):
  170. if clause == elem: # use == here so that Annotated's compare
  171. return True
  172. else:
  173. return False
  174. def surface_selectables(clause):
  175. stack = [clause]
  176. while stack:
  177. elem = stack.pop()
  178. yield elem
  179. if isinstance(elem, Join):
  180. stack.extend((elem.left, elem.right))
  181. elif isinstance(elem, FromGrouping):
  182. stack.append(elem.element)
  183. def surface_column_elements(clause):
  184. """traverse and yield only outer-exposed column elements, such as would
  185. be addressable in the WHERE clause of a SELECT if this element were
  186. in the columns clause."""
  187. stack = deque([clause])
  188. while stack:
  189. elem = stack.popleft()
  190. yield elem
  191. for sub in elem.get_children():
  192. if isinstance(sub, FromGrouping):
  193. continue
  194. stack.append(sub)
  195. def selectables_overlap(left, right):
  196. """Return True if left/right have some overlapping selectable"""
  197. return bool(
  198. set(surface_selectables(left)).intersection(
  199. surface_selectables(right)
  200. )
  201. )
  202. def bind_values(clause):
  203. """Return an ordered list of "bound" values in the given clause.
  204. E.g.::
  205. >>> expr = and_(
  206. ... table.c.foo==5, table.c.foo==7
  207. ... )
  208. >>> bind_values(expr)
  209. [5, 7]
  210. """
  211. v = []
  212. def visit_bindparam(bind):
  213. v.append(bind.effective_value)
  214. visitors.traverse(clause, {}, {'bindparam': visit_bindparam})
  215. return v
  216. def _quote_ddl_expr(element):
  217. if isinstance(element, util.string_types):
  218. element = element.replace("'", "''")
  219. return "'%s'" % element
  220. else:
  221. return repr(element)
  222. class _repr_base(object):
  223. _LIST = 0
  224. _TUPLE = 1
  225. _DICT = 2
  226. __slots__ = 'max_chars',
  227. def trunc(self, value):
  228. rep = repr(value)
  229. lenrep = len(rep)
  230. if lenrep > self.max_chars:
  231. segment_length = self.max_chars // 2
  232. rep = (
  233. rep[0:segment_length] +
  234. (" ... (%d characters truncated) ... "
  235. % (lenrep - self.max_chars)) +
  236. rep[-segment_length:]
  237. )
  238. return rep
  239. class _repr_row(_repr_base):
  240. """Provide a string view of a row."""
  241. __slots__ = 'row',
  242. def __init__(self, row, max_chars=300):
  243. self.row = row
  244. self.max_chars = max_chars
  245. def __repr__(self):
  246. trunc = self.trunc
  247. return "(%s%s)" % (
  248. ", ".join(trunc(value) for value in self.row),
  249. "," if len(self.row) == 1 else ""
  250. )
  251. class _repr_params(_repr_base):
  252. """Provide a string view of bound parameters.
  253. Truncates display to a given numnber of 'multi' parameter sets,
  254. as well as long values to a given number of characters.
  255. """
  256. __slots__ = 'params', 'batches',
  257. def __init__(self, params, batches, max_chars=300):
  258. self.params = params
  259. self.batches = batches
  260. self.max_chars = max_chars
  261. def __repr__(self):
  262. if isinstance(self.params, list):
  263. typ = self._LIST
  264. ismulti = self.params and isinstance(
  265. self.params[0], (list, dict, tuple))
  266. elif isinstance(self.params, tuple):
  267. typ = self._TUPLE
  268. ismulti = self.params and isinstance(
  269. self.params[0], (list, dict, tuple))
  270. elif isinstance(self.params, dict):
  271. typ = self._DICT
  272. ismulti = False
  273. else:
  274. return self.trunc(self.params)
  275. if ismulti and len(self.params) > self.batches:
  276. msg = " ... displaying %i of %i total bound parameter sets ... "
  277. return ' '.join((
  278. self._repr_multi(self.params[:self.batches - 2], typ)[0:-1],
  279. msg % (self.batches, len(self.params)),
  280. self._repr_multi(self.params[-2:], typ)[1:]
  281. ))
  282. elif ismulti:
  283. return self._repr_multi(self.params, typ)
  284. else:
  285. return self._repr_params(self.params, typ)
  286. def _repr_multi(self, multi_params, typ):
  287. if multi_params:
  288. if isinstance(multi_params[0], list):
  289. elem_type = self._LIST
  290. elif isinstance(multi_params[0], tuple):
  291. elem_type = self._TUPLE
  292. elif isinstance(multi_params[0], dict):
  293. elem_type = self._DICT
  294. else:
  295. assert False, \
  296. "Unknown parameter type %s" % (type(multi_params[0]))
  297. elements = ", ".join(
  298. self._repr_params(params, elem_type)
  299. for params in multi_params)
  300. else:
  301. elements = ""
  302. if typ == self._LIST:
  303. return "[%s]" % elements
  304. else:
  305. return "(%s)" % elements
  306. def _repr_params(self, params, typ):
  307. trunc = self.trunc
  308. if typ is self._DICT:
  309. return "{%s}" % (
  310. ", ".join(
  311. "%r: %s" % (key, trunc(value))
  312. for key, value in params.items()
  313. )
  314. )
  315. elif typ is self._TUPLE:
  316. return "(%s%s)" % (
  317. ", ".join(trunc(value) for value in params),
  318. "," if len(params) == 1 else ""
  319. )
  320. else:
  321. return "[%s]" % (
  322. ", ".join(trunc(value) for value in params)
  323. )
  324. def adapt_criterion_to_null(crit, nulls):
  325. """given criterion containing bind params, convert selected elements
  326. to IS NULL.
  327. """
  328. def visit_binary(binary):
  329. if isinstance(binary.left, BindParameter) \
  330. and binary.left._identifying_key in nulls:
  331. # reverse order if the NULL is on the left side
  332. binary.left = binary.right
  333. binary.right = Null()
  334. binary.operator = operators.is_
  335. binary.negate = operators.isnot
  336. elif isinstance(binary.right, BindParameter) \
  337. and binary.right._identifying_key in nulls:
  338. binary.right = Null()
  339. binary.operator = operators.is_
  340. binary.negate = operators.isnot
  341. return visitors.cloned_traverse(crit, {}, {'binary': visit_binary})
  342. def splice_joins(left, right, stop_on=None):
  343. if left is None:
  344. return right
  345. stack = [(right, None)]
  346. adapter = ClauseAdapter(left)
  347. ret = None
  348. while stack:
  349. (right, prevright) = stack.pop()
  350. if isinstance(right, Join) and right is not stop_on:
  351. right = right._clone()
  352. right._reset_exported()
  353. right.onclause = adapter.traverse(right.onclause)
  354. stack.append((right.left, right))
  355. else:
  356. right = adapter.traverse(right)
  357. if prevright is not None:
  358. prevright.left = right
  359. if ret is None:
  360. ret = right
  361. return ret
  362. def reduce_columns(columns, *clauses, **kw):
  363. r"""given a list of columns, return a 'reduced' set based on natural
  364. equivalents.
  365. the set is reduced to the smallest list of columns which have no natural
  366. equivalent present in the list. A "natural equivalent" means that two
  367. columns will ultimately represent the same value because they are related
  368. by a foreign key.
  369. \*clauses is an optional list of join clauses which will be traversed
  370. to further identify columns that are "equivalent".
  371. \**kw may specify 'ignore_nonexistent_tables' to ignore foreign keys
  372. whose tables are not yet configured, or columns that aren't yet present.
  373. This function is primarily used to determine the most minimal "primary
  374. key" from a selectable, by reducing the set of primary key columns present
  375. in the selectable to just those that are not repeated.
  376. """
  377. ignore_nonexistent_tables = kw.pop('ignore_nonexistent_tables', False)
  378. only_synonyms = kw.pop('only_synonyms', False)
  379. columns = util.ordered_column_set(columns)
  380. omit = util.column_set()
  381. for col in columns:
  382. for fk in chain(*[c.foreign_keys for c in col.proxy_set]):
  383. for c in columns:
  384. if c is col:
  385. continue
  386. try:
  387. fk_col = fk.column
  388. except exc.NoReferencedColumnError:
  389. # TODO: add specific coverage here
  390. # to test/sql/test_selectable ReduceTest
  391. if ignore_nonexistent_tables:
  392. continue
  393. else:
  394. raise
  395. except exc.NoReferencedTableError:
  396. # TODO: add specific coverage here
  397. # to test/sql/test_selectable ReduceTest
  398. if ignore_nonexistent_tables:
  399. continue
  400. else:
  401. raise
  402. if fk_col.shares_lineage(c) and \
  403. (not only_synonyms or
  404. c.name == col.name):
  405. omit.add(col)
  406. break
  407. if clauses:
  408. def visit_binary(binary):
  409. if binary.operator == operators.eq:
  410. cols = util.column_set(
  411. chain(*[c.proxy_set for c in columns.difference(omit)]))
  412. if binary.left in cols and binary.right in cols:
  413. for c in reversed(columns):
  414. if c.shares_lineage(binary.right) and \
  415. (not only_synonyms or
  416. c.name == binary.left.name):
  417. omit.add(c)
  418. break
  419. for clause in clauses:
  420. if clause is not None:
  421. visitors.traverse(clause, {}, {'binary': visit_binary})
  422. return ColumnSet(columns.difference(omit))
  423. def criterion_as_pairs(expression, consider_as_foreign_keys=None,
  424. consider_as_referenced_keys=None, any_operator=False):
  425. """traverse an expression and locate binary criterion pairs."""
  426. if consider_as_foreign_keys and consider_as_referenced_keys:
  427. raise exc.ArgumentError("Can only specify one of "
  428. "'consider_as_foreign_keys' or "
  429. "'consider_as_referenced_keys'")
  430. def col_is(a, b):
  431. # return a is b
  432. return a.compare(b)
  433. def visit_binary(binary):
  434. if not any_operator and binary.operator is not operators.eq:
  435. return
  436. if not isinstance(binary.left, ColumnElement) or \
  437. not isinstance(binary.right, ColumnElement):
  438. return
  439. if consider_as_foreign_keys:
  440. if binary.left in consider_as_foreign_keys and \
  441. (col_is(binary.right, binary.left) or
  442. binary.right not in consider_as_foreign_keys):
  443. pairs.append((binary.right, binary.left))
  444. elif binary.right in consider_as_foreign_keys and \
  445. (col_is(binary.left, binary.right) or
  446. binary.left not in consider_as_foreign_keys):
  447. pairs.append((binary.left, binary.right))
  448. elif consider_as_referenced_keys:
  449. if binary.left in consider_as_referenced_keys and \
  450. (col_is(binary.right, binary.left) or
  451. binary.right not in consider_as_referenced_keys):
  452. pairs.append((binary.left, binary.right))
  453. elif binary.right in consider_as_referenced_keys and \
  454. (col_is(binary.left, binary.right) or
  455. binary.left not in consider_as_referenced_keys):
  456. pairs.append((binary.right, binary.left))
  457. else:
  458. if isinstance(binary.left, Column) and \
  459. isinstance(binary.right, Column):
  460. if binary.left.references(binary.right):
  461. pairs.append((binary.right, binary.left))
  462. elif binary.right.references(binary.left):
  463. pairs.append((binary.left, binary.right))
  464. pairs = []
  465. visitors.traverse(expression, {}, {'binary': visit_binary})
  466. return pairs
  467. class ClauseAdapter(visitors.ReplacingCloningVisitor):
  468. """Clones and modifies clauses based on column correspondence.
  469. E.g.::
  470. table1 = Table('sometable', metadata,
  471. Column('col1', Integer),
  472. Column('col2', Integer)
  473. )
  474. table2 = Table('someothertable', metadata,
  475. Column('col1', Integer),
  476. Column('col2', Integer)
  477. )
  478. condition = table1.c.col1 == table2.c.col1
  479. make an alias of table1::
  480. s = table1.alias('foo')
  481. calling ``ClauseAdapter(s).traverse(condition)`` converts
  482. condition to read::
  483. s.c.col1 == table2.c.col1
  484. """
  485. def __init__(self, selectable, equivalents=None,
  486. include_fn=None, exclude_fn=None,
  487. adapt_on_names=False, anonymize_labels=False):
  488. self.__traverse_options__ = {
  489. 'stop_on': [selectable],
  490. 'anonymize_labels': anonymize_labels}
  491. self.selectable = selectable
  492. self.include_fn = include_fn
  493. self.exclude_fn = exclude_fn
  494. self.equivalents = util.column_dict(equivalents or {})
  495. self.adapt_on_names = adapt_on_names
  496. def _corresponding_column(self, col, require_embedded,
  497. _seen=util.EMPTY_SET):
  498. newcol = self.selectable.corresponding_column(
  499. col,
  500. require_embedded=require_embedded)
  501. if newcol is None and col in self.equivalents and col not in _seen:
  502. for equiv in self.equivalents[col]:
  503. newcol = self._corresponding_column(
  504. equiv, require_embedded=require_embedded,
  505. _seen=_seen.union([col]))
  506. if newcol is not None:
  507. return newcol
  508. if self.adapt_on_names and newcol is None:
  509. newcol = self.selectable.c.get(col.name)
  510. return newcol
  511. def replace(self, col):
  512. if isinstance(col, FromClause) and \
  513. self.selectable.is_derived_from(col):
  514. return self.selectable
  515. elif not isinstance(col, ColumnElement):
  516. return None
  517. elif self.include_fn and not self.include_fn(col):
  518. return None
  519. elif self.exclude_fn and self.exclude_fn(col):
  520. return None
  521. else:
  522. return self._corresponding_column(col, True)
  523. class ColumnAdapter(ClauseAdapter):
  524. """Extends ClauseAdapter with extra utility functions.
  525. Key aspects of ColumnAdapter include:
  526. * Expressions that are adapted are stored in a persistent
  527. .columns collection; so that an expression E adapted into
  528. an expression E1, will return the same object E1 when adapted
  529. a second time. This is important in particular for things like
  530. Label objects that are anonymized, so that the ColumnAdapter can
  531. be used to present a consistent "adapted" view of things.
  532. * Exclusion of items from the persistent collection based on
  533. include/exclude rules, but also independent of hash identity.
  534. This because "annotated" items all have the same hash identity as their
  535. parent.
  536. * "wrapping" capability is added, so that the replacement of an expression
  537. E can proceed through a series of adapters. This differs from the
  538. visitor's "chaining" feature in that the resulting object is passed
  539. through all replacing functions unconditionally, rather than stopping
  540. at the first one that returns non-None.
  541. * An adapt_required option, used by eager loading to indicate that
  542. We don't trust a result row column that is not translated.
  543. This is to prevent a column from being interpreted as that
  544. of the child row in a self-referential scenario, see
  545. inheritance/test_basic.py->EagerTargetingTest.test_adapt_stringency
  546. """
  547. def __init__(self, selectable, equivalents=None,
  548. chain_to=None, adapt_required=False,
  549. include_fn=None, exclude_fn=None,
  550. adapt_on_names=False,
  551. allow_label_resolve=True,
  552. anonymize_labels=False):
  553. ClauseAdapter.__init__(self, selectable, equivalents,
  554. include_fn=include_fn, exclude_fn=exclude_fn,
  555. adapt_on_names=adapt_on_names,
  556. anonymize_labels=anonymize_labels)
  557. if chain_to:
  558. self.chain(chain_to)
  559. self.columns = util.populate_column_dict(self._locate_col)
  560. if self.include_fn or self.exclude_fn:
  561. self.columns = self._IncludeExcludeMapping(self, self.columns)
  562. self.adapt_required = adapt_required
  563. self.allow_label_resolve = allow_label_resolve
  564. self._wrap = None
  565. class _IncludeExcludeMapping(object):
  566. def __init__(self, parent, columns):
  567. self.parent = parent
  568. self.columns = columns
  569. def __getitem__(self, key):
  570. if (
  571. self.parent.include_fn and not self.parent.include_fn(key)
  572. ) or (
  573. self.parent.exclude_fn and self.parent.exclude_fn(key)
  574. ):
  575. if self.parent._wrap:
  576. return self.parent._wrap.columns[key]
  577. else:
  578. return key
  579. return self.columns[key]
  580. def wrap(self, adapter):
  581. ac = self.__class__.__new__(self.__class__)
  582. ac.__dict__.update(self.__dict__)
  583. ac._wrap = adapter
  584. ac.columns = util.populate_column_dict(ac._locate_col)
  585. if ac.include_fn or ac.exclude_fn:
  586. ac.columns = self._IncludeExcludeMapping(ac, ac.columns)
  587. return ac
  588. def traverse(self, obj):
  589. return self.columns[obj]
  590. adapt_clause = traverse
  591. adapt_list = ClauseAdapter.copy_and_process
  592. def _locate_col(self, col):
  593. c = ClauseAdapter.traverse(self, col)
  594. if self._wrap:
  595. c2 = self._wrap._locate_col(c)
  596. if c2 is not None:
  597. c = c2
  598. if self.adapt_required and c is col:
  599. return None
  600. c._allow_label_resolve = self.allow_label_resolve
  601. return c
  602. def __getstate__(self):
  603. d = self.__dict__.copy()
  604. del d['columns']
  605. return d
  606. def __setstate__(self, state):
  607. self.__dict__.update(state)
  608. self.columns = util.PopulateDict(self._locate_col)