memo_wise 1.1.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/memo_wise.rb CHANGED
@@ -25,7 +25,7 @@ require "memo_wise/version"
25
25
  # - {.memo_wise} for API and usage examples.
26
26
  # - {file:README.md} for general project information.
27
27
  #
28
- module MemoWise # rubocop:disable Metrics/ModuleLength
28
+ module MemoWise
29
29
  # Constructor to set up memoization state before
30
30
  # [calling the original](https://medium.com/@jeremy_96642/ruby-method-auditing-using-module-prepend-4f4e69aacd95)
31
31
  # constructor.
@@ -56,7 +56,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
56
56
  # :nocov:
57
57
  all_args = RUBY_VERSION < "2.7" ? "*" : "..."
58
58
  # :nocov:
59
- class_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
59
+ class_eval <<~HEREDOC, __FILE__, __LINE__ + 1
60
60
  # On Ruby 2.7 or greater:
61
61
  #
62
62
  # def initialize(...)
@@ -75,7 +75,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
75
75
  MemoWise::InternalAPI.create_memo_wise_state!(self)
76
76
  super
77
77
  end
78
- END_OF_METHOD
78
+ HEREDOC
79
79
 
80
80
  # @private
81
81
  #
@@ -91,7 +91,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
91
91
  # prepend MemoWise
92
92
  # end
93
93
  #
94
- def self.prepended(target) # rubocop:disable Metrics/PerceivedComplexity
94
+ def self.prepended(target)
95
95
  class << target
96
96
  # Allocator to set up memoization state before
97
97
  # [calling the original](https://medium.com/@jeremy_96642/ruby-method-auditing-using-module-prepend-4f4e69aacd95)
@@ -111,7 +111,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
111
111
  end
112
112
 
113
113
  # NOTE: See YARD docs for {.memo_wise} directly below this method!
114
- def memo_wise(method_name_or_hash) # rubocop:disable Metrics/PerceivedComplexity
114
+ def memo_wise(method_name_or_hash)
115
115
  klass = self
116
116
  case method_name_or_hash
117
117
  when Symbol
@@ -156,100 +156,59 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
156
156
  klass = klass.singleton_class
157
157
  end
158
158
 
159
- unless method_name.is_a?(Symbol)
160
- raise ArgumentError, "#{method_name.inspect} must be a Symbol"
159
+ if klass.singleton_class?
160
+ # This ensures that a memoized method defined on a parent class can
161
+ # still be used in a child class.
162
+ klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
163
+ def inherited(subclass)
164
+ super
165
+ MemoWise::InternalAPI.create_memo_wise_state!(subclass)
166
+ end
167
+ HEREDOC
161
168
  end
162
169
 
163
- api = MemoWise::InternalAPI.new(klass)
164
- visibility = api.method_visibility(method_name)
165
- original_memo_wised_name =
166
- MemoWise::InternalAPI.original_memo_wised_name(method_name)
170
+ raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol)
171
+
172
+ visibility = MemoWise::InternalAPI.method_visibility(klass, method_name)
173
+ original_memo_wised_name = MemoWise::InternalAPI.original_memo_wised_name(method_name)
167
174
  method = klass.instance_method(method_name)
168
175
 
169
176
  klass.send(:alias_method, original_memo_wised_name, method_name)
170
177
  klass.send(:private, original_memo_wised_name)
171
178
 
172
- # Zero-arg methods can use simpler/more performant logic because the
173
- # hash key is just the method name.
174
- if method.arity.zero?
175
- klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
179
+ method_arguments = MemoWise::InternalAPI.method_arguments(method)
180
+
181
+ case method_arguments
182
+ when MemoWise::InternalAPI::NONE
183
+ klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
176
184
  def #{method_name}
