Skip to content

Commit 0f7d264

Browse files
authored
πŸ”€ Merge pull request #334 from ruby/responses-return-frozen_dup
✨ New config option to return frozen dup from `#responses`
2 parents 8c25109 + 566e668 commit 0f7d264

File tree

6 files changed

+411
-171
lines changed

6 files changed

+411
-171
lines changed

β€Žlib/net/imap.rb

Lines changed: 79 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2490,41 +2490,98 @@ def idle_done
24902490
end
24912491
end
24922492

2493+
RESPONSES_DEPRECATION_MSG =
2494+
"Pass a type or block to #responses, " \
2495+
"set config.responses_without_block to :frozen_dup " \
2496+
"or :silence_deprecation_warning, " \
2497+
"or use #extract_responses or #clear_responses."
2498+
private_constant :RESPONSES_DEPRECATION_MSG
2499+
24932500
# :call-seq:
2501+
# responses -> hash of {String => Array} (see config.responses_without_block)
2502+
# responses(type) -> frozen array
24942503
# responses {|hash| ...} -> block result
24952504
# responses(type) {|array| ...} -> block result
24962505
#
2497-
# Yields unhandled responses and returns the result of the block.
2506+
# Yields or returns unhandled server responses. Unhandled responses are
2507+
# stored in a hash, with arrays of UntaggedResponse#data keyed by
2508+
# UntaggedResponse#name and <em>non-+nil+</em> untagged ResponseCode#data
2509+
# keyed by ResponseCode#name.
2510+
#
2511+
# When a block is given, yields unhandled responses and returns the block's
2512+
# result. Without a block, returns the unhandled responses.
2513+
#
2514+
# [With +type+]
2515+
# Yield or return only the array of responses for that +type+.
2516+
# When no block is given, the returned array is a frozen copy.
2517+
# [Without +type+]
2518+
# Yield or return the entire responses hash.
2519+
#
2520+
# When no block is given, the behavior is determined by
2521+
# Config#responses_without_block:
2522+
# >>>
2523+
# [+:silence_deprecation_warning+ <em>(original behavior)</em>]
2524+
# Returns the mutable responses hash (without any warnings).
2525+
# <em>This is not thread-safe.</em>
2526+
#
2527+
# [+:warn+ <em>(default since +v0.5+)</em>]
2528+
# Prints a warning and returns the mutable responses hash.
2529+
# <em>This is not thread-safe.</em>
2530+
#
2531+
# [+:frozen_dup+ <em>(planned default for +v0.6+)</em>]
2532+
# Returns a frozen copy of the unhandled responses hash, with frozen
2533+
# array values.
24982534
#
2499-
# Unhandled responses are stored in a hash, with arrays of
2500-
# <em>non-+nil+</em> UntaggedResponse#data keyed by UntaggedResponse#name
2501-
# and ResponseCode#data keyed by ResponseCode#name. Call without +type+ to
2502-
# yield the entire responses hash. Call with +type+ to yield only the array
2503-
# of responses for that type.
2535+
# [+:raise+]
2536+
# Raise an +ArgumentError+ with the deprecation warning.
25042537
#
25052538
# For example:
25062539
#
25072540
# imap.select("inbox")
2508-
# p imap.responses("EXISTS", &:last)
2541+
# p imap.responses("EXISTS").last
25092542
# #=> 2
2543+
# p imap.responses("UIDNEXT", &:last)
2544+
# #=> 123456
25102545
# p imap.responses("UIDVALIDITY", &:last)
25112546
# #=> 968263756
2547+
# p imap.responses {|responses|
2548+
# {
2549+
# exists: responses.delete("EXISTS").last,
2550+
# uidnext: responses.delete("UIDNEXT").last,
2551+
# uidvalidity: responses.delete("UIDVALIDITY").last,
2552+
# }
2553+
# }
2554+
# #=> {:exists=>2, :uidnext=>123456, :uidvalidity=>968263756}
2555+
# # "EXISTS", "UIDNEXT", and "UIDVALIDITY" have been removed:
2556+
# p imap.responses(&:keys)
2557+
# #=> ["FLAGS", "OK", "PERMANENTFLAGS", "RECENT", "HIGHESTMODSEQ"]
2558+
#
2559+
# Related: #extract_responses, #clear_responses, #response_handlers, #greeting
25122560
#
2561+
# ===== Thread safety
25132562
# >>>
25142563
# *Note:* Access to the responses hash is synchronized for thread-safety.
25152564
# The receiver thread and response_handlers cannot process new responses
25162565
# until the block completes. Accessing either the response hash or its
2517-
# response type arrays outside of the block is unsafe.
2566+
# response type arrays outside of the block is unsafe. They can be safely
2567+
# updated inside the block. Consider using #clear_responses or
2568+
# #extract_responses instead.
2569+
#
2570+
# Net::IMAP will add and remove responses from the responses hash and its
2571+
# array values, in the calling threads for commands and in the receiver
2572+
# thread, but will not modify any responses after adding them to the
2573+
# responses hash.
25182574
#
2519-
# Calling without a block is unsafe and deprecated. Future releases will
2520-
# raise ArgumentError unless a block is given.
2521-
# See Config#responses_without_block.
2575+
# ===== Clearing responses
25222576
#
25232577
# Previously unhandled responses are automatically cleared before entering a
25242578
# mailbox with #select or #examine. Long-lived connections can receive many
25252579
# unhandled server responses, which must be pruned or they will continually
25262580
# consume more memory. Update or clear the responses hash or arrays inside
2527-
# the block, or use #clear_responses.
2581+
# the block, or remove responses with #extract_responses, #clear_responses,
2582+
# or #add_response_handler.
2583+
#
2584+
# ===== Missing responses
25282585
#
25292586
# Only non-+nil+ data is stored. Many important response codes have no data
25302587
# of their own, but are used as "tags" on the ResponseText object they are
@@ -2535,20 +2592,24 @@ def idle_done
25352592
# ResponseCode#data on tagged responses. Although some command methods do
25362593
# return the TaggedResponse directly, #add_response_handler must be used to
25372594
# handle all response codes.
2538-
#
2539-
# Related: #extract_responses, #clear_responses, #response_handlers, #greeting
25402595
def responses(type = nil)
25412596
if block_given?
25422597
synchronize { yield(type ? @responses[type.to_s.upcase] : @responses) }
25432598
elsif type
2544-
raise ArgumentError, "Pass a block or use #clear_responses"
2599+
synchronize { @responses[type.to_s.upcase].dup.freeze }
25452600
else
25462601
case config.responses_without_block
25472602
when :raise
2548-
raise ArgumentError, "Pass a block or use #clear_responses"
2603+
raise ArgumentError, RESPONSES_DEPRECATION_MSG
25492604
when :warn
2550-
warn("DEPRECATED: pass a block or use #clear_responses",
2551-
uplevel: 1, category: :deprecated)
2605+
warn(RESPONSES_DEPRECATION_MSG, uplevel: 1, category: :deprecated)
2606+
when :frozen_dup
2607+
synchronize {
2608+
responses = @responses.transform_values(&:freeze)
2609+
responses.default_proc = nil
2610+
responses.default = [].freeze
2611+
return responses.freeze
2612+
}
25522613
end
25532614
@responses
25542615
end

