Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for ruby 3 keywork args #95

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ jobs:
- ruby: 2.4.1
- ruby: 2.5.1
- ruby: 2.6.0
- ruby: 2.7.0
- ruby: 3.0.0
- ruby: ruby-head
- ruby: jruby-d19
- ruby: jruby-9.1.9.0
Expand Down
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,22 +104,35 @@ Each memoized function comes with a way to flush the existing value.

```ruby
person.social_security # returns the memoized value
person.social_security(true) # bypasses the memoized value and rememoizes it
person.social_security(true) # Same for rubies >= 3.0.0
```

This also works with a memoized method with arguments

```ruby
person.taxes_due(100_000) # returns the memoized value
person.taxes_due(100_000, true) # bypasses the memoized value and rememoizes it
person.taxes_due(100_000, reload_memoize: true) # Same for rubies >= 3.0.0
```

Ruby 3 separates keyword arguments from positional arguments, which comes
in cost of some incompatibility.
```ruby
person.update_attributes(age: 21, name: 'James')
# Works for rubies < 3.0.0, not works for rubies >= 3.0.0
person.update_attributes({age: 21, name: 'James'}, :reload)
# For rubies >= 3.0.0
person.update_attributes(age: 21, name: 'James', reload_memoize: true)
```


If you want to flush the entire memoization cache for an object

```ruby
person.flush_cache # returns an array of flushed memoized methods, e.g. ["social_security", "some_method"]
```


# Authors

Everyone who contributed to it in the rails repository.
Expand Down
67 changes: 49 additions & 18 deletions lib/memoist.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,11 @@ def self.memoist_eval(klass, *args, &block)
end
end

def self.extract_reload!(method, args)
def self.extract_reload!(method, args, kwargs={})
if args.length == method.arity.abs + 1 && (args.last == true || args.last == :reload)
reload = args.pop
elsif !kwargs.nil?
reload = kwargs.delete(:reload_memoize)
end
reload
end
Expand Down Expand Up @@ -202,27 +204,51 @@ def #{method_name}(reload = false)
# value
# end

