Full Code of tycooon/memery for AI

master 63b2fec75a3c cached
15 files
27.6 KB
8.7k tokens
57 symbols
1 requests
Download .txt
Repository: tycooon/memery
Branch: master
Commit: 63b2fec75a3c
Files: 15
Total size: 27.6 KB

Directory structure:
gitextract_hubzy71a/

├── .editorconfig
├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .rspec
├── .rubocop.yml
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── benchmark.rb
├── lib/
│   ├── memery/
│   │   └── version.rb
│   └── memery.rb
├── memery.gemspec
└── spec/
    ├── memery_spec.rb
    └── spec_helper.rb

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    # We want to run on external PRs, but not on our own internal PRs as they'll be run on push event
    if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != 'tycooon/memery'

    strategy:
      fail-fast: false
      matrix:
        ruby: ["3.2", "3.3", "3.4"]

    name: ${{ matrix.ruby }}

    steps:
    - uses: actions/checkout@v4

    - uses: ruby/setup-ruby@v1
      with:
        ruby-version: ${{ matrix.ruby }}
        bundler-cache: true

    - run: bundle exec rake

    - uses: coverallsapp/github-action@v2
      with:
        github-token: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .gitignore
================================================
/.bundle/
/.yardoc
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/
.ruby-version

# rspec failure tracking
.rspec_status


================================================
FILE: .rspec
================================================
--format documentation
--color
--require spec_helper
--warnings


================================================
FILE: .rubocop.yml
================================================
inherit_gem:
  rubocop-config-umbrellio: lib/rubocop.yml

AllCops:
  DisplayCopNames: true
  TargetRubyVersion: 3.2

Naming/MethodParameterName:
  AllowedNames: ["x", "y", "z"]

RSpec/EmptyLineAfterHook:
  Enabled: false


================================================
FILE: Gemfile
================================================
# frozen_string_literal: true

source "https://rubygems.org"
gemspec

gem "activesupport"
gem "benchmark-ips"
gem "benchmark-memory"
gem "bundler"
gem "pry"
gem "rake"
gem "rspec"
gem "rubocop-config-umbrellio"
gem "simplecov"
gem "simplecov-lcov"


================================================
FILE: LICENSE.txt
================================================
The MIT License (MIT)

Copyright (c) 2017 Yuri Smirnov

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


================================================
FILE: README.md
================================================
# Memery   [![Gem Version](https://badge.fury.io/rb/memery.svg)](https://badge.fury.io/rb/memery) ![Build Status](https://github.com/tycooon/memery/actions/workflows/ci.yml/badge.svg) [![Coverage Status](https://coveralls.io/repos/github/tycooon/memery/badge.svg?branch=master)](https://coveralls.io/github/tycooon/memery?branch=master)

Memery is a Ruby gem that simplifies memoization of method return values. In Ruby, memoization typically looks like this:

```ruby
def user
  @user ||= User.find(some_id)
end
```

However, this approach fails if the calculated result can be `nil` or `false`, or if the method uses arguments. Additionally, multi-line methods require extra `begin`/`end` blocks:

```ruby
def user
  @user ||= begin
    some_id = calculate_id
    klass = calculate_klass
    klass.find(some_id)
  end
end
```

To handle these situations, memoization gems like Memery exist. The example above can be rewritten using Memery as follows:

```ruby
memoize def user
  some_id = calculate_id
  klass = calculate_klass
  klass.find(some_id)
end
```

## Installation

Add `gem "memery"` to your Gemfile.

## Usage

```ruby
class A
  include Memery

  memoize def call
    puts "calculating"
    42
  end

  # Alternatively:
  # def call
  #   ...
  # end
  # memoize :call
end

a = A.new
a.call # => 42
a.call # => 42
a.call # => 42
# "calculating" will only be printed once.

a.call { 1 } # => 42
# "calculating" will be printed again because passing a block disables memoization.
```

Memoization works with methods that take arguments. The memoization is based on these arguments using an internal hash, so the following will work as expected:

```ruby
class A
  include Memery

  memoize def call(arg1, arg2)
    puts "calculating"
    arg1 + arg2
  end
end

a = A.new
a.call(1, 5) # => 6
a.call(2, 15) # => 17
a.call(1, 5) # => 6
# "calculating" will be printed twice, once for each unique argument list.
```

For class methods:

```ruby
class B
  class << self
    include Memery

    memoize def call
      puts "calculating"
      42
    end
  end
end

B.call # => 42
B.call # => 42
B.call # => 42
# "calculating" will only be printed once.
```

### Conditional Memoization

