memo_wise 0.4.0 → 1.0.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: 634bee32d07ccd97dc7670c67521a4091ebc0df0deae1479cc054aaa7a246e6c
4
- data.tar.gz: 59523369b3373389481388962039486b4f0f5cf002d24091ad2244048f100bbc
3
+ metadata.gz: e4c330faa7d0718ac3b4873dae8fb1933949481747f639fd6fe050724a2ce8dd
4
+ data.tar.gz: 52777a008d6b5b41a37cec36f1b9fe0198b85df97fb11329d73bac71ad22b600
5
5
  SHA512:
6
- metadata.gz: 3896bf8b001ad52afda6b14c5170d902d85a88c9faaa60d6c9097a01045e0ef90a379c130834b2a2fcbec356b51346f975fc96493dcb3d3c04943200eb7274a3
7
- data.tar.gz: 72718456d986ed21d593a6a99117ee74a0a385f94ba0b1a00fadb38cbad977202cea5d8e32325bdaa2e5eb273808a52ca9d62997cd927a6a98ffd31174bdcc6f
6
+ metadata.gz: 17a0e939940e73826fbca1876317bcd772e1cd07f16383a9d3e9c973c2545b4b8539c42110de98d8a8c8c4c4a4aa6a666b29cc40c6351001b2b9a7c6964918a3
7
+ data.tar.gz: fd4a09fe2dab6dbae43fae753c15530982d1cc26c0ef7b47cdff40f0bff603d7c7c9d0c7d0509b7eb98c0e15b2bd2ca7c005900a00d97201e44a9fd49603d4b0
data/.dokaz ADDED
@@ -0,0 +1,2 @@
1
+ README.md
2
+ --require ./spec/dokaz_helpers.rb
@@ -0,0 +1,20 @@
1
+ version: 2
2
+
3
+ updates:
4
+ # Maintain dependencies for Ruby's Bundler
5
+ - package-ecosystem: bundler
6
+ directory: "/"
7
+ schedule:
8
+ interval: daily
9
+
10
+ # Maintain dependencies for Ruby's Bundler (for Benchmarks!)
11
+ - package-ecosystem: bundler
12
+ directory: "/benchmarks"
13
+ schedule:
14
+ interval: daily
15
+
16
+ # Maintain dependencies for GitHub Actions
17
+ - package-ecosystem: "github-actions"
18
+ directory: "/"
19
+ schedule:
20
+ interval: "daily"
@@ -15,26 +15,34 @@ jobs:
15
15
  ruby: [jruby, 2.4, 2.5, 2.6, 2.7, 3.0]
16
16
  runs-on: ubuntu-latest
17
17
  steps:
18
- - uses: actions/checkout@main
18
+ - uses: actions/checkout@v2
19
+
20
+ # Conditionally configure bundler via environment variables as advised
21
+ # * https://github.com/ruby/setup-ruby#bundle-config
22
+ - name: Set bundler environment variables
23
+ run: |
24
+ echo "BUNDLE_WITHOUT=checks:docs" >> $GITHUB_ENV
25
+ if: matrix.ruby != 3.0
26
+
27
+ # Use 'bundler-cache: true' instead of actions/cache as advised:
28
+ # * https://github.com/actions/cache/blob/main/examples.md#ruby---bundler
19
29
  - uses: ruby/setup-ruby@v1
20
30
  with:
21
31
  ruby-version: ${{ matrix.ruby }}
22
- - uses: actions/cache@v1
23
- with:
24
- path: vendor/bundle
25
- key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
26
- restore-keys: |
27
- ${{ runner.os }}-gems-
28
- - run: bundle config path vendor/bundle
29
- - run: bundle config set without 'checks:docs'
30
- if: matrix.ruby != 3.0
31
- - run: bundle install --jobs 4 --retry 3
32
+ bundler-cache: true
33
+
32
34
  - run: bundle exec rspec
35
+
33
36
  - run: bundle exec rubocop
34
37
  if: matrix.ruby == 3.0
35
- - run: bundle exec yard doctest
36
- if: matrix.ruby == 3.0
38
+
37
39
  - run: |
40
+ bundle exec yard doctest
41
+ bundle exec dokaz
42
+ if: matrix.ruby == 3.0
43
+
44
+ - name: Run benchmarks on Ruby 2.7 or 3.0
45
+ run: |
38
46
  BUNDLE_GEMFILE=benchmarks/Gemfile bundle install --jobs 4 --retry 3
39
47
  BUNDLE_GEMFILE=benchmarks/Gemfile bundle exec ruby benchmarks/benchmarks.rb
40
48
  if: matrix.ruby == '2.7' || matrix.ruby == '3.0'
data/CHANGELOG.md CHANGED
@@ -8,6 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
  ## [Unreleased]
9
9
  (nothing yet!)
10
10
 
