Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# orm/properties.py 

2# Copyright (C) 2005-2020 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 

8"""MapperProperty implementations. 

9 

10This is a private module which defines the behavior of individual ORM- 

11mapped attributes. 

12 

13""" 

14from __future__ import absolute_import 

15 

16from . import attributes 

17from .interfaces import PropComparator 

18from .interfaces import StrategizedProperty 

19from .util import _orm_full_deannotate 

20from .. import log 

21from .. import util 

22from ..sql import expression 

23 

24 

25__all__ = ["ColumnProperty"] 

26 

27 

28@log.class_logger 

29class ColumnProperty(StrategizedProperty): 

30 """Describes an object attribute that corresponds to a table column. 

31 

32 Public constructor is the :func:`_orm.column_property` function. 

33 

34 """ 

35 

36 strategy_wildcard_key = "column" 

37 

38 __slots__ = ( 

39 "_orig_columns", 

40 "columns", 

41 "group", 

42 "deferred", 

43 "instrument", 

44 "comparator_factory", 

45 "descriptor", 

46 "extension", 

47 "active_history", 

48 "expire_on_flush", 

49 "info", 

50 "doc", 

51 "strategy_key", 

52 "_creation_order", 

53 "_is_polymorphic_discriminator", 

54 "_mapped_by_synonym", 

55 "_deferred_column_loader", 

56 ) 

57 

58 @util.deprecated_params( 

59 extension=( 

60 "0.7", 

61 ":class:`.AttributeExtension` is deprecated in favor of the " 

62 ":class:`.AttributeEvents` listener interface. The " 

63 ":paramref:`.column_property.extension` parameter will be " 

64 "removed in a future release.", 

65 ) 

66 ) 

67 def __init__(self, *columns, **kwargs): 

68 r"""Provide a column-level property for use with a mapping. 

69 

70 Column-based properties can normally be applied to the mapper's 

71 ``properties`` dictionary using the :class:`_schema.Column` 

72 element directly. 

73 Use this function when the given column is not directly present within 

74 the mapper's selectable; examples include SQL expressions, functions, 

75 and scalar SELECT queries. 

76 

77 The :func:`_orm.column_property` function returns an instance of 

78 :class:`.ColumnProperty`. 

79 

80 Columns that aren't present in the mapper's selectable won't be 

81 persisted by the mapper and are effectively "read-only" attributes. 

82 

83 :param \*cols: 

84 list of Column objects to be mapped. 

85 

86 :param active_history=False: 

87 When ``True``, indicates that the "previous" value for a 

88 scalar attribute should be loaded when replaced, if not 

89 already loaded. Normally, history tracking logic for 

90 simple non-primary-key scalar values only needs to be 

91 aware of the "new" value in order to perform a flush. This 

92 flag is available for applications that make use of 

93 :func:`.attributes.get_history` or :meth:`.Session.is_modified` 

94 which also need to know 

95 the "previous" value of the attribute. 

96 

97 :param comparator_factory: a class which extends 

98 :class:`.ColumnProperty.Comparator` which provides custom SQL 

99 clause generation for comparison operations. 

100 

101 :param group: 

102 a group name for this property when marked as deferred. 

103 

104 :param deferred: 

105 when True, the column property is "deferred", meaning that 

106 it does not load immediately, and is instead loaded when the 

107 attribute is first accessed on an instance. See also 

108 :func:`~sqlalchemy.orm.deferred`. 

109 

110 :param doc: 

111 optional string that will be applied as the doc on the 

112 class-bound descriptor. 

113 

114 :param expire_on_flush=True: 

115 Disable expiry on flush. A column_property() which refers 

116 to a SQL expression (and not a single table-bound column) 

117 is considered to be a "read only" property; populating it 

118 has no effect on the state of data, and it can only return 

119 database state. For this reason a column_property()'s value 

120 is expired whenever the parent object is involved in a 

121 flush, that is, has any kind of "dirty" state within a flush. 

122 Setting this parameter to ``False`` will have the effect of 

123 leaving any existing value present after the flush proceeds. 

124 Note however that the :class:`.Session` with default expiration 

125 settings still expires 

126 all attributes after a :meth:`.Session.commit` call, however. 

127 

128 :param info: Optional data dictionary which will be populated into the 

129 :attr:`.MapperProperty.info` attribute of this object. 

130 

131 :param extension: 

132 an :class:`.AttributeExtension` instance, or list of extensions, 

133 which will be prepended to the list of attribute listeners for the 

134 resulting descriptor placed on the class. 

135 

136 .. seealso:: 

137 

138 :ref:`column_property_options` - to map columns while including 

139 mapping options 

140 

141 :ref:`mapper_column_property_sql_expressions` - to map SQL 

142 expressions 

143 

144 """ 