```ruby
class A
  include Memery

  attr_accessor :environment

  def call
    puts "calculating"
    42
  end

  memoize :call, condition: -> { environment == 'production' }
end

a = A.new
a.environment = 'development'
a.call # => 42
# calculating
a.call # => 42
# calculating
a.call # => 42
# calculating
# Text will be printed every time because result of condition block is `false`.

a.environment = 'production'
a.call # => 42
# calculating
a.call # => 42
a.call # => 42
# Text will be printed only once because there is memoization
# with `true` result of condition block.
```

### Memoization with Time-to-Live (TTL)

```ruby
class A
  include Memery

  def call
    puts "calculating"
    42
  end

  memoize :call, ttl: 3 # seconds
end

a = A.new
a.call # => 42
# calculating
a.call # => 42
a.call # => 42
# Text will be printed again only after 3 seconds of time-to-live.
# 3 seconds later...
a.call # => 42
# calculating
a.call # => 42
a.call # => 42
# another 3 seconds later...
a.call # => 42
# calculating
a.call # => 42
a.call # => 42
```

### Checking if a Method is Memoized

```ruby
class A
  include Memery

  memoize def call
    puts "calculating"
    42
  end

  def execute
    puts "non-memoized"
  end
end

a = A.new

a.memoized?(:call) # => true
a.memoized?(:execute) # => false
```

### Marshal-compatible Memoization

In order for objects to be marshaled and loaded in a different Ruby process,
hashed arguments must be disabled in order for memoized values to be retained.
Note that this can have a performance impact if the memoized method contains
arguments.

```ruby
Memery.use_hashed_arguments = false

class A
  include Memery

  memoize def call
    puts "calculating"
    42
  end
end

a = A.new
a.call

Marshal.dump(a)
# => "\x04\bo:\x06A\x06:\x1D@_memery_memoized_values{\x06:\tcallS:3Memery::ClassMethods::MemoizationModule::Cache\a:\vresulti/:\ttimef\x14663237.14822323"

# ...in another Ruby process:
a = Marshal.load("\x04\bo:\x06A\x06:\x1D@_memery_memoized_values{\x06:\tcallS:3Memery::ClassMethods::MemoizationModule::Cache\a:\vresulti/:\ttimef\x14663237.14822323")
a.call # => 42
```

## Differences from Other Gems

Memery is similar to [Memoist](https://github.com/matthewrudy/memoist), but it doesn't override methods. Instead, it uses Ruby 2's `Module.prepend` feature. This approach is cleaner, allowing you to inspect the original method body with `method(:x).super_method.source`, and it ensures that subclasses' methods function properly. If you redefine a memoized method in a subclass, it won't be memoized by default. You can memoize it normally without needing an awkward `identifier: ` argument, and it will just work:

```ruby
class A
  include Memery

  memoize def x(param)
    param
  end
end

class B < A
  memoize def x(param)
    super(2) * param
  end
end

b = B.new
b.x(1) # => 2
b.x(2) # => 4
b.x(3) # => 6

b.instance_variable_get(:@_memery_memoized_values)
# => {:x_70318201388120=>{[1]=>2, [2]=>4, [3]=>6}, :x_70318184636620=>{[2]=>2}}
```

Note how both methods' return values are cached separately without interfering with each other.

Another key difference is that Memery doesn't change the method's signature (no extra `reload` parameter). If you need an unmemoized result, simply create an unmemoized version of the method:

```ruby
memoize def users
  get_users
end

def get_users
  # ...
end
```

Alternatively, you can clear the entire instance's cache:

```ruby
a.clear_memery_cache!
```

You can also provide a block, though this approach is somewhat hacky:

```ruby
a.users {}
```

## Object Shape Optimization

In Ruby 3.2, a new optimization called "object shape" was introduced, which can have negative interactions with dynamically added instance variables. Memery minimizes this impact by introducing only one new instance variable after initialization (`@_memery_memoized_values`). If you need to ensure a specific object shape, you can call `clear_memery_cache!` in your initializer to set the instance variable ahead of time.

## Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/tycooon/memery.

## License

The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).

## Author

Created by Yuri Smirnov.


================================================
FILE: Rakefile
================================================
# frozen_string_literal: true

require "bundler/gem_tasks"
require "rspec/core/rake_task"
require "rubocop/rake_task"

RSpec::Core::RakeTask.new(:spec)
RuboCop::RakeTask.new(:lint)

task default: %i[lint spec]

desc "run benchmark"
task :benchmark do
  require_relative "benchmark"
end


