gather 0.0.3
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/History.txt +2 -0
- data/Manifest.txt +8 -0
- data/README +22 -0
- data/Rakefile +27 -0
- data/lib/changes.rb +315 -0
- data/lib/gather.rb +96 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- metadata +82 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
data/README
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
== gather
|
2
|
+
|
3
|
+
A gem that provides modules to make working with properties a bit easier.
|
4
|
+
|
5
|
+
For example:
|
6
|
+
|
7
|
+
class SumThing
|
8
|
+
include Gather
|
9
|
+
property :thing, :spla, :gumf, :troob
|
10
|
+
end
|
11
|
+
|
12
|
+
s = SumThing.new.gather :spla => 'five', :thing => 'sigma' do
|
13
|
+
troob %w{one two three four}
|
14
|
+
gumf 15 % 4
|
15
|
+
end
|
16
|
+
|
17
|
+
Or
|
18
|
+
|
19
|
+
s = SumThing.new.gather :spla => 'five', :thing => 'sigma' do |s|
|
20
|
+
s.troob = %w{one two three four}
|
21
|
+
s.gumf = 15 % 4
|
22
|
+
end
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
%w[rubygems rake rake/clean fileutils newgem rubigen].each { |f| require f }
|
2
|
+
require File.dirname(__FILE__) + '/lib/version.rb'
|
3
|
+
|
4
|
+
# Generate all the Rake tasks
|
5
|
+
# Run 'rake -T' to see list of generated tasks (from gem root directory)
|
6
|
+
$hoe = Hoe.new('gather', Gather::VERSION) do |p|
|
7
|
+
p.developer('John Anderson', 'panic@semiosix.com')
|
8
|
+
p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
|
9
|
+
p.rubyforge_name = 'clevic'
|
10
|
+
# p.extra_deps = [
|
11
|
+
# ['activesupport','>= 2.0.2'],
|
12
|
+
# ]
|
13
|
+
p.extra_dev_deps = [
|
14
|
+
['newgem', ">= #{::Newgem::VERSION}"]
|
15
|
+
]
|
16
|
+
|
17
|
+
p.clean_globs |= %w[**/.DS_Store tmp *.log]
|
18
|
+
path = (p.rubyforge_name == p.name) ? p.rubyforge_name : "\#{p.rubyforge_name}/\#{p.name}"
|
19
|
+
p.remote_rdoc_dir = File.join(path.gsub(/^#{p.rubyforge_name}\/?/,''), 'rdoc')
|
20
|
+
p.rsync_args = '-av --delete --ignore-errors'
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'newgem/tasks' # load /tasks/*.rake
|
24
|
+
Dir['tasks/**/*.rake'].each { |t| load t }
|
25
|
+
|
26
|
+
# TODO - want other tests/tasks run by default? Add them to the list
|
27
|
+
# task :default => [:spec, :features]
|
data/lib/changes.rb
ADDED
@@ -0,0 +1,315 @@
|
|
1
|
+
require 'gather.rb'
|
2
|
+
|
3
|
+
class NilClass
|
4
|
+
def call_if( *args ); end
|
5
|
+
end
|
6
|
+
|
7
|
+
class Proc
|
8
|
+
alias_method :call_if, :call
|
9
|
+
end
|
10
|
+
|
11
|
+
=begin rdoc
|
12
|
+
Same as Gather, except that history is kept for changes, and on_change
|
13
|
+
is called each time a property value changes.
|
14
|
+
=end
|
15
|
+
module Changes
|
16
|
+
include Gather
|
17
|
+
|
18
|
+
class Change
|
19
|
+
attr_accessor :old, :new
|
20
|
+
def initialize
|
21
|
+
@old = []
|
22
|
+
@new = nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Gather values from the hash and the block, using the gather method.
|
27
|
+
unless instance_methods.include? 'initialize'
|
28
|
+
def initialize( hash = {}, &block )
|
29
|
+
init_properties( hash, &block )
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# should probably use an object creation hook to do this
|
34
|
+
def init_properties( hash, &block )
|
35
|
+
@changes = {}
|
36
|
+
gather( hash, &block )
|
37
|
+
end
|
38
|
+
|
39
|
+
def changed?
|
40
|
+
changes.empty?
|
41
|
+
end
|
42
|
+
|
43
|
+
def changes( *symbols )
|
44
|
+
if symbols.empty?
|
45
|
+
@changes ||= {}
|
46
|
+
else
|
47
|
+
symbols.map{|x| changes[x] }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# called after the property has changed
|
52
|
+
# including classes should define this
|
53
|
+
def on_change( property, old, new )
|
54
|
+
end
|
55
|
+
|
56
|
+
attr_writer :change_history
|
57
|
+
def change_history
|
58
|
+
@change_history ||= []
|
59
|
+
end
|
60
|
+
|
61
|
+
# make a mark in the change history
|
62
|
+
def mark
|
63
|
+
new_changes = @changes.clone
|
64
|
+
new_changes.each{|k,v| new_changes[k] = v.clone}
|
65
|
+
change_history << new_changes
|
66
|
+
end
|
67
|
+
|
68
|
+
# restore the last mark. Do not emit any changes.
|
69
|
+
def restore
|
70
|
+
@changes = change_history.pop unless change_history.empty?
|
71
|
+
end
|
72
|
+
|
73
|
+
# reset the passed properties, or all if none are specified
|
74
|
+
def reset!( *properties )
|
75
|
+
if properties.empty?
|
76
|
+
@changes = {}
|
77
|
+
else
|
78
|
+
properties.each {|x| changes[x] = Change.new}
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# execute the block, undo the given symbols, and
|
83
|
+
# return the result of the executed block. If no symbols
|
84
|
+
# are given, just use mark and restore
|
85
|
+
def preserve( *symbols )
|
86
|
+
mark if symbols.empty?
|
87
|
+
retval = yield
|
88
|
+
if symbols.empty?
|
89
|
+
restore
|
90
|
+
else
|
91
|
+
symbols.each {|symbol| undo( symbol ) }
|
92
|
+
end
|
93
|
+
retval
|
94
|
+
end
|
95
|
+
|
96
|
+
def undo( symbol )
|
97
|
+
unless changes[symbol].old.empty?
|
98
|
+
current = send( symbol )
|
99
|
+
if changes[symbol] && changes[symbol].old
|
100
|
+
eval "@#{symbol.to_s} = changes[:#{symbol.to_s}].old.pop"
|
101
|
+
end
|
102
|
+
current
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Add the property methods if
|
107
|
+
# they don't already exist, and if dynamic is in effect, which is
|
108
|
+
# the default. If static is in effect, the normal method_missing
|
109
|
+
# behaviour will be invoked.
|
110
|
+
def method_missing(sym, *args)
|
111
|
+
if self.class.dynamic?
|
112
|
+
self.class.property sym
|
113
|
+
send( sym, *args )
|
114
|
+
else
|
115
|
+
super
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Return a collection of property symbols
|
120
|
+
def property_names
|
121
|
+
self.class.properties.to_a
|
122
|
+
end
|
123
|
+
|
124
|
+
module ClassMethods
|
125
|
+
include Gather::ClassMethods
|
126
|
+
# Remove all but the absolutely essential methods from this class.
|
127
|
+
# Useful if you want a dynamic property list, but
|
128
|
+
# can have some surprising side effects, obviously.
|
129
|
+
def blankslate!
|
130
|
+
# remove unused methods that might clash with user accessors
|
131
|
+
keep_methods = %w( __send__ __id__ self send class inspect instance_eval instance_variables )
|
132
|
+
instance_methods.each do |method|
|
133
|
+
undef_method( method ) unless keep_methods.include?( method )
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def module_name( symbol )
|
138
|
+
"#{symbol.to_s.capitalize}Property"
|
139
|
+
end
|
140
|
+
|
141
|
+
# For each property in symbols, add methods:
|
142
|
+
# instance.property as reader
|
143
|
+
# instance.property( value ) as writer
|
144
|
+
# instance.property = value as writer
|
145
|
+
# instance.before_property {|old,new| ... }
|
146
|
+
# instance.after_property {|old,new| ... }
|
147
|
+
# instance.on_property {|old,new| ... }
|
148
|
+
# instance.property_changed?
|
149
|
+
# instance.undo_property!
|
150
|
+
def property( *symbols )
|
151
|
+
@stripper ||= /^([^\= ]+)\s*\=?\s*$/
|
152
|
+
symbols.each do |sym|
|
153
|
+
property = @stripper.match( sym.to_s )[1]
|
154
|
+
|
155
|
+
# TODO false here until I figure out how to remove properties
|
156
|
+
unless false && const_defined?( module_name( property ).to_sym )
|
157
|
+
own_properties << property.to_sym
|
158
|
+
line, st = __LINE__, <<-EOF
|
159
|
+
module #{module_name( property )}
|
160
|
+
|
161
|
+
public
|
162
|
+
def #{property}(*val)
|
163
|
+
if val.empty?
|
164
|
+
@#{property}
|
165
|
+
else
|
166
|
+
self.#{property} = *val
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def on_#{property}( &block )
|
171
|
+
@on_#{property}_block = block
|
172
|
+
end
|
173
|
+
|
174
|
+
def after_#{property}( &block )
|
175
|
+
@after_#{property}_block = block
|
176
|
+
end
|
177
|
+
|
178
|
+
def before_#{property}( &block )
|
179
|
+
@before_#{property}_block = block
|
180
|
+
end
|
181
|
+
|
182
|
+
def #{property}=(*new_value)
|
183
|
+
unwrapped_value = new_value.size == 1 ? new_value[0] : new_value
|
184
|
+
return if unwrapped_value == @#{property}
|
185
|
+
|
186
|
+
@before_#{property}_block.call_if( @#{property}, unwrapped_value )
|
187
|
+
|
188
|
+
changes[:#{property}] ||= Change.new
|
189
|
+
changes[:#{property}].old << @#{property}
|
190
|
+
changes[:#{property}].new = unwrapped_value
|
191
|
+
|
192
|
+
@#{property} = unwrapped_value
|
193
|
+
|
194
|
+
@after_#{property}_block.call_if( changes[:#{property}].old, unwrapped_value )
|
195
|
+
@on_#{property}_block.call_if( changes[:#{property}].old, unwrapped_value )
|
196
|
+
on_change( :#{property}, changes[:#{property}].old, unwrapped_value )
|
197
|
+
end
|
198
|
+
|
199
|
+
def #{property}_changed?
|
200
|
+
changes.has_key?(:#{property}) && !changes[:#{property}].old.empty?
|
201
|
+
end
|
202
|
+
|
203
|
+
# Undo the last change and return the undone value.
|
204
|
+
# return nil if there were no changes to undo
|
205
|
+
def undo_#{property}!
|
206
|
+
undo( :#{property} )
|
207
|
+
end
|
208
|
+
|
209
|
+
def reset_#{property}!
|
210
|
+
changes[x] = Change.new
|
211
|
+
end
|
212
|
+
|
213
|
+
private
|
214
|
+
def direct_#{property}=( value )
|
215
|
+
@#{property} = value
|
216
|
+
end
|
217
|
+
|
218
|
+
end # module
|
219
|
+
EOF
|
220
|
+
class_eval st, __FILE__, line + 1
|
221
|
+
end
|
222
|
+
|
223
|
+
# always include the module, for re-definitions
|
224
|
+
class_eval "include #{property.capitalize}Property"
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# not working yet. Once the module has been added, it seems like
|
229
|
+
# adding it again does not redefine the methods, even if they were
|
230
|
+
# removed in the interim.
|
231
|
+
#~ def remove( *symbols )
|
232
|
+
#~ symbols.each do |sym|
|
233
|
+
#~ if defined?( sym )
|
234
|
+
#~ # undefine methods
|
235
|
+
#~ module_const = eval "#{self.name}::#{module_name( sym )}"
|
236
|
+
#~ module_const.instance_methods.each do |im|
|
237
|
+
#~ undef_method( im )
|
238
|
+
#~ end
|
239
|
+
|
240
|
+
#~ # remove from properties
|
241
|
+
#~ properties.delete sym
|
242
|
+
#~ end
|
243
|
+
#~ end
|
244
|
+
#~ end
|
245
|
+
|
246
|
+
# Do not allow accessors to be added dynamically. In other words,
|
247
|
+
# a subclass like this
|
248
|
+
# class IndexCollector
|
249
|
+
# static_properties :row, :column
|
250
|
+
# end
|
251
|
+
# will fail if used like this
|
252
|
+
# collector = IndexCollector.new( :row => 4, :column => 6 ) do
|
253
|
+
# other 'oops'
|
254
|
+
# end
|
255
|
+
# because :other isn't added by static_properties.
|
256
|
+
def static_properties( *syms )
|
257
|
+
@dynamic = false
|
258
|
+
property( *syms )
|
259
|
+
end
|
260
|
+
|
261
|
+
# Allow accessors to be added dynamically, the default.
|
262
|
+
def dynamic_properties( *syms )
|
263
|
+
@dynamic = true
|
264
|
+
property( *syms )
|
265
|
+
end
|
266
|
+
|
267
|
+
def dynamic!; @dynamic = true; end
|
268
|
+
def static!; @dynamic = false; end
|
269
|
+
|
270
|
+
def dynamic?
|
271
|
+
# don't optimise this to @dynamic ||= true, because it will reset
|
272
|
+
# an @dynamic of false to true
|
273
|
+
@dynamic = false if @dynamic.nil?
|
274
|
+
@dynamic
|
275
|
+
end
|
276
|
+
|
277
|
+
# might use this instead of operation
|
278
|
+
#~ def method_added( *args )
|
279
|
+
#~ super
|
280
|
+
#~ end
|
281
|
+
|
282
|
+
# define a method 'name' (which can be symbol or a string)
|
283
|
+
# which takes args and a block
|
284
|
+
# calls gather and then executes the block. Any changes made
|
285
|
+
# from args and block are rolled back
|
286
|
+
# name! will not roll back values
|
287
|
+
def operation( name, &body )
|
288
|
+
# save the block to be executed later
|
289
|
+
class_eval "@@#{name.to_s}_operation_block = body"
|
290
|
+
|
291
|
+
# define the new method called name
|
292
|
+
class_eval <<-EOF, __FILE__, __LINE__.to_i + 1
|
293
|
+
def #{name.to_s}( args = {}, &block )
|
294
|
+
preserve do
|
295
|
+
gather( args, &block )
|
296
|
+
instance_eval( &@@#{name.to_s}_operation_block )
|
297
|
+
end
|
298
|
+
end
|
299
|
+
EOF
|
300
|
+
|
301
|
+
# define new method called name!
|
302
|
+
class_eval <<-EOF, __FILE__, __LINE__.to_i + 1
|
303
|
+
def #{name.to_s}!( args = {}, &block )
|
304
|
+
gather( args, &block )
|
305
|
+
instance_eval( &@@#{name.to_s}_operation_block )
|
306
|
+
end
|
307
|
+
EOF
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
def self.included( base )
|
312
|
+
base.extend( ClassMethods )
|
313
|
+
end
|
314
|
+
|
315
|
+
end
|
data/lib/gather.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
# Provides instance method gather and class method property
|
4
|
+
# Allows properties to be Enumerable.
|
5
|
+
module Gather
|
6
|
+
# Evaluate the block and gather options from args. Even if it's nil.
|
7
|
+
# Return self
|
8
|
+
def gather( args = {}, &block )
|
9
|
+
unless args.nil?
|
10
|
+
args.each do |key,value|
|
11
|
+
self.send( key, value )
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
unless block.nil?
|
16
|
+
if block.arity == -1
|
17
|
+
instance_eval &block
|
18
|
+
else
|
19
|
+
yield self
|
20
|
+
end
|
21
|
+
end
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
# return a hash of the properties
|
26
|
+
def to_hash( properties = self.class.properties )
|
27
|
+
properties.inject({}) {|s,x| s[x] = send(x); s}
|
28
|
+
end
|
29
|
+
|
30
|
+
# set all values specified in the hash
|
31
|
+
def merge!( hash )
|
32
|
+
hash.each {|k,v| send( k, v ) }
|
33
|
+
end
|
34
|
+
|
35
|
+
# return a new hash merged with the argument
|
36
|
+
def merge( hash )
|
37
|
+
to_hash.merge( hash )
|
38
|
+
end
|
39
|
+
|
40
|
+
def []( symbol )
|
41
|
+
send( symbol )
|
42
|
+
end
|
43
|
+
|
44
|
+
def []=( symbol, value )
|
45
|
+
send( symbol, value )
|
46
|
+
end
|
47
|
+
|
48
|
+
def each( &block )
|
49
|
+
self.class.properties.each( &block )
|
50
|
+
end
|
51
|
+
include Enumerable
|
52
|
+
|
53
|
+
def self.included( base )
|
54
|
+
base.extend( ClassMethods )
|
55
|
+
end
|
56
|
+
|
57
|
+
module ClassMethods
|
58
|
+
# return properties for only this class
|
59
|
+
def own_properties
|
60
|
+
@properties ||= Set.new
|
61
|
+
end
|
62
|
+
|
63
|
+
# return properties for this class and ancestors
|
64
|
+
def properties
|
65
|
+
own_properties +
|
66
|
+
if superclass.respond_to? :properties
|
67
|
+
superclass.properties
|
68
|
+
else
|
69
|
+
[]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def property( *symbols )
|
74
|
+
@stripper ||= /^([^\= ]+)\s*\=?\s*$/
|
75
|
+
symbols.each do |sym|
|
76
|
+
stripped = @stripper.match( sym.to_s )[1]
|
77
|
+
own_properties << stripped.to_sym
|
78
|
+
line, st = __LINE__, <<-EOF
|
79
|
+
def #{stripped}(*val)
|
80
|
+
if val.empty?
|
81
|
+
@#{stripped}
|
82
|
+
else
|
83
|
+
@#{stripped} = val.size == 1 ? val[0] : val
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def #{stripped}=(*val)
|
88
|
+
@#{stripped} = val.size == 1 ? val[0] : val
|
89
|
+
end
|
90
|
+
EOF
|
91
|
+
class_eval st, __FILE__, line + 1
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
data/script/destroy
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'rubigen'
|
6
|
+
rescue LoadError
|
7
|
+
require 'rubygems'
|
8
|
+
require 'rubigen'
|
9
|
+
end
|
10
|
+
require 'rubigen/scripts/destroy'
|
11
|
+
|
12
|
+
ARGV.shift if ['--help', '-h'].include?(ARGV[0])
|
13
|
+
RubiGen::Base.use_component_sources! [:newgem_simple, :test_unit]
|
14
|
+
RubiGen::Scripts::Destroy.new.run(ARGV)
|
data/script/generate
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'rubigen'
|
6
|
+
rescue LoadError
|
7
|
+
require 'rubygems'
|
8
|
+
require 'rubigen'
|
9
|
+
end
|
10
|
+
require 'rubigen/scripts/generate'
|
11
|
+
|
12
|
+
ARGV.shift if ['--help', '-h'].include?(ARGV[0])
|
13
|
+
RubiGen::Base.use_component_sources! [:newgem_simple, :test_unit]
|
14
|
+
RubiGen::Scripts::Generate.new.run(ARGV)
|
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gather
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- John Anderson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-01-30 00:00:00 +02:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: newgem
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.2.3
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: hoe
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.8.0
|
34
|
+
version:
|
35
|
+
description: ""
|
36
|
+
email:
|
37
|
+
- panic@semiosix.com
|
38
|
+
executables: []
|
39
|
+
|
40
|
+
extensions: []
|
41
|
+
|
42
|
+
extra_rdoc_files:
|
43
|
+
- History.txt
|
44
|
+
- Manifest.txt
|
45
|
+
files:
|
46
|
+
- History.txt
|
47
|
+
- Manifest.txt
|
48
|
+
- README
|
49
|
+
- Rakefile
|
50
|
+
- lib/gather.rb
|
51
|
+
- lib/changes.rb
|
52
|
+
- script/destroy
|
53
|
+
- script/generate
|
54
|
+
has_rdoc: true
|
55
|
+
homepage: A gem that provides modules to make working with properties a bit easier.
|
56
|
+
post_install_message:
|
57
|
+
rdoc_options:
|
58
|
+
- --main
|
59
|
+
- README
|
60
|
+
require_paths:
|
61
|
+
- lib
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: "0"
|
67
|
+
version:
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: "0"
|
73
|
+
version:
|
74
|
+
requirements: []
|
75
|
+
|
76
|
+
rubyforge_project: clevic
|
77
|
+
rubygems_version: 1.3.1
|
78
|
+
signing_key:
|
79
|
+
specification_version: 2
|
80
|
+
summary: ""
|
81
|
+
test_files: []
|
82
|
+
|