memo_wise 1.3.0 → 1.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ff51bfee4fcc634d50588005e79c4a74702fd0d8deb8ab23dd917134cedf42f
4
- data.tar.gz: 1cb14df9bcef27a3442b48cf1aee370835bbf9070b1f44bb3d8ec8266158bdab
3
+ metadata.gz: 3b26ef1a44fd13498359282c6d47923ecf90bdfd4e8ac487d389dd1d5c9c5d59
4
+ data.tar.gz: 51d2ddc7b1d0e7d770afcf081f566059e67b07867c10803db693fd2ec848f4d1
5
5
  SHA512:
6
- metadata.gz: 36a3f525833af24f076568ed9bdb43a06e90441788f2f8e8d26c0303ec7e0697a56c8401a3e24c06300cb3f2df199768ae00d8cf95cc90808fb4d9940ab9a931
7
- data.tar.gz: f60569c63d693272856137c179b826a598c63a5762fedd76faf630b26fd3301d6ca37756f99cb7940501d5bcfd4c375094176a617610c35fc8788b67580159ce
6
+ metadata.gz: ca1c06be2c7d0368e7708349563a0a79f93922d491ffe43448793bd6364938ea063bc3910d67ca7ce04c6cb409d784d23d19d5e97fea6430fcb20ebbb0819ab3
7
+ data.tar.gz: 93fc8811a80bd7f4d88517e0c3cd21f3b6ef29daea6e0578e734abecdc0067f5f9451261f5c47ad80106332075a4072a4a3e7322fe6181f2d61782d63c42caa0
@@ -12,7 +12,7 @@ jobs:
12
12
  strategy:
13
13
  fail-fast: false
14
14
  matrix:
15
- ruby: [jruby, 2.4, 2.5, 2.6, 2.7, 3.0]
15
+ ruby: [jruby, 2.4, 2.5, 2.6, 2.7, 3.0, truffleruby-head]
16
16
  runs-on: ubuntu-latest
17
17
  steps:
18
18
  - uses: actions/checkout@v2
data/.rubocop.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  inherit_gem:
2
- panolint: rubocop.yml
2
+ panolint: panolint-rubocop.yml
3
3
 
4
4
  Layout/LineLength:
5
5
  Max: 120
data/CHANGELOG.md CHANGED
@@ -9,8 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  - Nothing yet!
11
11
 
