memo_wise 1.0.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/memo_wise.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+
3
5
  require "memo_wise/internal_api"
4
6
  require "memo_wise/version"
5
7
 
@@ -23,7 +25,7 @@ require "memo_wise/version"
23
25
  # - {.memo_wise} for API and usage examples.
24
26
  # - {file:README.md} for general project information.
25
27
  #
26
- module MemoWise # rubocop:disable Metrics/ModuleLength
28
+ module MemoWise
27
29
  # Constructor to set up memoization state before
28
30
  # [calling the original](https://medium.com/@jeremy_96642/ruby-method-auditing-using-module-prepend-4f4e69aacd95)
29
31
  # constructor.
@@ -54,7 +56,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
54
56
  # :nocov:
55
57
  all_args = RUBY_VERSION < "2.7" ? "*" : "..."
56
58
  # :nocov:
57
- class_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
59
+ class_eval <<~HEREDOC, __FILE__, __LINE__ + 1
58
60
  # On Ruby 2.7 or greater:
59
61
  #
60
62
  # def initialize(...)
@@ -73,7 +75,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
73
75
  MemoWise::InternalAPI.create_memo_wise_state!(self)
74
76
  super
75
77
  end
76
- END_OF_METHOD
78
+ HEREDOC
77
79
 
78
80
  # @private
79
81
  #
@@ -89,7 +91,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
89
91
  # prepend MemoWise
90
92
  # end
91
93
  #
92
- def self.prepended(target) # rubocop:disable Metrics/PerceivedComplexity
94
+ def self.prepended(target)
93
95
  class << target
94
96
  # Allocator to set up memoization state before
95
97
  # [calling the original](https://medium.com/@jeremy_96642/ruby-method-auditing-using-module-prepend-4f4e69aacd95)
@@ -109,7 +111,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
109
111
  end
110
112
 
111
113
  # NOTE: See YARD docs for {.memo_wise} directly below this method!
112
- def memo_wise(method_name_or_hash) # rubocop:disable Metrics/PerceivedComplexity
114
+ def memo_wise(method_name_or_hash)
113
115
  klass = self
114
116
  case method_name_or_hash
115
117
  when Symbol
@@ -120,6 +122,23 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
120
122
  MemoWise::InternalAPI.original_class_from_singleton(klass)
121
123
  )
122
124
  end
125
+
126
+ # Ensures a module extended by another class/module still works
127
+ # e.g. rails `ClassMethods` module
128
+ if klass.is_a?(Module) && !klass.is_a?(Class)
129
+ # Using `extended` without `included` & `prepended`
130
+ # As a call to `create_memo_wise_state!` is already included in
131
+ # `.allocate`/`#initialize`
132
+ #
133
+ # But a module/class extending another module with memo_wise
134
+ # would not call `.allocate`/`#initialize` before calling methods
135
+ #
136
+ # On method call `@_memo_wise` would still be `nil`
137
+ # causing error when fetching cache from `@_memo_wise`
138
+ def klass.extended(base)
139
+ MemoWise::InternalAPI.create_memo_wise_state!(base)
140
+ end
141
+ end
123
142
  when Hash
124
143
  unless method_name_or_hash.keys == [:self]
125
144
  raise ArgumentError,
@@ -137,92 +156,159 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
137
156
  klass = klass.singleton_class
138
157
  end
139
158
 
140
- unless method_name.is_a?(Symbol)
141
- 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
142
168
  end
143
169
 
144
- api = MemoWise::InternalAPI.new(klass)
145
- visibility = api.method_visibility(method_name)
146
- original_memo_wised_name =
147
- 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)
148
174
  method = klass.instance_method(method_name)
149
175
 
150
176
  klass.send(:alias_method, original_memo_wised_name, method_name)
151
177
  klass.send(:private, original_memo_wised_name)
152
178
 
