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.
@@ -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
- })