memo_wise 0.4.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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