memo_wise 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
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