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 [](https://badge.fury.io/rb/memery)  [](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
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
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 [](https://badge.fury.io/rb/memery) \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.