11
+ ## [1.0.0] - 2021-06-24
12
+ ### Added
13
+ - Support for `.preset_memo_wise` on class methods
14
+ - Support for `.reset_memo_wise` on class methods
15
+ ### Updated
16
+ - Improved performance for common cases by reducing array allocations
17
+
11
18
  ## [0.4.0] - 2021-04-30
12
19
  ### Added
13
20
  - Documentation of confusing module test behavior
@@ -54,7 +61,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
54
61
  - Panolint
55
62
  - Dependabot setup
56
63
 
57
- [Unreleased]: https://github.com/panorama-ed/memo_wise/compare/v0.4.0...HEAD
64
+ [Unreleased]: https://github.com/panorama-ed/memo_wise/compare/v1.0.0...HEAD
65
+ [1.0.0]: https://github.com/panorama-ed/memo_wise/compare/v0.4.0...v1.0.0
58
66
  [0.4.0]: https://github.com/panorama-ed/memo_wise/compare/v0.3.0...v0.4.0
59
67
  [0.3.0]: https://github.com/panorama-ed/memo_wise/compare/v0.2.0...v0.3.0
60
68
  [0.2.0]: https://github.com/panorama-ed/memo_wise/compare/v0.1.2...v0.2.0
data/Gemfile CHANGED
@@ -19,6 +19,7 @@ end
19
19
 
20
20
  # Excluded from CI except on latest MRI Ruby, to reduce compatibility burden
21
21
  group :docs do
22
+ gem "dokaz"
22
23
  gem "redcarpet", "~> 3.5"
23
24
  gem "yard", "~> 0.9"
24
25
  gem "yard-doctest", "~> 0.1"
data/Gemfile.lock CHANGED
@@ -1,6 +1,6 @@
1
1
  GIT
2
2
  remote: https://github.com/panorama-ed/panolint.git
3
- revision: e2f76aa2482f02e68826eb8643a41998a09fc7ab
3
+ revision: 6f38f73dafdc25370ecc367dc87d7e6340ae664d
4
4
  branch: main
5
5
  specs:
6
6
  panolint (0.1.3)
@@ -14,28 +14,33 @@ GIT
14
14
  PATH
15
15
  remote: .
16
16
  specs:
17
- memo_wise (0.4.0)
17
+ memo_wise (1.0.0)
18
18
 
19
19
  GEM
20
20
  remote: https://rubygems.org/
21
21
  specs:
22
- activesupport (5.2.5)
22
+ activesupport (5.2.6)
23
23
  concurrent-ruby (~> 1.0, >= 1.0.2)
24
24
  i18n (>= 0.7, < 2)
25
25
  minitest (~> 5.1)
26
26
  tzinfo (~> 1.1)
27
+ ansi (1.5.0)
27
28
  ast (2.4.2)
28
- brakeman (5.0.0)
29
+ brakeman (5.0.4)
29
30
  codecov (0.5.2)
30
31
  simplecov (>= 0.15, < 0.22)
31
- concurrent-ruby (1.1.8)
32
+ concurrent-ruby (1.1.9)
32
33
  diff-lcs (1.4.4)
33
34
  docile (1.3.5)
35
+ dokaz (0.0.4)
36
+ ansi
37
+ rouge
38
+ slop (~> 3)
34
39
  i18n (1.8.10)
35
40
  concurrent-ruby (~> 1.0)
36
41
  minitest (5.14.4)
37
42
  parallel (1.20.1)
38
- parser (3.0.1.0)
43
+ parser (3.0.1.1)
39
44
  ast (~> 2.4.1)
40
45
  rack (2.2.3)
41
46
  rainbow (3.0.0)
@@ -43,6 +48,7 @@ GEM
43
48
  redcarpet (3.5.1)
44
49
  regexp_parser (2.1.1)
45
50
  rexml (3.2.5)
51
+ rouge (3.26.0)
46
52
  rspec (3.10.0)
47
53
  rspec-core (~> 3.10.0)
48
54
  rspec-expectations (~> 3.10.0)
@@ -84,6 +90,7 @@ GEM
84
90
  docile (~> 1.1)
85
91
  simplecov-html (~> 0.11)
86
92
  simplecov-html (0.12.3)
93
+ slop (3.6.0)
87
94
  thread_safe (0.3.6)
88
95
  tzinfo (1.2.9)
89
96
  thread_safe (~> 0.1)
@@ -99,6 +106,7 @@ PLATFORMS
99
106
 
100
107
  DEPENDENCIES
101
108
  codecov
109
+ dokaz
102
110
  memo_wise!
103
111
  panolint!