153
- # Zero-arg methods can use simpler/more performant logic because the
154
- # hash key is just the method name.
155
- if method.arity.zero?
156
- klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
157
- # def foo
158
- # @_memo_wise.fetch(:foo) do
159
- # @_memo_wise[:foo] = _memo_wise_original_foo
160
- # end
161
- # end
162
-
163
- def #{method_name}
164
- @_memo_wise.fetch(:#{method_name}) do
165
- @_memo_wise[:#{method_name}] = #{original_memo_wised_name}
179
+ method_arguments = MemoWise::InternalAPI.method_arguments(method)
180
+
181
+ case method_arguments
182
+ when MemoWise::InternalAPI::NONE
183
+ # Zero-arg methods can use simpler/more performant logic because the
184
+ # hash key is just the method name.
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
166
196
  end
197
+ HEREDOC
198
+
199
+ klass.send(visibility, method_name)
200
+ send(method_name)
201
+ end
202
+ when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL, MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD
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
225
+
226
+ klass.send(visibility, method_name)
227
+ send(method_name, *args)
167
228
  end
168
- END_OF_METHOD
169
- else
170
- if MemoWise::InternalAPI.has_only_required_args?(method)
171
- args_str = method.parameters.map do |type, name|
172
- "#{name}#{':' if type == :keyreq}"
173
- end.join(", ")
174
- args_str = "(#{args_str})"
175
- call_str = method.parameters.map do |type, name|
176
- type == :req ? name : "#{name}: #{name}"
177
- end.join(", ")
178
- call_str = "(#{call_str})"
179
- fetch_key = method.parameters.map(&:last)
180
- fetch_key = if fetch_key.size > 1
181
- "[#{fetch_key.join(', ')}].freeze"
182
- else
183
- fetch_key.first.to_s
184
- end
229
+ # :nocov:
185
230
  else
186
- # If our method has arguments, we need to separate out our handling
187
- # of normal args vs. keyword args due to the changes in Ruby 3.
188
- # See: <link>
189
- # By only including logic for *args, **kwargs when they are used in
190
- # the method, we can avoid allocating unnecessary arrays and hashes.
191
- has_arg = MemoWise::InternalAPI.has_arg?(method)
192
-
193
- if has_arg && MemoWise::InternalAPI.has_kwarg?(method)
194
- args_str = "(*args, **kwargs)"
195
- fetch_key = "[args, kwargs].freeze"
196
- elsif has_arg
197
- args_str = "(*args)"
198
- fetch_key = "args"
199
- else
200
- args_str = "(**kwargs)"
201
- fetch_key = "kwargs"
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)
202
247
  end
203
248
  end
204
-
205
- # Note that we don't need to freeze args before using it as a hash key
206
- # because Ruby always copies argument arrays when splatted.
207
- klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
208
- # def foo(*args, **kwargs)
209
- # hash = @_memo_wise.fetch(:foo) do
210
- # @_memo_wise[:foo] = {}
211
- # end
212
- # hash.fetch([args, kwargs].freeze) do
213
- # hash[[args, kwargs].freeze] = _memo_wise_original_foo(*args, **kwargs)
214
- # end
215
- # end
216
-
217
- def #{method_name}#{args_str}
218
- hash = @_memo_wise.fetch(:#{method_name}) do
219
- @_memo_wise[:#{method_name}] = {}
220
- end
221
- hash.fetch(#{fetch_key}) do
222
- hash[#{fetch_key}] = #{original_memo_wised_name}#{call_str || args_str}
223
- end
249
+ # MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT,
250
+ # MemoWise::InternalAPI::DOUBLE_SPLAT, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
251
+ else
252
+ # NOTE: When benchmarking this implementation against something like:
253
+ #
254
+ # @_memo_wise.fetch(key) do
255
+ # ...
256
+ # end
257
+ #
258
+ # this implementation may sometimes perform worse than the above. This
259
+ # is because this case uses a more complex hash key (see
260
+ # `MemoWise::InternalAPI.key_str`), and hashing that key has less
261
+ # consistent performance. In general, this should still be faster for
262
+ # truthy results because `Hash#[]` generally performs hash lookups
263
+ # faster than `Hash#fetch`.
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)
290
+ end
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)
224
310
  end
225
- END_OF_METHOD
311
+ end
226
312
  end
227
313
 
228
314
  klass.send(visibility, method_name)
@@ -248,8 +334,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
248
334
  # (`...` or `*args, **kwargs`), making reflection on method parameters
249
335
  # useless without this.
250
336
  def target.instance_method(symbol)
251
- # TODO: Extract this method naming pattern
252
- original_memo_wised_name = :"_memo_wise_original_#{symbol}"
337
+ original_memo_wised_name = MemoWise::InternalAPI.original_memo_wised_name(symbol)
253
338
 
254
339
  super.tap do |curr_method|
255
340
  # Start with calling the original `instance_method` on `symbol`,
@@ -341,7 +426,6 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
341
426
  # Example.method_called_times #=> nil
342
427
  ##
343
428
 
344
- # rubocop:disable Layout/LineLength
345
429
  ##
346
430
  # @!method self.reset_memo_wise(method_name = nil, *args, **kwargs)
347
431
  # Implementation of {#reset_memo_wise} for class methods.
@@ -375,7 +459,6 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
375
459
  #
376
460
  # Example.reset_memo_wise # reset "all methods" mode
377
461
  ##
378
- # rubocop:enable Layout/LineLength
379
462
 
380
463
  # Presets the memoized result for the given method to the result of the given
381
464
  # block.
@@ -428,21 +511,34 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
428
511
  # ex.method_called_times #=> nil
429
512
  #
430
513
  def preset_memo_wise(method_name, *args, **kwargs)
431
- unless block_given?
432
- raise ArgumentError,
433
- "Pass a block as the value to preset for #{method_name}, #{args}"
514
+ raise ArgumentError, "Pass a block as the value to preset for #{method_name}, #{args}" unless block_given?
515
+
516
+ MemoWise::InternalAPI.validate_memo_wised!(self, method_name)
517
+
518
+ method = method(MemoWise::InternalAPI.original_memo_wised_name(method_name))
519
+ method_arguments = MemoWise::InternalAPI.method_arguments(method)
520
+ index = MemoWise::InternalAPI.index(self, method_name)
521
+
522
+ if method_arguments == MemoWise::InternalAPI::NONE
523
+ @_memo_wise_sentinels[index] = true
524
+ @_memo_wise[index] = yield
525
+ return
434
526
  end
435
527
 
436
- api = MemoWise::InternalAPI.new(self)
437
- api.validate_memo_wised!(method_name)
528
+ hash = (@_memo_wise[index] ||= {})
438
529
 
439
- if method(method_name).arity.zero?
440
- @_memo_wise[method_name] = yield
441
- else
442
- hash = @_memo_wise.fetch(method_name) do
443
- @_memo_wise[method_name] = {}
530
+ case method_arguments
531
+ when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL then hash[args.first] = yield
532
+ when MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD then hash[kwargs.first.last] = yield
533
+ when MemoWise::InternalAPI::SPLAT then hash[args] = yield
534
+ when MemoWise::InternalAPI::DOUBLE_SPLAT then hash[kwargs] = yield
535
+ when MemoWise::InternalAPI::MULTIPLE_REQUIRED
536
+ key = method.parameters.map.with_index do |(type, name), idx|
537
+ type == :req ? args[idx] : kwargs[name]
444
538
  end
445
- hash[api.fetch_key(method_name, *args, **kwargs)] = yield
539
+ hash[key] = yield
540
+ else # MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
541
+ hash[[args, kwargs]] = yield
446
542
  end
447
543
  end
448
544
 
@@ -513,33 +609,64 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
513
609
  #
514
610
  def reset_memo_wise(method_name = nil, *args, **kwargs)
515
611
  if method_name.nil?
516
- unless args.empty?
517
- raise ArgumentError, "Provided args when method_name = nil"
518
- end
612
+ raise ArgumentError, "Provided args when method_name = nil" unless args.empty?
613
+ raise ArgumentError, "Provided kwargs when method_name = nil" unless kwargs.empty?
519
614
 
520
- unless kwargs.empty?
521
- raise ArgumentError, "Provided kwargs when method_name = nil"
522
- end
523
-
524
- return @_memo_wise.clear
615
+ @_memo_wise.clear
616
+ @_memo_wise_sentinels.clear
617
+ return
525
618
  end
526
619
 
527
- unless method_name.is_a?(Symbol)
528
- raise ArgumentError, "#{method_name.inspect} must be a Symbol"
529
- end
620
+ raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol)
621
+ raise ArgumentError, "#{method_name} is not a defined method" unless respond_to?(method_name, true)
530
622
 
531
- unless respond_to?(method_name, true)
532
- raise ArgumentError, "#{method_name} is not a defined method"
533
- end
623
+ MemoWise::InternalAPI.validate_memo_wised!(self, method_name)
534
624
 
535
- api = MemoWise::InternalAPI.new(self)
536
- api.validate_memo_wised!(method_name)
625
+ method = method(MemoWise::InternalAPI.original_memo_wised_name(method_name))
626
+ method_arguments = MemoWise::InternalAPI.method_arguments(method)
627
+ index = MemoWise::InternalAPI.index(self, method_name)
537
628
 
538
- if args.empty? && kwargs.empty?
539
- @_memo_wise.delete(method_name)
540
- else
541
- @_memo_wise[method_name]&.
542
- delete(api.fetch_key(method_name, *args, **kwargs))
629
+ case method_arguments
630
+ when MemoWise::InternalAPI::NONE
631
+ @_memo_wise_sentinels[index] = nil
632
+ @_memo_wise[index] = nil
633
+ when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL
634
+ if args.empty?
635
+ @_memo_wise[index]&.clear
636
+ else
637
+ @_memo_wise[index]&.delete(args.first)
638
+ end
639
+ when MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD
640
+ if kwargs.empty?
641
+ @_memo_wise[index]&.clear
642
+ else
643
+ @_memo_wise[index]&.delete(kwargs.first.last)
644
+ end
645
+ when MemoWise::InternalAPI::SPLAT
646
+ if args.empty?
647
+ @_memo_wise[index]&.clear
648
+ else
649
+ @_memo_wise[index]&.delete(args)
650
+ end
651
+ when MemoWise::InternalAPI::DOUBLE_SPLAT
652
+ if kwargs.empty?
653
+ @_memo_wise[index]&.clear
654
+ else
655
+ @_memo_wise[index]&.delete(kwargs)
656
+ end
657
+ else # MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
658
+ if args.empty? && kwargs.empty?
659
+ @_memo_wise[index]&.clear
660
+ else
661
+ key = if method_arguments == MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
662
+ [args, kwargs]
663
+ else
664
+ method.parameters.map.with_index do |(type, name), i|
665
+ type == :req ? args[i] : kwargs[name] # rubocop:disable Metrics/BlockNesting
666
+ end
667
+ end
668
+ @_memo_wise[index]&.delete(key)
669
+ end
543
670
  end
544
671
  end
545
672
  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.0.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-06-24 00:00:00.000000000 Z
14
+ date: 2021-12-10 00:00:00.000000000 Z
15
15
  dependencies: []
16
16
  description:
17
17
  email:
@@ -39,9 +39,7 @@ files:
39
39
  - LICENSE.txt
40
40
  - README.md
41
41
  - Rakefile
42
- - benchmarks/.ruby-version
43
42
  - benchmarks/Gemfile
44
- - benchmarks/Gemfile.lock
45
43
  - benchmarks/benchmarks.rb
46
44
  - bin/console
47
45
  - bin/setup
@@ -54,6 +52,7 @@ homepage: https://github.com/panorama-ed/memo_wise
54
52
  licenses:
55
53
  - MIT
56
54
  metadata:
55
+ rubygems_mfa_required: 'true'
57
56
  changelog_uri: https://github.com/panorama-ed/memo_wise/blob/main/CHANGELOG.md
58
57
  source_code_uri: https://github.com/panorama-ed/memo_wise
59
58
  post_install_message:
@@ -1 +0,0 @@
1
- 3.0.0
@@ -1,29 +0,0 @@
1
- PATH
2
- remote: ..
3
- specs:
4
- memo_wise (0.4.0)
5
-
6
- GEM
7
- remote: https://rubygems.org/
8
- specs:
9
- benchmark-ips (2.9.1)
10
- memoist (0.16.2)
11
- memoized (1.0.2)
12
- memoizer (1.0.3)
13
-
14
- PLATFORMS
15
- x86_64-darwin-19
16
- x86_64-linux
17
-
18
- DEPENDENCIES
19
- benchmark-ips (= 2.9.1)
20
- memo_wise!
21
- memoist (= 0.16.2)
22
- memoized (= 1.0.2)
23
- memoizer (= 1.0.3)
24
-
25
- RUBY VERSION
26
- ruby 3.0.0p0
27
-
28
- BUNDLED WITH
29
- 2.2.3