Skip to content

Commit 65cc985

Browse files
Allow memoization when included modules prepend MemoWise and define initialize
Fixes #302 Co-authored-by: alpaca-tc <alpaca-tc@alpaca.tc>
1 parent 13f7ae7 commit 65cc985

File tree

4 files changed

+132
-27
lines changed

4 files changed

+132
-27
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
99

1010
**Gem enhancements:** none
1111

12+
- Allow memoization when `include`d modules `prepend MemoWise` and define `initialize` [[#327]](https://github.com/panorama-ed/memo_wise/pull/327)
13+
1214
_No breaking changes!_
1315

1416
**Project enhancements:** none

lib/memo_wise.rb

+16-8
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,9 @@ module MemoWise
5353
# [this article](https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/)
5454
# for more information.
5555
#
56+
5657
# :nocov:
57-
all_args = RUBY_VERSION < "2.7" ? "*" : "..."
58-
# :nocov:
59-
class_eval <<~HEREDOC, __FILE__, __LINE__ + 1
58+
INITIALIZE_LITERAL = <<~HEREDOC
6059
# On Ruby 2.7 or greater:
6160
#
6261
# def initialize(...)
@@ -71,11 +70,14 @@ module MemoWise
7170
# super
7271
# end
7372
74-
def initialize(#{all_args})
73+
def initialize(#{RUBY_VERSION < '2.7' ? '*' : '...'})
7574
MemoWise::InternalAPI.create_memo_wise_state!(self)
7675
super
7776
end
7877
HEREDOC
78+
# :nocov:
79+
80+
class_eval(INITIALIZE_LITERAL, __FILE__, __LINE__ + 1)
7981

8082
module CreateMemoWiseStateOnExtended
8183
def extended(base)
@@ -93,6 +95,13 @@ def inherited(subclass)
9395
end
9496
private_constant(:CreateMemoWiseStateOnInherited)
9597

98+
module CreateMemoWiseStateOnIncluded
99+
def included(base)
100+
base.prepend(MemoWise)
101+
end
102+
end
103+
private_constant(:CreateMemoWiseStateOnIncluded)
104+
96105
# @private
97106
#
98107
# Private setup method, called automatically by `prepend MemoWise` in a class.
@@ -154,10 +163,7 @@ def memo_wise(method_name_or_hash)
154163
klass.singleton_class.prepend(CreateMemoWiseStateOnExtended)
155164
end
156165
when Hash
157-
unless method_name_or_hash.keys == [:self]
158-
raise ArgumentError,
159-
"`:self` is the only key allowed in memo_wise"
160-
end
166+
raise ArgumentError, "`:self` is the only key allowed in memo_wise" unless method_name_or_hash.keys == [:self]
161167

162168
method_name = method_name_or_hash[:self]
163169

@@ -175,6 +181,8 @@ def memo_wise(method_name_or_hash)
175181
if klass.is_a?(Class) && !klass.singleton_class?
176182
klass.singleton_class.prepend(CreateMemoWiseStateOnInherited)
177183
else
184+
klass.singleton_class.prepend(CreateMemoWiseStateOnIncluded) if klass.is_a?(Module) && !klass.singleton_class?
185+
178186
klass.prepend(CreateMemoWiseStateOnInherited)
179187
end
180188

spec/memo_wise_spec.rb

+47
Original file line numberDiff line numberDiff line change
@@ -351,11 +351,32 @@ def module2_method
351351
end
352352
end
353353

354+
let(:klass_with_initializer) do
355+
Class.new do
356+
include Module1
357+
def initialize(*); end
358+
end
359+
end
360+
361+
let(:module_with_initializer) do
362+
Module.new do
363+
include Module1
364+
def initialize(*); end
365+
end
366+
end
367+
368+
let(:klass_with_module_with_initializer) do
369+
Class.new do
370+
include Module3
371+
end
372+
end
373+
354374
let(:instance) { klass.new }
355375

356376
before(:each) do
357377
stub_const("Module1", module1)
358378
stub_const("Module2", module2)
379+
stub_const("Module3", module_with_initializer)
359380
end
360381

361382
it "memoizes inherited methods separately" do
@@ -364,6 +385,32 @@ def module2_method
364385
expect(Array.new(4) { instance.module2_method }).to all eq("module2_method")
365386
expect(instance.module2_method_counter).to eq(1)
366387
end
388+
389+
it "can memoize klass with initializer" do
390+
instance = klass_with_initializer.new(true)
391+
expect { instance.module1_method }.not_to raise_error
392+
393+
expect(Array.new(4) { instance.module1_method }).to all eq("module1_method")
394+
expect(instance.module1_method_counter).to eq(1)
395+
end
396+
397+
it "can memoize klass with module with initializer" do
398+
instance = klass_with_module_with_initializer.new(true)
399+
expect { instance.module1_method }.not_to raise_error
400+
401+
expect(Array.new(4) { instance.module1_method }).to all eq("module1_method")
402+
expect(instance.module1_method_counter).to eq(1)
403+
end
404+
405+
it "can reset klass with initializer" do
406+
instance = klass_with_initializer.new(true)
407+
expect { instance.reset_memo_wise }.not_to raise_error
408+
end
409+
410+
it "can reset klass with module with initializer" do
411+
instance = klass_with_module_with_initializer.new(true)
412+
expect { instance.reset_memo_wise }.not_to raise_error
413+
end
367414
end
368415

369416
context "when the class, its superclass, and its module all memoize methods" do

spec/proxying_original_method_params_spec.rb

+67-19
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,10 @@
11
# frozen_string_literal: true
22

33
RSpec.describe "proxying original method params" do # rubocop:disable RSpec/DescribeClass
4-
describe ".instance_method" do
4+
shared_examples ".instance_method proxies parameters" do
55
subject { unbound_method&.parameters }
66

7-
let(:unbound_method) { class_with_memo.instance_method(method_name) }
8-
9-
let(:class_with_memo) do
10-
Class.new do
11-
prepend MemoWise
12-
13-
def initialize(foo, bar:); end # rubocop:disable Style/RedundantInitialize
14-
15-
DefineMethodsForTestingMemoWise.define_methods_for_testing_memo_wise(
16-
target: self,
17-
via: :instance
18-
)
19-
20-
def unmemoized_with_positional_and_keyword_args(a, b:) # rubocop:disable Naming/MethodParameterName
21-
[a, b]
22-
end
23-
end
24-
end
7+
let(:unbound_method) { target.instance_method(method_name) }
258

269
context "when #initialize" do
2710
let(:method_name) { :initialize }
@@ -86,4 +69,69 @@ def unmemoized_with_positional_and_keyword_args(a, b:) # rubocop:disable Naming/
8669
end
8770
end
8871
end
72+
73+
context "when class prepends MemoWise" do
74+
let(:target) do
75+
Class.new do
76+
prepend MemoWise
77+
78+
def initialize(foo, bar:); end
79+
80+
DefineMethodsForTestingMemoWise.define_methods_for_testing_memo_wise(
81+
target: self,
82+
via: :instance
83+
)
84+
85+
def unmemoized_with_positional_and_keyword_args(a, b:) # rubocop:disable Naming/MethodParameterName
86+
[a, b]
87+
end
88+
end
89+
end
90+
91+
it_behaves_like ".instance_method proxies parameters"
92+
end
93+
94+
context "module which prepends MemoWise" do
95+
let(:module1) do
96+
Module.new do
97+
prepend MemoWise
98+
99+
def initialize(foo, bar:); end
100+
101+
DefineMethodsForTestingMemoWise.define_methods_for_testing_memo_wise(
102+
target: self,
103+
via: :instance
104+
)
105+
106+
def unmemoized_with_positional_and_keyword_args(a, b:) # rubocop:disable Naming/MethodParameterName
107+
[a, b]
108+
end
109+
end
110+
end
111+
112+
before(:each) { stub_const("Module1", module1) }
113+
114+
context "when class includes module" do
115+
let(:target) do
116+
Class.new do
117+
include Module1
118+
def initialize(foo, bar:); end
119+
end
120+
end
121+
122+
it_behaves_like ".instance_method proxies parameters"
123+
end
124+
125+
context "when class prepends MemoWise and includes module" do
126+
let(:target) do
127+
Class.new do
128+
prepend MemoWise
129+
include Module1
130+
def initialize(foo, bar:); end
131+
end
132+
end
133+
134+
it_behaves_like ".instance_method proxies parameters"
135+
end
136+
end
89137
end

0 commit comments

Comments
 (0)