β€Žlib/net/imap/config.rb

Lines changed: 70 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
module Net
88
class IMAP
99

10-
# Net::IMAP::Config stores configuration options for Net::IMAP clients.
11-
# The global configuration can be seen at either Net::IMAP.config or
12-
# Net::IMAP::Config.global, and the client-specific configuration can be
13-
# seen at Net::IMAP#config.
10+
# Net::IMAP::Config <em>(available since +v0.4.13+)</em> stores
11+
# configuration options for Net::IMAP clients. The global configuration can
12+
# be seen at either Net::IMAP.config or Net::IMAP::Config.global, and the
13+
# client-specific configuration can be seen at Net::IMAP#config.
1414
#
1515
# When creating a new client, all unhandled keyword arguments to
1616
# Net::IMAP.new are delegated to Config.new. Every client has its own
@@ -128,7 +128,7 @@ def self.default; @default end
128128
# The global config object. Also available from Net::IMAP.config.
129129
def self.global; @global if defined?(@global) end
130130

131-
# A hash of hard-coded configurations, indexed by version number.
131+
# A hash of hard-coded configurations, indexed by version number or name.
132132
def self.version_defaults; @version_defaults end
133133
@version_defaults = {}
134134

@@ -172,9 +172,16 @@ def self.[](config)
172172
include AttrInheritance
173173
include AttrTypeCoercion
174174

