memo_wise 1.1.0 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/PULL_REQUEST_TEMPLATE.md +2 -2
- data/.github/workflows/main.yml +1 -1
- data/.rubocop.yml +13 -1
- data/CHANGELOG.md +46 -6
- data/Gemfile.lock +29 -29
- data/README.md +72 -39
- data/benchmarks/Gemfile +2 -2
- data/benchmarks/benchmarks.rb +103 -179
- data/lib/memo_wise/internal_api.rb +110 -202
- data/lib/memo_wise/version.rb +1 -1
- data/lib/memo_wise.rb +95 -137
- data/memo_wise.gemspec +1 -0
- metadata +4 -3
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
|
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
|
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
|
-
|
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)
|
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)
|
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
|
-
|
160
|
-
|
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
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
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
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
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
|
-
|
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
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
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
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
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
|
-
|
252
|
-
|
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
|
459
|
-
|
460
|
-
|
461
|
-
|
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
|
-
|
464
|
-
|
419
|
+
method = method(MemoWise::InternalAPI.original_memo_wised_name(method_name))
|
420
|
+
method_arguments = MemoWise::InternalAPI.method_arguments(method)
|
465
421
|
|
466
|
-
if
|
422
|
+
if method_arguments == MemoWise::InternalAPI::NONE
|
467
423
|
@_memo_wise[method_name] = yield
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
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)
|
509
|
+
def reset_memo_wise(method_name = nil, *args, **kwargs)
|
547
510
|
if method_name.nil?
|
548
|
-
unless args.empty?
|
549
|
-
|
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
|
-
|
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
|
-
|
566
|
-
raise ArgumentError, "#{method_name} is not a defined method"
|
567
|
-
end
|
521
|
+
MemoWise::InternalAPI.validate_memo_wised!(self, method_name)
|
568
522
|
|
569
|
-
|
570
|
-
|
523
|
+
method = method(MemoWise::InternalAPI.original_memo_wised_name(method_name))
|
524
|
+
method_arguments = MemoWise::InternalAPI.method_arguments(method)
|
571
525
|
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
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.
|
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-
|
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.
|
73
|
+
rubygems_version: 3.2.22
|
73
74
|
signing_key:
|
74
75
|
specification_version: 4
|
75
76
|
summary: The wise choice for Ruby memoization
|