memoized 1.0.1 → 1.1.1

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: 9ab7a8649195530ae1e3ace893313e3731c1dcf3af93f6ebeaebad4a715efa65
4
- data.tar.gz: 543fd2584dbf7a34fd45c23394888cf8935a8f93f93be9a293b684e1054c3eed
3
+ metadata.gz: 38881d8fc70ecdbf1920e7bbb40d684403d88c4225e53d7a3e426f97ef5f41ac
4
+ data.tar.gz: 750f0290f9258b48bcabdf993e0c0a11f9919643b2a944efa5046cdd930d05a8
5
5
  SHA512:
6
- metadata.gz: 13673e0131ff6560848e8dcac44ea746db4c18602b3fa68d3072dbefeaca95bf93ea1bbd4159ce49c8be30cd7361e616237c1ef61d19bc6e2d72efaba6a5d9fe
7
- data.tar.gz: 99f27fd8d150aaf3283d29cd159f16ed31beb13c6a293338709f15397c562294caf55a971b7764847895919aaf46c1a87ea77c123f2680691b1765b361b129b8
6
+ metadata.gz: 35dd34457b8c984200f9fe3eefc72539cf6dcf4661d6ade672197a9c0760439fb27d7b577400eb9416db66fb2f69fc8b6302f78f36f6dbb25d79350f5b7ca521
7
+ data.tar.gz: 94360c1c038704a77d06258aa76971bdcb875064cbec5a26b090f5764c50eecccddb01c42ecd67b3a15b3c577768a2740160163111900543582ab4677577d994
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: Tests
3
+ 'on':
4
+ push:
5
+ branches:
6
+ - master
7
+ pull_request:
8
+ branches:
9
+ - master
10
+ jobs:
11
+ test:
12
+ runs-on: ubuntu-20.04
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ include:
17
+ - ruby: 2.5.3
18
+ gemfile: Gemfile
19
+ - ruby: 2.6.7
20
+ gemfile: Gemfile
21
+ - ruby: 2.7.3
22
+ gemfile: Gemfile
23
+ - ruby: 3.0.1
24
+ gemfile: Gemfile
25
+ env:
26
+ BUNDLE_GEMFILE: "${{ matrix.gemfile }}"
27
+ steps:
28
+ - uses: actions/checkout@v2
29
+ - name: Install ruby
30
+ uses: ruby/setup-ruby@v1
31
+ with:
32
+ ruby-version: "${{ matrix.ruby }}"
33
+ - name: Bundle
34
+ run: |
35
+ gem install bundler:2.2.21
36
+ bundle install --no-deployment
37
+ - name: Run tests
38
+ run: bundle exec rspec
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.6.1
1
+ 2.6.7
data/CHANGELOG.md CHANGED
@@ -13,6 +13,32 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
13
13
 
14
14
  -
15
15
 
16
+ ## 1.1.1 - 2022-06-22
17
+
18
+ ### Breaking changes
19
+
20
+ - (The `.parameters` of a memoized method are no longer renamed to `arg1` ... `argn` and instead retain their original names)
21
+
22
+ ### Compatible changes
23
+
24
+ - Methods with keyword arguments can now be properly memoized
25
+ - In addition to a methods `.arity`, memoized now also preserves its `.parameters`
26
+
27
+ ## 1.1.0 - 2022-03-16
28
+
29
+ ### Breaking changes
30
+
31
+ - Remove no longer supported ruby versions (2.3.8, 2.4.5)
32
+
33
+ ### Compatible changes
34
+
35
+ - Activate rubygems MFA
36
+
37
+ ## 1.0.2 - 2019-05-22
38
+
39
+ ### Compatible changes
40
+
41
+ - Preserve arity of methods with optional arguments
16
42
 
17
43
  ## 1.0.1 - 2019-02-27
18
44
 
data/Gemfile CHANGED
@@ -1,2 +1,3 @@
1
1
  source 'https://rubygems.org'
2
+
2
3
  gemspec