175-
# The debug mode (boolean)
175+
# The debug mode (boolean). The default value is +false+.
176176
#
177-
# The default value is +false+.
177+
# When #debug is +true+:
178+
# * Data sent to and received from the server will be logged.
179+
# * ResponseParser will print warnings with extra detail for parse
180+
# errors. _This may include recoverable errors._
181+
# * ResponseParser makes extra assertions.
182+
#
183+
# *NOTE:* Versioned default configs inherit #debug from Config.global, and
184+
# #load_defaults will not override #debug.
178185
attr_accessor :debug, type: :boolean
179186

180187
# method: debug?
@@ -200,60 +207,84 @@ def self.[](config)
200207
# The default value is +5+ seconds.
201208
attr_accessor :idle_response_timeout, type: Integer
202209

203-
# :markup: markdown
204-
#
205210
# Whether to use the +SASL-IR+ extension when the server and \SASL
206-
# mechanism both support it.
211+
# mechanism both support it. Can be overridden by the +sasl_ir+ keyword
212+
# parameter to Net::IMAP#authenticate.
213+
#
214+
# <em>(Support for +SASL-IR+ was added in +v0.4.0+.)</em>
207215
#
208-
# See Net::IMAP#authenticate.
216+
# ==== Valid options
209217
#
210-
# | Starting with version | The default value is |
211-
# |-----------------------|------------------------------------------|
212-
# | _original_ | +false+ <em>(extension unsupported)</em> |
213-
# | v0.4 | +true+ <em>(support added)</em> |
218+
# [+false+ <em>(original behavior, before support was added)</em>]
219+
# Do not use +SASL-IR+, even when it is supported by the server and the
220+
# mechanism.
221+
#
222+
# [+true+ <em>(default since +v0.4+)</em>]
223+
# Use +SASL-IR+ when it is supported by the server and the mechanism.
214224
attr_accessor :sasl_ir, type: :boolean
215225

216-
# :markup: markdown
217-
#
218-
# Controls the behavior of Net::IMAP#login when the `LOGINDISABLED`
226+
# Controls the behavior of Net::IMAP#login when the +LOGINDISABLED+
219227
# capability is present. When enforced, Net::IMAP will raise a
220-
# LoginDisabledError when that capability is present. Valid values are:
228+
# LoginDisabledError when that capability is present.
221229
#
222-
# [+false+]
230+
# <em>(Support for +LOGINDISABLED+ was added in +v0.5.0+.)</em>
231+
#
232+
# ==== Valid options
233+
#
234+
# [+false+ <em>(original behavior, before support was added)</em>]
223235
# Send the +LOGIN+ command without checking for +LOGINDISABLED+.
224236
#
225237
# [+:when_capabilities_cached+]
226238
# Enforce the requirement when Net::IMAP#capabilities_cached? is true,
227239
# but do not send a +CAPABILITY+ command to discover the capabilities.
228240
#
229-
# [+true+]
241+
# [+true+ <em>(default since +v0.5+)</em>]
230242
# Only send the +LOGIN+ command if the +LOGINDISABLED+ capability is not
231243
# present. When capabilities are unknown, Net::IMAP will automatically
232244
# send a +CAPABILITY+ command first before sending +LOGIN+.
233245
#
234-
# | Starting with version | The default value is |
235-
# |-------------------------|--------------------------------|
236-
# | _original_ | `false` |
237-
# | v0.5 | `true` |
238246
attr_accessor :enforce_logindisabled, type: [
239247
false, :when_capabilities_cached, true
240248
]
241249

