memo_wise 0.4.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MemoWise
4
- VERSION = "0.4.0"
4
+ VERSION = "1.0.0"
5
5
  end
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.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-04-30 00:00:00.000000000 Z
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
- - ".dependabot/config.yml"
26
+ - ".dokaz"
27
27
  - ".github/PULL_REQUEST_TEMPLATE.md"
28
- - ".github/workflows/auto-approve-dependabot.yml"
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.1.4
74
+ rubygems_version: 3.2.3
73
75
  signing_key:
74
76
  specification_version: 4
75
77
  summary: The wise choice for Ruby memoization
@@ -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
- })