memo_wise 1.6.0 → 1.8.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
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
3
+ # Disable RuboCop here because Ruby < 3.2 does not load `set` by default.
4
+ require "set" # rubocop:disable Lint/RedundantRequireStatement
4
5
 
5
6
  require "memo_wise/internal_api"
6
7
  require "memo_wise/version"
@@ -30,12 +31,12 @@ module MemoWise
30
31
  # [calling the original](https://medium.com/@jeremy_96642/ruby-method-auditing-using-module-prepend-4f4e69aacd95)
31
32
  # constructor.
32
33
  #
33
- # - **Q:** Why is [Module#prepend](https://ruby-doc.org/core-3.1.0/Module.html#method-i-prepend)
34
+ # - **Q:** Why is [Module#prepend](https://ruby-doc.org/3.2.1/Module.html#method-i-prepend)
34
35
  # important here
35
36
  # ([more info](https://medium.com/@leo_hetsch/ruby-modules-include-vs-prepend-vs-extend-f09837a5b073))?
36
37
  # - **A:** To set up *mutable state* inside the instance, even if the original
37
38
  # constructor will then call
38
- # [Object#freeze](https://ruby-doc.org/core-3.1.0/Object.html#method-i-freeze).
39
+ # [Object#freeze](https://ruby-doc.org/3.2.1/Object.html#method-i-freeze).
39
40
  #
40
41
  # This approach supports memoization on frozen (immutable) objects -- for
41
42
  # example, classes created by the
@@ -84,7 +85,7 @@ module MemoWise
84
85
  # @param target [Class]
85
86
  # The `Class` into to prepend the MemoWise methods e.g. `memo_wise`
86
87
  #
87
- # @see https://ruby-doc.org/core-3.1.0/Module.html#method-i-prepended
88
+ # @see https://ruby-doc.org/3.2.1/Module.html#method-i-prepend
88
89
  #
89
90
  # @example
90
91
  # class Example
@@ -99,7 +100,7 @@ module MemoWise
99
100
  #
100
101
  # This is necessary in addition to the `#initialize` method definition
101
102
  # above because
102
- # [`Class#allocate`](https://ruby-doc.org/core-3.1.0/Class.html#method-i-allocate)
103
+ # [`Class#allocate`](https://ruby-doc.org/3.2.1/Class.html#method-i-allocate)
103
104
  # bypasses `#initialize`, and when it's used (e.g.,
104
105
  # [in ActiveRecord](https://github.com/rails/rails/blob/a395c3a6af1e079740e7a28994d77c8baadd2a9d/activerecord/lib/active_record/persistence.rb#L411))
105
106
  # we still need to be able to access MemoWise's instance variable. Despite
@@ -195,9 +196,40 @@ module MemoWise
195
196
  end
196
197
  end
197
198
  HEREDOC