145 super(ColumnProperty, self).__init__() 

146 self._orig_columns = [expression._labeled(c) for c in columns] 

147 self.columns = [ 

148 expression._labeled(_orm_full_deannotate(c)) for c in columns 

149 ] 

150 self.group = kwargs.pop("group", None) 

151 self.deferred = kwargs.pop("deferred", False) 

152 self.instrument = kwargs.pop("_instrument", True) 

153 self.comparator_factory = kwargs.pop( 

154 "comparator_factory", self.__class__.Comparator 

155 ) 

156 self.descriptor = kwargs.pop("descriptor", None) 

157 self.extension = kwargs.pop("extension", None) 

158 self.active_history = kwargs.pop("active_history", False) 

159 self.expire_on_flush = kwargs.pop("expire_on_flush", True) 

160 

161 if "info" in kwargs: 

162 self.info = kwargs.pop("info") 

163 

164 if "doc" in kwargs: 

165 self.doc = kwargs.pop("doc") 

166 else: 

167 for col in reversed(self.columns): 

168 doc = getattr(col, "doc", None) 

169 if doc is not None: 

170 self.doc = doc 

171 break 

172 else: 

173 self.doc = None 

174 

175 if kwargs: 

176 raise TypeError( 

177 "%s received unexpected keyword argument(s): %s" 

178 % (self.__class__.__name__, ", ".join(sorted(kwargs.keys()))) 

179 ) 

180 

181 util.set_creation_order(self) 

182 

183 self.strategy_key = ( 

184 ("deferred", self.deferred), 

185 ("instrument", self.instrument), 

186 ) 

187 

188 @util.dependencies("sqlalchemy.orm.state", "sqlalchemy.orm.strategies") 

189 def _memoized_attr__deferred_column_loader(self, state, strategies): 

190 return state.InstanceState._instance_level_callable_processor( 

191 self.parent.class_manager, 

192 strategies.LoadDeferredColumns(self.key), 

193 self.key, 

194 ) 

195 

196 def __clause_element__(self): 

197 """Allow the ColumnProperty to work in expression before it is turned 

198 into an instrumented attribute. 

199 """ 

200 

201 return self.expression 

202 

203 @property 

204 def expression(self): 

205 """Return the primary column or expression for this ColumnProperty. 

206 

207 E.g.:: 

208 

209 

210 class File(Base): 

211 # ... 

212 

213 name = Column(String(64)) 

214 extension = Column(String(8)) 

215 filename = column_property(name + '.' + extension) 

216 path = column_property('C:/' + filename.expression) 

217 

218 .. seealso:: 

219 

220 :ref:`mapper_column_property_sql_expressions_composed` 

221 

222 """ 

223 return self.columns[0] 

224 

225 def instrument_class(self, mapper): 

226 if not self.instrument: 

227 return 

228 

229 attributes.register_descriptor( 

230 mapper.class_, 

231 self.key, 

232 comparator=self.comparator_factory(self, mapper), 

233 parententity=mapper, 

234 doc=self.doc, 

235 ) 

236 

237 def do_init(self): 

238 super(ColumnProperty, self).do_init() 

239 if len(self.columns) > 1 and set(self.parent.primary_key).issuperset( 

240 self.columns 

241 ): 

