diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..870ebe4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + push: + branches: [ master ] + pull_request: + +jobs: + tests: + name: Tests + + runs-on: ubuntu-latest + strategy: + matrix: + ruby-version: ['3.0', '3.2'] + + steps: + - name: Check out repository + uses: actions/checkout@v2 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler: 2.3 + bundler-cache: true + env: + BUNDLE_GITHUB__COM: ${{ secrets.BUNDLE_GITHUB__COM }}:x-oauth-basic + - name: Run tests + run: bundle exec rake diff --git a/.gitignore b/.gitignore index d87d4be..8f994bd 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ .bundle .config .yardoc -Gemfile.lock InstalledFiles _yardoc coverage diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2d505a8..0000000 --- a/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -sudo: false -cache: bundler -language: ruby -rvm: - - 1.9.2 - - 1.9.3 - - 2.0.0 - - 2.1.10 - - 2.2.7 - - 2.3.4 - - 2.4.1 - - 2.5.1 - - ruby-head - - jruby-19mode - - jruby-9.1.5.0 - - jruby-head - -before_install: - - gem install bundler --no-document -v '~> 1.13' -before_script: - - unset JRUBY_OPTS diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..e2bc09f --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,24 @@ +PATH + remote: . + specs: + memoist (0.16.0) + +GEM + remote: https://rubygems.org/ + specs: + benchmark-ips (2.11.0) + minitest (5.18.0) + rake (13.0.6) + +PLATFORMS + x86_64-linux + +DEPENDENCIES + benchmark-ips + bundler + memoist! + minitest (~> 5.10) + rake + +BUNDLED WITH + 2.4.3 diff --git a/lib/memoist.rb b/lib/memoist.rb index 54b67e6..6b561a4 100644 --- a/lib/memoist.rb +++ b/lib/memoist.rb @@ -50,11 +50,11 @@ def self.escape_punctuation(string) string end - def self.memoist_eval(klass, *args, &block) + def self.memoist_eval(klass, *args, **kwargs, &block) if klass.respond_to?(:class_eval) - klass.class_eval(*args, &block) + klass.class_eval(*args, **kwargs, &block) else - klass.singleton_class.class_eval(*args, &block) + klass.singleton_class.class_eval(*args, **kwargs, &block) end end @@ -132,8 +132,9 @@ def memoize(*method_names) include InstanceMethods if method_defined?(unmemoized_method) - warn "Already memoized #{method_name}" - return + default = "AnIdentifier" + suggestion = respond_to?(:name) ? name || default : default + raise "Already memoized :#{method_name}. Try `memoize :#{method_name}, identifier: '#{suggestion}'` or use `@#{method_name} ||= compute` pattern instead." end alias_method unmemoized_method, method_name @@ -203,21 +204,21 @@ def #{method_name}(reload = false) # end module_eval <<-EOS, __FILE__, __LINE__ + 1 - def #{method_name}(*args) + def #{method_name}(*args, **kwargs) 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)) + skip_cache = reload || !(instance_variable_defined?(#{memoized_ivar.inspect}) && #{memoized_ivar} && #{memoized_ivar}.has_key?(args+kwargs.to_a)) set_cache = skip_cache && !frozen? if skip_cache - value = #{unmemoized_method}(*args) + value = #{unmemoized_method}(*args, **kwargs) else - value = #{memoized_ivar}[args] + value = #{memoized_ivar}[args+kwargs.to_a] end if set_cache #{memoized_ivar} ||= {} - #{memoized_ivar}[args] = value + #{memoized_ivar}[args+kwargs.to_a] = value end value diff --git a/test/memoist_test.rb b/test/memoist_test.rb index 49a09d0..9b18aa5 100644 --- a/test/memoist_test.rb +++ b/test/memoist_test.rb @@ -87,6 +87,26 @@ def update_attributes_calls @counter.count(:update_attributes) end + def do_with_special(_regular = 10, special_one: true, special_two: true) + @counter.call(:do_with_special) + true + end + memoize :do_with_special + + def do_with_special_calls + @counter.count(:do_with_special) + end + + def format_metadata(metadata) + @counter.call(:format_metadata) + "#{metadata[:grade]}: #{metadata[:comment]}" + end + memoize :format_metadata + + def format_metadata_calls + @counter.count(:format_metadata) + end + protected def memoize_protected_test @@ -265,6 +285,35 @@ def test_memoize_with_options_hash assert_equal 4, @person.update_attributes_calls end + def test_memoize_with_kwargs + assert_equal true, @person.do_with_special(1, special_one: true) + assert_equal 1, @person.do_with_special_calls + + 3.times { assert_equal true, @person.do_with_special(1, special_one: true) } + assert_equal 1, @person.do_with_special_calls + + assert_equal true, @person.do_with_special(2) + assert_equal 2, @person.do_with_special_calls + + assert_equal true, @person.do_with_special(1, special_one: false) + assert_equal 3, @person.do_with_special_calls + + assert_equal true, @person.do_with_special(1, special_two: false) + assert_equal 4, @person.do_with_special_calls + end + + def test_memoize_with_hash_for_ruby3 + metadata = { grade: 42, comment: 'The meaning.' } + assert_equal '42: The meaning.', @person.format_metadata(metadata) + assert_equal 1, @person.format_metadata_calls + + assert_equal '42: The meaning.', @person.format_metadata(metadata) + assert_equal 1, @person.format_metadata_calls + + assert_equal '44: The name.', @person.format_metadata({ grade: 44, comment: 'The name.'}) + assert_equal 2, @person.format_metadata_calls + end + def test_memoization_with_punctuation assert_equal true, @person.name? @@ -348,7 +397,7 @@ def test_all_memoized_structs # Student < Person memoize :name, :identifier => :student # Teacher < Person memoize :seniority - expected = %w[age is_developer? memoize_protected_test name name? sleep update update_attributes] + expected = %w[age do_with_special format_metadata is_developer? memoize_protected_test name name? sleep update update_attributes] 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 @@ -547,4 +596,35 @@ def test_private_method_memoization assert_equal 'Yes', person.send(:is_developer?) assert_equal 1, person.is_developer_calls end + + def test_exception_when_subclass_without_identifier + err = assert_raises(StandardError) do + Class.new(Person) do + def self.name + "Staff" + end + def name + 'Overwritten' + end + memoize :name + end + end + assert_equal( + "Already memoized :name. Try `memoize :name, identifier: 'Staff'` or use `@name ||= compute` pattern instead.", + err.message + ) + + err = assert_raises(StandardError) do + Class.new(Person) do + def name + 'Overwritten' + end + memoize :name + end + end + assert_equal( + "Already memoized :name. Try `memoize :name, identifier: 'AnIdentifier'` or use `@name ||= compute` pattern instead.", + err.message + ) + end end