memoist3 1.0.0

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/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')