data/Gemfile.lock CHANGED
@@ -1,12 +1,16 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- memoized (1.0.0)
4
+ memoized (1.1.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
+ amazing_print (1.4.0)
9
10
  diff-lcs (1.3)
11
+ gemika (0.7.1)
12
+ prop_check (0.14.1)
13
+ amazing_print (~> 1.2)
10
14
  rake (10.4.2)
11
15
  rspec (3.5.0)
12
16
  rspec-core (~> 3.5.0)
@@ -27,10 +31,12 @@ PLATFORMS
27
31
  ruby
28
32
 
29
33
  DEPENDENCIES
34
+ gemika
30
35
  memoized!
36
+ prop_check (~> 0.14.1)
31
37
  rake (~> 10.4.2)
32
38
  rspec (~> 3.5.0)
33
39
  timecop (~> 0.8.0)
34
40
 
35
41
  BUNDLED WITH
36
- 1.17.2
42
+ 2.2.21
data/README.md CHANGED
@@ -1,6 +1,4 @@
1
- [![Build Status](https://travis-ci.org/makandra/memoized.svg?branch=master)](https://travis-ci.org/makandra/memoized)
2
-
3
- # Memoized
1
+ # Memoized [![Tests](https://github.com/makandra/memoized/workflows/Tests/badge.svg)](https://github.com/makandra/memoized/actions)
4
2
 
5
3
  Memoized will memoize the results of your methods. It acts much like
6
4
  `ActiveSupport::Memoizable` without all of that freezing business. The API for
@@ -14,7 +12,7 @@ $ gem install memoized
14
12
 
15
13
  ## Usage
16
14
 
17
- To define a memoized instance method, use `memoized def``:
15
+ To define a memoized instance method, use `memoize def`:
18
16
 
19
17
  ```ruby
20
18
  class A
@@ -78,6 +76,64 @@ instance.goodbye # the goodbye method is now memoized
78
76
  instance.unmemoize_all # neither hello nor goodbye are memoized anymore
79
77
  ```
80
78
 
79
+ ## Limitations
80
+
81
+ When you are using Memoized with default arguments or default keyword arguments, there are some edge cased you have to
82
+ keep in mind.
83
+
84
+ When you memoize a method with (keyword) arguments that have an expression as default value, you should be aware
85
+ that the expression is evaluated only once.
86
+
87
+ ```ruby
88
+ memoize def print_time(time = Time.now)
89
+ time
90
+ end
91
+
92
+ print_time
93
+ => 2021-07-23 14:23:18 +0200
94
+
95
+ sleep(1.minute)
96
+ print_time
97
+ => 2021-07-23 14:23:18 +0200
98
+ ```
99
+
100
+ When you memoize a method with (keyword) arguments that have default values, you should be aware that Memoized
101
+ differentiates between a method call without arguments and the default values.
102
+
103
+ ```ruby
104
+ def true_or_false(default = true)
105
+ puts 'calculate value ...'
106
+ default
107
+ end
108
+
109
+ true_or_false
110
+ calculate value ...
111
+ => true
112
+
113
+ true_or_false
114
+ => true
115
+
116
+ true_or_false(true)
117
+ calculate value ...
118
+ => true
119
+ ```
120
+
121
+ ## Development
122
+
123
+ There are tests in `spec`. We only accept PRs with tests. To run tests:
124
+
125
+ - Install Ruby 2.6.1
126
+ - Install development dependencies using `bundle install`
127
+ - Run tests using `bundle exec rake current_rspec`
128
+
129
+ We recommend to test large changes against multiple versions of Ruby. Supported combinations are configured in `.github/workflows/test.yml`. We provide some rake tasks to help with this:
130
+
131
+ - Install development dependencies using `bundle exec rake matrix:install`
132
+ - Run tests using `bundle exec rake matrix:spec`
133
+
134
+ Note that we have configured GitHub Actions to automatically run tests in all supported Ruby versions and dependency sets after each push. We will only merge pull requests after a green GitHub Actions run.
135
+
136
+ I'm very eager to keep this gem leightweight and on topic. If you're unsure whether a change would make it into the gem, [talk to me beforehand](mailto:henning.koch@makandra.de).
81
137
 
82
138
  ## License
83
139
 
data/Rakefile CHANGED
@@ -1,2 +1,10 @@
1
1
  require 'rake'
2
2
  require 'bundler/gem_tasks'
3
+
4
+ begin
5
+ require 'gemika/tasks'
6
+ rescue LoadError
7
+ puts 'Run `gem install gemika` for additional tasks'
8
+ end
9
+
10
+ task :default => 'matrix:spec'
@@ -0,0 +1,98 @@
1
+ module Memoized
2
+ class Parameters
3
+ UNIQUE = 42.freeze
4
+
5
+ attr_accessor :req_params, :opt_params, :rest_params, :keyreq_params, :key_params, :keyrest_params
6
+
7
+ def initialize(parameters = [])
8
+ # This constructor does not check, whether the parameters were ordered correctly
9
+ # with respect to the Ruby language specification. However, all outputs will be sorted correctly.
10
+ @req_params = []
11
+ @opt_params = []
12
+ @rest_params = []
13
+ @keyreq_params = []
14
+ @key_params = []
15
+ @keyrest_params = []
16
+
17
+ parameters.each do |(param_type, param_name)|
18
+ case param_type
19
+ when :req
20
+ @req_params << [param_type, param_name]
21
+ when :opt
22
+ @opt_params << [param_type, param_name]
23
+ when :rest
24
+ @rest_params << [param_type, param_name]
25
+ when :keyreq
26
+ @keyreq_params << [param_type, param_name]
27
+ when :key
28
+ @key_params << [param_type, param_name]
29
+ when :keyrest
30
+ @keyrest_params << [param_type, param_name]
31
+ when :block
32
+ raise Memoized::CannotMemoize, 'Cannot memoize a method that takes a block!'
33
+ else
34
+ raise Memoized::CannotMemoize, 'Unknown parameter type!'
35
+ end
36
+ end
37
+
38
+ if @rest_params.size > 1 || @keyrest_params.size > 1
39
+ raise Memoized::CannotMemoize "Multiple rest or keyrest parameters, invalid signature!"
40
+ end
41
+ end
42
+
43
+ def params
44
+ @req_params + @opt_params + @rest_params + @keyreq_params + @key_params + @keyrest_params
45
+ end
46
+
47
+ def signature
48
+ params.map(&Parameters.method(:to_signature)).join(', ')
49
+ end
50
+
51
+ def self.to_signature((param_type, param_name))
52
+ case param_type
53
+ when :req
54
+ "#{param_name}"
55
+ when :opt
56
+ "#{param_name} = Memoized::Parameters::UNIQUE"
57
+ when :rest
58
+ "*#{param_name}"
59
+ when :keyreq
60
+ "#{param_name}:"
61
+ when :key
62
+ "#{param_name}: Memoized::Parameters::UNIQUE"
63
+ when :keyrest
64
+ "**#{param_name}"
65
+ else raise "unknown parameter type"
66
+ end
67
+ end
68
+
69
+ def cache_key
70
+ <<-STRING
71
+ all_args = []
72
+ all_kwargs = {}
73
+
74
+ #{params.map(&Parameters.method(:to_cache_key)).join("\n")}
75
+
76
+ cache_key = [all_args, all_kwargs]
77
+ STRING
78
+ end
79
+
80
+ def self.to_cache_key((param_type, param_name))
81
+ case param_type
82
+ when :req
83
+ "all_args.push(#{param_name})"
84
+ when :opt
85
+ "all_args.push(#{param_name}) unless #{param_name}.equal?(Memoized::Parameters::UNIQUE)"
86
+ when :rest
87
+ "all_args.push(*#{param_name})"
88
+ when :keyreq
89
+ "all_kwargs[:#{param_name}] = #{param_name}"
90
+ when :key
91
+ "all_kwargs[:#{param_name}] = #{param_name} unless #{param_name}.equal?(Memoized::Parameters::UNIQUE)"
92
+ when :keyrest
93
+ "all_kwargs.merge!(#{param_name})"
94
+ else raise "unknown parameter type"
95
+ end
96
+ end
97
+ end
98
+ end
@@ -1,3 +1,3 @@
1
1
  module Memoized
2
- VERSION = '1.0.1'
2
+ VERSION = '1.1.1'
3
3
  end
data/lib/memoized.rb CHANGED
@@ -1,4 +1,8 @@
1
+ require 'memoized/parameters'
2
+
1
3
  module Memoized
4
+ class CannotMemoize < ::StandardError; end
5
+
2
6
  def self.included(base)
3
7
  base.extend ClassMethods
4
8
  base.send :include, InstanceMethods
@@ -20,45 +24,27 @@ module Memoized
20
24
 
21
25
  alias_method unmemoized_method, method_name
22
26
 
23
- arity = instance_method(unmemoized_method).arity
24
-
25
- case arity
26
- when 0
27
- module_eval(<<-RUBY)
28
- def #{method_name}()
29
- #{memoized_ivar_name} ||= [#{unmemoized_method}()]
30
- #{memoized_ivar_name}.first
31
- end
32
- RUBY
33
-
34
- when -1
35
- module_eval(<<-RUBY)
36
- def #{method_name}(*args)
37
- #{memoized_ivar_name} ||= {}
38
- if #{memoized_ivar_name}.has_key?(args)
39
- #{memoized_ivar_name}[args]
40
- else
41
- #{memoized_ivar_name}[args] = #{unmemoized_method}(*args)
42
- end
43
- end
44
- RUBY
27
+ parameters = Parameters.new(instance_method(unmemoized_method).parameters)
45
28
 
46
- else
47
- arg_names = (0..(arity - 1)).map { |i| "arg#{i}" }
48
- args_ruby = arg_names.join(', ')
29
+ module_eval(<<-RUBY)
30
+ def #{method_name}(#{parameters.signature})
31
+ #{parameters.cache_key}
49
32
 
50
- module_eval(<<-RUBY)
51
- def #{method_name}(#{args_ruby})
52
- args = [#{args_ruby}]
53
- #{memoized_ivar_name} ||= {}
54
- if #{memoized_ivar_name}.has_key?(args)
55
- #{memoized_ivar_name}[args]
56
- else
57
- #{memoized_ivar_name}[args] = #{unmemoized_method}(#{args_ruby})
33
+ #{memoized_ivar_name} ||= {}
34
+
35
+ if #{memoized_ivar_name}.key?(cache_key)
36
+ #{memoized_ivar_name}[cache_key]
37
+ else
38
+ live_result = if all_kwargs.empty?
39
+ #{unmemoized_method}(*all_args)
40
+ else
41
+ #{unmemoized_method}(*all_args, **all_kwargs)
58
42
  end
43
+ #{memoized_ivar_name}[cache_key] = live_result
44
+ live_result
59
45
  end
60
- RUBY
61
- end
46
+ end
47
+ RUBY
62
48
 
63
49
  if self.private_method_defined?(unmemoized_method)
64
50
  private method_name
data/memoized.gemspec CHANGED
@@ -8,6 +8,12 @@ Gem::Specification.new do |s|
8
8
  s.homepage = "https://github.com/makandra/memoized"
9
9
  s.summary = "Memoized caches the results of your method calls"
10
10
  s.description = s.summary
11
+ s.metadata = {
12
+ 'source_code_uri' => s.homepage,
13
+ 'bug_tracker_uri' => 'https://github.com/makandra/memoized/issues',
14
+ 'changelog_uri' => 'https://github.com/makandra/memoized/blob/master/CHANGELOG.md',
15
+ 'rubygems_mfa_required' => 'true',
16
+ }
11
17
 
12
18
  s.files = `git ls-files`.split("\n").reject { |path| File.lstat(path).symlink? }
13
19
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n").reject { |path| File.lstat(path).symlink? }
@@ -19,4 +25,6 @@ Gem::Specification.new do |s|
19
25
  s.add_development_dependency('rake', '~> 10.4.2')
20
26
  s.add_development_dependency('rspec', '~> 3.5.0')
21
27
  s.add_development_dependency('timecop', '~> 0.8.0')
28
+ s.add_development_dependency('prop_check', '~> 0.14.1')
29
+ s.add_development_dependency('gemika')
22
30
  end
@@ -3,7 +3,13 @@ class MemoizedSpecClass
3
3
  def no_params() Date.today; end
4
4
  def with_params?(ndays, an_array) Date.today + ndays + an_array.length; end
5
5
  def returning_nil!() Date.today; nil; end
6
- memoize :no_params, :with_params?, :returning_nil!
6
+ def all_param_types(req, opt = 3, *rest, keyreq:, key: 11, **keyrest)
7
+ Date.today + (req * opt * rest.inject(&:*) * keyreq * key * keyrest.values.inject(&:*))
8
+ end
9
+ def only_kwargs(**keyrest)
10
+ Date.today + keyrest.values.inject(&:*)
11
+ end
12
+ memoize :no_params, :with_params?, :returning_nil!, :all_param_types, :only_kwargs
7
13
  end
8
14
  class Beepbop < MemoizedSpecClass; end
9
15
 
@@ -49,6 +55,40 @@ describe Memoized do
49
55
  end
50
56
  end
51
57
 
58
+ context "for a method with all param types" do
59
+ it "stores memoized value" do
60
+ Timecop.freeze(today)
61
+ expect(object.all_param_types(2, 3, 5, 5, keyreq: 7, key: 11, **{ first: 13, second: 13 })).to eq(today + 1951950)
62
+ Timecop.freeze(tomorrow)
63
+ expect(object.all_param_types(2, 3, 5, 5, keyreq: 7, key: 11, **{ first: 13, second: 13 })).to eq(today + 1951950)
64
+ end
65
+ it "does not confuse one set of inputs for another" do
66
+ Timecop.freeze(today)
67
+ expect(object.all_param_types(2, 3, 5, 5, keyreq: 7, key: 11, **{ first: 13, second: 13 })).to eq(today + 1951950)
68
+ expect(object.all_param_types(2, 9, 5, keyreq: 7, key: 121, **{ first: 13 })).to eq(today + 990990)
69
+ Timecop.freeze(tomorrow)
70
+ expect(object.all_param_types(2, 3, 5, 5, keyreq: 7, key: 11, **{ first: 13, second: 13 })).to eq(today + 1951950)
71
+ expect(object.all_param_types(2, 9, 5, keyreq: 7, key: 121, **{ first: 13 })).to eq(today + 990990)
72
+ end
73
+ end
74
+
75
+ context "for a method with only keyword rest arguments" do
76
+ it "stores memoized value" do
77
+ Timecop.freeze(today)
78
+ expect(object.only_kwargs(**{ first: 2, second: 3 })).to eq(today + 6)
79
+ Timecop.freeze(tomorrow)
80
+ expect(object.only_kwargs(**{ first: 2, second: 3 })).to eq(today + 6)
81
+ end
82
+ it "does not confuse one set of inputs for another" do
83
+ Timecop.freeze(today)
84
+ expect(object.only_kwargs(**{ first: 2, second: 3 })).to eq(today + 6)
85
+ expect(object.only_kwargs(**{ first: 7 })).to eq(today + 7)
86
+ Timecop.freeze(tomorrow)
87
+ expect(object.only_kwargs(**{ first: 2, second: 3 })).to eq(today + 6)
88
+ expect(object.only_kwargs(**{ first: 7 })).to eq(today + 7)
89
+ end
90
+ end
91
+
52
92
  context "for subclasses" do
53
93
  let(:object) { Beepbop.new }
54
94
  it "still memoizes things" do
@@ -114,7 +154,6 @@ describe Memoized do
114
154
  it 'creates a memoized method with an arity of 0' do
115
155
  expect(Arity0.instance_method(:foo).arity).to eq(0)
116
156
  end
117
-
118
157
  end
119
158
 
120
159
  context 'for methods with an arity of 2' do
@@ -128,7 +167,6 @@ describe Memoized do
128
167
  it 'creates a memoized method with an arity of 2' do
129
168
  expect(Arity2.instance_method(:foo).arity).to eq(2)
130
169
  end
131
-
132
170
  end
133
171
 
134
172
  context 'for methods with splat args' do
@@ -142,7 +180,92 @@ describe Memoized do
142
180
  it 'creates a memoized method with an arity of -1' do
143
181
  expect(AritySplat.instance_method(:foo).arity).to eq(-1)
144
182
  end
183
+ end
145
184
 
185
+ context 'for methods with a required and an optional arg' do
186
+ class ArityRequiredAndOptional < MemoizedSpecClass
187
+ def foo(a, b = 'default')
188
+ return [a, b]
189
+ end
190
+
191
+ memoize :foo
192
+ end
193
+
194
+ it 'creates a memoized method with a arity of -2' do
195
+ expect(ArityRequiredAndOptional.instance_method(:foo).arity).to eq(-2)
196
+ end
197
+
198
+ it "preserves the optional arg's default value" do
199
+ instance = ArityRequiredAndOptional.new
200
+ expect(instance.foo('foo')).to eq ['foo', 'default']
201
+ end
202
+ end
203
+
204
+ context 'for methods with a required arg and splat args' do
205
+ class ArityArgAndOptional < MemoizedSpecClass
206
+ def foo(a, *args)
207
+ return [a, args]
208
+ end
209
+
210
+ memoize :foo
211
+ end
212
+
213
+ it 'creates a memoized method with a arity of -2' do
214
+ expect(ArityArgAndOptional.instance_method(:foo).arity).to eq(-2)
215
+ end
216
+
217
+ it "passes the splat args to the memoized method" do
218
+ instance = ArityArgAndOptional.new
219
+ expect(instance.foo('foo', 'bar', 'baz')).to eq ['foo', ['bar', 'baz']]
220
+ end
221
+ end
222
+
223
+ context 'for methods with all types of args' do
224
+ class AllArgTypes < MemoizedSpecClass
225
+ def foo(required, optional = 3, *rest, req_keyword:, opt_keyword: 11, **keyrest)
226
+ return [required, optional, rest, req_keyword, opt_keyword, keyrest]
227
+ end
228
+
229
+ memoize :foo
230
+ end
231
+
232
+ it 'the memoized method has the same arity as the original method' do
233
+ expect(AllArgTypes.instance_method(:_unmemoized_foo).arity).to eq(-3)
234
+ expect(AllArgTypes.instance_method(:foo).arity).to eq(-3)
235
+ end
236
+
237
+ it 'the memoized method has the same parameters as the original method' do
238
+ expect(AllArgTypes.instance_method(:_unmemoized_foo).parameters)
239
+ .to eq([
240
+ [:req, :required],
241
+ [:opt, :optional],
242
+ [:rest, :rest],
243
+ [:keyreq, :req_keyword],
244
+ [:key, :opt_keyword],
245
+ [:keyrest, :keyrest]
246
+ ])
247
+ expect(AllArgTypes.instance_method(:foo).parameters)
248
+ .to eq([
249
+ [:req, :required],
250
+ [:opt, :optional],
251
+ [:rest, :rest],
252
+ [:keyreq, :req_keyword],
253
+ [:key, :opt_keyword],
254
+ [:keyrest, :keyrest]
255
+ ])
256
+ end
257
+
258
+ it "passes all args to the original method correctly" do
259
+ instance = AllArgTypes.new
260
+ expect(instance.foo(2, 333, 5, 5, req_keyword: 7, opt_keyword: 1111, first: 13, second: 17))
261
+ .to eq [2, 333, [5, 5], 7, 1111, { first: 13, second: 17 }]
262
+ end
263
+
264
+ it "preserves the original method's default values" do
265
+ instance = AllArgTypes.new
266
+ expect(instance.foo(2, req_keyword: 7, third: 19))
267
+ .to eq [2, 3, [], 7, 11, { third: 19 }]
268
+ end
146
269
  end
147
270
 
148
271
  end
@@ -0,0 +1,222 @@
1
+ unless RUBY_VERSION == '2.5.3'
2
+ describe "#memoize" do
3
+ include PropCheck
4
+ include PropCheck::Generators
5
+ include Memoized
6
+
7
+ before do
8
+ PropCheck::Property.configure do |config|
9
+ # CAUTION:
10
+ # 100 (default) takes 8 seconds
11
+ # 300 takes 11 minutes
12
+ config.n_runs = 100
13
+ end
14
+ end
15
+
16
+ it "does not change the method's arity" do
17
+ forall(
18
+ array(
19
+ tuple(
20
+ one_of(
21
+ constant(:req), constant(:opt), constant(:rest), constant(:keyreq), constant(:key), constant(:keyrest)
22
+ ),
23
+ simple_symbol.map do |sym|
24
+ "param_#{sym}".to_sym
25
+ end
26
+ )
27
+ )
28
+ ) do |parameters|
29
+ # params now have proper names (no :"", no Ruby keywords) due to the .map in the generator above
30
+ unique_names = parameters.uniq { |v| v[1] }
31
+ single_args_and_kwargs = unique_names.uniq do |v|
32
+ if [:rest, :keyrest].include?(v[0])
33
+ v[0]
34
+ else
35
+ v[1]
36
+ end
37
+ end
38
+
39
+ mp = Memoized::Parameters.new(single_args_and_kwargs)
40
+ puts mp.debug_info if ENV['DEBUG'] == 'true'
41
+
42
+ eval(<<-RUBY)
43
+ class MemoizedPropertyClass
44
+ include Memoized
45
+
46
+ def parameter_dummy(#{mp.signature})
47
+ 42
48
+ end
49
+
50
+ @old_arity = new.method(:parameter_dummy).arity
51
+ memoize :parameter_dummy
52
+ @new_arity = new.method(:parameter_dummy).arity
53
+
54
+ # cleanup to get rid of warnings
55
+ remove_method :_unmemoized_parameter_dummy
56
+ remove_method :parameter_dummy
57
+ end
58
+ RUBY
59
+
60
+ expect(MemoizedPropertyClass.instance_variable_get(:@new_arity))
61
+ .to eq(MemoizedPropertyClass.instance_variable_get(:@old_arity))
62
+ end
63
+ end
64
+
65
+ it "does not change the method's parameters" do
66
+ forall(
67
+ array(
68
+ tuple(
69
+ one_of(
70
+ constant(:req), constant(:opt), constant(:rest), constant(:keyreq), constant(:key), constant(:keyrest)
71
+ ),
72
+ simple_symbol.map do |sym|
73
+ "param_#{sym}".to_sym
74
+ end
75
+ )
76
+ )
77
+ ) do |parameters|
78
+ # params now have proper names (no :"", no Ruby keywords) due to the .map in the generator above
79
+ unique_names = parameters.uniq { |v| v[1] }
80
+ single_args_and_kwargs = unique_names.uniq do |v|
81
+ if [:rest, :keyrest].include?(v[0])
82
+ v[0]
83
+ else
84
+ v[1]
85
+ end
86
+ end
87
+
88
+ mp = Memoized::Parameters.new(single_args_and_kwargs)
89
+ puts mp.debug_info if ENV['DEBUG'] == 'true'
90
+
91
+ eval(<<-RUBY)
92
+ class MemoizedPropertyClass
93
+ include Memoized
94
+
95
+ def parameter_dummy(#{mp.signature})
96
+ 42
97
+ end
98
+
99
+ @old_parameters = new.method(:parameter_dummy).parameters
100
+ memoize :parameter_dummy
101
+ @new_parameters = new.method(:parameter_dummy).parameters
102
+
103
+ # cleanup to get rid of warnings
104
+ remove_method :_unmemoized_parameter_dummy
105
+ remove_method :parameter_dummy
106
+ end
107
+ RUBY
108
+
109
+ expect(MemoizedPropertyClass.instance_variable_get(:@new_parameters))
110
+ .to eq(MemoizedPropertyClass.instance_variable_get(:@old_parameters))
111
+ end
112
+ end
113
+
114
+ it "does not change the method's value" do
115
+ forall(
116
+ array(
117
+ tuple(
118
+ one_of(
119
+ constant(:req), constant(:opt), constant(:rest), constant(:keyreq), constant(:key), constant(:keyrest)
120
+ ),
121
+ simple_symbol.map do |sym|
122
+ "param_#{sym}".to_sym
123
+ end
124
+ )
125
+ )
126
+ ) do |parameters|
127
+ # params now have proper names (no :"", no Ruby keywords) due to the .map in the generator above
128
+ unique_names = parameters.uniq { |v| v[1] }
129
+ single_args_and_kwargs = unique_names.uniq do |v|
130
+ if [:rest, :keyrest].include?(v[0])
131
+ v[0]
132
+ else
133
+ v[1]
134
+ end
135
+ end
136
+
137
+ mp = Memoized::Parameters.new(single_args_and_kwargs)
138
+ puts mp.debug_info if ENV['DEBUG'] == 'true'
139
+
140
+ eval(<<-RUBY)
141
+ class MemoizedPropertyClass
142
+ include Memoized
143
+
144
+ def parameter_dummy(#{mp.signature})
145
+ #{mp.test_body}
146
+ end
147
+
148
+ @old_value = new.parameter_dummy(#{mp.test_arguments})
149
+ memoize :parameter_dummy
150
+ @new_value = new.parameter_dummy(#{mp.test_arguments})
151
+
152
+ # cleanup to get rid of warnings
153
+ remove_method :_unmemoized_parameter_dummy
154
+ remove_method :parameter_dummy
155
+ end
156
+ RUBY
157
+
158
+ expect(MemoizedPropertyClass.instance_variable_get(:@new_value))
159
+ .to eq(MemoizedPropertyClass.instance_variable_get(:@old_value))
160
+ end
161
+ end
162
+
163
+ it "computes the correct value" do
164
+ forall(array(choose(15), min: 6, max: 6)) do |parameter_multiplicities|
165
+
166
+ params = []
167
+
168
+ parameter_multiplicities[0].times.with_index do |counter|
169
+ params << [:req, "required_#{counter}".to_sym]
170
+ end
171
+
172
+ parameter_multiplicities[1].times.with_index do |counter|
173
+ params << [:opt, "optional_#{counter}".to_sym]
174
+ end
175
+
176
+ if parameter_multiplicities[2] != 0
177
+ params << [:rest, :args]
178
+ end
179
+
180
+ parameter_multiplicities[3].times.with_index do |counter|
181
+ params << [:keyreq, "required_kw_#{counter}".to_sym]
182
+ end
183
+
184
+ parameter_multiplicities[4].times.with_index do |counter|
185
+ params << [:key, "optional_kw_#{counter}".to_sym]
186
+ end
187
+
188
+ if parameter_multiplicities[5] != 0
189
+ params << [:keyrest, :kwargs]
190
+ end
191
+
192
+ mp = Memoized::Parameters.new(params, parameter_multiplicities[2], parameter_multiplicities[5])
193
+
194
+ puts mp.debug_info if ENV['DEBUG'] == 'true'
195
+
196
+ eval(<<-RUBY)
197
+ class MemoizedPropertyClass
198
+ include Memoized
199
+
200
+ def parameter_dummy(#{mp.signature})
201
+ #{mp.test_body}
202
+ end
203
+
204
+ memoize :parameter_dummy
205
+ @value = new.parameter_dummy(#{mp.test_arguments})
206
+
207
+ # cleanup to get rid of warnings
208
+ remove_method :_unmemoized_parameter_dummy
209
+ remove_method :parameter_dummy
210
+ end
211
+ RUBY
212
+
213
+ expected_result = [2, 3, 5, 7, 11, 13].zip(parameter_multiplicities).map do |base, exponent|
214
+ base ** exponent
215
+ end.inject(&:*)
216
+
217
+ expect(MemoizedPropertyClass.instance_variable_get(:@value))
218
+ .to eq(expected_result)
219
+ end
220
+ end
221
+ end
222
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,9 +1,88 @@
1
1
  require 'date'
2
2
  require 'timecop'
3
3
  require 'memoized'
4
+ if Gem::Version.new(RUBY_VERSION) > Gem::Version.new('2.5.3')
5
+ require 'prop_check'
6
+ end
4
7
 
5
8
  RSpec.configure do |config|
6
9
  config.warnings = true
7
10
  config.order = :random
8
11
  Kernel.srand config.seed
9
12
  end
13
+
14
+ module Memoized
15
+ class Parameters
16
+ attr_accessor :args_count, :kwargs_count
17
+
18
+ def test_body
19
+ params.map(&Parameters.method(:to_test_body)).join(" * ")
20
+ end
21
+
22
+ def self.to_test_body((param_type, param_name))
23
+ case param_type
24
+ when :req
25
+ "#{param_name}"
26
+ when :opt
27
+ "#{param_name}"
28
+ when :rest
29
+ "#{param_name}.inject(&:*)"
30
+ when :keyreq
31
+ "#{param_name}"
32
+ when :key
33
+ "#{param_name}"
34
+ when :keyrest
35
+ "#{param_name}.values.inject(&:*)"
36
+ else raise "unknown parameter type"
37
+ end
38
+ end
39
+
40
+ def test_arguments
41
+ params.map(&Parameters.new([], @args_count, @kwargs_count).method(:to_test_arguments)).join(", ")
42
+ end
43
+
44
+ def to_test_arguments((param_type, param_name))
45
+ case param_type
46
+ when :req
47
+ "2"
48
+ when :opt
49
+ "3"
50
+ when :rest
51
+ if @args_count.nil?
52
+ "5, 5, 5"
53
+ else
54
+ (["5"] * @args_count).join(', ')
55
+ end
56
+ when :keyreq
57
+ "#{param_name}: 7"
58
+ when :key
59
+ "#{param_name}: 11"
60
+ when :keyrest
61
+ if @kwargs_count.nil?
62
+ "**{ first: 13, second: 13 }"
63
+ else
64
+ kwargs_list = (1..@kwargs_count).map.with_index do |counter|
65
+ "kwarg_#{counter}: 13"
66
+ end
67
+ "**{ #{kwargs_list.join(', ')} }"
68
+ end
69
+ else raise "unknown parameter type"
70
+ end
71
+ end
72
+
73
+ def debug_info
74
+ "#{@req_params.size} - #{@opt_params.size} - #{@args_count || @rest_params.size} " \
75
+ "| #{@keyreq_params.size} - #{@key_params.size} - #{@kwargs_count || @keyrest_params.size}"
76
+ end
77
+ end
78
+ end
79
+
80
+ module ParametersTestExtension
81
+ def initialize(parameters = [], args_count = nil, kwargs_count = nil)
82
+ super(parameters)
83
+ @args_count = args_count
84
+ @kwargs_count = kwargs_count
85
+ end
86
+ end
87
+
88
+ Memoized::Parameters.prepend(ParametersTestExtension)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: memoized
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Barun Singh
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2019-02-27 00:00:00.000000000 Z
12
+ date: 2022-06-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -53,16 +53,44 @@ dependencies:
53
53
  - - "~>"
54
54
  - !ruby/object:Gem::Version
55
55
  version: 0.8.0
56
+ - !ruby/object:Gem::Dependency
57
+ name: prop_check
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: 0.14.1
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: 0.14.1
70
+ - !ruby/object:Gem::Dependency
71
+ name: gemika
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
56
84
  description: Memoized caches the results of your method calls
57
85
  email:
58
86
  executables: []
59
87
  extensions: []
60
88
  extra_rdoc_files: []
61
89
  files:
90
+ - ".github/workflows/test.yml"
62
91
  - ".gitignore"
63
92
  - ".rspec"
64
93
  - ".ruby-version"
65
- - ".travis.yml"
66
94
  - CHANGELOG.md
67
95
  - Gemfile
68
96
  - Gemfile.lock
@@ -70,14 +98,20 @@ files:
70
98
  - README.md
71
99
  - Rakefile
72
100
  - lib/memoized.rb
101
+ - lib/memoized/parameters.rb
73
102
  - lib/memoized/version.rb
74
103
  - memoized.gemspec
75
104
  - spec/memoized_spec.rb
105
+ - spec/properties_spec.rb
76
106
  - spec/spec_helper.rb
77
107
  homepage: https://github.com/makandra/memoized
78
108
  licenses:
79
109
  - MIT
80
- metadata: {}
110
+ metadata:
111
+ source_code_uri: https://github.com/makandra/memoized
112
+ bug_tracker_uri: https://github.com/makandra/memoized/issues
113
+ changelog_uri: https://github.com/makandra/memoized/blob/master/CHANGELOG.md
114
+ rubygems_mfa_required: 'true'
81
115
  post_install_message:
82
116
  rdoc_options: []
83
117
  require_paths:
@@ -93,7 +127,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
93
127
  - !ruby/object:Gem::Version
94
128
  version: '0'
95
129
  requirements: []
96
- rubygems_version: 3.0.1
130
+ rubygems_version: 3.0.3.1
97
131
  signing_key:
98
132
  specification_version: 4
99
133
  summary: Memoized caches the results of your method calls
data/.travis.yml DELETED
@@ -1,26 +0,0 @@
1
- language: ruby
2
-
3
- rvm:
4
- - 2.1.8
5
- - 2.3.8
6
- - 2.4.5
7
- - 2.5.3
8
- - 2.6.1
9
-
10
- gemfile:
11
- - Gemfile
12
-
13
- script: bundle exec rspec spec
14
-
15
- sudo: false
16
-
17
- cache: bundler
18
-
19
- notifications:
20
- email:
21
- - fail@makandra.de
22
-
23
- install:
24
- # Replace default Travis CI bundler script with a version that doesn't
25
- # explode when lockfile doesn't match recently bumped version
26
- - bundle install --no-deployment --jobs=3 --retry=3 --path=${BUNDLE_PATH:-vendor/bundle}