================================================
FILE: benchmark.rb
================================================
# frozen_string_literal: true

require "bundler/setup"
Bundler.setup

require "benchmark"
require "benchmark/ips"
require "benchmark/memory"

puts "```ruby"
puts File.read(__FILE__)
puts "```"
puts
puts "### Output"
puts
puts "```"

require_relative "lib/memery"

class Foo
  class << self
    include Memery

    def base_find(char)
      ("a".."k").find { |letter| letter == char }
    end

    memoize def find_z
      base_find("z")
    end

    memoize def find_new(char)
      base_find(char)
    end

    memoize def find_optional(*)
      base_find("z")
    end
  end
end

def test_no_args
  Foo.find_z
end

def test_with_args
  Foo.find_new("d")
end

def test_empty_args
  Foo.find_optional
end

Benchmark.ips do |x|
  x.report("test_no_args") { test_no_args }
end

Benchmark.memory do |x|
  x.report("test_no_args") { 100.times { test_no_args } }
end

Benchmark.ips do |x|
  x.report("test_empty_args") { test_empty_args }
end

Benchmark.memory do |x|
  x.report("test_empty_args") { 100.times { test_empty_args } }
end

Benchmark.ips do |x|
  x.report("test_with_args") { test_with_args }
end

Benchmark.memory do |x|
  x.report("test_with_args") { 100.times { test_with_args } }
end

Memery.use_hashed_arguments = false
Benchmark.ips do |x|
  x.report("test_with_args_no_hash") { test_with_args }
end

Benchmark.memory do |x|
  x.report("test_with_args_no_hash") { 100.times { test_with_args } }
end
Memery.use_hashed_arguments = true

puts "```"


================================================
FILE: lib/memery/version.rb
================================================
# frozen_string_literal: true

module Memery
  VERSION = "1.8.0"
end


================================================
FILE: lib/memery.rb
================================================
# frozen_string_literal: true

require "memery/version"

module Memery
  class << self
    attr_accessor :use_hashed_arguments

    def monotonic_clock
      Process.clock_gettime(Process::CLOCK_MONOTONIC)
    end
  end

  @use_hashed_arguments = true

  OUR_BLOCK = lambda do
    extend(ClassMethods)
    include(InstanceMethods)
    extend ModuleMethods if instance_of?(Module)
  end

  private_constant :OUR_BLOCK

  module ModuleMethods
    def included(base = nil, &block)
      if base.nil? && block
        super do
          instance_exec(&block)
          instance_exec(&OUR_BLOCK)
        end
      else
        base.instance_exec(&OUR_BLOCK)
      end
    end
  end

  extend ModuleMethods

  module ClassMethods
    def memoize(*method_names, condition: nil, ttl: nil)
      if method_names.empty?
        @_memery_memoize_next_method = { condition: condition, ttl: ttl }
        return
      end
      prepend_memery_module!
      method_names.each do |method_name|
        define_memoized_method!(method_name, condition: condition, ttl: ttl)
      end
      method_names.length > 1 ? method_names : method_names.first
    end

    def memoized?(method_name)
      return false unless defined?(@_memery_module)

      @_memery_module.method_defined?(method_name) ||
      @_memery_module.private_method_defined?(method_name)
    end

    def method_added(name)
      super

      return unless @_memery_memoize_next_method

      memoize(name, **@_memery_memoize_next_method)
      @_memery_memoize_next_method = nil
    end

    private

    def prepend_memery_module!
      return if defined?(@_memery_module)
      @_memery_module = Module.new { extend MemoizationModule }
      prepend(@_memery_module)
    end

    def define_memoized_method!(method_name, **)
      @_memery_module.define_memoized_method!(self, method_name, **)
    end

    module MemoizationModule
      Cache = Struct.new(:result, :time) do
        def fresh?(ttl)
          return true if ttl.nil?
          Memery.monotonic_clock <= time + ttl
        end
      end

      # rubocop:disable Metrics/MethodLength
      def define_memoized_method!(klass, method_name, condition: nil, ttl: nil)
        # Include a suffix in the method key to differentiate between methods of the same name
        # being memoized throughout a class inheritance hierarchy
        method_key = "#{method_name}_#{klass.name || object_id}"
        original_visibility = method_visibility(klass, method_name)

        define_method(method_name) do |*args, &block|
          if block || (condition && !instance_exec(&condition))
            return super(*args, &block)
          end

          cache_store = (@_memery_memoized_values ||= {})
          cache_key = if args.empty?
                        method_key
                      else
                        key_parts = [method_key, *args]
                        Memery.use_hashed_arguments ? key_parts.hash : key_parts
                      end
          cache = cache_store[cache_key]

          return cache.result if cache&.fresh?(ttl)

          result = super(*args)
          new_cache = Cache.new(result, Memery.monotonic_clock)
          cache_store[cache_key] = new_cache

          result
        end

        ruby2_keywords(method_name)
        send(original_visibility, method_name)
      end
      # rubocop:enable Metrics/MethodLength

      private

      def method_visibility(klass, method_name)
        if klass.private_method_defined?(method_name)
          :private
        elsif klass.protected_method_defined?(method_name)
          :protected
        elsif klass.public_method_defined?(method_name)
          :public
        else
          raise ArgumentError, "Method #{method_name} is not defined on #{klass}"
        end
      end
    end

    private_constant :MemoizationModule
  end

  module InstanceMethods
    def clear_memery_cache!
      @_memery_memoized_values = {}
    end
  end
