gem_kit-release 0.1.0 → 0.2.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.
@@ -1,285 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rubygems/deprecate"
4
-
5
- module GemKit
6
- module Release
7
- # A deprecation is a dated promise: it names the replacement *and* the
8
- # version the old name stops existing in. Built on Gem::Deprecate, which
9
- # gets the message format and the skip_during escape hatch right, plus one
10
- # addition — a registry, so the set of outstanding promises is data the
11
- # release tooling can enforce rather than prose someone has to remember.
12
- #
13
- # Deprecate a method:
14
- #
15
- # class Session
16
- # extend GemKit::Release::Deprecate
17
- #
18
- # def old_reset = new_reset
19
- # deprecate :old_reset, "Session#new_reset", "5.0"
20
- # end
21
- #
22
- # Deprecate a whole constant that has moved or been renamed — leave the old
23
- # name in place as a subclass of the new one, then declare it:
24
- #
25
- # class Completion < Brute::Completion::OpenRouter
26
- # extend GemKit::Release::Deprecate
27
- # superseded_by "Brute::Completion::OpenRouter", "5.0"
28
- # end
29
- #
30
- # Both warn on use, naming the caller. Gem::Deprecate.skip_during silences
31
- # them, so a test suite can exercise the old path in quiet.
32
- module Deprecate
33
- extend Gem::Deprecate
34
-
35
- # One outstanding deprecation. `removed_in` is the deadline the release
36
- # gate reads.
37
- Entry = Struct.new(:name, :replacement, :removed_in, :declared_at, keyword_init: true) do
38
- def to_s
39
- "#{name} -> #{replacement == :none ? "(no replacement)" : replacement}"
40
- end
41
- end
42
-
43
- class << self
44
- # Every deprecation declared in the loaded library, in declaration order.
45
- def registry
46
- @registry ||= []
47
- end
48
-
49
- def register(name:, replacement:, removed_in:, declared_at: nil)
50
- entry = Entry.new(
51
- name: name.to_s,
52
- replacement: replacement,
53
- removed_in: Gem::Version.new(removed_in.to_s),
54
- declared_at: declared_at || location(1),
55
- )
56
- registry << entry
57
- entry
58
- end
59
-
60
- # The deprecations that come due at `version` — every deadline that has
61
- # arrived or passed. Releasing `version` with any of these still in the
62
- # tree breaks the promise the warning made.
63
- def pending(version)
64
- target = Gem::Version.new(version.to_s)
65
- registry.select { |entry| entry.removed_in <= target }
66
- end
67
-
68
- # Deprecations still inside their grace period at `version`.
69
- def upcoming(version)
70
- target = Gem::Version.new(version.to_s)
71
- registry.reject { |entry| entry.removed_in <= target }
72
- end
73
-
74
- # Single funnel for every warning: Gem::Deprecate.skip_during works
75
- # across all of them, and specs have one place to listen.
76
- def warn(message)
77
- Kernel.warn(message) unless Gem::Deprecate.skip
78
- end
79
-
80
- # The Gem::Deprecate-shaped message. `origin` must be computed at the
81
- # call site — one frame deeper and it names this file rather than the
82
- # code that needs changing.
83
- def message(target, replacement, removed_in, origin)
84
- [
85
- "NOTE: #{target} is deprecated",
86
- replacement == :none ? " with no replacement" : "; use #{replacement} instead",
87
- ". It will be removed in #{removed_in}",
88
- "\n#{target} called from #{origin}.",
89
- ].join
90
- end
91
-
92
- def location(depth)
93
- caller_locations(depth + 1, 1)&.first&.then { |l| "#{l.path}:#{l.lineno}" }
94
- end
95
- end
96
-
97
- # Deprecate one method. Mirrors Gem::Deprecate#rubygems_deprecate, but the
98
- # deadline is explicit — a deprecation added late in a cycle usually wants
99
- # the major after next, and guessing that is not the tool's business.
100
- def deprecate(name, replacement, removed_in)
101
- label = singleton_class? ? "#{attached_object}.#{name}" : "#{self}##{name}"
102
- Deprecate.register(name: label, replacement: replacement, removed_in: removed_in,
103
- declared_at: Deprecate.location(1))
104
-
105
- class_eval do
106
- old = "_deprecated_#{name}"
107
- alias_method old, name
108
- define_method name do |*args, &block|
109
- target = is_a?(Module) ? "#{self}.#{name}" : "#{self.class}##{name}"
110
- origin = Gem.location_of_caller.join(":")
111
- Deprecate.warn(Deprecate.message(target, replacement, removed_in, origin))
112
- send(old, *args, &block)
113
- end
114
- ruby2_keywords name if respond_to?(:ruby2_keywords, true)
115
- end
116
- end
117
-
118
- # Deprecate the constant this is called in — the renamed-or-moved case.
119
- # Named `superseded_by` rather than `deprecate_constant` because Module
120
- # already has a method by that name and shadowing it would be rude.
121
- def superseded_by(replacement, removed_in)
122
- Deprecate.register(name: name || to_s, replacement: replacement, removed_in: removed_in,
123
- declared_at: Deprecate.location(1))
124
-
125
- return unless respond_to?(:new)
126
-
127
- define_singleton_method(:new) do |*args, **options, &block|
128
- origin = Gem.location_of_caller.join(":")
129
- Deprecate.warn(Deprecate.message(name || to_s, replacement, removed_in, origin))
130
- super(*args, **options, &block)
131
- end
132
- end
133
- end
134
- end
135
- end
136
-
137
- __END__
138
-
139
- describe "gem_kit/release/deprecate" do
140
- Deprecate = GemKit::Release::Deprecate unless defined?(Deprecate)
141
-
142
- captured = []
143
- # Capture what Deprecate.warn emits and keep the shared registry clean —
144
- # these specs declare throwaway deprecations.
145
- isolated = lambda do |&block|
146
- saved = Deprecate.registry.dup
147
- original = Deprecate.method(:warn)
148
- captured.clear
149
- Deprecate.define_singleton_method(:warn) { |message| captured << message }
150
- begin
151
- block.call
152
- ensure
153
- Deprecate.define_singleton_method(:warn, original)
154
- Deprecate.registry.replace(saved)
155
- end
156
- end
157
-
158
- it "warns on a deprecated method, naming replacement, version and caller" do
159
- isolated.call do
160
- klass = Class.new do
161
- extend Deprecate
162
- def new_name = :result
163
- def old_name = new_name
164
- deprecate :old_name, "Thing#new_name", "9.0"
165
- end
166
-
167
- klass.new.old_name.should == :result # still works
168
- captured.size.should == 1
169
- captured.first.should.match(/is deprecated/)
170
- captured.first.should.match(/use Thing#new_name instead/)
171
- captured.first.should.match(/removed in 9\.0/)
172
- captured.first.should.match(/called from /)
173
- end
174
- end
175
-
176
- it "names the caller, not the deprecation machinery" do
177
- isolated.call do
178
- klass = Class.new do
179
- extend Deprecate
180
- def old_name = :result
181
- deprecate :old_name, "Thing#new_name", "9.0"
182
- end
183
-
184
- # These specs live in this file's __END__, so "the caller" is a line in
185
- # deprecate.rb either way — pin the exact line to tell them apart.
186
- klass.new.old_name; call_line = __LINE__
187
- captured.first.should.match(/called from .*deprecate\.rb:#{call_line}\./)
188
- end
189
- end
190
-
191
- it "labels a class-method deprecation by the class, not its singleton" do
192
- isolated.call do
193
- Class.new do
194
- def self.to_s = "Demo"
195
- def self.old_thing = :ok
196
- class << self
197
- extend Deprecate
198
- deprecate :old_thing, "Other.new_thing", "9.0"
199
- end
200
- end
201
-
202
- Deprecate.registry.last.name.should == "Demo.old_thing"
203
- end
204
- end
205
-
206
- it "warns on a superseded constant but keeps it working" do
207
- isolated.call do
208
- modern = Class.new { def initialize(x); @x = x; end; attr_reader :x }
209
- legacy = Class.new(modern) do
210
- extend Deprecate
211
- def self.name = "Old::Name"
212
- superseded_by "New::Name", "9.0"
213
- end
214
-
215
- legacy.new(42).x.should == 42 # still works
216
- captured.size.should == 1
217
- captured.first.should.match(/Old::Name is deprecated; use New::Name instead/)
218
- end
219
- end
220
-
221
- it "supports :none for a deprecation with no replacement" do
222
- isolated.call do
223
- klass = Class.new do
224
- extend Deprecate
225
- def gone = :ok
226
- deprecate :gone, :none, "9.0"
227
- end
228
-
229
- klass.new.gone
230
- captured.first.should.match(/with no replacement/)
231
- end
232
- end
233
-
234
- it "registers each declaration with its deadline and source" do
235
- isolated.call do
236
- Class.new do
237
- extend Deprecate
238
- def gone = nil
239
- deprecate :gone, "Other#kept", "9.0"
240
- end
241
-
242
- entry = Deprecate.registry.last
243
- entry.replacement.should == "Other#kept"
244
- entry.removed_in.should == Gem::Version.new("9.0")
245
- entry.declared_at.should.match(/deprecate\.rb:\d+/)
246
- end
247
- end
248
-
249
- it "splits the registry into pending and upcoming at a version" do
250
- isolated.call do
251
- Deprecate.registry.clear
252
- Deprecate.register(name: "A", replacement: "A2", removed_in: "5.0")
253
- Deprecate.register(name: "B", replacement: "B2", removed_in: "6.0")
254
-
255
- Deprecate.pending("5.0.0").map(&:name).should == ["A"]
256
- Deprecate.upcoming("5.0.0").map(&:name).should == ["B"]
257
- Deprecate.pending("4.9.0").should.be.empty
258
- Deprecate.pending("6.1.0").map(&:name).should == ["A", "B"]
259
- end
260
- end
261
-
262
- it "stays quiet inside Gem::Deprecate.skip_during" do
263
- saved = Deprecate.registry.dup
264
- begin
265
- klass = Class.new do
266
- extend Deprecate
267
- def quiet = :ok
268
- deprecate :quiet, "Other#loud", "9.0"
269
- end
270
-
271
- warned = []
272
- original = Kernel.method(:warn)
273
- Kernel.define_singleton_method(:warn) { |*args| warned << args.join }
274
- begin
275
- Gem::Deprecate.skip_during { klass.new.quiet.should == :ok }
276
- ensure
277
- Kernel.define_singleton_method(:warn, original)
278
- end
279
-
280
- warned.should.be.empty
281
- ensure
282
- Deprecate.registry.replace(saved)
283
- end
284
- end
285
- end