104
112
  rake
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2020 Panorama Education
3
+ Copyright (c) 2020-2021 Panorama Education
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -50,6 +50,7 @@ methods:
50
50
  ```ruby
51
51
  class Example
52
52
  prepend MemoWise
53
+
53
54
  def slow_value(x)
54
55
  sleep x
55
56
  x
@@ -71,7 +72,31 @@ ex.slow_value(3) # => 4 # Returns immediately because the result is memoized
71
72
  ex.reset_memo_wise # Resets all memoized results for all methods on ex
72
73
  ```
73
74
 
74
- Methods which take implicit or explicit block arguments cannot be memoized.
75
+ The same three methods are exposed for class methods as well:
76
+
77
+ ```ruby
78
+ class Example
79
+ prepend MemoWise
80
+
81
+ def self.class_slow_value(x)
82
+ sleep x
83
+ x
84
+ end
85
+ memo_wise self: :class_slow_value
86
+ end
87
+
88
+ Example.class_slow_value(2) # => 2 # Sleeps for 2 seconds before returning
89
+ Example.class_slow_value(2) # => 2 # Returns immediately because the result is memoized
90
+
91
+ Example.reset_memo_wise(:class_slow_value) # Resets all memoized results for class_slow_value
92
+
93
+ Example.preset_memo_wise(:class_slow_value, 3) { 4 } # Store 4 as the result for slow_value(3)
94
+ Example.class_slow_value(3) # => 4 # Returns immediately because the result is memoized
95
+ Example.reset_memo_wise # Resets all memoized results for all methods on class
96
+ ```
97
+
98
+ **NOTE:** Methods which take implicit or explicit block arguments cannot be
99
+ memoized.
75
100
 