177
- output = @_memo_wise[:#{method_name}]
178
- if output || @_memo_wise.key?(:#{method_name})
179
- output
180
- else
185
+ @_memo_wise.fetch(:#{method_name}) do
181
186
  @_memo_wise[:#{method_name}] = #{original_memo_wised_name}
182
187
  end
183
188
  end
184
- END_OF_METHOD
185
- else
186
- if MemoWise::InternalAPI.has_only_required_args?(method)
187
- args_str = method.parameters.map do |type, name|
188
- "#{name}#{':' if type == :keyreq}"
189
- end.join(", ")
190
- args_str = "(#{args_str})"
191
- call_str = method.parameters.map do |type, name|
192
- type == :req ? name : "#{name}: #{name}"
193
- end.join(", ")
194
- call_str = "(#{call_str})"
195
- fetch_key_params = method.parameters.map(&:last)
196
- if fetch_key_params.size > 1
197
- fetch_key_init =
198
- "[:#{method_name}, #{fetch_key_params.join(', ')}].hash"
199
- use_hashed_key = true
200
- else
201
- fetch_key = fetch_key_params.first.to_s
202
- end
203
- else
204
- # If our method has arguments, we need to separate out our handling
205
- # of normal args vs. keyword args due to the changes in Ruby 3.
206
- # See: <link>
207
- # By only including logic for *args, **kwargs when they are used in
208
- # the method, we can avoid allocating unnecessary arrays and hashes.
209
- has_arg = MemoWise::InternalAPI.has_arg?(method)
210
-
211
- if has_arg && MemoWise::InternalAPI.has_kwarg?(method)
212
- args_str = "(*args, **kwargs)"
213
- fetch_key_init = "[:#{method_name}, args, kwargs].hash"
214
- use_hashed_key = true
215
- elsif has_arg
216
- args_str = "(*args)"
217
- fetch_key_init = "args.hash"
218
- else
219
- args_str = "(**kwargs)"
220
- fetch_key_init = "kwargs.hash"
221
- end
222
- end
223
-
224
- if use_hashed_key
225
- klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
226
- def #{method_name}#{args_str}
227
- key = #{fetch_key_init}
228
- output = @_memo_wise[key]
229
- if output || @_memo_wise.key?(key)
230
- output
231
- else
232
- hashes = (@_memo_wise_hashes[:#{method_name}] ||= Set.new)
233
- hashes << key
234
- @_memo_wise[key] = #{original_memo_wised_name}#{call_str || args_str}
235
- end
189
+ HEREDOC
190
+ when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL, MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD
191
+ key = method.parameters.first.last
192
+ klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
193
+ def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
194
+ _memo_wise_hash = (@_memo_wise[:#{method_name}] ||= {})
195
+ _memo_wise_hash.fetch(#{key}) do
196
+ _memo_wise_hash[#{key}] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
236
197
  end
237
- END_OF_METHOD
238
- else
239
- fetch_key ||= "key"
240
- klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
241
- def #{method_name}#{args_str}
242
- hash = (@_memo_wise[:#{method_name}] ||= {})
243
- #{"key = #{fetch_key_init}" if fetch_key_init}
244
- output = hash[#{fetch_key}]
245
- if output || hash.key?(#{fetch_key})
246
- output
247
- else
248
- hash[#{fetch_key}] = #{original_memo_wised_name}#{call_str || args_str}
249
- end
198
+ end
199
+ HEREDOC
200
+ # MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT,
201
+ # MemoWise::InternalAPI::DOUBLE_SPLAT, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
202
+ else
203
+ klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
204
+ def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
205
+ _memo_wise_hash = (@_memo_wise[:#{method_name}] ||= {})
206
+ _memo_wise_key = #{MemoWise::InternalAPI.key_str(method)}
207
+ _memo_wise_hash.fetch(_memo_wise_key) do
208
+ _memo_wise_hash[_memo_wise_key] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
250
209
  end
251
- END_OF_METHOD
252
- end
210
+ end
211
+ HEREDOC
253
212
  end
254
213
 
255
214
  klass.send(visibility, method_name)
@@ -275,8 +234,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
275
234
  # (`...` or `*args, **kwargs`), making reflection on method parameters
276
235
  # useless without this.
277
236
  def target.instance_method(symbol)
278
- original_memo_wised_name =
279
- MemoWise::InternalAPI.original_memo_wised_name(symbol)
237
+ original_memo_wised_name = MemoWise::InternalAPI.original_memo_wised_name(symbol)
280
238
 
281
239
  super.tap do |curr_method|
282
240
  # Start with calling the original `instance_method` on `symbol`,
@@ -368,7 +326,6 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
368
326
  # Example.method_called_times #=> nil
369
327
  ##
370
328
 
371
- # rubocop:disable Layout/LineLength
372
329
  ##
373
330
  # @!method self.reset_memo_wise(method_name = nil, *args, **kwargs)
374
331
  # Implementation of {#reset_memo_wise} for class methods.
@@ -402,7 +359,6 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
402
359
  #
403
360
  # Example.reset_memo_wise # reset "all methods" mode
404
361
  ##
405
- # rubocop:enable Layout/LineLength
406
362
 
407
363
  # Presets the memoized result for the given method to the result of the given
408
364
  # block.
@@ -455,26 +411,33 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
455
411
  # ex.method_called_times #=> nil
456
412
  #
457
413
  def preset_memo_wise(method_name, *args, **kwargs)
458
- unless block_given?
459
- raise ArgumentError,
460
- "Pass a block as the value to preset for #{method_name}, #{args}"
461
- end
414
+ raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol)
415
+ raise ArgumentError, "Pass a block as the value to preset for #{method_name}, #{args}" unless block_given?
416
+
417
+ MemoWise::InternalAPI.validate_memo_wised!(self, method_name)
462
418
 
463
- api = MemoWise::InternalAPI.new(self)
464
- api.validate_memo_wised!(method_name)
419
+ method = method(MemoWise::InternalAPI.original_memo_wised_name(method_name))
420
+ method_arguments = MemoWise::InternalAPI.method_arguments(method)
465
421
 
466
- if method(method_name).arity.zero?
422
+ if method_arguments == MemoWise::InternalAPI::NONE
467
423
  @_memo_wise[method_name] = yield
468
- else
469
- key = api.fetch_key(method_name, *args, **kwargs)
470
- if api.use_hashed_key?(method_name)
471
- hashes = @_memo_wise_hashes[method_name] ||= []
472
- hashes << key
473
- @_memo_wise[key] = yield
474
- else
475
- hash = @_memo_wise[method_name] ||= {}
476
- hash[key] = yield
424
+ return
425
+ end
426
+
427
+ hash = (@_memo_wise[method_name] ||= {})
428
+
429
+ case method_arguments
430
+ when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL then hash[args.first] = yield
431
+ when MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD then hash[kwargs.first.last] = yield
432
+ when MemoWise::InternalAPI::SPLAT then hash[args] = yield
433
+ when MemoWise::InternalAPI::DOUBLE_SPLAT then hash[kwargs] = yield
434
+ when MemoWise::InternalAPI::MULTIPLE_REQUIRED
435
+ key = method.parameters.map.with_index do |(type, name), idx|
436
+ type == :req ? args[idx] : kwargs[name]
477
437
  end
438
+ hash[key] = yield
439
+ else # MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
440
+ hash[[args, kwargs]] = yield
478
441
  end
479
442
  end
480
443
 
@@ -543,46 +506,41 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
543
506
  #
544
507
  # ex.reset_memo_wise # reset "all methods" mode
545
508
  #
546
- def reset_memo_wise(method_name = nil, *args, **kwargs) # rubocop:disable Metrics/PerceivedComplexity
509
+ def reset_memo_wise(method_name = nil, *args, **kwargs)
547
510
  if method_name.nil?
548
- unless args.empty?
549
- raise ArgumentError, "Provided args when method_name = nil"
550
- end
551
-
552
- unless kwargs.empty?
553
- raise ArgumentError, "Provided kwargs when method_name = nil"
554
- end
511
+ raise ArgumentError, "Provided args when method_name = nil" unless args.empty?
512
+ raise ArgumentError, "Provided kwargs when method_name = nil" unless kwargs.empty?
555
513
 
556
514
  @_memo_wise.clear
557
- @_memo_wise_hashes.clear
558
515
  return
559
516
  end
560
517
 
561
- unless method_name.is_a?(Symbol)
562
- raise ArgumentError, "#{method_name.inspect} must be a Symbol"
563
- end
518
+ raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol)
519
+ raise ArgumentError, "#{method_name} is not a defined method" unless respond_to?(method_name, true)
564
520
 
565
- unless respond_to?(method_name, true)
566
- raise ArgumentError, "#{method_name} is not a defined method"
567
- end
521
+ MemoWise::InternalAPI.validate_memo_wised!(self, method_name)
568
522
 
569
- api = MemoWise::InternalAPI.new(self)
570
- api.validate_memo_wised!(method_name)
523
+ method = method(MemoWise::InternalAPI.original_memo_wised_name(method_name))
524
+ method_arguments = MemoWise::InternalAPI.method_arguments(method)
571
525
 
572
- if args.empty? && kwargs.empty?
573
- @_memo_wise.delete(method_name)
574
- @_memo_wise_hashes[method_name]&.each do |hash|
575
- @_memo_wise.delete(hash)
576
- end
577
- @_memo_wise_hashes.delete(method_name)
578
- else
579
- key = api.fetch_key(method_name, *args, **kwargs)
580
- if api.use_hashed_key?(method_name)
581
- @_memo_wise_hashes[method_name]&.delete(key)
582
- @_memo_wise.delete(key)
583
- else
584
- @_memo_wise[method_name]&.delete(key)
585
- end
526
+ # method_name == MemoWise::InternalAPI::NONE will be covered by this case.
527
+ @_memo_wise.delete(method_name) if args.empty? && kwargs.empty?
528
+ method_hash = @_memo_wise[method_name]
529
+
530
+ case method_arguments
531
+ when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL then method_hash&.delete(args.first)
532
+ when MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD then method_hash&.delete(kwargs.first.last)
533
+ when MemoWise::InternalAPI::SPLAT then method_hash&.delete(args)
534
+ when MemoWise::InternalAPI::DOUBLE_SPLAT then method_hash&.delete(kwargs)
535
+ else # MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
536
+ key = if method_arguments == MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
537
+ [args, kwargs]
538
+ else
539
+ method.parameters.map.with_index do |(type, name), i|
540
+ type == :req ? args[i] : kwargs[name]
541
+ end
542
+ end
543
+ method_hash&.delete(key)
586
544
  end
587
545
  end
588
546
  end
data/memo_wise.gemspec CHANGED
@@ -36,6 +36,7 @@ Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
36
36
  spec.require_paths = ["lib"]
37
37
 
38
38
  spec.metadata = {
39
+ "rubygems_mfa_required" => "true",
39
40
  "changelog_uri" => "https://github.com/panorama-ed/memo_wise/blob/main/CHANGELOG.md",
40
41
  "source_code_uri" => "https://github.com/panorama-ed/memo_wise"
41
42
  }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: memo_wise
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Panorama Education
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2021-07-30 00:00:00.000000000 Z
14
+ date: 2021-12-20 00:00:00.000000000 Z
15
15
  dependencies: []
16
16
  description:
17
17
  email:
@@ -52,6 +52,7 @@ homepage: https://github.com/panorama-ed/memo_wise
52
52
  licenses:
53
53
  - MIT
54
54
  metadata:
55
+ rubygems_mfa_required: 'true'
55
56
  changelog_uri: https://github.com/panorama-ed/memo_wise/blob/main/CHANGELOG.md
56
57
  source_code_uri: https://github.com/panorama-ed/memo_wise
57
58
  post_install_message:
@@ -69,7 +70,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
69
70
  - !ruby/object:Gem::Version
70
71
  version: '0'
71
72
  requirements: []
72
- rubygems_version: 3.2.3
73
+ rubygems_version: 3.2.22
73
74
  signing_key:
74
75
  specification_version: 4
75
76
  summary: The wise choice for Ruby memoization