memo_wise 1.1.0 → 1.5.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/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