76
101
  For more usage details, see our detailed [documentation](#documentation).
77
102
 
@@ -79,21 +104,26 @@ For more usage details, see our detailed [documentation](#documentation).
79
104
 
80
105
  Benchmarks measure memoized value retrieval time using
81
106
  [`benchmark-ips`](https://github.com/evanphx/benchmark-ips). All benchmarks are
82
- run on Ruby 3.0.0, except as indicated below for specific gems. Benchmarks are
107
+ run on Ruby 3.0.1, except as indicated below for specific gems. Benchmarks are
83
108
  run in GitHub Actions and updated in every PR that changes code.
84
109
 
85
- |Method arguments|**`memo_wise` (0.1.0)**|`memery` (1.3.0)|`memoist`\* (0.16.2)|`memoized`\* (1.0.2)|`memoizer`\* (1.0.3)|
86
- |--|--|--|--|--|--|
87
- |`()` (none)|**baseline**|13.17x slower|2.85x slower|1.30x slower|3.05x slower|
88
- |`(a, b)`|**baseline**|1.93x slower|2.20x slower|1.97x slower|1.86x slower|
89
- |`(a:, b:)`|**baseline**|3.05x slower|2.34x slower|2.27x slower|2.14x slower|
90
- |`(a, b:)`|**baseline**|1.50x slower|1.63x slower|1.56x slower|1.48x slower|
91
- |`(a, *args)`|**baseline**|1.91x slower|2.13x slower|1.90x slower|1.88x slower|
92
- |`(a:, **kwargs)`|**baseline**|3.08x slower|2.37x slower|2.24x slower|2.09x slower|
93
- |`(a, *args, b:, **kwargs)`|**baseline**|1.72x slower|1.61x slower|1.56x slower|1.67x slower|
110
+ **Values >1.00x represent how much _slower_ each gem’s memoized value retrieval
111
+ is than the latest commit of `memo_wise`.**
94
112
 
95
- _\*Indicates a benchmark run on Ruby 2.7.2 because the gem raises errors in Ruby
96
- 3.0.0 due to its incorrect handling of keyword arguments._
113
+ |Method arguments|`memery` (1.4.0)|`memoist`\* (0.16.2)|`memoized`\* (1.0.2)|`memoizer`\* (1.0.3)|
114
+ |--|--|--|--|--|
115
+ |`()` (none)|13.17x|2.64x|1.46x|3.10x|
116
+ |`(a)`|9.76x|15.44x|11.98x|13.12x|
117
+ |`(a, b)`|1.98x|2.25x|1.82x|1.98x|
118
+ |`(a:)`|17.65x|23.64x|20.69x|21.61x|
119
+ |`(a:, b:)`|4.16x|3.94x|3.53x|3.67x|
120
+ |`(a, b:)`|3.96x|3.72x|3.27x|3.42x|
121
+ |`(a, *args)`|1.93x|2.25x|1.93x|1.95x|
122
+ |`(a:, **kwargs)`|3.06x|2.38x|2.10x|2.20x|
123
+ |`(a, *args, b:, **kwargs)`|1.52x|1.79x|1.65x|1.65x|
124
+
125
+ _\*Indicates a benchmark run on Ruby 2.7.3 because the gem raises errors in Ruby
126
+ 3.0.1 due to its incorrect handling of keyword arguments._
97
127
 
98
128
  You can run benchmarks yourself with:
99
129
 
@@ -106,18 +136,6 @@ $ bundle exec ruby benchmarks.rb
106
136
  If your results differ from what's posted here,
107
137
  [let us know](https://github.com/panorama-ed/memo_wise/issues/new)!
108
138
 
109
- ## Development
110
-
111
- After checking out the repo, run `bin/setup` to install dependencies. Then, run
112
- `rake spec` to run the tests. You can also run `bin/console` for an interactive
113
- prompt that will allow you to experiment.
114
-
115
- To install this gem onto your local machine, run `bundle exec rake install`. To
116
- release a new version, update the version number in `version.rb`, and then run
117
- `bundle exec rake release`, which will create a git tag for the version, push
118
- git commits and tags, and push the `.gem` file to
119
- [rubygems.org](https://rubygems.org).
120
-
121
139
  ## Documentation
122
140
 
123
141
  ### Documentation is Automatically Generated
@@ -140,6 +158,14 @@ code examples in our YARD documentation. To run `doctest` locally:
140
158
  bundle exec yard doctest
141
159
  ```
142
160
 
161
+ We use [dokaz](https://github.com/zverok/dokaz) to test all code examples in
162
+ this README.md file, and all other non-code documentation. To run `dokaz`
163
+ locally:
164
+
165
+ ```bash
166
+ bundle exec dokaz
167
+ ```
168
+
143
169
  ### A Note on Testing
144
170
 
145
171
  When testing memoized *module* methods, note that some testing setups will
@@ -180,7 +206,7 @@ To make a new release of `MemoWise` to
180
206
  dependencies (e.g. `rake`) as follows:
181
207
 
182
208
  ```shell
183
- bundle config set --local with 'release'
209
+ bundle config --local with 'release'
184
210
  bundle install
185
211
  ```
186
212
 
data/benchmarks/Gemfile CHANGED
@@ -4,10 +4,10 @@ source "https://rubygems.org"
4
4
 
5
5
  ruby ">= 2.7.2"
6
6
 
7
- gem "benchmark-ips", "2.8.4"
7
+ gem "benchmark-ips", "2.9.1"
8
8
 
9
9
  if RUBY_VERSION > "3"
10
- gem "memery", "1.3.0"
10
+ gem "memery", "1.4.0"
11
11
  else
12
12
  gem "memoist", "0.16.2"
13
13
  gem "memoized", "1.0.2"
@@ -1,23 +1,26 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- memo_wise (0.3.0)
4
+ memo_wise (0.4.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
- benchmark-ips (2.8.4)
10
- memery (1.3.0)
11
- ruby2_keywords (~> 0.0.2)
12
- ruby2_keywords (0.0.4)
9
+ benchmark-ips (2.9.1)
10
+ memoist (0.16.2)
11
+ memoized (1.0.2)
12
+ memoizer (1.0.3)
13
13
 
14
14
  PLATFORMS
15
15
  x86_64-darwin-19
16
+ x86_64-linux
16
17
 
17
18
  DEPENDENCIES
18
- benchmark-ips (= 2.8.4)
19
- memery (= 1.3.0)
19
+ benchmark-ips (= 2.9.1)
20
20
  memo_wise!
21
+ memoist (= 0.16.2)
22
+ memoized (= 1.0.2)
23
+ memoizer (= 1.0.3)
21
24
 
22
25
  RUBY VERSION
23
26
  ruby 3.0.0p0
@@ -68,11 +68,21 @@ BENCHMARK_GEMS.each do |benchmark_gem|
68
68
  end
69
69
  #{benchmark_gem.memoization_method} :no_args
70
70
 
71
+ def one_positional_arg(a)
72
+ 100
73
+ end
74
+ #{benchmark_gem.memoization_method} :one_positional_arg
75
+
71
76
  def positional_args(a, b)
72
77
  100
73
78
  end
74
79
  #{benchmark_gem.memoization_method} :positional_args
75
80
 
81
+ def one_keyword_arg(a:)
82
+ 100
83
+ end
84
+ #{benchmark_gem.memoization_method} :one_keyword_arg
85
+
76
86
  def keyword_args(a:, b:)
77
87
  100
78
88
  end
@@ -126,6 +136,23 @@ Benchmark.ips do |x|
126
136
  x.compare!
127
137
  end
128
138
 
139
+ Benchmark.ips do |x|
140
+ x.config(suite: suite)
141
+ BENCHMARK_GEMS.each do |benchmark_gem|
142
+ instance = Object.const_get("#{benchmark_gem.klass}Example").new
143
+
144
+ # Run once with each set of arguments to memoize the result values, so our
145
+ # benchmark only tests memoized retrieval time.
146
+ ARGUMENTS.each { |a, _| instance.one_positional_arg(a) }
147
+
148
+ x.report("#{benchmark_gem.benchmark_name}: (a)") do
149
+ ARGUMENTS.each { |a, _| instance.one_positional_arg(a) }
150
+ end
151
+ end
152
+
153
+ x.compare!
154
+ end
155
+
129
156
  Benchmark.ips do |x|
130
157
  x.config(suite: suite)
131
158
  BENCHMARK_GEMS.each do |benchmark_gem|
@@ -143,6 +170,23 @@ Benchmark.ips do |x|
143
170
  x.compare!
144
171
  end
145
172
 
173
+ Benchmark.ips do |x|
174
+ x.config(suite: suite)
175
+ BENCHMARK_GEMS.each do |benchmark_gem|
176
+ instance = Object.const_get("#{benchmark_gem.klass}Example").new
177
+
178
+ # Run once with each set of arguments to memoize the result values, so our
179
+ # benchmark only tests memoized retrieval time.
180
+ ARGUMENTS.each { |a, _| instance.one_keyword_arg(a: a) }
181
+
182
+ x.report("#{benchmark_gem.benchmark_name}: (a:)") do
183
+ ARGUMENTS.each { |a, _| instance.one_keyword_arg(a: a) }
184
+ end
185
+ end
186
+
187
+ x.compare!
188
+ end
189
+
146
190
  Benchmark.ips do |x|
147
191
  x.config(suite: suite)
148
192
  BENCHMARK_GEMS.each do |benchmark_gem|
data/lib/memo_wise.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "memo_wise/internal_api"
3
4
  require "memo_wise/version"
4
5
 
5
6
  # MemoWise is the wise choice for memoization in Ruby.
@@ -57,167 +58,23 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
57
58
  # On Ruby 2.7 or greater:
58
59
  #
59
60
  # def initialize(...)
60
- # MemoWise.create_memo_wise_state!(self)
61
+ # MemoWise::InternalAPI.create_memo_wise_state!(self)
61
62
  # super
62
63
  # end
63
64
  #
64
65
  # On Ruby 2.6 or lower:
65
66
  #
66
67
  # def initialize(*)
67
- # MemoWise.create_memo_wise_state!(self)
68
+ # MemoWise::InternalAPI.create_memo_wise_state!(self)
68
69
  # super
69
70
  # end
70
71
 
71
72
  def initialize(#{all_args})
72
- MemoWise.create_memo_wise_state!(self)
73
+ MemoWise::InternalAPI.create_memo_wise_state!(self)
73
74
  super
74
75
  end
75
76
  END_OF_METHOD
76
77
 
77
- # @private
78
- #
79
- # Determine whether `method` takes any *positional* args.
80
- #
81
- # These are the types of positional args:
82
- #
83
- # * *Required* -- ex: `def foo(a)`
84
- # * *Optional* -- ex: `def foo(b=1)`
85
- # * *Splatted* -- ex: `def foo(*c)`
86
- #
87
- # @param method [Method, UnboundMethod]
88
- # Arguments of this method will be checked
89
- #
90
- # @return [Boolean]
91
- # Return `true` if `method` accepts one or more positional arguments
92
- #
93
- # @example
94
- # class Example
95
- # def no_args
96
- # end
97
- #
98
- # def position_arg(a)
99
- # end
100
- # end
101
- #
102
- # MemoWise.has_arg?(Example.instance_method(:no_args)) #=> false
103
- #
104
- # MemoWise.has_arg?(Example.instance_method(:position_arg)) #=> true
105
- #
106
- def self.has_arg?(method) # rubocop:disable Naming/PredicateName
107
- method.parameters.any? do |(param, _)|
108
- param == :req || param == :opt || param == :rest # rubocop:disable Style/MultipleComparison
109
- end
110
- end
111
-
112
- # @private
113
- #
114
- # Determine whether `method` takes any *keyword* args.
115
- #
116
- # These are the types of keyword args:
117
- #
118
- # * *Keyword Required* -- ex: `def foo(a:)`
119
- # * *Keyword Optional* -- ex: `def foo(b: 1)`
120
- # * *Keyword Splatted* -- ex: `def foo(**c)`
121
- #
122
- # @param method [Method, UnboundMethod]
123
- # Arguments of this method will be checked
124
- #
125
- # @return [Boolean]
126
- # Return `true` if `method` accepts one or more keyword arguments
127
- #
128
- # @example
129
- # class Example
130
- # def position_args(a, b=1)
131
- # end
132
- #
133
- # def keyword_args(a:, b: 1)
134
- # end
135
- # end
136
- #
137
- # MemoWise.has_kwarg?(Example.instance_method(:position_args)) #=> false
138
- #
139
- # MemoWise.has_kwarg?(Example.instance_method(:keyword_args)) #=> true
140
- #
141
- def self.has_kwarg?(method) # rubocop:disable Naming/PredicateName
142
- method.parameters.any? do |(param, _)|
143
- param == :keyreq || param == :key || param == :keyrest # rubocop:disable Style/MultipleComparison
144
- end
145
- end
146
-
147
- # @private
148
- #
149
- # Returns visibility of an instance method defined on a class.
150
- #
151
- # @param klass [Class]
152
- # Class in which to find the visibility of an existing *instance* method.
153
- #
154
- # @param method_name [Symbol]
155
- # Name of existing *instance* method find the visibility of.
156
- #
157
- # @return [:private, :protected, :public]
158
- # Visibility of existing instance method of the class.
159
- #
160
- # @raise ArgumentError
161
- # Raises `ArgumentError` unless `method_name` is a `Symbol` corresponding
162
- # to an existing **instance** method defined on `klass`.
163
- #
164
- def self.method_visibility(klass, method_name)
165
- if klass.private_method_defined?(method_name)
166
- :private
167
- elsif klass.protected_method_defined?(method_name)
168
- :protected
169
- elsif klass.public_method_defined?(method_name)
170
- :public
171
- else
172
- raise ArgumentError, "#{method_name.inspect} must be a method on #{klass}"
173
- end
174
- end
175
-
176
- # @private
177
- #
178
- # Find the original class for which the given class is the corresponding
179
- # "singleton class".
180
- #
181
- # See https://stackoverflow.com/questions/54531270/retrieve-a-ruby-object-from-its-singleton-class
182
- #
183
- # @param klass [Class]
184
- # Singleton class to find the original class of
185
- #
186
- # @return Class
187
- # Original class for which `klass` is the singleton class.
188
- #
189
- # @raise ArgumentError
190
- # Raises if `klass` is not a singleton class.
191
- #
192
- def self.original_class_from_singleton(klass)
193
- unless klass.singleton_class?
194
- raise ArgumentError, "Must be a singleton class: #{klass.inspect}"
195
- end
196
-
197
- # Search ObjectSpace
198
- # * 1:1 relationship of singleton class to original class is documented
199
- # * Performance concern: searches all Class objects
200
- # But, only runs at load time
201
- ObjectSpace.each_object(Class).find { |cls| cls.singleton_class == klass }
202
- end
203
-
204
- # @private
205
- #
206
- # Create initial mutable state to store memoized values if it doesn't
207
- # already exist
208
- #
209
- # @param [Object] obj
210
- # Object in which to create mutable state to store future memoized values
211
- #
212
- # @return [Object] the passed-in obj
213
- def self.create_memo_wise_state!(obj)
214
- unless obj.instance_variables.include?(:@_memo_wise)
215
- obj.instance_variable_set(:@_memo_wise, {})
216
- end
217
-
218
- obj
219
- end
220
-
221
78
  # @private
222
79
  #
223
80
  # Private setup method, called automatically by `prepend MemoWise` in a class.
@@ -248,7 +105,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
248
105
  # `Class#allocate`, so we need to override both.
249
106
  #
250
107
  def allocate
251
- MemoWise.create_memo_wise_state!(super)
108
+ MemoWise::InternalAPI.create_memo_wise_state!(super)
252
109
  end
253
110
 
254
111
  # NOTE: See YARD docs for {.memo_wise} directly below this method!
@@ -259,8 +116,8 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
259
116
  method_name = method_name_or_hash
260
117
 
261
118
  if klass.singleton_class?
262
- MemoWise.create_memo_wise_state!(
263
- MemoWise.original_class_from_singleton(klass)
119
+ MemoWise::InternalAPI.create_memo_wise_state!(
120
+ MemoWise::InternalAPI.original_class_from_singleton(klass)
264
121
  )
265
122
  end
266
123
  when Hash
@@ -271,7 +128,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
271
128
 
272
129
  method_name = method_name_or_hash[:self]
273
130
 
274
- MemoWise.create_memo_wise_state!(self)
131
+ MemoWise::InternalAPI.create_memo_wise_state!(self)
275
132
 
276
133
  # In Ruby, "class methods" are implemented as normal instance methods
277
134
  # on the "singleton class" of a given Class object, found via
@@ -284,10 +141,12 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
284
141
  raise ArgumentError, "#{method_name.inspect} must be a Symbol"
285
142
  end
286
143
 
287
- visibility = MemoWise.method_visibility(klass, method_name)
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)
288
148
  method = klass.instance_method(method_name)
289
149
 
290
- original_memo_wised_name = :"_memo_wise_original_#{method_name}"
291
150
  klass.send(:alias_method, original_memo_wised_name, method_name)
292
151
  klass.send(:private, original_memo_wised_name)
293
152
 
@@ -308,22 +167,39 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
308
167
  end
309
168
  END_OF_METHOD
310
169
  else
311
- # If our method has arguments, we need to separate out our handling of
312
- # normal args vs. keyword args due to the changes in Ruby 3.
313
- # See: <link>
314
- # By only including logic for *args or **kwargs when they are used in
315
- # the method, we can avoid allocating unnecessary arrays and hashes.
316
- has_arg = MemoWise.has_arg?(method)
317
-
318
- if has_arg && MemoWise.has_kwarg?(method)
319
- args_str = "(*args, **kwargs)"
320
- fetch_key = "[args, kwargs].freeze"
321
- elsif has_arg
322
- args_str = "(*args)"
323
- fetch_key = "args"
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
324
185
  else
325
- args_str = "(**kwargs)"
326
- fetch_key = "kwargs"
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"
202
+ end
327
203
  end
328
204
 
329
205
  # Note that we don't need to freeze args before using it as a hash key
@@ -343,7 +219,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
343
219
  @_memo_wise[:#{method_name}] = {}
344
220
  end
345
221
  hash.fetch(#{fetch_key}) do
346
- hash[#{fetch_key}] = #{original_memo_wised_name}#{args_str}
222
+ hash[#{fetch_key}] = #{original_memo_wised_name}#{call_str || args_str}
347
223
  end
348
224
  end
349
225
  END_OF_METHOD
@@ -352,6 +228,53 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
352
228
  klass.send(visibility, method_name)
353
229
  end
354
230
  end
231
+
232
+ unless target.singleton_class?
233
+ # Create class methods to implement .preset_memo_wise and .reset_memo_wise
234
+ %i[preset_memo_wise reset_memo_wise].each do |method_name|
235
+ # Like calling 'module_function', but original method stays public
236
+ target.define_singleton_method(
237
+ method_name,
238
+ MemoWise.instance_method(method_name)
239
+ )
240
+ end
241
+
242
+ # Override [Module#instance_method](https://ruby-doc.org/core-3.0.0/Module.html#method-i-instance_method)
243
+ # to proxy the original `UnboundMethod#parameters` results. We want the
244
+ # parameters to reflect the original method in order to support callers
245
+ # who want to use Ruby reflection to process the method parameters,
246
+ # because our overridden `#initialize` method, and in some cases the
247
+ # generated memoized methods, will have a generic set of parameters
248
+ # (`...` or `*args, **kwargs`), making reflection on method parameters
249
+ # useless without this.
250
+ def target.instance_method(symbol)
251
+ # TODO: Extract this method naming pattern
252
+ original_memo_wised_name = :"_memo_wise_original_#{symbol}"
253
+
254
+ super.tap do |curr_method|
255
+ # Start with calling the original `instance_method` on `symbol`,
256
+ # which returns an `UnboundMethod`.
257
+ # IF it was replaced by MemoWise,
258
+ # THEN find the original method's parameters, and modify current
259
+ # `UnboundMethod#parameters` to return them.
260
+ if symbol == :initialize
261
+ # For `#initialize` - because `prepend MemoWise` overrides the same
262
+ # method in the module ancestors, use `UnboundMethod#super_method`
263
+ # to find the original method.
264
+ orig_method = curr_method.super_method
265
+ orig_params = orig_method.parameters
266
+ curr_method.define_singleton_method(:parameters) { orig_params }
267
+ elsif private_method_defined?(original_memo_wised_name)
268
+ # For any memoized method - because the original method was renamed,
269
+ # call the original `instance_method` again to find the renamed
270
+ # original method.
271
+ orig_method = super(original_memo_wised_name)
272
+ orig_params = orig_method.parameters
273
+ curr_method.define_singleton_method(:parameters) { orig_params }
274
+ end
275
+ end
276
+ end
277
+ end
355
278
  end
356
279
 
357
280
  ##
@@ -392,6 +315,68 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
392
315
  # ex.method_to_memoize("b") #=> 2
393
316
  ##
394
317
 
318
+ ##
319
+ # @!method self.preset_memo_wise(method_name, *args, **kwargs)
320
+ # Implementation of {#preset_memo_wise} for class methods.
321
+ #
322
+ # @example
323
+ # class Example
324
+ # prepend MemoWise
325
+ #
326
+ # def self.method_called_times
327
+ # @method_called_times
328
+ # end
329
+ #
330
+ # def self.method_to_preset
331
+ # @method_called_times = (@method_called_times || 0) + 1
332
+ # "A"
333
+ # end
334
+ # memo_wise self: :method_to_preset
335
+ # end
336
+ #
337
+ # Example.preset_memo_wise(:method_to_preset) { "B" }
338
+ #
339
+ # Example.method_to_preset #=> "B"
340
+ #
341
+ # Example.method_called_times #=> nil
342
+ ##
343
+
344
+ # rubocop:disable Layout/LineLength
345
+ ##
346
+ # @!method self.reset_memo_wise(method_name = nil, *args, **kwargs)
347
+ # Implementation of {#reset_memo_wise} for class methods.
348
+ #
349
+ # @example
350
+ # class Example
351
+ # prepend MemoWise
352
+ #
353
+ # def self.method_to_reset(x)
354
+ # @method_called_times = (@method_called_times || 0) + 1
355
+ # end
356
+ # memo_wise self: :method_to_reset
357
+ # end
358
+ #
359
+ # Example.method_to_reset("a") #=> 1
360
+ # Example.method_to_reset("a") #=> 1
361
+ # Example.method_to_reset("b") #=> 2
362
+ # Example.method_to_reset("b") #=> 2
363
+ #
364
+ # Example.reset_memo_wise(:method_to_reset, "a") # reset "method + args" mode
365
+ #
366
+ # Example.method_to_reset("a") #=> 3
367
+ # Example.method_to_reset("a") #=> 3
368
+ # Example.method_to_reset("b") #=> 2
369
+ # Example.method_to_reset("b") #=> 2
370
+ #
371
+ # Example.reset_memo_wise(:method_to_reset) # reset "method" (any args) mode
372
+ #
373
+ # Example.method_to_reset("a") #=> 4
374
+ # Example.method_to_reset("b") #=> 5
375
+ #
376
+ # Example.reset_memo_wise # reset "all methods" mode
377
+ ##
378
+ # rubocop:enable Layout/LineLength
379
+
395
380
  # Presets the memoized result for the given method to the result of the given
396
381
  # block.
397
382
  #
@@ -443,14 +428,13 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
443
428
  # ex.method_called_times #=> nil
444
429
  #
445
430
  def preset_memo_wise(method_name, *args, **kwargs)
446
- validate_memo_wised!(method_name)
447
-
448
431
  unless block_given?
449
432
  raise ArgumentError,
450
433
  "Pass a block as the value to preset for #{method_name}, #{args}"
451
434
  end
452
435
 
453
- validate_params!(method_name, args)
436
+ api = MemoWise::InternalAPI.new(self)
437
+ api.validate_memo_wised!(method_name)
454
438
 
455
439
  if method(method_name).arity.zero?
456
440
  @_memo_wise[method_name] = yield
@@ -458,7 +442,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
458
442
  hash = @_memo_wise.fetch(method_name) do
459
443
  @_memo_wise[method_name] = {}
460
444
  end
461
- hash[fetch_key(method_name, *args, **kwargs)] = yield
445
+ hash[api.fetch_key(method_name, *args, **kwargs)] = yield
462
446
  end
463
447
  end
464
448
 
@@ -510,7 +494,6 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
510
494
  #
511
495
  # ex.method_to_reset("a") #=> 1
512
496
  # ex.method_to_reset("a") #=> 1
513
- #
514
497
  # ex.method_to_reset("b") #=> 2
515
498
  # ex.method_to_reset("b") #=> 2
516
499
  #
@@ -518,7 +501,6 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
518
501
  #
519
502
  # ex.method_to_reset("a") #=> 3
520
503
  # ex.method_to_reset("a") #=> 3
521
- #
522
504
  # ex.method_to_reset("b") #=> 2
523
505
  # ex.method_to_reset("b") #=> 2
524
506
  #
@@ -550,40 +532,14 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
550
532
  raise ArgumentError, "#{method_name} is not a defined method"
551
533
  end
552
534
 
553
- validate_memo_wised!(method_name)
535
+ api = MemoWise::InternalAPI.new(self)
536
+ api.validate_memo_wised!(method_name)
554
537
 
555
538
  if args.empty? && kwargs.empty?
556
539
  @_memo_wise.delete(method_name)
557
540
  else
558
- @_memo_wise[method_name]&.delete(fetch_key(method_name, *args, **kwargs))
541
+ @_memo_wise[method_name]&.
542
+ delete(api.fetch_key(method_name, *args, **kwargs))
559
543
  end
560
544
  end
561
-
562
- private
563
-
564
- # Validates that {.memo_wise} has already been called on `method_name`.
565
- def validate_memo_wised!(method_name)
566
- original_memo_wised_name = :"_memo_wise_original_#{method_name}"
567
-
568
- unless self.class.private_method_defined?(original_memo_wised_name)
569
- raise ArgumentError, "#{method_name} is not a memo_wised method"
570
- end
571
- end
572
-
573
- # Returns arguments key to lookup memoized results for given `method_name`.
574
- def fetch_key(method_name, *args, **kwargs)
575
- method = self.class.instance_method(method_name)
576
- has_arg = MemoWise.has_arg?(method)
577
-
578
- if has_arg && MemoWise.has_kwarg?(method)
579
- [args, kwargs].freeze
580
- elsif has_arg
581
- args
582
- else
583
- kwargs
584
- end
585
- end
586
-
587
- # TODO: Parameter validation for presetting values
588
- def validate_params!(method_name, args); end
589
545
  end