12
+ ## [1.4.0] - 2021-12-10
13
+
14
+ ### Fixed
15
+
16
+ - Fix several bugs related to classes inheriting memoized methods
17
+ from multiple modules or a parent class ([#241](https://github.com/panorama-ed/memo_wise/pull/241))
18
+
12
19
  ## [1.3.0] - 2021-11-22
13
20
 
21
+ ### Fixed
22
+
14
23
  - Fix thread-safety issue in concurrent calls to zero-arg method in unmemoized
15
24
  state which resulted in a `nil` value being accidentally returned in one thread
16
25
  - Fix bugs related to child classes inheriting from parent classes that use
@@ -101,7 +110,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
101
110
  - Panolint
102
111
  - Dependabot setup
103
112
 
104
- [Unreleased]: https://github.com/panorama-ed/memo_wise/compare/v1.3.0...HEAD
113
+ [Unreleased]: https://github.com/panorama-ed/memo_wise/compare/v1.4.0...HEAD
114
+ [1.4.0]: https://github.com/panorama-ed/memo_wise/compare/v1.3.0...v1.4.0
105
115
  [1.3.0]: https://github.com/panorama-ed/memo_wise/compare/v1.2.0...v1.3.0
106
116
  [1.2.0]: https://github.com/panorama-ed/memo_wise/compare/v1.1.0...v1.2.0
107
117
  [1.1.0]: https://github.com/panorama-ed/memo_wise/compare/v1.0.0...v1.1.0
data/Gemfile.lock CHANGED
@@ -1,9 +1,9 @@
1
1
  GIT
2
2
  remote: https://github.com/panorama-ed/panolint.git
3
- revision: c709ebcc5fd9593db959df4a92c12cae1d1fb9af
3
+ revision: 5850a218b96da7dd196438fb65d41169cd6747e5
4
4
  branch: main
5
5
  specs:
6
- panolint (0.1.3)
6
+ panolint (0.1.4)
7
7
  brakeman (>= 4.8, < 6.0)
8
8
  rubocop (>= 0.83, < 2.0)
9
9
  rubocop-performance (~> 1.5)
@@ -14,19 +14,20 @@ GIT
14
14
  PATH
15
15
  remote: .
16
16
  specs:
17
- memo_wise (1.3.0)
17
+ memo_wise (1.4.0)
18
18
 
19
19
  GEM
20
20
  remote: https://rubygems.org/
21
21
  specs:
22
- activesupport (5.2.6)
22
+ activesupport (6.1.4.1)
23
23
  concurrent-ruby (~> 1.0, >= 1.0.2)
24
- i18n (>= 0.7, < 2)
25
- minitest (~> 5.1)
26
- tzinfo (~> 1.1)
24
+ i18n (>= 1.6, < 2)
25
+ minitest (>= 5.1)
26
+ tzinfo (~> 2.0)
27
+ zeitwerk (~> 2.3)
27
28
  ansi (1.5.0)
28
29
  ast (2.4.2)
29
- brakeman (5.1.1)
30
+ brakeman (5.1.2)
30
31
  codecov (0.6.0)
31
32
  simplecov (>= 0.15, < 0.22)
32
33
  concurrent-ruby (1.1.9)
@@ -36,11 +37,11 @@ GEM
36
37
  ansi
37
38
  rouge
38
39
  slop (~> 3)
39
- i18n (1.8.10)
40
+ i18n (1.8.11)
40
41
  concurrent-ruby (~> 1.0)
41
42
  minitest (5.14.4)
42
- parallel (1.20.1)
43
- parser (3.0.2.0)
43
+ parallel (1.21.0)
44
+ parser (3.0.3.1)
44
45
  ast (~> 2.4.1)
45
46
  rack (2.2.3)
46
47
  rainbow (3.0.0)
@@ -62,44 +63,43 @@ GEM
62
63
  diff-lcs (>= 1.2.0, < 2.0)
63
64
  rspec-support (~> 3.10.0)
64
65
  rspec-support (3.10.0)
65
- rubocop (1.12.1)
66
+ rubocop (1.23.0)
66
67
  parallel (~> 1.10)
67
68
  parser (>= 3.0.0.0)
68
69
  rainbow (>= 2.2.2, < 4.0)
69
70
  regexp_parser (>= 1.8, < 3.0)
70
71
  rexml
71
- rubocop-ast (>= 1.2.0, < 2.0)
72
+ rubocop-ast (>= 1.12.0, < 2.0)
72
73
  ruby-progressbar (~> 1.7)
73
74
  unicode-display_width (>= 1.4.0, < 3.0)
74
- rubocop-ast (1.4.1)
75
- parser (>= 2.7.1.5)
76
- rubocop-performance (1.10.2)
77
- rubocop (>= 0.90.0, < 2.0)
75
+ rubocop-ast (1.13.0)
76
+ parser (>= 3.0.1.1)
77
+ rubocop-performance (1.12.0)
78
+ rubocop (>= 1.7.0, < 2.0)
78
79
  rubocop-ast (>= 0.4.0)
79
- rubocop-rails (2.9.1)
80
+ rubocop-rails (2.12.4)
80
81
  activesupport (>= 4.2.0)
81
82
  rack (>= 1.1)
82
- rubocop (>= 0.90.0, < 2.0)
83
- rubocop-rake (0.5.1)
84
- rubocop
85
- rubocop-rspec (2.2.0)
83
+ rubocop (>= 1.7.0, < 2.0)
84
+ rubocop-rake (0.6.0)
86
85
  rubocop (~> 1.0)
87
- rubocop-ast (>= 1.1.0)
86
+ rubocop-rspec (2.6.0)
87
+ rubocop (~> 1.19)
88
88
  ruby-progressbar (1.11.0)
89
89
  simplecov (0.18.5)
90
90
  docile (~> 1.1)
91
91
  simplecov-html (~> 0.11)
92
92
  simplecov-html (0.12.3)
93
93
  slop (3.6.0)
94
- thread_safe (0.3.6)
95
- tzinfo (1.2.9)
96
- thread_safe (~> 0.1)
97
- unicode-display_width (2.0.0)
94
+ tzinfo (2.0.4)
95
+ concurrent-ruby (~> 1.0)
96
+ unicode-display_width (2.1.0)
98
97
  values (1.8.0)
99
98
  yard (0.9.26)
100
99
  yard-doctest (0.1.17)
101
100
  minitest
102
101
  yard
102
+ zeitwerk (2.5.1)
103
103
 
104
104
  PLATFORMS
105
105
  ruby
@@ -117,4 +117,4 @@ DEPENDENCIES
117
117
  yard-doctest (~> 0.1)
118
118
 
119
119
  BUNDLED WITH
120
- 2.2.28
120
+ 2.2.32
data/README.md CHANGED
@@ -112,38 +112,38 @@ For more usage details, see our detailed [documentation](#documentation).
112
112
 
113
113
  ## Benchmarks
114
114
 
115
- Benchmarks are run in GitHub Actions, and the tables below are updated with every code change. **Values >1.00x represent how much _slower_ each gem’s memoized value retrieval is than the latest commit of `MemoWise`**, according to [`benchmark-ips`](https://github.com/evanphx/benchmark-ips) (2.9.1).
115
+ Benchmarks are run in GitHub Actions, and the tables below are updated with every code change. **Values >1.00x represent how much _slower_ each gem’s memoized value retrieval is than the latest commit of `MemoWise`**, according to [`benchmark-ips`](https://github.com/evanphx/benchmark-ips) (2.9.2).
116
116
 
117
- Results using Ruby 3.0.2:
117
+ Results using Ruby 3.0.3:
118
118
 
119
119
  |Method arguments|`Dry::Core`\* (0.7.1)|`Memery` (1.4.0)|
120
120
  |--|--|--|
121
- |`()` (none)|1.42x|17.84x|
122
- |`(a)`|2.48x|11.48x|
123
- |`(a, b)`|0.46x|2.05x|
124
- |`(a:)`|2.18x|20.50x|
125
- |`(a:, b:)`|0.48x|4.22x|
126
- |`(a, b:)`|0.48x|4.08x|
127
- |`(a, *args)`|0.90x|1.97x|
128
- |`(a:, **kwargs)`|0.80x|3.02x|
129
- |`(a, *args, b:, **kwargs)`|0.61x|1.54x|
121
+ |`()` (none)|1.36x|19.42x|
122
+ |`(a)`|2.47x|11.39x|
123
+ |`(a, b)`|0.44x|2.16x|
124
+ |`(a:)`|2.30x|22.89x|
125
+ |`(a:, b:)`|0.47x|4.54x|
126
+ |`(a, b:)`|0.47x|4.33x|
127
+ |`(a, *args)`|0.83x|2.09x|
128
+ |`(a:, **kwargs)`|0.76x|2.85x|
129
+ |`(a, *args, b:, **kwargs)`|0.61x|1.55x|
130
130
 
131
131
  \* `Dry::Core`
132
132
  [may cause incorrect behavior caused by hash collisions](https://github.com/dry-rb/dry-core/issues/63).
133
133
 
134
- Results using Ruby 2.7.4 (because these gems raise errors in Ruby 3.x):
134
+ Results using Ruby 2.7.5 (because these gems raise errors in Ruby 3.x):
135
135
 
136
136
  |Method arguments|`DDMemoize` (1.0.0)|`Memoist` (0.16.2)|`Memoized` (1.0.2)|`Memoizer` (1.0.3)|
137
137
  |--|--|--|--|--|
138
- |`()` (none)|33.90x|3.44x|1.56x|4.03x|
139
- |`(a)`|24.56x|17.02x|12.94x|14.91x|
140
- |`(a, b)`|3.14x|2.35x|1.84x|2.03x|
141
- |`(a:)`|34.42x|28.14x|23.83x|25.26x|
142
- |`(a:, b:)`|5.13x|4.28x|3.77x|3.97x|
143
- |`(a, b:)`|4.83x|4.08x|3.50x|3.66x|
144
- |`(a, *args)`|3.03x|2.25x|1.95x|1.95x|
145
- |`(a:, **kwargs)`|2.90x|2.44x|2.14x|2.24x|
146
- |`(a, *args, b:, **kwargs)`|2.05x|1.77x|1.65x|1.64x|
138
+ |`()` (none)|36.84x|3.56x|1.68x|4.19x|
139
+ |`(a)`|27.50x|18.85x|13.97x|15.99x|
140
+ |`(a, b)`|3.27x|2.34x|1.85x|2.05x|
141
+ |`(a:)`|37.22x|30.09x|25.57x|27.28x|
142
+ |`(a:, b:)`|5.25x|4.38x|3.80x|4.02x|
143
+ |`(a, b:)`|5.08x|4.15x|3.56x|3.78x|
144
+ |`(a, *args)`|3.17x|2.32x|1.96x|2.01x|
145
+ |`(a:, **kwargs)`|2.87x|2.42x|2.10x|2.21x|
146
+ |`(a, *args, b:, **kwargs)`|2.05x|1.76x|1.63x|1.65x|
147
147
 
148
148
  You can run benchmarks yourself with:
149
149
 
@@ -156,6 +156,24 @@ $ bundle exec ruby benchmarks.rb
156
156
  If your results differ from what's posted here,
157
157
  [let us know](https://github.com/panorama-ed/memo_wise/issues/new)!
158
158
 
159
+ ## Thread Safety
160
+
161
+ MemoWise makes the following **thread safety** guarantees on all supported Ruby
162
+ versions:
163
+
164
+ 1. **Before** a value has been memoized
165
+
166
+ * Contended calls from multiple threads...
167
+ * May each call the original method
168
+ * May return different valid results (when the method is nondeterministic,
169
+ like `rand`)
170
+ * Will memoize exactly one valid return value
171
+
172
+ 2. **After** a value has been memoized
173
+
174
+ * Contended calls from multiple threads...
175
+ * Always return the same memoized value
176
+
159
177
  ## Documentation
160
178
 
161
179
  ### Documentation is Automatically Generated
@@ -250,10 +268,12 @@ Then carry out these steps:
250
268
  - Add an entry for the upcoming version _x.y.z_
251
269
  - Add a link for this version's comparison to the bottom of `CHANGELOG.md`
252
270
  - Move content from _Unreleased_ to the upcoming version _x.y.z_
271
+ - Change _Unreleased_ section to say `- Nothing yet!`
253
272
  - Commit with title `Update CHANGELOG.md for x.y.z`
254
273
 
255
274
  2. Update `lib/memo_wise/version.rb`
256
275
  - Replace with upcoming version _x.y.z_
276
+ - Run `bundle install` to update `Gemfile.lock`
257
277
  - Commit with title `Bump version to x.y.z`
258
278
 
259
279
  3. `bundle exec rake release`
data/benchmarks/Gemfile CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- ruby ">= 2.7.4"
5
+ ruby ">= 2.7.5"
6
6
 
7
- gem "benchmark-ips", "2.9.1"
7
+ gem "benchmark-ips", "2.9.2"
8
8
 
9
9
  if RUBY_VERSION > "3"
10
10
  gem "dry-core", "0.7.1"
@@ -175,58 +175,6 @@ module MemoWise
175
175
  end
176
176
  end
177
177
 
178
- # Increment the class's method index counter, and return an index to use for
179
- # the given method name.
180
- #
181
- # @param klass [Class]
182
- # Original class on which a method is being memoized
183
- #
184
- # @param method_name [Symbol]
185
- # The name of the method being memoized
186
- #
187
- # @return [Integer]
188
- # The index within `@_memo_wise` to store the method's memoized results
189
- def self.next_index!(klass, method_name)
190
- # `@_memo_wise_indices` stores the `@_memo_wise` indices of different
191
- # method names. We only use this data structure when resetting or
192
- # presetting memoization. It looks like:
193
- # {
194
- # single_arg_method_name: 0,
195
- # other_single_arg_method_name: 1
196
- # }
197
- memo_wise_indices = klass.instance_variable_get(:@_memo_wise_indices)
198
- memo_wise_indices ||= klass.instance_variable_set(:@_memo_wise_indices, {})
199
-
200
- # When a parent and child class both use `class << self` to define
201
- # memoized class methods, the child class' singleton is not considered a
202
- # descendent of the parent class' singleton. Because we store the index
203
- # counter as a class variable that can be shared up the inheritance chain,
204
- # we want to detect this case and store it on the original class instead
205
- # of the singleton to make the counter shared correctly.
206
- counter_class = klass.singleton_class? ? original_class_from_singleton(klass) : klass
207
-
208
- # We use a class variable for tracking the index to make this work with
209
- # inheritance structures. When a parent and child class both use
210
- # MemoWise, we want the child class's index to not "reset" back to 0 and
211
- # overwrite the behavior of a memoized parent method. Using a class
212
- # variable will share the index data between parent and child classes.
213
- #
214
- # However, we don't use a class variable for `@_memo_wise_indices`
215
- # because we want to allow instance and class methods with the same name
216
- # to both be memoized, and using a class variable would share that index
217
- # data between them.
218
- index = if counter_class.class_variable_defined?(:@@_memo_wise_index_counter)
219
- counter_class.class_variable_get(:@@_memo_wise_index_counter)
220
- else
221
- 0
222
- end
223
-
224
- memo_wise_indices[method_name] = index
225
- counter_class.class_variable_set(:@@_memo_wise_index_counter, index + 1) # rubocop:disable Style/ClassVars
226
-
227
- index
228
- end
229
-
230
178
  # Convention we use for renaming the original method when we replace with
231
179
  # the memoized version in {MemoWise.memo_wise}.
232
180
  #
@@ -240,25 +188,20 @@ module MemoWise
240
188
  :"_memo_wise_original_#{method_name}"
241
189
  end
242
190
 
243
- # @param target [Class, Module]
244
- # The class to which we are prepending MemoWise to provide memoization;
245
- # the `InternalAPI` *instance* methods will refer to this `target` class.
246
- def initialize(target)
247
- @target = target
248
- end
249
-
250
- # @return [Class, Module]
251
- attr_reader :target
252
-
253
191
  # @param method_name [Symbol] the name of the memoized method
254
192
  # @return [Integer] the array index in `@_memo_wise_indices` to use to find
255
193
  # the memoization data for the given method
256
- def index(method_name)
257
- target_class.instance_variable_get(:@_memo_wise_indices)[method_name]
194
+ def self.index(target, method_name)
195
+ klass = target_class(target)
196
+ indices = klass.instance_variable_get(:@_memo_wise_indices)
197
+ indices&.[](method_name) || next_index!(klass, method_name)
258
198
  end
259
199
 
260
200
  # Returns visibility of an instance method defined on class `target`.
261
201
  #
202
+ # @param target [Class, Module]
203
+ # The class to which we are prepending MemoWise to provide memoization.
204
+ #
262
205
  # @param method_name [Symbol]
263
206
  # Name of existing *instance* method find the visibility of.
264
207
  #
@@ -269,7 +212,7 @@ module MemoWise
269
212
  # Raises `ArgumentError` unless `method_name` is a `Symbol` corresponding
270
213
  # to an existing **instance** method defined on `klass`.
271
214
  #
272
- def method_visibility(method_name)
215
+ def self.method_visibility(target, method_name)
273
216
  if target.private_method_defined?(method_name)
274
217
  :private
275
218
  elsif target.protected_method_defined?(method_name)
@@ -283,20 +226,23 @@ module MemoWise
283
226
 
284
227
  # Validates that {.memo_wise} has already been called on `method_name`.
285
228
  #
229
+ # @param target [Class, Module]
230
+ # The class to which we are prepending MemoWise to provide memoization.
231
+ #
286
232
  # @param method_name [Symbol]
287
233
  # Name of method to validate has already been setup with {.memo_wise}
288
- def validate_memo_wised!(method_name)
289
- original_name = self.class.original_memo_wised_name(method_name)
234
+ def self.validate_memo_wised!(target, method_name)
235
+ original_name = original_memo_wised_name(method_name)
290
236
 
291
- unless target_class.private_method_defined?(original_name)
237
+ unless target_class(target).private_method_defined?(original_name)
292
238
  raise ArgumentError, "#{method_name} is not a memo_wised method"
293
239
  end
294
240
  end
295
241
 
296
- private
297
-
242
+ # @param target [Class, Module]
243
+ # The class to which we are prepending MemoWise to provide memoization.
298
244
  # @return [Class] where we look for method definitions
299
- def target_class
245
+ def self.target_class(target)
300
246
  if target.instance_of?(Class)
301
247
  # A class's methods are defined in its singleton class
302
248
  target.singleton_class
@@ -305,5 +251,36 @@ module MemoWise
305
251
  target.class
306
252
  end
307
253
  end
254
+ private_class_method :target_class
255
+
256
+ # Increment the class's method index counter, and return an index to use for
257
+ # the given method name.
258
+ #
259
+ # @param klass [Class]
260
+ # Original class on which a method is being memoized
261
+ #
262
+ # @param method_name [Symbol]
263
+ # The name of the method being memoized
264
+ #
265
+ # @return [Integer]
266
+ # The index within `@_memo_wise` to store the method's memoized results
267
+ def self.next_index!(klass, method_name)
268
+ # `@_memo_wise_indices` stores the `@_memo_wise` indices of different
269
+ # method names. We only use this data structure when resetting or
270
+ # presetting memoization. It looks like:
271
+ # {
272
+ # single_arg_method_name: 0,
273
+ # other_single_arg_method_name: 1
274
+ # }
275
+ memo_wise_indices = klass.instance_variable_get(:@_memo_wise_indices)
276
+ memo_wise_indices ||= klass.instance_variable_set(:@_memo_wise_indices, {})
277
+
278
+ index = klass.instance_variable_get(:@_memo_wise_index_counter) || 0
279
+ memo_wise_indices[method_name] = index
280
+ klass.instance_variable_set(:@_memo_wise_index_counter, index + 1)
281
+
282
+ index
283
+ end
284
+ private_class_method :next_index!
308
285
  end
309
286
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MemoWise
4
- VERSION = "1.3.0"
4
+ VERSION = "1.4.0"
5
5
  end
data/lib/memo_wise.rb CHANGED
@@ -169,8 +169,7 @@ module MemoWise
169
169
 
170
170
  raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol)
171
171
 
172
- api = MemoWise::InternalAPI.new(klass)
173
- visibility = api.method_visibility(method_name)
172
+ visibility = MemoWise::InternalAPI.method_visibility(klass, method_name)
174
173
  original_memo_wised_name = MemoWise::InternalAPI.original_memo_wised_name(method_name)
175
174
  method = klass.instance_method(method_name)
176
175
 
@@ -178,37 +177,75 @@ module MemoWise
178
177
  klass.send(:private, original_memo_wised_name)
179
178
 
180
179
  method_arguments = MemoWise::InternalAPI.method_arguments(method)
181
- index = MemoWise::InternalAPI.next_index!(klass, method_name)
182
180
 
183
181
  case method_arguments
184
182
  when MemoWise::InternalAPI::NONE
185
183
  # Zero-arg methods can use simpler/more performant logic because the
186
184
  # hash key is just the method name.
187
- klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
188
- def #{method_name}
189
- if @_memo_wise_sentinels[#{index}]
190
- @_memo_wise[#{index}]
191
- else
192
- ret = @_memo_wise[#{index}] = #{original_memo_wised_name}
193
- @_memo_wise_sentinels[#{index}] = true
194
- ret
185
+ klass.send(:define_method, method_name) do # Ruby 2.4's `define_method` is private in some cases
186
+ index = MemoWise::InternalAPI.index(self, method_name)
187
+ klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
188
+ def #{method_name}
189
+ if @_memo_wise_sentinels[#{index}]
190
+ @_memo_wise[#{index}]
191
+ else
192
+ ret = @_memo_wise[#{index}] = #{original_memo_wised_name}
193
+ @_memo_wise_sentinels[#{index}] = true
194
+ ret
195
+ end
195
196
  end
196
- end
197
- HEREDOC
197
+ HEREDOC
198
+
199
+ klass.send(visibility, method_name)
200
+ send(method_name)
201
+ end
198
202
  when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL, MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD
199
203
  key = method.parameters.first.last
204
+ # NOTE: Ruby 2.6 and below, and TruffleRuby 3.0, break when we use
205
+ # `define_method(...) do |*args, **kwargs|`. Instead we must use the
206
+ # simpler `|*args|` pattern. We can't just do this always though
207
+ # because Ruby 2.7 and above require `|*args, **kwargs|` to work
208
+ # correctly.
209
+ # See: https://blog.saeloun.com/2019/10/07/ruby-2-7-keyword-arguments-redesign.html#ruby-26
210
+ # :nocov:
211
+ if RUBY_VERSION < "2.7" || RUBY_ENGINE == "truffleruby"
212
+ klass.send(:define_method, method_name) do |*args| # Ruby 2.4's `define_method` is private in some cases
213
+ index = MemoWise::InternalAPI.index(self, method_name)
214
+ klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
215
+ def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
216
+ _memo_wise_hash = (@_memo_wise[#{index}] ||= {})
217
+ _memo_wise_output = _memo_wise_hash[#{key}]
218
+ if _memo_wise_output || _memo_wise_hash.key?(#{key})
219
+ _memo_wise_output
220
+ else
221
+ _memo_wise_hash[#{key}] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
222
+ end
223
+ end
224
+ HEREDOC
200
225
 
201
- klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
202
- def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
203
- _memo_wise_hash = (@_memo_wise[#{index}] ||= {})
204
- _memo_wise_output = _memo_wise_hash[#{key}]
205
- if _memo_wise_output || _memo_wise_hash.key?(#{key})
206
- _memo_wise_output
207
- else
208
- _memo_wise_hash[#{key}] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
209
- end
226
+ klass.send(visibility, method_name)
227
+ send(method_name, *args)
210
228
  end
211
- HEREDOC
229
+ # :nocov:
230
+ else
231
+ klass.define_method(method_name) do |*args, **kwargs|
232
+ index = MemoWise::InternalAPI.index(self, method_name)
233
+ klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
234
+ def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
235
+ _memo_wise_hash = (@_memo_wise[#{index}] ||= {})
236
+ _memo_wise_output = _memo_wise_hash[#{key}]
237
+ if _memo_wise_output || _memo_wise_hash.key?(#{key})
238
+ _memo_wise_output
239
+ else
240
+ _memo_wise_hash[#{key}] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
241
+ end
242
+ end
243
+ HEREDOC
244
+
245
+ klass.send(visibility, method_name)
246
+ send(method_name, *args, **kwargs)
247
+ end
248
+ end
212
249
  # MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT,
213
250
  # MemoWise::InternalAPI::DOUBLE_SPLAT, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
214
251
  else
@@ -224,18 +261,54 @@ module MemoWise
224
261
  # consistent performance. In general, this should still be faster for
225
262
  # truthy results because `Hash#[]` generally performs hash lookups
226
263
  # faster than `Hash#fetch`.
227
- klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
228
- def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
229
- _memo_wise_hash = (@_memo_wise[#{index}] ||= {})
230
- _memo_wise_key = #{MemoWise::InternalAPI.key_str(method)}
231
- _memo_wise_output = _memo_wise_hash[_memo_wise_key]
232
- if _memo_wise_output || _memo_wise_hash.key?(_memo_wise_key)
233
- _memo_wise_output
234
- else
235
- _memo_wise_hash[_memo_wise_key] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
236
- end
264
+ #
265
+ # NOTE: Ruby 2.6 and below, and TruffleRuby 3.0, break when we use
266
+ # `define_method(...) do |*args, **kwargs|`. Instead we must use the
267
+ # simpler `|*args|` pattern. We can't just do this always though
268
+ # because Ruby 2.7 and above require `|*args, **kwargs|` to work
269
+ # correctly.
270
+ # See: https://blog.saeloun.com/2019/10/07/ruby-2-7-keyword-arguments-redesign.html#ruby-26
271
+ # :nocov:
272
+ if RUBY_VERSION < "2.7" || RUBY_ENGINE == "truffleruby"
273
+ klass.send(:define_method, method_name) do |*args| # Ruby 2.4's `define_method` is private in some cases
274
+ index = MemoWise::InternalAPI.index(self, method_name)
275
+ klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
276
+ def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
277
+ _memo_wise_hash = (@_memo_wise[#{index}] ||= {})
278
+ _memo_wise_key = #{MemoWise::InternalAPI.key_str(method)}
279
+ _memo_wise_output = _memo_wise_hash[_memo_wise_key]
280
+ if _memo_wise_output || _memo_wise_hash.key?(_memo_wise_key)
281
+ _memo_wise_output
282
+ else
283
+ _memo_wise_hash[_memo_wise_key] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
284
+ end
285
+ end
286
+ HEREDOC
287
+
288
+ klass.send(visibility, method_name)
289
+ send(method_name, *args)
237
290
  end
238
- HEREDOC
291
+ # :nocov:
292
+ else # Ruby 2.7 and above break with (*args)
293
+ klass.define_method(method_name) do |*args, **kwargs|
294
+ index = MemoWise::InternalAPI.index(self, method_name)
295
+ klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
296
+ def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
297
+ _memo_wise_hash = (@_memo_wise[#{index}] ||= {})
298
+ _memo_wise_key = #{MemoWise::InternalAPI.key_str(method)}
299
+ _memo_wise_output = _memo_wise_hash[_memo_wise_key]
300
+ if _memo_wise_output || _memo_wise_hash.key?(_memo_wise_key)
301
+ _memo_wise_output
302
+ else
303
+ _memo_wise_hash[_memo_wise_key] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
304
+ end
305
+ end
306
+ HEREDOC
307
+
308
+ klass.send(visibility, method_name)
309
+ send(method_name, *args, **kwargs)
310
+ end
311
+ end
239
312
  end
240
313
 
241
314
  klass.send(visibility, method_name)
@@ -440,12 +513,11 @@ module MemoWise
440
513
  def preset_memo_wise(method_name, *args, **kwargs)
441
514
  raise ArgumentError, "Pass a block as the value to preset for #{method_name}, #{args}" unless block_given?
442
515
 
443
- api = MemoWise::InternalAPI.new(self)
444
- api.validate_memo_wised!(method_name)
516
+ MemoWise::InternalAPI.validate_memo_wised!(self, method_name)
445
517
 
446
- method = method(method_name)
518
+ method = method(MemoWise::InternalAPI.original_memo_wised_name(method_name))
447
519
  method_arguments = MemoWise::InternalAPI.method_arguments(method)
448
- index = api.index(method_name)
520
+ index = MemoWise::InternalAPI.index(self, method_name)
449
521
 
450
522
  if method_arguments == MemoWise::InternalAPI::NONE
451
523
  @_memo_wise_sentinels[index] = true
@@ -548,12 +620,11 @@ module MemoWise
548
620
  raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol)
549
621
  raise ArgumentError, "#{method_name} is not a defined method" unless respond_to?(method_name, true)
550
622
 
551
- api = MemoWise::InternalAPI.new(self)
552
- api.validate_memo_wised!(method_name)
623
+ MemoWise::InternalAPI.validate_memo_wised!(self, method_name)
553
624
 
554
- method = method(method_name)
625
+ method = method(MemoWise::InternalAPI.original_memo_wised_name(method_name))
555
626
  method_arguments = MemoWise::InternalAPI.method_arguments(method)
556
- index = api.index(method_name)
627
+ index = MemoWise::InternalAPI.index(self, method_name)
557
628
 
558
629
  case method_arguments
559
630
  when MemoWise::InternalAPI::NONE
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.3.0
4
+ version: 1.4.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-11-23 00:00:00.000000000 Z
14
+ date: 2021-12-10 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.22
73
+ rubygems_version: 3.2.3
73
74
  signing_key:
74
75
  specification_version: 4
75
76
  summary: The wise choice for Ruby memoization