mongo_mapper-unstable 2009.10.16 → 2009.10.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +3 -1
- data/README.rdoc +3 -0
- data/Rakefile +31 -65
- data/VERSION +1 -1
- data/lib/mongo_mapper/associations/base.rb +31 -4
- data/lib/mongo_mapper/associations/many_documents_as_proxy.rb +0 -2
- data/lib/mongo_mapper/associations/many_documents_proxy.rb +21 -15
- data/lib/mongo_mapper/associations/many_embedded_polymorphic_proxy.rb +2 -2
- data/lib/mongo_mapper/associations/many_embedded_proxy.rb +21 -36
- data/lib/mongo_mapper/associations/many_polymorphic_proxy.rb +1 -1
- data/lib/mongo_mapper/associations/proxy.rb +1 -0
- data/lib/mongo_mapper/associations.rb +114 -17
- data/lib/mongo_mapper/callbacks.rb +18 -0
- data/lib/mongo_mapper/document.rb +230 -95
- data/lib/mongo_mapper/dynamic_finder.rb +1 -1
- data/lib/mongo_mapper/embedded_document.rb +7 -3
- data/lib/mongo_mapper/finder_options.rb +88 -56
- data/lib/mongo_mapper/pagination.rb +2 -0
- data/lib/mongo_mapper/serialization.rb +2 -3
- data/lib/mongo_mapper/serializers/json_serializer.rb +1 -1
- data/lib/mongo_mapper/support.rb +9 -0
- data/lib/mongo_mapper/validations.rb +14 -42
- data/lib/mongo_mapper.rb +15 -13
- data/mongo_mapper.gemspec +13 -13
- data/specs.watchr +2 -2
- data/test/functional/associations/test_belongs_to_polymorphic_proxy.rb +5 -5
- data/test/functional/associations/test_belongs_to_proxy.rb +28 -30
- data/test/functional/associations/test_many_documents_as_proxy.rb +4 -4
- data/test/functional/associations/test_many_embedded_polymorphic_proxy.rb +27 -3
- data/test/functional/associations/test_many_embedded_proxy.rb +58 -38
- data/test/functional/associations/test_many_polymorphic_proxy.rb +49 -7
- data/test/functional/associations/test_many_proxy.rb +65 -15
- data/test/functional/test_associations.rb +3 -3
- data/test/functional/test_binary.rb +1 -1
- data/test/functional/test_callbacks.rb +1 -1
- data/test/functional/test_dirty.rb +3 -3
- data/test/functional/test_document.rb +178 -57
- data/test/functional/test_embedded_document.rb +1 -1
- data/test/functional/test_pagination.rb +18 -18
- data/test/functional/test_rails_compatibility.rb +1 -1
- data/test/functional/test_validations.rb +80 -27
- data/test/models.rb +93 -17
- data/test/support/{test_timing.rb → timing.rb} +1 -1
- data/test/test_helper.rb +8 -11
- data/test/unit/test_association_base.rb +23 -1
- data/test/unit/test_document.rb +29 -12
- data/test/unit/test_embedded_document.rb +13 -4
- data/test/unit/test_finder_options.rb +74 -58
- data/test/unit/test_mongomapper.rb +2 -2
- data/test/unit/test_pagination.rb +4 -0
- metadata +7 -7
@@ -13,12 +13,12 @@ module MongoMapper
|
|
13
13
|
extend Validations::Macros
|
14
14
|
extend ClassMethods
|
15
15
|
extend Finders
|
16
|
-
|
16
|
+
|
17
17
|
def self.per_page
|
18
18
|
25
|
19
19
|
end unless respond_to?(:per_page)
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
descendants << model
|
23
23
|
end
|
24
24
|
|
@@ -32,17 +32,29 @@ module MongoMapper
|
|
32
32
|
create_indexes_for(key)
|
33
33
|
key
|
34
34
|
end
|
35
|
-
|
35
|
+
|
36
36
|
def ensure_index(name_or_array, options={})
|
37
37
|
keys_to_index = if name_or_array.is_a?(Array)
|
38
38
|
name_or_array.map { |pair| [pair[0], pair[1]] }
|
39
39
|
else
|
40
40
|
name_or_array
|
41
41
|
end
|
42
|
-
|
42
|
+
|
43
43
|
MongoMapper.ensure_index(self, keys_to_index, options)
|
44
44
|
end
|
45
|
-
|
45
|
+
|
46
|
+
# @overload find(:first, options)
|
47
|
+
# @see Document.first
|
48
|
+
#
|
49
|
+
# @overload find(:last, options)
|
50
|
+
# @see Document.last
|
51
|
+
#
|
52
|
+
# @overload find(:all, options)
|
53
|
+
# @see Document.all
|
54
|
+
#
|
55
|
+
# @overload find(ids, options)
|
56
|
+
#
|
57
|
+
# @raise DocumentNotFound raised when no ID or arguments are provided
|
46
58
|
def find(*args)
|
47
59
|
options = args.extract_options!
|
48
60
|
case args.first
|
@@ -55,7 +67,7 @@ module MongoMapper
|
|
55
67
|
when 0
|
56
68
|
raise DocumentNotFound, "Couldn't find without an ID"
|
57
69
|
when 1
|
58
|
-
find_one(args[0]
|
70
|
+
find_one!(options.merge({:_id => args[0]}))
|
59
71
|
else
|
60
72
|
find_some(args, options)
|
61
73
|
end
|
@@ -63,68 +75,113 @@ module MongoMapper
|
|
63
75
|
end
|
64
76
|
|
65
77
|
def paginate(options)
|
66
|
-
per_page = options.delete(:per_page) ||
|
78
|
+
per_page = options.delete(:per_page) || self.per_page
|
67
79
|
page = options.delete(:page)
|
68
|
-
total_entries = count(options
|
69
|
-
|
70
|
-
|
71
|
-
options[:limit] = collection.limit
|
72
|
-
options[:skip] = collection.skip
|
80
|
+
total_entries = count(options)
|
81
|
+
pagination = Pagination::PaginationProxy.new(total_entries, page, per_page)
|
73
82
|
|
74
|
-
|
75
|
-
|
83
|
+
options.merge!(:limit => pagination.limit, :skip => pagination.skip)
|
84
|
+
pagination.subject = find_every(options)
|
85
|
+
pagination
|
76
86
|
end
|
77
87
|
|
88
|
+
# @param [Hash] options any conditions understood by
|
89
|
+
# FinderOptions.to_mongo_criteria
|
90
|
+
#
|
91
|
+
# @return the first document in the ordered collection as described by
|
92
|
+
# +options+
|
93
|
+
#
|
94
|
+
# @see FinderOptions
|
78
95
|
def first(options={})
|
79
|
-
options
|
80
|
-
find_every(options)[0]
|
96
|
+
find_one(options)
|
81
97
|
end
|
82
98
|
|
99
|
+
# @param [Hash] options any conditions understood by
|
100
|
+
# FinderOptions.to_mongo_criteria
|
101
|
+
# @option [String] :order this *mandatory* option describes how to
|
102
|
+
# identify the ordering of the documents in your collection. Note that
|
103
|
+
# the *last* document in this collection will be selected.
|
104
|
+
#
|
105
|
+
# @return the last document in the ordered collection as described by
|
106
|
+
# +options+
|
107
|
+
#
|
108
|
+
# @raise Exception when no <tt>:order</tt> option has been defined
|
83
109
|
def last(options={})
|
84
|
-
if options[:order].blank?
|
85
|
-
|
86
|
-
end
|
87
|
-
|
88
|
-
options.merge!(:limit => 1)
|
89
|
-
options[:order] = invert_order_clause(options[:order])
|
90
|
-
find_every(options)[0]
|
110
|
+
raise ':order option must be provided when using last' if options[:order].blank?
|
111
|
+
find_one(options.merge(:order => invert_order_clause(options[:order])))
|
91
112
|
end
|
92
113
|
|
114
|
+
# @param [Hash] options any conditions understood by
|
115
|
+
# FinderOptions.to_mongo_criteria
|
116
|
+
#
|
117
|
+
# @return [Array] all documents in your collection that match the
|
118
|
+
# provided conditions
|
119
|
+
#
|
120
|
+
# @see FinderOptions
|
93
121
|
def all(options={})
|
94
122
|
find_every(options)
|
95
123
|
end
|
96
124
|
|
97
125
|
def find_by_id(id)
|
98
|
-
|
99
|
-
if doc = collection.find_one(criteria)
|
100
|
-
new(doc)
|
101
|
-
end
|
126
|
+
find_one(:_id => id)
|
102
127
|
end
|
103
128
|
|
104
|
-
def count(
|
105
|
-
collection.find(
|
129
|
+
def count(options={})
|
130
|
+
collection.find(to_criteria(options)).count
|
106
131
|
end
|
107
132
|
|
108
|
-
def exists?(
|
109
|
-
!count(
|
133
|
+
def exists?(options={})
|
134
|
+
!count(options).zero?
|
110
135
|
end
|
111
136
|
|
137
|
+
# @overload create(doc_attributes)
|
138
|
+
# Create a single new document
|
139
|
+
# @param [Hash] doc_attributes key/value pairs to create a new
|
140
|
+
# document
|
141
|
+
#
|
142
|
+
# @overload create(docs_attributes)
|
143
|
+
# Create many new documents
|
144
|
+
# @param [Array<Hash>] provide many Hashes of key/value pairs to create
|
145
|
+
# multiple documents
|
146
|
+
#
|
147
|
+
# @example Creating a single document
|
148
|
+
# MyModel.create({ :foo => "bar" })
|
149
|
+
#
|
150
|
+
# @example Creating multiple documents
|
151
|
+
# MyModel.create([{ :foo => "bar" }, { :foo => "baz" })
|
152
|
+
#
|
153
|
+
# @return [Boolean] when a document is successfully created, +true+ will
|
154
|
+
# be returned. If a document fails to create, +false+ will be returned.
|
112
155
|
def create(*docs)
|
113
156
|
initialize_each(*docs) { |doc| doc.save }
|
114
157
|
end
|
115
158
|
|
159
|
+
# @see Document.create
|
160
|
+
#
|
161
|
+
# @raise [DocumentNotValid] raised if a document fails to create
|
116
162
|
def create!(*docs)
|
117
163
|
initialize_each(*docs) { |doc| doc.save! }
|
118
164
|
end
|
119
165
|
|
120
|
-
#
|
166
|
+
# @overload update(id, attributes)
|
167
|
+
# Update a single document
|
168
|
+
# @param id the ID of the document you wish to update
|
169
|
+
# @param [Hash] attributes the key to update on the document with a new
|
170
|
+
# value
|
171
|
+
#
|
172
|
+
# @overload update(ids_and_attributes)
|
173
|
+
# Update multiple documents
|
174
|
+
# @param [Hash] ids_and_attributes each key is the ID of some document
|
175
|
+
# you wish to update. The value each key points toward are those
|
176
|
+
# applied to the target document
|
177
|
+
#
|
178
|
+
# @example Updating single document
|
121
179
|
# Person.update(1, {:foo => 'bar'})
|
122
180
|
#
|
123
|
-
#
|
181
|
+
# @example Updating multiple documents at once:
|
124
182
|
# Person.update({'1' => {:foo => 'bar'}, '2' => {:baz => 'wick'}})
|
125
183
|
def update(*args)
|
126
|
-
|
127
|
-
if updating_multiple
|
184
|
+
if args.length == 1
|
128
185
|
update_multiple(args[0])
|
129
186
|
else
|
130
187
|
id, attributes = args
|
@@ -132,24 +189,55 @@ module MongoMapper
|
|
132
189
|
end
|
133
190
|
end
|
134
191
|
|
192
|
+
# Removes ("deletes") one or many documents from the collection. Note
|
193
|
+
# that this will bypass any +destroy+ hooks defined by your class.
|
194
|
+
#
|
195
|
+
# @param [Array] ids the ID or IDs of the records you wish to delete
|
135
196
|
def delete(*ids)
|
136
|
-
|
137
|
-
collection.remove(criteria)
|
197
|
+
collection.remove(to_criteria(:_id => ids.flatten))
|
138
198
|
end
|
139
199
|
|
140
|
-
def delete_all(
|
141
|
-
|
142
|
-
collection.remove(criteria)
|
200
|
+
def delete_all(options={})
|
201
|
+
collection.remove(to_criteria(options))
|
143
202
|
end
|
144
203
|
|
204
|
+
# Iterates over each document found by the provided IDs and calls their
|
205
|
+
# +destroy+ method. This has the advantage of processing your document's
|
206
|
+
# +destroy+ call-backs.
|
207
|
+
#
|
208
|
+
# @overload destroy(id)
|
209
|
+
# Destroy a single document by ID
|
210
|
+
# @param id the ID of the document to destroy
|
211
|
+
#
|
212
|
+
# @overload destroy(ids)
|
213
|
+
# Destroy many documents by their IDs
|
214
|
+
# @param [Array] the IDs of each document you wish to destroy
|
215
|
+
#
|
216
|
+
# @example Destroying a single document
|
217
|
+
# Person.destroy("34")
|
218
|
+
#
|
219
|
+
# @example Destroying multiple documents
|
220
|
+
# Person.destroy("34", "45", ..., "54")
|
221
|
+
#
|
222
|
+
# # OR...
|
223
|
+
#
|
224
|
+
# Person.destroy(["34", "45", ..., "54"])
|
145
225
|
def destroy(*ids)
|
146
226
|
find_some(ids.flatten).each(&:destroy)
|
147
227
|
end
|
148
228
|
|
149
|
-
def destroy_all(
|
150
|
-
|
229
|
+
def destroy_all(options={})
|
230
|
+
all(options).each(&:destroy)
|
151
231
|
end
|
152
232
|
|
233
|
+
# @overload connection()
|
234
|
+
# @return [Mongo::Connection] the connection used by your document class
|
235
|
+
#
|
236
|
+
# @overload connection(mongo_connection)
|
237
|
+
# @param [Mongo::Connection] mongo_connection a new connection for your
|
238
|
+
# document class to use
|
239
|
+
# @return [Mongo::Connection] a new Mongo::Connection for yoru document
|
240
|
+
# class
|
153
241
|
def connection(mongo_connection=nil)
|
154
242
|
if mongo_connection.nil?
|
155
243
|
@connection ||= MongoMapper.connection
|
@@ -159,42 +247,73 @@ module MongoMapper
|
|
159
247
|
@connection
|
160
248
|
end
|
161
249
|
|
162
|
-
|
163
|
-
|
164
|
-
|
250
|
+
# Changes the database name from the default to whatever you want
|
251
|
+
#
|
252
|
+
# @param [#to_s] name the new database name to use.
|
253
|
+
def set_database_name(name)
|
254
|
+
@database_name = name
|
255
|
+
end
|
256
|
+
|
257
|
+
# Returns the database name
|
258
|
+
#
|
259
|
+
# @return [String] the database name
|
260
|
+
def database_name
|
261
|
+
@database_name
|
262
|
+
end
|
263
|
+
|
264
|
+
# Returns the database the document should use. Defaults to
|
265
|
+
# MongoMapper.database if other database is not set.
|
266
|
+
#
|
267
|
+
# @return [Mongo::DB] the mongo database instance
|
268
|
+
def database
|
269
|
+
if database_name.nil?
|
270
|
+
MongoMapper.database
|
165
271
|
else
|
166
|
-
|
272
|
+
connection.db(database_name)
|
167
273
|
end
|
168
|
-
@database
|
169
274
|
end
|
170
|
-
|
275
|
+
|
171
276
|
# Changes the collection name from the default to whatever you want
|
172
|
-
|
173
|
-
|
277
|
+
#
|
278
|
+
# @param [#to_s] name the new collection name to use.
|
279
|
+
def set_collection_name(name)
|
174
280
|
@collection_name = name
|
175
281
|
end
|
176
|
-
|
282
|
+
|
177
283
|
# Returns the collection name, if not set, defaults to class name tableized
|
284
|
+
#
|
285
|
+
# @return [String] the collection name, if not set, defaults to class
|
286
|
+
# name tableized
|
178
287
|
def collection_name
|
179
|
-
@collection_name ||= self.to_s.
|
288
|
+
@collection_name ||= self.to_s.tableize.gsub(/\//, '.')
|
180
289
|
end
|
181
290
|
|
182
|
-
#
|
291
|
+
# @return the Mongo Ruby driver +collection+ object
|
183
292
|
def collection
|
184
|
-
|
293
|
+
database.collection(collection_name)
|
185
294
|
end
|
186
295
|
|
296
|
+
# Defines a +created_at+ and +updated_at+ attribute (with a +Time+
|
297
|
+
# value) on your document. These attributes are updated by an
|
298
|
+
# injected +update_timestamps+ +before_save+ hook.
|
187
299
|
def timestamps!
|
188
300
|
key :created_at, Time
|
189
301
|
key :updated_at, Time
|
190
|
-
|
191
302
|
class_eval { before_save :update_timestamps }
|
192
303
|
end
|
193
|
-
|
304
|
+
|
305
|
+
def single_collection_inherited?
|
306
|
+
keys.has_key?('_type') && single_collection_inherited_superclass?
|
307
|
+
end
|
308
|
+
|
309
|
+
def single_collection_inherited_superclass?
|
310
|
+
superclass.respond_to?(:keys) && superclass.keys.has_key?('_type')
|
311
|
+
end
|
312
|
+
|
194
313
|
protected
|
195
314
|
def method_missing(method, *args)
|
196
315
|
finder = DynamicFinder.new(method)
|
197
|
-
|
316
|
+
|
198
317
|
if finder.found?
|
199
318
|
meta_def(finder.method) { |*args| dynamic_find(finder, args) }
|
200
319
|
send(finder.method, *args)
|
@@ -204,50 +323,41 @@ module MongoMapper
|
|
204
323
|
end
|
205
324
|
|
206
325
|
private
|
207
|
-
|
326
|
+
def create_indexes_for(key)
|
327
|
+
ensure_index key.name if key.options[:index]
|
328
|
+
end
|
329
|
+
|
208
330
|
def initialize_each(*docs)
|
209
331
|
instances = []
|
210
332
|
docs = [{}] if docs.blank?
|
211
333
|
docs.flatten.each do |attrs|
|
212
|
-
doc =
|
334
|
+
doc = initialize_doc(attrs)
|
213
335
|
yield(doc)
|
214
336
|
instances << doc
|
215
337
|
end
|
216
338
|
instances.size == 1 ? instances[0] : instances
|
217
339
|
end
|
218
340
|
|
219
|
-
def
|
220
|
-
|
341
|
+
def initialize_doc(doc)
|
342
|
+
begin
|
343
|
+
klass = doc['_type'].present? ? doc['_type'].constantize : self
|
344
|
+
klass.new(doc)
|
345
|
+
rescue NameError
|
346
|
+
new(doc)
|
347
|
+
end
|
221
348
|
end
|
222
|
-
|
349
|
+
|
223
350
|
def find_every(options)
|
224
|
-
criteria, options =
|
351
|
+
criteria, options = to_finder_options(options)
|
225
352
|
collection.find(criteria, options).to_a.map do |doc|
|
226
|
-
|
227
|
-
klass = doc['_type'].present? ? doc['_type'].constantize : self
|
228
|
-
klass.new(doc)
|
229
|
-
rescue NameError
|
230
|
-
new(doc)
|
231
|
-
end
|
353
|
+
initialize_doc(doc)
|
232
354
|
end
|
233
355
|
end
|
234
356
|
|
235
|
-
def invert_order_clause(order)
|
236
|
-
order.split(',').map do |order_segment|
|
237
|
-
if order_segment =~ /\sasc/i
|
238
|
-
order_segment.sub /\sasc/i, ' desc'
|
239
|
-
elsif order_segment =~ /\sdesc/i
|
240
|
-
order_segment.sub /\sdesc/i, ' asc'
|
241
|
-
else
|
242
|
-
"#{order_segment.strip} desc"
|
243
|
-
end
|
244
|
-
end.join(',')
|
245
|
-
end
|
246
|
-
|
247
357
|
def find_some(ids, options={})
|
248
|
-
ids
|
249
|
-
documents = find_every(options.
|
250
|
-
|
358
|
+
ids = ids.flatten.compact.uniq
|
359
|
+
documents = find_every(options.merge(:_id => ids))
|
360
|
+
|
251
361
|
if ids.size == documents.size
|
252
362
|
documents
|
253
363
|
else
|
@@ -255,14 +365,29 @@ module MongoMapper
|
|
255
365
|
end
|
256
366
|
end
|
257
367
|
|
258
|
-
def find_one(
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
raise DocumentNotFound, "Document with id of #{id} does not exist in collection named #{collection.name}"
|
368
|
+
def find_one(options={})
|
369
|
+
criteria, options = to_finder_options(options)
|
370
|
+
if doc = collection.find_one(criteria, options)
|
371
|
+
initialize_doc(doc)
|
263
372
|
end
|
264
373
|
end
|
265
374
|
|
375
|
+
def find_one!(options={})
|
376
|
+
find_one(options) || raise(DocumentNotFound, "Document match #{options.inspect} does not exist in #{collection.name} collection")
|
377
|
+
end
|
378
|
+
|
379
|
+
def invert_order_clause(order)
|
380
|
+
order.split(',').map do |order_segment|
|
381
|
+
if order_segment =~ /\sasc/i
|
382
|
+
order_segment.sub /\sasc/i, ' desc'
|
383
|
+
elsif order_segment =~ /\sdesc/i
|
384
|
+
order_segment.sub /\sdesc/i, ' asc'
|
385
|
+
else
|
386
|
+
"#{order_segment.strip} desc"
|
387
|
+
end
|
388
|
+
end.join(',')
|
389
|
+
end
|
390
|
+
|
266
391
|
def update_single(id, attrs)
|
267
392
|
if id.blank? || attrs.blank? || !attrs.is_a?(Hash)
|
268
393
|
raise ArgumentError, "Updating a single document requires an id and a hash of attributes"
|
@@ -282,9 +407,17 @@ module MongoMapper
|
|
282
407
|
docs.each_pair { |id, attrs| instances << update(id, attrs) }
|
283
408
|
instances
|
284
409
|
end
|
410
|
+
|
411
|
+
def to_criteria(options={})
|
412
|
+
FinderOptions.new(self, options).criteria
|
413
|
+
end
|
414
|
+
|
415
|
+
def to_finder_options(options={})
|
416
|
+
FinderOptions.new(self, options).to_a
|
417
|
+
end
|
285
418
|
end
|
286
419
|
|
287
|
-
module InstanceMethods
|
420
|
+
module InstanceMethods
|
288
421
|
def collection
|
289
422
|
self.class.collection
|
290
423
|
end
|
@@ -303,12 +436,14 @@ module MongoMapper
|
|
303
436
|
|
304
437
|
def destroy
|
305
438
|
return false if frozen?
|
306
|
-
|
307
|
-
criteria = FinderOptions.to_mongo_criteria(:_id => id)
|
308
|
-
collection.remove(criteria) unless new?
|
439
|
+
self.class.delete(id) unless new?
|
309
440
|
freeze
|
310
441
|
end
|
311
442
|
|
443
|
+
def reload
|
444
|
+
self.class.find(id)
|
445
|
+
end
|
446
|
+
|
312
447
|
private
|
313
448
|
def create_or_update
|
314
449
|
result = new? ? create : update
|
@@ -319,7 +454,7 @@ module MongoMapper
|
|
319
454
|
assign_id
|
320
455
|
save_to_collection
|
321
456
|
end
|
322
|
-
|
457
|
+
|
323
458
|
def assign_id
|
324
459
|
if read_attribute(:_id).blank?
|
325
460
|
write_attribute(:_id, Mongo::ObjectID.new.to_s)
|
@@ -340,7 +475,7 @@ module MongoMapper
|
|
340
475
|
write_attribute('created_at', now) if new?
|
341
476
|
write_attribute('updated_at', now)
|
342
477
|
end
|
343
|
-
|
478
|
+
|
344
479
|
def clear_custom_id_flag
|
345
480
|
@using_custom_id = nil
|
346
481
|
end
|
@@ -332,9 +332,13 @@ module MongoMapper
|
|
332
332
|
end
|
333
333
|
|
334
334
|
def read_attribute(name)
|
335
|
-
|
336
|
-
|
337
|
-
|
335
|
+
if key = _keys[name]
|
336
|
+
value = key.get(instance_variable_get("@#{name}"))
|
337
|
+
instance_variable_set "@#{name}", value if !frozen?
|
338
|
+
value
|
339
|
+
else
|
340
|
+
raise KeyNotFound, "Could not find key: #{name.inspect}"
|
341
|
+
end
|
338
342
|
end
|
339
343
|
|
340
344
|
def read_attribute_before_typecast(name)
|