memo_wise 0.4.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.dokaz +2 -0
- data/.github/dependabot.yml +20 -0
- data/.github/workflows/main.yml +21 -13
- data/CHANGELOG.md +9 -1
- data/Gemfile +1 -0
- data/Gemfile.lock +14 -6
- data/LICENSE.txt +1 -1
- data/README.md +52 -26
- data/benchmarks/Gemfile +2 -2
- data/benchmarks/Gemfile.lock +10 -7
- data/benchmarks/benchmarks.rb +44 -0
- data/lib/memo_wise.rb +161 -205
- data/lib/memo_wise/internal_api.rb +258 -0
- data/lib/memo_wise/version.rb +1 -1
- data/memo_wise.gemspec +6 -1
- metadata +9 -7
- data/.dependabot/config.yml +0 -13
- data/.github/workflows/auto-approve-dependabot.yml +0 -26
- data/.github/workflows/remove-needs-qa.yml +0 -35
@@ -0,0 +1,258 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MemoWise
|
4
|
+
class InternalAPI
|
5
|
+
# Create initial mutable state to store memoized values if it doesn't
|
6
|
+
# already exist
|
7
|
+
#
|
8
|
+
# @param [Object] obj
|
9
|
+
# Object in which to create mutable state to store future memoized values
|
10
|
+
#
|
11
|
+
# @return [Object] the passed-in obj
|
12
|
+
def self.create_memo_wise_state!(obj)
|
13
|
+
unless obj.instance_variables.include?(:@_memo_wise)
|
14
|
+
obj.instance_variable_set(:@_memo_wise, {})
|
15
|
+
end
|
16
|
+
|
17
|
+
obj
|
18
|
+
end
|
19
|
+
|
20
|
+
# Determine whether `method` takes any *positional* args.
|
21
|
+
#
|
22
|
+
# These are the types of positional args:
|
23
|
+
#
|
24
|
+
# * *Required* -- ex: `def foo(a)`
|
25
|
+
# * *Optional* -- ex: `def foo(b=1)`
|
26
|
+
# * *Splatted* -- ex: `def foo(*c)`
|
27
|
+
#
|
28
|
+
# @param method [Method, UnboundMethod]
|
29
|
+
# Arguments of this method will be checked
|
30
|
+
#
|
31
|
+
# @return [Boolean]
|
32
|
+
# Return `true` if `method` accepts one or more positional arguments
|
33
|
+
#
|
34
|
+
# @example
|
35
|
+
# class Example
|
36
|
+
# def no_args
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# def position_arg(a)
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# MemoWise::InternalAPI.
|
44
|
+
# has_arg?(Example.instance_method(:no_args)) #=> false
|
45
|
+
#
|
46
|
+
# MemoWise::InternalAPI.
|
47
|
+
# has_arg?(Example.instance_method(:position_arg)) #=> true
|
48
|
+
#
|
49
|
+
def self.has_arg?(method) # rubocop:disable Naming/PredicateName
|
50
|
+
method.parameters.any? do |param, _|
|
51
|
+
param == :req || param == :opt || param == :rest # rubocop:disable Style/MultipleComparison
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Determine whether `method` takes any *keyword* args.
|
56
|
+
#
|
57
|
+
# These are the types of keyword args:
|
58
|
+
#
|
59
|
+
# * *Keyword Required* -- ex: `def foo(a:)`
|
60
|
+
# * *Keyword Optional* -- ex: `def foo(b: 1)`
|
61
|
+
# * *Keyword Splatted* -- ex: `def foo(**c)`
|
62
|
+
#
|
63
|
+
# @param method [Method, UnboundMethod]
|
64
|
+
# Arguments of this method will be checked
|
65
|
+
#
|
66
|
+
# @return [Boolean]
|
67
|
+
# Return `true` if `method` accepts one or more keyword arguments
|
68
|
+
#
|
69
|
+
# @example
|
70
|
+
# class Example
|
71
|
+
# def position_args(a, b=1)
|
72
|
+
# end
|
73
|
+
#
|
74
|
+
# def keyword_args(a:, b: 1)
|
75
|
+
# end
|
76
|
+
# end
|
77
|
+
#
|
78
|
+
# MemoWise::InternalAPI.
|
79
|
+
# has_kwarg?(Example.instance_method(:position_args)) #=> false
|
80
|
+
#
|
81
|
+
# MemoWise::InternalAPI.
|
82
|
+
# has_kwarg?(Example.instance_method(:keyword_args)) #=> true
|
83
|
+
#
|
84
|
+
def self.has_kwarg?(method) # rubocop:disable Naming/PredicateName
|
85
|
+
method.parameters.any? do |param, _|
|
86
|
+
param == :keyreq || param == :key || param == :keyrest # rubocop:disable Style/MultipleComparison
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Determine whether `method` takes only *required* args.
|
91
|
+
#
|
92
|
+
# These are the types of required args:
|
93
|
+
#
|
94
|
+
# * *Required* -- ex: `def foo(a)`
|
95
|
+
# * *Keyword Required* -- ex: `def foo(a:)`
|
96
|
+
#
|
97
|
+
# @param method [Method, UnboundMethod]
|
98
|
+
# Arguments of this method will be checked
|
99
|
+
#
|
100
|
+
# @return [Boolean]
|
101
|
+
# Return `true` if `method` accepts only required arguments
|
102
|
+
#
|
103
|
+
# @example
|
104
|
+
# class Ex
|
105
|
+
# def optional_args(a=1, b: 1)
|
106
|
+
# end
|
107
|
+
#
|
108
|
+
# def required_args(a, b:)
|
109
|
+
# end
|
110
|
+
# end
|
111
|
+
#
|
112
|
+
# MemoWise::InternalAPI.
|
113
|
+
# has_only_required_args?(Ex.instance_method(:optional_args))
|
114
|
+
# #=> false
|
115
|
+
#
|
116
|
+
# MemoWise::InternalAPI.
|
117
|
+
# has_only_required_args?(Ex.instance_method(:required_args))
|
118
|
+
# #=> true
|
119
|
+
def self.has_only_required_args?(method) # rubocop:disable Naming/PredicateName
|
120
|
+
method.parameters.all? { |type, _| type == :req || type == :keyreq } # rubocop:disable Style/MultipleComparison
|
121
|
+
end
|
122
|
+
|
123
|
+
# Find the original class for which the given class is the corresponding
|
124
|
+
# "singleton class".
|
125
|
+
#
|
126
|
+
# See https://stackoverflow.com/questions/54531270/retrieve-a-ruby-object-from-its-singleton-class
|
127
|
+
#
|
128
|
+
# @param klass [Class]
|
129
|
+
# Singleton class to find the original class of
|
130
|
+
#
|
131
|
+
# @return Class
|
132
|
+
# Original class for which `klass` is the singleton class.
|
133
|
+
#
|
134
|
+
# @raise ArgumentError
|
135
|
+
# Raises if `klass` is not a singleton class.
|
136
|
+
#
|
137
|
+
def self.original_class_from_singleton(klass)
|
138
|
+
unless klass.singleton_class?
|
139
|
+
raise ArgumentError, "Must be a singleton class: #{klass.inspect}"
|
140
|
+
end
|
141
|
+
|
142
|
+
# Search ObjectSpace
|
143
|
+
# * 1:1 relationship of singleton class to original class is documented
|
144
|
+
# * Performance concern: searches all Class objects
|
145
|
+
# But, only runs at load time
|
146
|
+
ObjectSpace.each_object(Class).find { |cls| cls.singleton_class == klass }
|
147
|
+
end
|
148
|
+
|
149
|
+
# Convention we use for renaming the original method when we replace with
|
150
|
+
# the memoized version in {MemoWise.memo_wise}.
|
151
|
+
#
|
152
|
+
# @param method_name [Symbol]
|
153
|
+
# Name for which to return the renaming for the original method
|
154
|
+
#
|
155
|
+
# @return [Symbol]
|
156
|
+
# Renamed method to use for the original method with name `method_name`
|
157
|
+
#
|
158
|
+
def self.original_memo_wised_name(method_name)
|
159
|
+
:"_memo_wise_original_#{method_name}"
|
160
|
+
end
|
161
|
+
|
162
|
+
# @param target [Class, Module]
|
163
|
+
# The class to which we are prepending MemoWise to provide memoization;
|
164
|
+
# the `InternalAPI` *instance* methods will refer to this `target` class.
|
165
|
+
def initialize(target)
|
166
|
+
@target = target
|
167
|
+
end
|
168
|
+
|
169
|
+
# @return [Class, Module]
|
170
|
+
attr_reader :target
|
171
|
+
|
172
|
+
# Returns the "fetch key" for the given `method_name` and parameters, to be
|
173
|
+
# used to lookup the memoized results specifically for this method and these
|
174
|
+
# parameters.
|
175
|
+
#
|
176
|
+
# @param method_name [Symbol]
|
177
|
+
# Name of method to derive the "fetch key" for, with given parameters.
|
178
|
+
# @param args [Array]
|
179
|
+
# Zero or more positional parameters
|
180
|
+
# @param kwargs [Hash]
|
181
|
+
# Zero or more keyword parameters
|
182
|
+
#
|
183
|
+
# @return [Array, Hash, Object]
|
184
|
+
# Returns one of:
|
185
|
+
# - An `Array` if only positional parameters.
|
186
|
+
# - A nested `Array<Array, Hash>` if *both* positional and keyword.
|
187
|
+
# - A `Hash` if only keyword parameters.
|
188
|
+
# - A single object if there is only a single parameter.
|
189
|
+
def fetch_key(method_name, *args, **kwargs)
|
190
|
+
method = target_class.instance_method(method_name)
|
191
|
+
|
192
|
+
if MemoWise::InternalAPI.has_only_required_args?(method)
|
193
|
+
key = method.parameters.map.with_index do |(type, name), index|
|
194
|
+
type == :req ? args[index] : kwargs[name]
|
195
|
+
end
|
196
|
+
key.size == 1 ? key.first : key
|
197
|
+
else
|
198
|
+
has_arg = MemoWise::InternalAPI.has_arg?(method)
|
199
|
+
|
200
|
+
if has_arg && MemoWise::InternalAPI.has_kwarg?(method)
|
201
|
+
[args, kwargs].freeze
|
202
|
+
elsif has_arg
|
203
|
+
args
|
204
|
+
else
|
205
|
+
kwargs
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Returns visibility of an instance method defined on class `target`.
|
211
|
+
#
|
212
|
+
# @param method_name [Symbol]
|
213
|
+
# Name of existing *instance* method find the visibility of.
|
214
|
+
#
|
215
|
+
# @return [:private, :protected, :public]
|
216
|
+
# Visibility of existing instance method of the class.
|
217
|
+
#
|
218
|
+
# @raise ArgumentError
|
219
|
+
# Raises `ArgumentError` unless `method_name` is a `Symbol` corresponding
|
220
|
+
# to an existing **instance** method defined on `klass`.
|
221
|
+
#
|
222
|
+
def method_visibility(method_name)
|
223
|
+
if target.private_method_defined?(method_name)
|
224
|
+
:private
|
225
|
+
elsif target.protected_method_defined?(method_name)
|
226
|
+
:protected
|
227
|
+
elsif target.public_method_defined?(method_name)
|
228
|
+
:public
|
229
|
+
else
|
230
|
+
raise ArgumentError,
|
231
|
+
"#{method_name.inspect} must be a method on #{target}"
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# Validates that {.memo_wise} has already been called on `method_name`.
|
236
|
+
#
|
237
|
+
# @param method_name [Symbol]
|
238
|
+
# Name of method to validate has already been setup with {.memo_wise}
|
239
|
+
def validate_memo_wised!(method_name)
|
240
|
+
original_name = self.class.original_memo_wised_name(method_name)
|
241
|
+
|
242
|
+
unless target_class.private_method_defined?(original_name)
|
243
|
+
raise ArgumentError, "#{method_name} is not a memo_wised method"
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
# @return [Class] where we look for method definitions
|
248
|
+
def target_class
|
249
|
+
if target.instance_of?(Class)
|
250
|
+
# A class's methods are defined in its singleton class
|
251
|
+
target.singleton_class
|
252
|
+
else
|
253
|
+
# An object's methods are defined in its class
|
254
|
+
target.class
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
data/lib/memo_wise/version.rb
CHANGED
data/memo_wise.gemspec
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
require_relative "lib/memo_wise/version"
|
4
4
|
|
5
|
-
Gem::Specification.new do |spec|
|
5
|
+
Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
|
6
6
|
spec.name = "memo_wise"
|
7
7
|
spec.version = MemoWise::VERSION
|
8
8
|
spec.summary = "The wise choice for Ruby memoization"
|
@@ -34,4 +34,9 @@ Gem::Specification.new do |spec|
|
|
34
34
|
end
|
35
35
|
end
|
36
36
|
spec.require_paths = ["lib"]
|
37
|
+
|
38
|
+
spec.metadata = {
|
39
|
+
"changelog_uri" => "https://github.com/panorama-ed/memo_wise/blob/main/CHANGELOG.md",
|
40
|
+
"source_code_uri" => "https://github.com/panorama-ed/memo_wise"
|
41
|
+
}
|
37
42
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: memo_wise
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Panorama Education
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date: 2021-
|
14
|
+
date: 2021-06-24 00:00:00.000000000 Z
|
15
15
|
dependencies: []
|
16
16
|
description:
|
17
17
|
email:
|
@@ -23,11 +23,10 @@ executables: []
|
|
23
23
|
extensions: []
|
24
24
|
extra_rdoc_files: []
|
25
25
|
files:
|
26
|
-
- ".
|
26
|
+
- ".dokaz"
|
27
27
|
- ".github/PULL_REQUEST_TEMPLATE.md"
|
28
|
-
- ".github/
|
28
|
+
- ".github/dependabot.yml"
|
29
29
|
- ".github/workflows/main.yml"
|
30
|
-
- ".github/workflows/remove-needs-qa.yml"
|
31
30
|
- ".gitignore"
|
32
31
|
- ".rspec"
|
33
32
|
- ".rubocop.yml"
|
@@ -47,13 +46,16 @@ files:
|
|
47
46
|
- bin/console
|
48
47
|
- bin/setup
|
49
48
|
- lib/memo_wise.rb
|
49
|
+
- lib/memo_wise/internal_api.rb
|
50
50
|
- lib/memo_wise/version.rb
|
51
51
|
- logo/logo.png
|
52
52
|
- memo_wise.gemspec
|
53
53
|
homepage: https://github.com/panorama-ed/memo_wise
|
54
54
|
licenses:
|
55
55
|
- MIT
|
56
|
-
metadata:
|
56
|
+
metadata:
|
57
|
+
changelog_uri: https://github.com/panorama-ed/memo_wise/blob/main/CHANGELOG.md
|
58
|
+
source_code_uri: https://github.com/panorama-ed/memo_wise
|
57
59
|
post_install_message:
|
58
60
|
rdoc_options: []
|
59
61
|
require_paths:
|
@@ -69,7 +71,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
69
71
|
- !ruby/object:Gem::Version
|
70
72
|
version: '0'
|
71
73
|
requirements: []
|
72
|
-
rubygems_version: 3.
|
74
|
+
rubygems_version: 3.2.3
|
73
75
|
signing_key:
|
74
76
|
specification_version: 4
|
75
77
|
summary: The wise choice for Ruby memoization
|
data/.dependabot/config.yml
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
# Reference: https://dependabot.com/docs/config-file/
|
2
|
-
version: 1
|
3
|
-
update_configs:
|
4
|
-
- package_manager: "ruby:bundler"
|
5
|
-
directory: "/"
|
6
|
-
update_schedule: "live"
|
7
|
-
automerged_updates:
|
8
|
-
- match:
|
9
|
-
dependency_type: "development"
|
10
|
-
update_type: "all"
|
11
|
-
- package_manager: "ruby:bundler"
|
12
|
-
directory: "/benchmarks"
|
13
|
-
update_schedule: "live"
|
@@ -1,26 +0,0 @@
|
|
1
|
-
# This workflow auto-approves pull-requests when the github actor is a
|
2
|
-
# dependabot user and it is opening a new pull request.
|
3
|
-
#
|
4
|
-
# The problem that this workflow solves is that we have branch protection on
|
5
|
-
# our repositories that prevent PRs from merging unless there is an
|
6
|
-
# approval present. The problem is that this will block PRs that dependabot
|
7
|
-
# may want to merge automatically. Auto-approving dependabot PRs will allow
|
8
|
-
# the automatic merge to complete. We control what gets automerged through
|
9
|
-
# the dependabot configuration.
|
10
|
-
#
|
11
|
-
# This is a known issue: https://github.com/dependabot/feedback/issues/852
|
12
|
-
name: Auto-approve dependabot pull requests
|
13
|
-
on:
|
14
|
-
pull_request:
|
15
|
-
types: [opened]
|
16
|
-
|
17
|
-
jobs:
|
18
|
-
dependabot-triage:
|
19
|
-
runs-on: ubuntu-latest
|
20
|
-
if: (github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]')
|
21
|
-
|
22
|
-
steps:
|
23
|
-
- name: Auto-approve for dependabot
|
24
|
-
uses: hmarr/auto-approve-action@v2.0.0
|
25
|
-
with:
|
26
|
-
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
@@ -1,35 +0,0 @@
|
|
1
|
-
# This workflow removes a "Needs QA" label from a PR when the actor is the
|
2
|
-
# dependabot user merging a PR.
|
3
|
-
#
|
4
|
-
# We need this mechanism to allow for automerging whitelisted dependencies while
|
5
|
-
# also allowing for blocking a merge to main for deployment (in the way that
|
6
|
-
# our other PRs work). When the automerge script runs in henchman, it looks
|
7
|
-
# for `Needs QA` on github pull requests, and if the label is present,
|
8
|
-
# blocks the commit from merging.
|
9
|
-
name: Remove 'Needs QA' label for auto-merged PRs.
|
10
|
-
on:
|
11
|
-
pull_request:
|
12
|
-
types: [closed]
|
13
|
-
|
14
|
-
jobs:
|
15
|
-
remove-label:
|
16
|
-
runs-on: ubuntu-latest
|
17
|
-
if: >
|
18
|
-
(github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]')
|
19
|
-
&& github.event.pull_request.merged
|
20
|
-
|
21
|
-
steps:
|
22
|
-
# Our triage workflow adds 'Needs QA' to the PR in order to block it from
|
23
|
-
# merging to production. This removes that label when dependabot is doing
|
24
|
-
# the merging.
|
25
|
-
- name: Remove QA Label
|
26
|
-
uses: actions/github-script@0.4.0
|
27
|
-
with:
|
28
|
-
github-token: ${{ secrets.GITHUB_TOKEN }}
|
29
|
-
script: |
|
30
|
-
github.issues.removeLabel({
|
31
|
-
issue_number: context.issue.number,
|
32
|
-
owner: context.repo.owner,
|
33
|
-
repo: context.repo.repo,
|
34
|
-
name: 'Needs QA'
|
35
|
-
})
|