242-
# :markup: markdown
250+
# Controls the behavior of Net::IMAP#responses when called without any
251+
# arguments (+type+ or +block+).
252+
#
253+
# ==== Valid options
254+
#
255+
# [+:silence_deprecation_warning+ <em>(original behavior)</em>]
256+
# Returns the mutable responses hash (without any warnings).
257+
# <em>This is not thread-safe.</em>
258+
#
259+
# [+:warn+ <em>(default since +v0.5+)</em>]
260+
# Prints a warning and returns the mutable responses hash.
261+
# <em>This is not thread-safe.</em>
243262
#
244-
# Controls the behavior of Net::IMAP#responses when called without a
245-
# block. Valid options are `:warn`, `:raise`, or
246-
# `:silence_deprecation_warning`.
263+
# [+:frozen_dup+ <em>(planned default for +v0.6+)</em>]
264+
# Returns a frozen copy of the unhandled responses hash, with frozen
265+
# array values.
247266
#
248-
# | Starting with version | The default value is |
249-
# |-------------------------|--------------------------------|
250-
# | v0.4.13 | +:silence_deprecation_warning+ |
251-
# | v0.5 | +:warn+ |
252-
# | _eventually_ | +:raise+ |
267+
# Note that calling IMAP#responses with a +type+ and without a block is
268+
# not configurable and always behaves like +:frozen_dup+.
269+
#
270+
# <em>(+:frozen_dup+ config option was added in +v0.4.17+)</em>
271+
#
272+
# [+:raise+]
273+
# Raise an ArgumentError with the deprecation warning.
274+
#
275+
# Note: #responses_without_args is an alias for #responses_without_block.
253276
attr_accessor :responses_without_block, type: [
254-
:silence_deprecation_warning, :warn, :raise,
277+
:silence_deprecation_warning, :warn, :frozen_dup, :raise,
255278
]
256279

280+
alias responses_without_args responses_without_block # :nodoc:
281+
alias responses_without_args= responses_without_block= # :nodoc:
282+
283+
##
284+
# :attr_accessor: responses_without_args
285+
#
286+
# Alias for responses_without_block
287+
257288
# Creates a new config object and initialize its attribute with +attrs+.
258289
#
259290
# If +parent+ is not given, the global config is used by default.
@@ -357,12 +388,11 @@ def defaults_hash
357388

358389
version_defaults[0.5] = Config[:current]
359390

360-
version_defaults[0.6] = Config[0.5]
361-
version_defaults[:next] = Config[0.6]
362-
363-
version_defaults[:future] = Config[0.6].dup.update(
364-
responses_without_block: :raise,
391+
version_defaults[0.6] = Config[0.5].dup.update(
392+
responses_without_block: :frozen_dup,
365393
).freeze
394+
version_defaults[:next] = Config[0.6]
395+
version_defaults[:future] = Config[:next]
366396

367397
version_defaults.freeze
368398
end

β€Žtest/lib/helper.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,16 @@
22
require "core_assertions"
33

44
Test::Unit::TestCase.include Test::Unit::CoreAssertions
5+
6+
class Test::Unit::TestCase
7+
def wait_for_response_count(imap, type:, count:,
8+
timeout: 0.5, interval: 0.001)
9+
deadline = Time.now + timeout
10+
loop do
11+
current_count = imap.responses(type, &:size)
12+
break :count if count <= current_count
13+
break :deadline if deadline < Time.now
14+
sleep interval
15+
end
16+
end
17+
end

β€Žtest/net/imap/test_config.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ class ConfigTest < Test::Unit::TestCase
190190
assert_same Config.default, Config.new(Config.default).parent
191191
assert_same Config.global, Config.new(Config.global).parent
192192
assert_same Config[0.4], Config.new(0.4).parent
193-
assert_same Config[0.5], Config.new(:next).parent
193+
assert_same Config[0.6], Config.new(:next).parent
194194
assert_equal true, Config.new({debug: true}, debug: false).parent.debug?
195195
assert_equal true, Config.new({debug: true}, debug: false).parent.frozen?
196196
end

0 commit comments

Comments
Β (0)