module_eval <<-EOS, __FILE__, __LINE__ + 1
def #{method_name}(*args)
reload = Memoist.extract_reload!(method(#{unmemoized_method.inspect}), args)
if Memoist.ruby_3_kw_args?
module_eval <<-EOS, __FILE__, __LINE__ + 1
def #{method_name}(*args, **kwargs)
reload = Memoist.extract_reload!(method(#{unmemoized_method.inspect}), args, kwargs)

skip_cache = reload || !(instance_variable_defined?(#{memoized_ivar.inspect}) && #{memoized_ivar} && #{memoized_ivar}.has_key?(args))
set_cache = skip_cache && !frozen?
skip_cache = reload || !(instance_variable_defined?(#{memoized_ivar.inspect}) && #{memoized_ivar} && #{memoized_ivar}.has_key?([args, kwargs]))
set_cache = skip_cache && !frozen?

if skip_cache
value = #{unmemoized_method}(*args)
else
value = #{memoized_ivar}[args]
end
if skip_cache
value = #{unmemoized_method}(*args, **kwargs)
else
value = #{memoized_ivar}[[args, kwargs]]
end

if set_cache
#{memoized_ivar} ||= {}
#{memoized_ivar}[args] = value
end
if set_cache
#{memoized_ivar} ||= {}
#{memoized_ivar}[[args, kwargs]] = value
end

value
end
EOS
value
end
EOS
else
module_eval <<-EOS, __FILE__, __LINE__ + 1
def #{method_name}(*args)
reload = Memoist.extract_reload!(method(#{unmemoized_method.inspect}), args)

skip_cache = reload || !(instance_variable_defined?(#{memoized_ivar.inspect}) && #{memoized_ivar} && #{memoized_ivar}.has_key?(args))
set_cache = skip_cache && !frozen?

if skip_cache
value = #{unmemoized_method}(*args)
else
value = #{memoized_ivar}[args]
end

if set_cache
#{memoized_ivar} ||= {}
#{memoized_ivar}[args] = value
end

value
end
EOS
end
end

if private_method_defined?(unmemoized_method)
Expand All @@ -235,4 +261,9 @@ def #{method_name}(*args)
# return a chainable method_name symbol if we can
method_names.length == 1 ? method_names.first : method_names
end


def self.ruby_3_kw_args?
RUBY_VERSION.split('.', 2).first.to_i >= 3
end
end
44 changes: 39 additions & 5 deletions test/memoist_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ def update_attributes_calls
@counter.count(:update_attributes)
end

def update_attributes_kw(name: nil, age: nil)
@counter.call(:update_attributes_kw)
true
end
memoize :update_attributes_kw

def update_attributes_kw_calls
@counter.count(:update_attributes_kw)
end

protected

def memoize_protected_test
Expand Down Expand Up @@ -258,6 +268,11 @@ def test_memoize_with_optional_arguments

3.times { assert_equal 4, @person.sleep(4, :reload) }
assert_equal 4, @person.sleep_calls

if Memoist.ruby_3_kw_args?
3.times { assert_equal 4, @person.sleep(4, reload_memoize: true) }
assert_equal 7, @person.sleep_calls
end
end

def test_memoize_with_options_hash
Expand All @@ -269,8 +284,27 @@ def test_memoize_with_options_hash

3.times { assert_equal true, @person.update_attributes({ age: 21, name: 'James' }, :reload) }
assert_equal 4, @person.update_attributes_calls

if Memoist.ruby_3_kw_args?
3.times { assert_equal true, @person.update_attributes({ age: 21, name: 'James' }, reload_memoize: true) }
assert_equal 7, @person.update_attributes_calls
end
end

def test_memoize_with_kw_args
assert_equal true, @person.update_attributes_kw(age: 21, name: 'James')
assert_equal 1, @person.update_attributes_kw_calls

3.times { assert_equal true, @person.update_attributes_kw(age: 21, name: 'James') }
assert_equal 1, @person.update_attributes_kw_calls

if Memoist.ruby_3_kw_args?
3.times { assert_equal true, @person.update_attributes_kw(age: 21, name: 'James', reload_memoize: true) }
assert_equal 4, @person.update_attributes_kw_calls
end
end


def test_memoization_with_punctuation
assert_equal true, @person.name?

Expand Down Expand Up @@ -357,11 +391,11 @@ def test_unmemoize_all
end

def test_all_memoized_structs
# Person memoize :age, :age?, :is_developer?, :memoize_protected_test, :name, :name?, :sleep, :update, :update_attributes
# Person memoize :age, :age?, :is_developer?, :memoize_protected_test, :name, :name?, :sleep, :update, :update_attributes, :update_attributes_kw
# Student < Person memoize :name, :identifier => :student
# Teacher < Person memoize :seniority

expected = %w[age age? is_developer? memoize_protected_test name name? sleep update update_attributes]
expected = %w[age age? is_developer? memoize_protected_test name name? sleep update update_attributes update_attributes_kw]
structs = Person.all_memoized_structs
assert_equal expected, structs.collect(&:memoized_method).collect(&:to_s).sort
assert_equal '@_memoized_name', structs.detect { |s| s.memoized_method == :name }.ivar
Expand All @@ -378,7 +412,7 @@ def test_all_memoized_structs
end

def test_unmemoize_all_subclasses
# Person memoize :age, :is_developer?, :memoize_protected_test, :name, :name?, :sleep, :update, :update_attributes
# Person memoize :age, :is_developer?, :memoize_protected_test, :name, :name?, :sleep, :update, :update_attributes, :update_attributes_kw
# Student < Person memoize :name, :identifier => :student
# Teacher < Person memoize :seniority

Expand Down Expand Up @@ -407,7 +441,7 @@ def test_memoize_all
end

def test_memoize_all_subclasses
# Person memoize :age, :is_developer?, :memoize_protected_test, :name, :name?, :sleep, :update, :update_attributes
# Person memoize :age, :is_developer?, :memoize_protected_test, :name, :name?, :sleep, :update, :update_attributes, :update_attributes_kw
# Student < Person memoize :name, :identifier => :student
# Teacher < Person memoize :seniority

Expand Down Expand Up @@ -500,7 +534,7 @@ def test_object_memoized_module_methods
end

def test_double_memoization_with_identifier
# Person memoize :age, :is_developer?, :memoize_protected_test, :name, :name?, :sleep, :update, :update_attributes
# Person memoize :age, :is_developer?, :memoize_protected_test, :name, :name?, :sleep, :update, :update_attributes, :update_attributes_kw
# Student < Person memoize :name, :identifier => :student
# Teacher < Person memoize :seniority

Expand Down