memoist3 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/memoist.gemspec ADDED
@@ -0,0 +1,41 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'memoist/version'
4
+
5
+ AUTHORS = [
6
+ ['Joshua Peek', 'josh@joshpeek.com'],
7
+ ['Tarmo Tänav', 'tarmo@itech.ee'],
8
+ ['Jeremy Kemper', 'jeremy@bitsweat.net'],
9
+ ['Eugene Pimenov', 'libc@mac.com'],
10
+ ['Xavier Noria', 'fxn@hashref.com'],
11
+ ['Niels Ganser', 'niels@herimedia.co'],
12
+ ['Carl Lerche & Yehuda Katz', 'wycats@gmail.com'],
13
+ ['jeem', 'jeem@hughesorama.com'],
14
+ ['Jay Pignata', 'john.pignata@gmail.com'],
15
+ ['Damien Mathieu', '42@dmathieu.com'],
16
+ ['José Valim', 'jose.valim@gmail.com'],
17
+ ['Matthew Rudy Jacobs', 'matthewrudyjacobs@gmail.com'],
18
+ ['Jan Sterba', 'info@jansterba.com']
19
+ ].freeze
20
+
21
+ Gem::Specification.new do |spec|
22
+ spec.name = 'memoist3'
23
+ spec.version = Memoist::VERSION
24
+ spec.authors = AUTHORS.map { |name, _email| name }
25
+ spec.email = AUTHORS.map { |_name, email| email }
26
+ spec.summary = 'memoize methods invocation'
27
+ spec.homepage = 'https://github.com/honzasterba/memoist'
28
+ spec.license = 'MIT'
29
+
30
+ spec.files = `git ls-files -z`.split("\x0")
31
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
32
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
33
+ spec.require_paths = ['lib']
34
+
35
+ spec.required_ruby_version = '>= 2.7.2'
36
+
37
+ spec.add_development_dependency 'benchmark-ips'
38
+ spec.add_development_dependency 'bundler'
39
+ spec.add_development_dependency 'minitest', '~> 5.10'
40
+ spec.add_development_dependency 'rake'
41
+ end
@@ -0,0 +1,48 @@
1
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
2
+ require 'benchmark/ips'
3
+
4
+ require 'memoist'
5
+
6
+ class Benchy
7
+ extend Memoist
8
+
9
+ def arity_0
10
+ 'Hello World'
11
+ end
12
+ memoize :arity_0
13
+
14
+ def arity_1(name)
15
+ "Hello #{name}"
16
+ end
17
+ memoize :arity_1
18
+ end
19
+
20
+ OBJECT = Benchy.new
21
+
22
+ puts "Benchmarking: #{Memoist::VERSION}"
23
+
24
+ Benchmark.ips do |x|
25
+ x.report('arity 0 - memoized') do |times|
26
+ times.times do
27
+ OBJECT.arity_0
28
+ end
29
+ end
30
+
31
+ # x.report("arity 0 - unmemoized") do |times|
32
+ # times.times do
33
+ # OBJECT._unmemoized_arity_0
34
+ # end
35
+ # end
36
+
37
+ x.report('arity 1 - memoized') do |times|
38
+ times.times do
39
+ OBJECT.arity_1(:World)
40
+ end
41
+ end
42
+
43
+ # x.report("arity 1 - unmemoized") do |times|
44
+ # times.times do
45
+ # OBJECT._unmemoized_arity_1(:World)
46
+ # end
47
+ # end
48
+ end
@@ -0,0 +1,590 @@
1
+ require 'test_helper'
2
+ require 'memoist'
3
+
4
+ class MemoistTest < Minitest::Test
5
+ class CallCounter
6
+ def initialize
7
+ @calls = {}
8
+ end
9
+
10
+ def call(method_name)
11
+ @calls[method_name] ||= 0
12
+ @calls[method_name] += 1
13
+ end
14
+
15
+ def count(method_name)
16
+ @calls[method_name] ||= 0
17
+ end
18
+ end
19
+
20
+ class Person
21
+ extend Memoist
22
+
23
+ def initialize
24
+ @counter = CallCounter.new
25
+ end
26
+
27
+ def name_calls
28
+ @counter.count(:name)
29
+ end
30
+
31
+ def student_name_calls
32
+ @counter.count(:student_name)
33
+ end
34
+
35
+ def name_query_calls
36
+ @counter.count(:name?)
37
+ end
38
+
39
+ def is_developer_calls
40
+ @counter.count(:is_developer?)
41
+ end
42
+
43
+ def age_calls
44
+ @counter.count(:age)
45
+ end
46
+
47
+ def name
48
+ @counter.call(:name)
49
+ 'Josh'
50
+ end
51
+
52
+ def name?
53
+ @counter.call(:name?)
54
+ true
55
+ end
56
+ memoize :name?
57
+
58
+ def update(_name)
59
+ 'Joshua'
60
+ end
61
+ memoize :update
62
+
63
+ def age
64
+ @counter.call(:age)
65
+ nil
66
+ end
67
+
68
+ memoize :name, :age
69
+
70
+ def age?
71
+ @counter.call(:age?)
72
+ true
73
+ end
74
+ memoize 'age?'
75
+
76
+ def sleep(hours = 8)
77
+ @counter.call(:sleep)
78
+ hours
79
+ end
80
+ memoize :sleep
81
+
82
+ def sleep_calls
83
+ @counter.count(:sleep)
84
+ end
85
+
86
+ def update_attributes(_options = {})
87
+ @counter.call(:update_attributes)
88
+ true
89
+ end
90
+ memoize :update_attributes
91
+
92
+ def update_attributes_calls
93
+ @counter.count(:update_attributes)
94
+ end
95
+
96
+ def do_with_special(_regular = 10, special_one: true, special_two: true)
97
+ @counter.call(:do_with_special)
98
+ true
99
+ end
100
+ memoize :do_with_special
101
+
102
+ def do_with_special_calls
103
+ @counter.count(:do_with_special)
104
+ end
105
+
106
+ protected
107
+
108
+ def memoize_protected_test
109
+ 'protected'
110
+ end
111
+ memoize :memoize_protected_test
112
+
113
+ private
114
+
115
+ def is_developer?
116
+ @counter.call(:is_developer?)
117
+ 'Yes'
118
+ end
119
+ memoize :is_developer?
120
+ end
121
+
122
+ class Student < Person
123
+ def name
124
+ @counter.call(:student_name)
125
+ "Student #{super}"
126
+ end
127
+ memoize :name, identifier: :student
128
+ end
129
+
130
+ class Teacher < Person
131
+ def seniority
132
+ 'very_senior'
133
+ end
134
+ memoize :seniority
135
+ end
136
+
137
+ class Company
138
+ attr_reader :name_calls
139
+ def initialize
140
+ @name_calls = 0
141
+ end
142
+
143
+ def name
144
+ @name_calls += 1
145
+ '37signals'
146
+ end
147
+ end
148
+
149
+ module Rates
150
+ extend Memoist
151
+
152
+ attr_reader :sales_tax_calls
153
+ def sales_tax(price)
154
+ @sales_tax_calls ||= 0
155
+ @sales_tax_calls += 1
156
+ price * 0.1025
157
+ end
158
+ memoize :sales_tax
159
+ end
160
+
161
+ class Calculator
162
+ extend Memoist
163
+ include Rates
164
+
165
+ attr_reader :fib_calls
166
+ def initialize
167
+ @fib_calls = 0
168
+ end
169
+
170
+ def fib(n)
171
+ @fib_calls += 1
172
+
173
+ if n == 0 || n == 1
174
+ n
175
+ else
176
+ fib(n - 1) + fib(n - 2)
177
+ end
178
+ end
179
+ memoize :fib
180
+
181
+ def add_or_subtract(i, j, add)
182
+ if add
183
+ i + j
184
+ else
185
+ i - j
186
+ end
187
+ end
188
+ memoize :add_or_subtract
189
+
190
+ def counter
191
+ @count ||= 0
192
+ @count += 1
193
+ end
194
+ memoize :counter
195
+ end
196
+
197
+ class Book
198
+ extend Memoist
199
+ STATUSES = %w[new used].freeze
200
+ CLASSIFICATION = %w[fiction nonfiction].freeze
201
+ GENRES = %w[humor romance reference sci-fi classic philosophy].freeze
202
+
203
+ attr_reader :title, :author
204
+ def initialize(title, author)
205
+ @title = title
206
+ @author = author
207
+ end
208
+
209
+ def full_title
210
+ "#{@title} by #{@author}"
211
+ end
212
+ memoize :full_title
213
+
214
+ class << self
215
+ extend Memoist
216
+
217
+ def all_types
218
+ STATUSES.product(CLASSIFICATION).product(GENRES).collect(&:flatten)
219
+ end
220
+ memoize :all_types
221
+ end
222
+ end
223
+
224
+ class Abb
225
+ extend Memoist
226
+
227
+ def run(*_args)
228
+ flush_cache if respond_to?(:flush_cache)
229
+ execute
230
+ end
231
+
232
+ def execute
233
+ some_method
234
+ end
235
+
236
+ def some_method
237
+ # Override this
238
+ end
239
+ end
240
+
241
+ class Bbb < Abb
242
+ def some_method
243
+ :foo
244
+ end
245
+ memoize :some_method
246
+ end
247
+
248
+ def setup
249
+ @person = Person.new
250
+ @calculator = Calculator.new
251
+ @book = Book.new('My Life', "Brian 'Fudge' Turmuck")
252
+ end
253
+
254
+ def test_memoization
255
+ assert_equal 'Josh', @person.name
256
+ assert_equal 1, @person.name_calls
257
+
258
+ 3.times { assert_equal 'Josh', @person.name }
259
+ assert_equal 1, @person.name_calls
260
+ end
261
+
262
+ def test_memoize_with_optional_arguments
263
+ assert_equal 4, @person.sleep(4)
264
+ assert_equal 1, @person.sleep_calls
265
+
266
+ 3.times { assert_equal 4, @person.sleep(4) }
267
+ assert_equal 1, @person.sleep_calls
268
+
269
+ 3.times { assert_equal 4, @person.sleep(4, :reload) }
270
+ assert_equal 4, @person.sleep_calls
271
+ end
272
+
273
+ def test_memoize_with_options_hash
274
+ assert_equal true, @person.update_attributes(age: 21, name: 'James')
275
+ assert_equal 1, @person.update_attributes_calls
276
+
277
+ 3.times { assert_equal true, @person.update_attributes(age: 21, name: 'James') }
278
+ assert_equal 1, @person.update_attributes_calls
279
+
280
+ 3.times { assert_equal true, @person.update_attributes({ age: 21, name: 'James' }, :reload) }
281
+ assert_equal 4, @person.update_attributes_calls
282
+ end
283
+
284
+ def test_memoize_with_kwargs
285
+ assert_equal true, @person.do_with_special(1, special_one: true)
286
+ assert_equal 1, @person.do_with_special_calls
287
+
288
+ 3.times { assert_equal true, @person.do_with_special(1, special_one: true) }
289
+ assert_equal 1, @person.do_with_special_calls
290
+
291
+ assert_equal true, @person.do_with_special(2)
292
+ assert_equal 2, @person.do_with_special_calls
293
+
294
+ assert_equal true, @person.do_with_special(1, special_one: false)
295
+ assert_equal 3, @person.do_with_special_calls
296
+
297
+ assert_equal true, @person.do_with_special(1, special_two: false)
298
+ assert_equal 4, @person.do_with_special_calls
299
+ end
300
+
301
+ def test_memoization_with_punctuation
302
+ assert_equal true, @person.name?
303
+
304
+ @person.memoize_all
305
+ @person.unmemoize_all
306
+ end
307
+
308
+ def test_memoization_when_memoize_is_called_with_punctuated_string
309
+ assert_equal true, @person.age?
310
+
311
+ @person.memoize_all
312
+ @person.unmemoize_all
313
+ end
314
+
315
+ def test_memoization_flush_with_punctuation
316
+ assert_equal true, @person.name?
317
+ @person.flush_cache(:name?)
318
+ 3.times { assert_equal true, @person.name? }
319
+ assert_equal 2, @person.name_query_calls
320
+ end
321
+
322
+ def test_memoization_with_nil_value
323
+ assert_nil @person.age
324
+ assert_equal 1, @person.age_calls
325
+
326
+ 3.times { assert_nil @person.age }
327
+ assert_equal 1, @person.age_calls
328
+ end
329
+
330
+ def test_reloadable
331
+ assert_equal 1, @calculator.counter
332
+ assert_equal 2, @calculator.counter(:reload)
333
+ assert_equal 2, @calculator.counter
334
+ assert_equal 3, @calculator.counter(true)
335
+ assert_equal 3, @calculator.counter
336
+ end
337
+
338
+ def test_flush_cache
339
+ assert_equal 1, @calculator.counter
340
+
341
+ assert @calculator.instance_variable_get(:@_memoized_counter)
342
+ @calculator.flush_cache(:counter)
343
+ assert_equal false, @calculator.instance_variable_defined?(:@_memoized_counter)
344
+
345
+ assert_equal 2, @calculator.counter
346
+ end
347
+
348
+ def test_class_flush_cache
349
+ @book.memoize_all
350
+ assert_equal "My Life by Brian 'Fudge' Turmuck", @book.full_title
351
+
352
+ Book.memoize_all
353
+ assert_instance_of Array, Book.instance_variable_get(:@_memoized_all_types)
354
+ Book.flush_cache
355
+ assert_equal false, Book.instance_variable_defined?(:@_memoized_all_types)
356
+ end
357
+
358
+ def test_class_flush_cache_preserves_instances
359
+ @book.memoize_all
360
+ Book.memoize_all
361
+ assert_equal "My Life by Brian 'Fudge' Turmuck", @book.full_title
362
+
363
+ Book.flush_cache
364
+ assert_equal false, Book.instance_variable_defined?(:@_memoized_all_types)
365
+ assert_equal "My Life by Brian 'Fudge' Turmuck", @book.full_title
366
+ end
367
+
368
+ def test_flush_cache_in_child_class
369
+ x = Bbb.new
370
+
371
+ # This should not throw error
372
+ x.run
373
+ end
374
+
375
+ def test_unmemoize_all
376
+ assert_equal 1, @calculator.counter
377
+
378
+ assert_equal true, @calculator.instance_variable_defined?(:@_memoized_counter)
379
+ assert @calculator.instance_variable_get(:@_memoized_counter)
380
+ @calculator.unmemoize_all
381
+ assert_equal false, @calculator.instance_variable_defined?(:@_memoized_counter)
382
+
383
+ assert_equal 2, @calculator.counter
384
+ end
385
+
386
+ def test_all_memoized_structs
387
+ # Person memoize :age, :age?, :is_developer?, :memoize_protected_test, :name, :name?, :sleep, :update, :update_attributes
388
+ # Student < Person memoize :name, :identifier => :student
389
+ # Teacher < Person memoize :seniority
390
+
391
+ expected = %w[age age? do_with_special is_developer? memoize_protected_test name name? sleep update update_attributes]
392
+ structs = Person.all_memoized_structs
393
+ assert_equal expected, structs.collect(&:memoized_method).collect(&:to_s).sort
394
+ assert_equal '@_memoized_name', structs.detect { |s| s.memoized_method == :name }.ivar
395
+
396
+ # Same expected methods
397
+ structs = Student.all_memoized_structs
398
+ assert_equal expected, structs.collect(&:memoized_method).collect(&:to_s).sort
399
+ assert_equal '@_memoized_student_name', structs.detect { |s| s.memoized_method == :name }.ivar
400
+
401
+ expected = (expected << 'seniority').sort
402
+ structs = Teacher.all_memoized_structs
403
+ assert_equal expected, structs.collect(&:memoized_method).collect(&:to_s).sort
404
+ assert_equal '@_memoized_name', structs.detect { |s| s.memoized_method == :name }.ivar
405
+ end
406
+
407
+ def test_unmemoize_all_subclasses
408
+ # Person memoize :age, :is_developer?, :memoize_protected_test, :name, :name?, :sleep, :update, :update_attributes
409
+ # Student < Person memoize :name, :identifier => :student
410
+ # Teacher < Person memoize :seniority
411
+
412
+ teacher = Teacher.new
413
+ assert_equal 'Josh', teacher.name
414
+ assert_equal 'Josh', teacher.instance_variable_get(:@_memoized_name)
415
+ assert_equal 'very_senior', teacher.seniority
416
+ assert_equal 'very_senior', teacher.instance_variable_get(:@_memoized_seniority)
417
+
418
+ teacher.unmemoize_all
419
+ assert_equal false, teacher.instance_variable_defined?(:@_memoized_name)
420
+ assert_equal false, teacher.instance_variable_defined?(:@_memoized_seniority)
421
+
422
+ student = Student.new
423
+ assert_equal 'Student Josh', student.name
424
+ assert_equal 'Student Josh', student.instance_variable_get(:@_memoized_student_name)
425
+ assert_equal false, student.instance_variable_defined?(:@_memoized_seniority)
426
+
427
+ student.unmemoize_all
428
+ assert_equal false, @calculator.instance_variable_defined?(:@_memoized_student_name)
429
+ end
430
+
431
+ def test_memoize_all
432
+ @calculator.memoize_all
433
+ assert_equal true, @calculator.instance_variable_defined?(:@_memoized_counter)
434
+ end
435
+
436
+ def test_memoize_all_subclasses
437
+ # Person memoize :age, :is_developer?, :memoize_protected_test, :name, :name?, :sleep, :update, :update_attributes
438
+ # Student < Person memoize :name, :identifier => :student
439
+ # Teacher < Person memoize :seniority
440
+
441
+ teacher = Teacher.new
442
+ teacher.memoize_all
443
+
444
+ assert_equal 'very_senior', teacher.instance_variable_get(:@_memoized_seniority)
445
+ assert_equal 'Josh', teacher.instance_variable_get(:@_memoized_name)
446
+
447
+ student = Student.new
448
+ student.memoize_all
449
+
450
+ assert_equal 'Student Josh', student.instance_variable_get(:@_memoized_student_name)
451
+ assert_equal 'Student Josh', student.name
452
+ assert_equal false, student.instance_variable_defined?(:@_memoized_seniority)
453
+ end
454
+
455
+ def test_memoization_cache_is_different_for_each_instance
456
+ assert_equal 1, @calculator.counter
457
+ assert_equal 2, @calculator.counter(:reload)
458
+ assert_equal 1, Calculator.new.counter
459
+ end
460
+
461
+ def test_memoization_class_variables
462
+ @book.memoize_all
463
+ assert_equal "My Life by Brian 'Fudge' Turmuck", @book.instance_variable_get(:@_memoized_full_title)
464
+ assert_equal "My Life by Brian 'Fudge' Turmuck", @book.full_title
465
+
466
+ Book.memoize_all
467
+ assert_instance_of Array, Book.instance_variable_get(:@_memoized_all_types)
468
+ assert_equal 24, Book.all_types.count
469
+ end
470
+
471
+ def test_memoized_is_not_affected_by_freeze
472
+ @person.freeze
473
+ assert_equal 'Josh', @person.name
474
+ assert_equal 'Joshua', @person.update('Joshua')
475
+ end
476
+
477
+ def test_memoization_with_args
478
+ assert_equal 55, @calculator.fib(10)
479
+ assert_equal 11, @calculator.fib_calls
480
+ end
481
+
482
+ def test_reloadable_with_args
483
+ assert_equal 55, @calculator.fib(10)
484
+ assert_equal 11, @calculator.fib_calls
485
+ assert_equal 55, @calculator.fib(10, :reload)
486
+ assert_equal 12, @calculator.fib_calls
487
+ assert_equal 55, @calculator.fib(10, true)
488
+ assert_equal 13, @calculator.fib_calls
489
+ end
490
+
491
+ def test_memoization_with_boolean_arg
492
+ assert_equal 4, @calculator.add_or_subtract(2, 2, true)
493
+ assert_equal 2, @calculator.add_or_subtract(4, 2, false)
494
+ end
495
+
496
+ def test_object_memoization
497
+ [Company.new, Company.new, Company.new].each do |company|
498
+ company.extend Memoist
499
+ company.memoize :name
500
+
501
+ assert_equal '37signals', company.name
502
+ assert_equal 1, company.name_calls
503
+ assert_equal '37signals', company.name
504
+ assert_equal 1, company.name_calls
505
+ end
506
+ end
507
+
508
+ def test_memoized_module_methods
509
+ assert_equal 1.025, @calculator.sales_tax(10)
510
+ assert_equal 1, @calculator.sales_tax_calls
511
+ assert_equal 1.025, @calculator.sales_tax(10)
512
+ assert_equal 1, @calculator.sales_tax_calls
513
+ assert_equal 2.5625, @calculator.sales_tax(25)
514
+ assert_equal 2, @calculator.sales_tax_calls
515
+ end
516
+
517
+ def test_object_memoized_module_methods
518
+ company = Company.new
519
+ company.extend(Rates)
520
+
521
+ assert_equal 1.025, company.sales_tax(10)
522
+ assert_equal 1, company.sales_tax_calls
523
+ assert_equal 1.025, company.sales_tax(10)
524
+ assert_equal 1, company.sales_tax_calls
525
+ assert_equal 2.5625, company.sales_tax(25)
526
+ assert_equal 2, company.sales_tax_calls
527
+ end
528
+
529
+ def test_double_memoization_with_identifier
530
+ # Person memoize :age, :is_developer?, :memoize_protected_test, :name, :name?, :sleep, :update, :update_attributes
531
+ # Student < Person memoize :name, :identifier => :student
532
+ # Teacher < Person memoize :seniority
533
+
534
+ Person.memoize :name, identifier: :again
535
+ p = Person.new
536
+ assert_equal 'Josh', p.name
537
+ assert p.instance_variable_get(:@_memoized_again_name)
538
+
539
+ # HACK: tl;dr: Don't memoize classes in test that are used elsewhere.
540
+ # Calling Person.memoize :name, :identifier => :again pollutes Person
541
+ # and descendents since we cache the memoized method structures.
542
+ # This populates those structs, verifies Person is polluted, resets the
543
+ # structs, cleans up cached memoized_methods
544
+ Student.all_memoized_structs
545
+ Person.all_memoized_structs
546
+ Teacher.all_memoized_structs
547
+ assert Person.memoized_methods.any? { |m| m.ivar == '@_memoized_again_name' }
548
+
549
+ [Student, Teacher, Person].each(&:clear_structs)
550
+ assert Person.memoized_methods.reject! { |m| m.ivar == '@_memoized_again_name' }
551
+ assert_nil Student.memoized_methods.reject! { |m| m.ivar == '@_memoized_again_name' }
552
+ assert_nil Teacher.memoized_methods.reject! { |m| m.ivar == '@_memoized_again_name' }
553
+ end
554
+
555
+ def test_memoization_with_a_subclass
556
+ student = Student.new
557
+ student.name
558
+ student.name
559
+ assert_equal 1, student.student_name_calls
560
+ assert_equal 1, student.name_calls
561
+ end
562
+
563
+ def test_memoization_is_chainable
564
+ klass = Class.new do
565
+ def foo
566
+ 'bar'
567
+ end
568
+ end
569
+ klass.extend Memoist
570
+ chainable = klass.memoize :foo
571
+ assert_equal :foo, chainable
572
+ end
573
+
574
+ def test_protected_method_memoization
575
+ person = Person.new
576
+
577
+ assert_raises(NoMethodError) { person.memoize_protected_test }
578
+ assert_equal 'protected', person.send(:memoize_protected_test)
579
+ end
580
+
581
+ def test_private_method_memoization
582
+ person = Person.new
583
+
584
+ assert_raises(NoMethodError) { person.is_developer? }
585
+ assert_equal 'Yes', person.send(:is_developer?)
586
+ assert_equal 1, person.is_developer_calls
587
+ assert_equal 'Yes', person.send(:is_developer?)
588
+ assert_equal 1, person.is_developer_calls
589
+ end
590
+ end
@@ -0,0 +1,3 @@
1
+ require 'minitest/autorun'
2
+
3
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')