198
- # MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT,
199
- # MemoWise::InternalAPI::DOUBLE_SPLAT, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
200
- else
199
+ when MemoWise::InternalAPI::MULTIPLE_REQUIRED
200
+ # When we have multiple required params, we store the memoized values in a deeply nested hash, like:
201
+ # { method_name: { arg1 => { arg2 => { arg3 => memoized_value } } } }
202
+ last_index = method.parameters.size
203
+ layers = method.parameters.map.with_index(1) do |(_, name), index|
204
+ prev_hash = "_memo_wise_hash#{index - 1 if index > 1}"
205
+ fallback = if index == last_index
206
+ "#{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})"
207
+ else
208
+ "{}"
209
+ end
210
+ "_memo_wise_hash#{index} = #{prev_hash}.fetch(#{name}) { #{prev_hash}[#{name}] = #{fallback} }"
211
+ end
212
+ klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
213
+ def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
214
+ _memo_wise_hash = (@_memo_wise[:#{method_name}] ||= {})
215
+ #{layers.join("\n ")}
216
+ end
217
+ HEREDOC
218
+ when MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
219
+ # When we have both *args and **kwargs, we store the memoized values in a deeply nested hash, like:
220
+ # { method_name: { args => { kwargs => memoized_value } } }
221
+ klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
222
+ def #{method_name}(*args, **kwargs)
223
+ _memo_wise_hash = (@_memo_wise[:#{method_name}] ||= {})
224
+ _memo_wise_kwargs_hash = _memo_wise_hash.fetch(args) do
225
+ _memo_wise_hash[args] = {}
226
+ end
227
+ _memo_wise_kwargs_hash.fetch(kwargs) do
228
+ _memo_wise_kwargs_hash[kwargs] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
229
+ end
230
+ end
231
+ HEREDOC
232
+ else # MemoWise::InternalAPI::SPLAT, MemoWise::InternalAPI::DOUBLE_SPLAT
201
233
  klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
202
234
  def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
203
235
  _memo_wise_hash = (@_memo_wise[:#{method_name}] ||= {})
@@ -223,7 +255,7 @@ module MemoWise
223
255
  )
224
256
  end
225
257
 
226
- # Override [Module#instance_method](https://ruby-doc.org/core-3.1.0/Module.html#method-i-instance_method)
258
+ # Override [Module#instance_method](https://ruby-doc.org/3.2.1/Module.html#method-i-instance_method)
227
259
  # to proxy the original `UnboundMethod#parameters` results. We want the
228
260
  # parameters to reflect the original method in order to support callers
229
261
  # who want to use Ruby reflection to process the method parameters,
@@ -430,12 +462,23 @@ module MemoWise
430
462
  when MemoWise::InternalAPI::SPLAT then hash[args] = yield
431
463
  when MemoWise::InternalAPI::DOUBLE_SPLAT then hash[kwargs] = yield
432
464
  when MemoWise::InternalAPI::MULTIPLE_REQUIRED
433
- key = method.parameters.map.with_index do |(type, name), idx|
434
- type == :req ? args[idx] : kwargs[name]
465
+ n_parameters = method.parameters.size
466
+ method.parameters.each_with_index do |(type, name), index|
467
+ val = type == :req ? args[index] : kwargs[name]
468
+
469
+ # Walk through the layers of nested hashes. When we get to the final
470
+ # layer, yield to the block to set its value.
471
+ if index < n_parameters - 1
472
+ hash = (hash[val] ||= {})
473
+ else
474
+ hash[val] = yield
475
+ end
435
476
  end
436
- hash[key] = yield
437
477
  else # MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
438
- hash[[args, kwargs]] = yield
478
+ # When we have both *args and **kwargs, we store the memoized values like:
479
+ # { method_name: { args => { kwargs => memoized_value } } }
480
+ # so we need to initialize `hash[args]`` if it does not already exist.
481
+ (hash[args] ||= {})[kwargs] = yield
439
482
  end
440
483
  end
441
484
 
@@ -530,15 +573,26 @@ module MemoWise
530
573
  when MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD then method_hash&.delete(kwargs.first.last)
531
574
  when MemoWise::InternalAPI::SPLAT then method_hash&.delete(args)
532
575
  when MemoWise::InternalAPI::DOUBLE_SPLAT then method_hash&.delete(kwargs)
533
- else # MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
534
- key = if method_arguments == MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
535
- [args, kwargs]
536
- else
537
- method.parameters.map.with_index do |(type, name), i|
538
- type == :req ? args[i] : kwargs[name]
539
- end
540
- end
541
- method_hash&.delete(key)
576
+ when MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
577
+ # Here, memoized values are stored like:
578
+ # { method_name: { args => { kwargs => memoized_value } } }
579
+ # so we need to delete the innermost value (because the same args array
580
+ # may have multiple memoized values for different kwargs hashes).
581
+ method_hash&.[](args)&.delete(kwargs)
582
+ else # MemoWise::InternalAPI::MULTIPLE_REQUIRED
583
+ n_parameters = method.parameters.size
584
+ method.parameters.each_with_index do |(type, name), index|
585
+ val = type == :req ? args[index] : kwargs[name]
586
+
587
+ # Walk through the layers of nested hashes. When we get to the final
588
+ # layer, delete its value. We use the safe navigation operator to
589
+ # gracefully handle any layer not yet existing.
590
+ if index < n_parameters - 1
591
+ method_hash = method_hash&.[](val)
592
+ else
593
+ method_hash&.delete(val)
594
+ end
595
+ end
542
596
  end
543
597
  end
544
598
  end
data/memo_wise.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "lib/memo_wise/version"
4
4
 
5
- Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
5
+ Gem::Specification.new do |spec|
6
6
  spec.name = "memo_wise"
7
7
  spec.version = MemoWise::VERSION
8
8
  spec.summary = "The wise choice for Ruby memoization"
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.6.0
4
+ version: 1.8.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: 2022-01-24 00:00:00.000000000 Z
14
+ date: 2023-10-30 00:00:00.000000000 Z
15
15
  dependencies: []
16
16
  description:
17
17
  email:
@@ -26,6 +26,7 @@ files:
26
26
  - ".dokaz"
27
27
  - ".github/PULL_REQUEST_TEMPLATE.md"
28
28
  - ".github/dependabot.yml"
29
+ - ".github/workflows/dependency-review.yml"
29
30
  - ".github/workflows/main.yml"
30
31
  - ".gitignore"
31
32
  - ".rspec"
@@ -70,7 +71,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
70
71
  - !ruby/object:Gem::Version
71
72
  version: '0'
72
73
  requirements: []
73
- rubygems_version: 3.3.3
74
+ rubygems_version: 3.4.6
74
75
  signing_key:
75
76
  specification_version: 4
76
77
  summary: The wise choice for Ruby memoization