242 util.warn( 

243 ( 

244 "On mapper %s, primary key column '%s' is being combined " 

245 "with distinct primary key column '%s' in attribute '%s'. " 

246 "Use explicit properties to give each column its own " 

247 "mapped attribute name." 

248 ) 

249 % (self.parent, self.columns[1], self.columns[0], self.key) 

250 ) 

251 

252 def copy(self): 

253 return ColumnProperty( 

254 deferred=self.deferred, 

255 group=self.group, 

256 active_history=self.active_history, 

257 *self.columns 

258 ) 

259 

260 def _getcommitted( 

261 self, state, dict_, column, passive=attributes.PASSIVE_OFF 

262 ): 

263 return state.get_impl(self.key).get_committed_value( 

264 state, dict_, passive=passive 

265 ) 

266 

267 def merge( 

268 self, 

269 session, 

270 source_state, 

271 source_dict, 

272 dest_state, 

273 dest_dict, 

274 load, 

275 _recursive, 

276 _resolve_conflict_map, 

277 ): 

278 if not self.instrument: 

279 return 

280 elif self.key in source_dict: 

281 value = source_dict[self.key] 

282 

283 if not load: 

284 dest_dict[self.key] = value 

285 else: 

286 impl = dest_state.get_impl(self.key) 

287 impl.set(dest_state, dest_dict, value, None) 

288 elif dest_state.has_identity and self.key not in dest_dict: 

289 dest_state._expire_attributes( 

290 dest_dict, [self.key], no_loader=True 

291 ) 

292 

293 class Comparator(util.MemoizedSlots, PropComparator): 

294 """Produce boolean, comparison, and other operators for 

295 :class:`.ColumnProperty` attributes. 

296 

297 See the documentation for :class:`.PropComparator` for a brief 

298 overview. 

299 

300 .. seealso:: 

301 

302 :class:`.PropComparator` 

303 

304 :class:`.ColumnOperators` 

305 

306 :ref:`types_operators` 

307 

308 :attr:`.TypeEngine.comparator_factory` 

309 

310 """ 

311 

312 __slots__ = "__clause_element__", "info", "expressions" 

313 

314 def _memoized_method___clause_element__(self): 

315 if self.adapter: 

316 return self.adapter(self.prop.columns[0]) 

317 else: 

318 # no adapter, so we aren't aliased 

319 # assert self._parententity is self._parentmapper 

320 return self.prop.columns[0]._annotate( 

321 { 

322 "parententity": self._parententity, 

323 "parentmapper": self._parententity, 

324 } 

325 ) 

326 

327 def _memoized_attr_info(self): 

328 """The .info dictionary for this attribute.""" 

329 

330 ce = self.__clause_element__() 

331 try: 

332 return ce.info 

333 except AttributeError: 

334 return self.prop.info 

335 

336 def _memoized_attr_expressions(self): 

337 """The full sequence of columns referenced by this 

338 attribute, adjusted for any aliasing in progress. 

339 

340 .. versionadded:: 1.3.17 

341 

342 """ 

343 if self.adapter: 

344 return [self.adapter(col) for col in self.prop.columns] 

345 else: 

346 # no adapter, so we aren't aliased 

347 # assert self._parententity is self._parentmapper 

348 return [ 

349 col._annotate( 

350 { 

351 "parententity": self._parententity, 

352 "parentmapper": self._parententity, 

353 "orm_key": self.prop.key, 

354 } 

355 ) 

356 for col in self.prop.columns 

357 ] 

358 

359 def _fallback_getattr(self, key): 

360 """proxy attribute access down to the mapped column. 

361 

362 this allows user-defined comparison methods to be accessed. 

363 """ 

364 return getattr(self.__clause_element__(), key) 

365 

366 def operate(self, op, *other, **kwargs): 

367 return op(self.__clause_element__(), *other, **kwargs) 

368 

369 def reverse_operate(self, op, other, **kwargs): 

370 col = self.__clause_element__() 

371 return op(col._bind_param(op, other), col, **kwargs) 

372 

373 def __str__(self): 

374 return str(self.parent.class_.__name__) + "." + self.key