end


================================================
FILE: memery.gemspec
================================================
# frozen_string_literal: true

lib = File.expand_path("lib", __dir__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require "memery/version"

Gem::Specification.new do |spec|
  spec.required_ruby_version = ">= 3.2.0"

  spec.name          = "memery"
  spec.version       = Memery::VERSION
  spec.authors       = ["Yuri Smirnov"]
  spec.email         = ["tycoooon@gmail.com"]

  spec.summary       = "A gem for memoization."
  spec.description   = "Memery is a gem for memoization."
  spec.homepage      = "https://github.com/tycooon/memery"
  spec.license       = "MIT"

  spec.files         = `git ls-files -z`.split("\x0").reject do |f|
    f.match(%r{^(test|spec|features)/})
  end
  spec.require_paths = ["lib"]
end


================================================
FILE: spec/memery_spec.rb
================================================
# frozen_string_literal: true

# rubocop:disable Style/MutableConstant
CALLS = []
B_CALLS = []
# rubocop:enable Style/MutableConstant

class A
  include Memery

  attr_accessor :environment

  memoize def m
    m_private
  end

  memoize
  def m_different_line
    CALLS << :m_different_line
    :m_different_line
  end

  def not_memoized; end

  memoize def m_nil
    m_protected
  end

  memoize def m_args(x, y)
    CALLS << [x, y]
    [x, y]
  end

  memoize def m_kwargs(x, y: 42)
    CALLS << [x, y]
    [x, y]
  end

  memoize def m_double_splat(x, **kwargs)
    CALLS << [x, kwargs]
    [x, kwargs]
  end

  def m_condition
    CALLS << __method__
    __method__
  end

  memoize :m_condition, condition: -> { environment == "production" }

  def m_ttl(x, y)
    CALLS << [x, y]
    [x, y]
  end

  memoize :m_ttl, ttl: 3

  protected

  memoize def m_protected
    CALLS << nil
    nil
  end

  private

  memoize def m_private
    CALLS << :m
    :m
  end
end

class B < A
  memoize def m_args(x, y)
    B_CALLS << [x, y]
    super(1, 2)
    100
  end
end

module M
  include Memery

  memoize def m
    CALLS << :m
    :m
  end

  memoize

  def m_different_line
    CALLS << :m_different_line
    :m_different_line
  end

  def not_memoized; end

  private

  memoize def m_private; end
end

class C
  include M

  memoize def m_class
    CALLS << __method__
    __method__
  end
end

class D
  class << self
    include Memery

    memoize def m_args(x, y)
      CALLS << [x, y]
      [x, y]
    end
  end
end

class E
  extend Forwardable
  def_delegator :a, :m

  include Memery

  memoize def a
    A.new
  end
end

class F
  include Memery

  def m; end
end

class G
  include Memery

  def self.macro(name)
    define_method(:macro_received) { name }
  end

  macro memoize def g; end
end

class H
  include Memery

  [:a, :b, :m, :n, :x, :y].each do |name|
    define_method(name) do
      CALLS << name
      name
    end
  end

  memoize :m, :n
  memoize :x, :y, ttl: 3
end

RSpec.describe Memery do
  subject(:a) { A.new }

  before { CALLS.clear }
  before { B_CALLS.clear }
  before { Memery.use_hashed_arguments = true }

  let(:unmemoized_class) do
    Class.new do
      include Memery
      attr_reader :a, :b, :m, :n, :x, :y
    end
  end

  context "methods without args" do
    specify do
      values = [ a.m, a.m_nil, a.m, a.m_nil ]
      expect(values).to eq([:m, nil, :m, nil])
      expect(CALLS).to eq([:m, nil])
    end
  end

  context "methods without args memoize on new line" do
    specify do
      values = [ a.m_different_line, a.m_nil, a.m_different_line, a.m_nil ]
      expect(values).to eq([:m_different_line, nil, :m_different_line, nil])
      expect(CALLS).to eq([:m_different_line, nil])
    end
  end

  context "flushing cache" do
    specify do
      values = [ a.m, a.m ]
      a.clear_memery_cache!
      values << a.m
      expect(values).to eq([:m, :m, :m])
      expect(CALLS).to eq([:m, :m])
    end
  end

  context "method with args" do
    specify do
      values = [ a.m_args(1, 1), a.m_args(1, 1), a.m_args(1, 2) ]
      expect(values).to eq([[1, 1], [1, 1], [1, 2]])
      expect(CALLS).to eq([[1, 1], [1, 2]])
    end

    context "receiving Hash-like object" do
      let(:object_class) do
        Struct.new(:first_name, :last_name) do
          # For example, Sequel models have such implicit coercion,
          # which conflicts with `**kwargs`.
          alias_method :to_hash, :to_h
        end
      end

      let(:object) { object_class.new("John", "Wick") }

      specify do
        values = [ a.m_args(1, object), a.m_args(1, object), a.m_args(1, 2) ]
        expect(values).to eq([[1, object], [1, object], [1, 2]])
        expect(CALLS).to eq([[1, object], [1, 2]])
      end
    end
  end

  context "method with keyword args" do
    specify do
      values = [ a.m_kwargs(1, y: 2), a.m_kwargs(1, y: 2), a.m_kwargs(1, y: 3) ]
      expect(values).to eq([[1, 2], [1, 2], [1, 3]])
      expect(CALLS).to eq([[1, 2], [1, 3]])
    end
  end

  context "method with double splat argument" do
    specify do
      values = [ a.m_double_splat(1, y: 2), a.m_double_splat(1, y: 2), a.m_double_splat(1, y: 3) ]
      expect(values).to eq([[1, { y: 2 }], [1, { y: 2 }], [1, { y: 3 }]])
      expect(CALLS).to eq([[1, { y: 2 }], [1, { y: 3 }]])
    end
  end

  context "calling method with block" do
    specify do
      values = []
      values << a.m_args(1, 1) { nil }
      values << a.m_args(1, 1) { nil }

      expect(values).to eq([[1, 1], [1, 1]])
      expect(CALLS).to eq([[1, 1], [1, 1]])
    end
  end

  context "calling private method" do
    specify do
      expect { a.m_private }.to raise_error(NoMethodError, /private method/)
    end
  end

  context "calling protected method" do
    specify do
      expect { a.m_protected }.to raise_error(NoMethodError, /protected method/)
    end
  end

  context "Chaining macros" do
    subject(:g) { G.new }

    specify do
      expect(g.macro_received).to eq :g
    end
  end

  context "inherited class" do
    subject(:b) { B.new }

    specify do
      values = [ b.m_args(1, 1), b.m_args(1, 2), b.m_args(1, 1) ]
      expect(values).to eq([100, 100, 100])
      expect(CALLS).to eq([[1, 2]])
      expect(B_CALLS).to eq([[1, 1], [1, 2]])
    end
  end

  context "anonymous inherited class" do
    let(:anonymous_class) do
      Class.new(A) do
        memoize def m_args(x, y)
          B_CALLS << [x, y]
          super(1, 2)
          100
        end
      end
    end

    subject(:b) { anonymous_class.new }

    specify do
      values = [ b.m_args(1, 1), b.m_args(1, 2), b.m_args(1, 1) ]
      expect(values).to eq([100, 100, 100])
      expect(CALLS).to eq([[1, 2]])
      expect(B_CALLS).to eq([[1, 1], [1, 2]])
    end
  end

  context "module" do
    subject(:c) { C.new }

    specify do
      values = [c.m, c.m, c.m]
      expect(values).to eq([:m, :m, :m])
      expect(CALLS).to eq([:m])
    end

    specify do
      values = [c.m_different_line, c.m_different_line, c.m_different_line]
      expect(values).to eq([:m_different_line, :m_different_line, :m_different_line])
      expect(CALLS).to eq([:m_different_line])
    end

    context "memoization in class" do
      specify do
        values = [c.m_class, c.m_class, c.m_class]
        expect(values).to eq([:m_class, :m_class, :m_class])
        expect(CALLS).to eq([:m_class])
      end
    end
  end

  context "module with self.included method defined" do
    subject(:c) { C.new }

    before { C.include(some_mixin) }

    let(:some_mixin) do
      Module.new do
        extend ActiveSupport::Concern
        include Memery

        included do
          attr_accessor :a
        end
      end
    end

    it "doesn't override existing method" do
      c.a = 15
      expect(c.a).to eq(15)
    end
  end

  context "class method with args" do
    subject(:d) { D }

    specify do
      values = [ d.m_args(1, 1), d.m_args(1, 1), d.m_args(1, 2) ]
      expect(values).to eq([[1, 1], [1, 1], [1, 2]])
      expect(CALLS).to eq([[1, 1], [1, 2]])
    end
  end

  context "memoizing inexistent method" do
    subject(:klass) do
      Class.new do
        include Memery
        memoize :foo
      end
    end

    specify do
      expect { klass }.to raise_error(ArgumentError, /Method foo is not defined/)
    end
  end

  context "Forwardable" do
    subject(:e) { E.new }

    specify do
      values = [e.m, e.m, e.m]
      expect(values).to eq([:m, :m, :m])
      expect(CALLS).to eq([:m])
    end
  end

  context "without hashed arguments" do
    before { Memery.use_hashed_arguments = false }

    context "methods without args" do
      specify do
        values = [ a.m, a.m_nil, a.m, a.m_nil ]
        expect(values).to eq([:m, nil, :m, nil])
        expect(CALLS).to eq([:m, nil])
      end
    end

    context "method with args" do
      specify do
        values = [ a.m_args(1, 1), a.m_args(1, 1), a.m_args(1, 2) ]
        expect(values).to eq([[1, 1], [1, 1], [1, 2]])
        expect(CALLS).to eq([[1, 1], [1, 2]])
      end
    end
  end

  describe ":condition option" do
    before do
      a.environment = environment
    end

    context "returns true" do
      let(:environment) { "production" }

      specify do
        values = [ a.m_condition, a.m_nil, a.m_condition, a.m_nil ]
        expect(values).to eq([:m_condition, nil, :m_condition, nil])
        expect(CALLS).to eq([:m_condition, nil])
      end
    end

    context "returns false" do
      let(:environment) { "development" }

      specify do
        values = [ a.m_condition, a.m_nil, a.m_condition, a.m_nil ]
        expect(values).to eq([:m_condition, nil, :m_condition, nil])
        expect(CALLS).to eq([:m_condition, nil, :m_condition])
      end
    end
  end

  describe ":ttl option" do
    specify do
      values = [ a.m_ttl(1, 1), a.m_ttl(1, 1), a.m_ttl(1, 2) ]
      expect(values).to eq([[1, 1], [1, 1], [1, 2]])
      expect(CALLS).to eq([[1, 1], [1, 2]])

      allow(Process).to receive(:clock_gettime).with(Process::CLOCK_MONOTONIC)
        .and_wrap_original { |m, *args| m.call(*args) + 5 }

      values = [ a.m_ttl(1, 1), a.m_ttl(1, 1), a.m_ttl(1, 2) ]
      expect(values).to eq([[1, 1], [1, 1], [1, 2]])
      expect(CALLS).to eq([[1, 1], [1, 2], [1, 1], [1, 2]])
    end

    context "returns false" do
      let(:environment) { "development" }

      specify do
        values = [ a.m_condition, a.m_nil, a.m_condition, a.m_nil ]
        expect(values).to eq([:m_condition, nil, :m_condition, nil])
        expect(CALLS).to eq([:m_condition, nil, :m_condition])
      end
    end
  end

  describe "with multiple methods" do
    let(:h) { H.new }

    specify do
      values = [h.m, h.n, h.m, h.n]
      expect(values).to eq([:m, :n, :m, :n])
      expect(CALLS).to eq([:m, :n])
    end

    specify do
      values = [h.x, h.y, h.x, h.y]
      expect(values).to eq([:x, :y, :x, :y])
      expect(CALLS).to eq([:x, :y])
    end

    specify do
      expect(unmemoized_class.memoize(:x, :y, ttl: 3)).to eq([:x, :y])
    end
  end

  describe ".memoize return value" do
    specify do
      expect(unmemoized_class.memoize(:x)).to eq(:x)
      expect(unmemoized_class.memoize(:m, ttl: 3)).to eq(:m)
      expect(unmemoized_class.memoize(:a, condition: -> { 1 == 2 })).to eq(:a)
    end

    specify do
      expect(unmemoized_class.memoize(:x, :y)).to eq([:x, :y])
      expect(unmemoized_class.memoize(:m, :n, ttl: 3)).to eq([:m, :n])
      expect(unmemoized_class.memoize(:a, :b, condition: -> { 1 == 2 })).to eq([:a, :b])
    end
  end

  describe ".memoized?" do
    subject { object.memoized?(method_name) }

    context "class without memoized methods" do
      let(:object) { F }
      let(:method_name) { :m }

      it { is_expected.to be false }
    end

    shared_examples "works correctly" do
      context "public memoized method" do
        let(:method_name) { :m }

        it { is_expected.to be true }
      end

      context "memoize is on a different line" do
        let(:method_name) { :m_different_line }

        it { is_expected.to be true }
      end

      context "private memoized method" do
        let(:method_name) { :m_private }

        it { is_expected.to be true }
      end

      context "non-memoized method" do
        let(:method_name) { :not_memoized }

        it { is_expected.to be false }
      end

      context "standard class method" do
        let(:method_name) { :constants }

        it { is_expected.to be false }
      end

      context "standard instance method" do
        let(:method_name) { :to_s }

        it { is_expected.to be false }
      end
    end

    context "class" do
      let(:object) { A }

      it_behaves_like "works correctly"
    end

    context "module" do
      let(:object) { M }

      it_behaves_like "works correctly"
    end
  end
end


================================================
FILE: spec/spec_helper.rb
================================================
# frozen_string_literal: true

require "simplecov"
require "simplecov-lcov"

SimpleCov::Formatter::LcovFormatter.config do |config|
  config.report_with_single_file = true
  config.single_report_path = "coverage/lcov.info"
end

SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([
  SimpleCov::Formatter::HTMLFormatter,
  SimpleCov::Formatter::LcovFormatter,
])

SimpleCov.start do
  enable_coverage(:branch)
  minimum_coverage(line: 100, branch: 100)
end

require "memery"
require "active_support/concern"

RSpec.configure do |config|
  # Enable flags like --only-failures and --next-failure
  config.example_status_persistence_file_path = ".rspec_status"

  # Disable RSpec exposing methods globally on `Module` and `main`
  config.disable_monkey_patching!

  config.expect_with :rspec do |c|
    c.syntax = :expect
  end
end
Download .txt
gitextract_hubzy71a/

├── .editorconfig
├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .rspec
├── .rubocop.yml
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── benchmark.rb
├── lib/
│   ├── memery/
│   │   └── version.rb
│   └── memery.rb
├── memery.gemspec
└── spec/
    ├── memery_spec.rb
    └── spec_helper.rb
Download .txt
SYMBOL INDEX (57 symbols across 4 files)

FILE: benchmark.rb
  class Foo (line 20) | class Foo
    method base_find (line 24) | def base_find(char)
    method find_z (line 28) | def find_z
    method find_new (line 32) | def find_new(char)
    method find_optional (line 36) | def find_optional(*)
  function test_no_args (line 42) | def test_no_args
  function test_with_args (line 46) | def test_with_args
  function test_empty_args (line 50) | def test_empty_args

FILE: lib/memery.rb
  type Memery (line 5) | module Memery
    function monotonic_clock (line 9) | def monotonic_clock
    type ModuleMethods (line 24) | module ModuleMethods
      function included (line 25) | def included(base = nil, &block)
    type ClassMethods (line 39) | module ClassMethods
      function memoize (line 40) | def memoize(*method_names, condition: nil, ttl: nil)
      function memoized? (line 52) | def memoized?(method_name)
      function method_added (line 59) | def method_added(name)
      function prepend_memery_module! (line 70) | def prepend_memery_module!
      function define_memoized_method! (line 76) | def define_memoized_method!(method_name, **)
      type MemoizationModule (line 80) | module MemoizationModule
        function fresh? (line 82) | def fresh?(ttl)
        function define_memoized_method! (line 89) | def define_memoized_method!(klass, method_name, condition: nil, tt...
        function method_visibility (line 125) | def method_visibility(klass, method_name)
    type InstanceMethods (line 141) | module InstanceMethods
      function clear_memery_cache! (line 142) | def clear_memery_cache!

FILE: lib/memery/version.rb
  type Memery (line 3) | module Memery

FILE: spec/memery_spec.rb
  class A (line 8) | class A
    method m (line 13) | def m
    method m_different_line (line 18) | def m_different_line
    method not_memoized (line 23) | def not_memoized; end
    method m_nil (line 25) | def m_nil
    method m_args (line 29) | def m_args(x, y)
    method m_kwargs (line 34) | def m_kwargs(x, y: 42)
    method m_double_splat (line 39) | def m_double_splat(x, **kwargs)
    method m_condition (line 44) | def m_condition
    method m_ttl (line 51) | def m_ttl(x, y)
    method m_protected (line 60) | def m_protected
    method m_private (line 67) | def m_private
  class B (line 73) | class B < A
    method m_args (line 74) | def m_args(x, y)
  type M (line 81) | module M
    function m (line 84) | def m
    function m_different_line (line 91) | def m_different_line
    function not_memoized (line 96) | def not_memoized; end
    function m_private (line 100) | def m_private; end
  class C (line 103) | class C
    method m_class (line 106) | def m_class
  class D (line 112) | class D
    method m_args (line 116) | def m_args(x, y)
  class E (line 123) | class E
    method a (line 129) | def a
  class F (line 134) | class F
    method m (line 137) | def m; end
  class G (line 140) | class G
    method macro (line 143) | def self.macro(name)
    method g (line 147) | def g; end
  class H (line 150) | class H
  function m_args (line 291) | def m_args(x, y)
Condensed preview — 15 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (30K chars).
[
  {
    "path": ".editorconfig",
    "chars": 147,
    "preview": "root = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ni"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 694,
    "preview": "name: CI\n\non: [push, pull_request]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    # We want to run on external PRs, but "
  },
  {
    "path": ".gitignore",
    "chars": 117,
    "preview": "/.bundle/\n/.yardoc\n/coverage/\n/doc/\n/pkg/\n/spec/reports/\n/tmp/\n.ruby-version\n\n# rspec failure tracking\n.rspec_status\n"
  },
  {
    "path": ".rspec",
    "chars": 64,
    "preview": "--format documentation\n--color\n--require spec_helper\n--warnings\n"
  },
  {
    "path": ".rubocop.yml",
    "chars": 221,
    "preview": "inherit_gem:\n  rubocop-config-umbrellio: lib/rubocop.yml\n\nAllCops:\n  DisplayCopNames: true\n  TargetRubyVersion: 3.2\n\nNam"
  },
  {
    "path": "Gemfile",
    "chars": 248,
    "preview": "# frozen_string_literal: true\n\nsource \"https://rubygems.org\"\ngemspec\n\ngem \"activesupport\"\ngem \"benchmark-ips\"\ngem \"bench"
  },
  {
    "path": "LICENSE.txt",
    "chars": 1079,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2017 Yuri Smirnov\n\nPermission is hereby granted, free of charge, to any person obta"
  },
  {
    "path": "README.md",
    "chars": 6479,
    "preview": "# Memery   [![Gem Version](https://badge.fury.io/rb/memery.svg)](https://badge.fury.io/rb/memery) ![Build Status](https:"
  },
  {
    "path": "Rakefile",
    "chars": 286,
    "preview": "# frozen_string_literal: true\n\nrequire \"bundler/gem_tasks\"\nrequire \"rspec/core/rake_task\"\nrequire \"rubocop/rake_task\"\n\nR"
  },
  {
    "path": "benchmark.rb",
    "chars": 1459,
    "preview": "# frozen_string_literal: true\n\nrequire \"bundler/setup\"\nBundler.setup\n\nrequire \"benchmark\"\nrequire \"benchmark/ips\"\nrequir"
  },
  {
    "path": "lib/memery/version.rb",
    "chars": 69,
    "preview": "# frozen_string_literal: true\n\nmodule Memery\n  VERSION = \"1.8.0\"\nend\n"
  },
  {
    "path": "lib/memery.rb",
    "chars": 3945,
    "preview": "# frozen_string_literal: true\n\nrequire \"memery/version\"\n\nmodule Memery\n  class << self\n    attr_accessor :use_hashed_arg"
  },
  {
    "path": "memery.gemspec",
    "chars": 730,
    "preview": "# frozen_string_literal: true\n\nlib = File.expand_path(\"lib\", __dir__)\n$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?"
  },
  {
    "path": "spec/memery_spec.rb",
    "chars": 11860,
    "preview": "# frozen_string_literal: true\n\n# rubocop:disable Style/MutableConstant\nCALLS = []\nB_CALLS = []\n# rubocop:enable Style/Mu"
  },
  {
    "path": "spec/spec_helper.rb",
    "chars": 840,
    "preview": "# frozen_string_literal: true\n\nrequire \"simplecov\"\nrequire \"simplecov-lcov\"\n\nSimpleCov::Formatter::LcovFormatter.config "
  }
]

About this extraction

This page contains the full source code of the tycooon/memery GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 15 files (27.6 KB), approximately 8.7k tokens, and a symbol index with 57 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!