Skip to content

Commit bb4a2c9

Browse files
authored
🔀 Merge pull request #339 from ruby/backport/0.4/334/responses-return-frozen_dup
✨ New config option to return frozen dup from #responses [backport: #334]
2 parents 2302839 + 0c8df40 commit bb4a2c9

File tree

6 files changed

+403
-156
lines changed

6 files changed

+403
-156
lines changed

‎lib/net/imap.rb

Lines changed: 79 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2496,41 +2496,98 @@ def idle_done
24962496
end
24972497
end
24982498

2499+
RESPONSES_DEPRECATION_MSG =
2500+
"Pass a type or block to #responses, " \
2501+
"set config.responses_without_block to :frozen_dup " \
2502+
"or :silence_deprecation_warning, " \
2503+
"or use #extract_responses or #clear_responses."
2504+
private_constant :RESPONSES_DEPRECATION_MSG
2505+
24992506
# :call-seq:
2507+
# responses -> hash of {String => Array} (see config.responses_without_block)
2508+
# responses(type) -> frozen array
25002509
# responses {|hash| ...} -> block result
25012510
# responses(type) {|array| ...} -> block result
25022511
#
2503-
# Yields unhandled responses and returns the result of the block.
2512+
# Yields or returns unhandled server responses. Unhandled responses are
2513+
# stored in a hash, with arrays of UntaggedResponse#data keyed by
2514+
# UntaggedResponse#name and <em>non-+nil+</em> untagged ResponseCode#data
2515+
# keyed by ResponseCode#name.
2516+
#
2517+
# When a block is given, yields unhandled responses and returns the block's
2518+
# result. Without a block, returns the unhandled responses.
2519+
#
2520+
# [With +type+]
2521+
# Yield or return only the array of responses for that +type+.
2522+
# When no block is given, the returned array is a frozen copy.
2523+
# [Without +type+]
2524+
# Yield or return the entire responses hash.
2525+
#
2526+
# When no block is given, the behavior is determined by
2527+
# Config#responses_without_block:
2528+
# >>>
2529+
# [+:silence_deprecation_warning+ <em>(original behavior)</em>]
2530+
# Returns the mutable responses hash (without any warnings).
2531+
# <em>This is not thread-safe.</em>
2532+
#
2533+
# [+:warn+ <em>(default since +v0.5+)</em>]
2534+
# Prints a warning and returns the mutable responses hash.
2535+
# <em>This is not thread-safe.</em>
2536+
#
2537+
# [+:frozen_dup+ <em>(planned default for +v0.6+)</em>]
2538+
# Returns a frozen copy of the unhandled responses hash, with frozen
2539+
# array values.
25042540
#
2505-
# Unhandled responses are stored in a hash, with arrays of
2506-
# <em>non-+nil+</em> UntaggedResponse#data keyed by UntaggedResponse#name
2507-
# and ResponseCode#data keyed by ResponseCode#name. Call without +type+ to
2508-
# yield the entire responses hash. Call with +type+ to yield only the array
2509-
# of responses for that type.
2541+
# [+:raise+]
2542+
# Raise an +ArgumentError+ with the deprecation warning.
25102543
#
25112544
# For example:
25122545
#
25132546
# imap.select("inbox")
2514-
# p imap.responses("EXISTS", &:last)
2547+
# p imap.responses("EXISTS").last
25152548
# #=> 2
2549+
# p imap.responses("UIDNEXT", &:last)
2550+
# #=> 123456
25162551
# p imap.responses("UIDVALIDITY", &:last)
25172552
# #=> 968263756
2553+
# p imap.responses {|responses|
2554+
# {
2555+
# exists: responses.delete("EXISTS").last,
2556+
# uidnext: responses.delete("UIDNEXT").last,
2557+
# uidvalidity: responses.delete("UIDVALIDITY").last,
2558+
# }
2559+
# }
2560+
# #=> {:exists=>2, :uidnext=>123456, :uidvalidity=>968263756}
2561+
# # "EXISTS", "UIDNEXT", and "UIDVALIDITY" have been removed:
2562+
# p imap.responses(&:keys)
2563+
# #=> ["FLAGS", "OK", "PERMANENTFLAGS", "RECENT", "HIGHESTMODSEQ"]
2564+
#
2565+
# Related: #extract_responses, #clear_responses, #response_handlers, #greeting
25182566
#
2567+
# ===== Thread safety
25192568
# >>>
25202569
# *Note:* Access to the responses hash is synchronized for thread-safety.
25212570
# The receiver thread and response_handlers cannot process new responses
25222571
# until the block completes. Accessing either the response hash or its
2523-
# response type arrays outside of the block is unsafe.
2572+
# response type arrays outside of the block is unsafe. They can be safely
2573+
# updated inside the block. Consider using #clear_responses or
2574+
# #extract_responses instead.
2575+
#
2576+
# Net::IMAP will add and remove responses from the responses hash and its
2577+
# array values, in the calling threads for commands and in the receiver
2578+
# thread, but will not modify any responses after adding them to the
2579+
# responses hash.
25242580
#
2525-
# Calling without a block is unsafe and deprecated. Future releases will
2526-
# raise ArgumentError unless a block is given.
2527-
# See Config#responses_without_block.
2581+
# ===== Clearing responses
25282582
#
25292583
# Previously unhandled responses are automatically cleared before entering a
25302584
# mailbox with #select or #examine. Long-lived connections can receive many
25312585
# unhandled server responses, which must be pruned or they will continually
25322586
# consume more memory. Update or clear the responses hash or arrays inside
2533-
# the block, or use #clear_responses.
2587+
# the block, or remove responses with #extract_responses, #clear_responses,
2588+
# or #add_response_handler.
2589+
#
2590+
# ===== Missing responses
25342591
#
25352592
# Only non-+nil+ data is stored. Many important response codes have no data
25362593
# of their own, but are used as "tags" on the ResponseText object they are
@@ -2541,19 +2598,24 @@ def idle_done
25412598
# ResponseCode#data on tagged responses. Although some command methods do
25422599
# return the TaggedResponse directly, #add_response_handler must be used to
25432600
# handle all response codes.
2544-
#
2545-
# Related: #extract_responses, #clear_responses, #response_handlers, #greeting
25462601
def responses(type = nil)
25472602
if block_given?
25482603
synchronize { yield(type ? @responses[type.to_s.upcase] : @responses) }
25492604
elsif type
2550-
raise ArgumentError, "Pass a block or use #clear_responses"
2605+
synchronize { @responses[type.to_s.upcase].dup.freeze }
25512606
else
25522607
case config.responses_without_block
25532608
when :raise
2554-
raise ArgumentError, "Pass a block or use #clear_responses"
2609+
raise ArgumentError, RESPONSES_DEPRECATION_MSG
25552610
when :warn
2556-
warn("DEPRECATED: pass a block or use #clear_responses", uplevel: 1)
2611+
warn(RESPONSES_DEPRECATION_MSG, uplevel: 1)
2612+
when :frozen_dup
2613+
synchronize {
2614+
responses = @responses.transform_values(&:freeze)
2615+
responses.default_proc = nil
2616+
responses.default = [].freeze
2617+
return responses.freeze
2618+
}
25572619
end
25582620
@responses
25592621
end

‎lib/net/imap/config.rb

Lines changed: 62 additions & 27 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,34 +207,61 @@ 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>
215+
#
216+
# ==== Valid options
207217
#
208-
# See Net::IMAP#authenticate.
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.
209221
#
210-
# | Starting with version | The default value is |
211-
# |-----------------------|------------------------------------------|
212-
# | _original_ | +false+ <em>(extension unsupported)</em> |
213-
# | v0.4 | +true+ <em>(support added)</em> |
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
226+
227+
# Controls the behavior of Net::IMAP#responses when called without any
228+
# arguments (+type+ or +block+).
229+
#
230+
# ==== Valid options
231+
#
232+
# [+:silence_deprecation_warning+ <em>(original behavior)</em>]
233+
# Returns the mutable responses hash (without any warnings).
234+
# <em>This is not thread-safe.</em>
217235
#
218-
# Controls the behavior of Net::IMAP#responses when called without a
219-
# block. Valid options are `:warn`, `:raise`, or
220-
# `:silence_deprecation_warning`.
236+
# [+:warn+ <em>(default since +v0.5+)</em>]
237+
# Prints a warning and returns the mutable responses hash.
238+
# <em>This is not thread-safe.</em>
221239
#
222-
# | Starting with version | The default value is |
223-
# |-------------------------|--------------------------------|
224-
# | v0.4.13 | +:silence_deprecation_warning+ |
225-
# | v0.5 <em>(planned)</em> | +:warn+ |
226-
# | _eventually_ | +:raise+ |
240+
# [+:frozen_dup+ <em>(planned default for +v0.6+)</em>]
241+
# Returns a frozen copy of the unhandled responses hash, with frozen
242+
# array values.
243+
#
244+
# Note that calling IMAP#responses with a +type+ and without a block is
245+
# not configurable and always behaves like +:frozen_dup+.
246+
#
247+
# <em>(+:frozen_dup+ config option was added in +v0.4.17+)</em>
248+
#
249+
# [+:raise+]
250+
# Raise an ArgumentError with the deprecation warning.
251+
#
252+
# Note: #responses_without_args is an alias for #responses_without_block.
227253
attr_accessor :responses_without_block, type: [
228-
:silence_deprecation_warning, :warn, :raise,
254+
:silence_deprecation_warning, :warn, :frozen_dup, :raise,
229255
]
230256

257+
alias responses_without_args responses_without_block # :nodoc:
258+
alias responses_without_args= responses_without_block= # :nodoc:
259+
260+
##
261+
# :attr_accessor: responses_without_args
262+
#
263+
# Alias for responses_without_block
264+
231265
# Creates a new config object and initialize its attribute with +attrs+.
232266
#
233267
# If +parent+ is not given, the global config is used by default.
@@ -329,9 +363,10 @@ def defaults_hash
329363
version_defaults[:current] = Config[0.4]
330364
version_defaults[:next] = Config[0.5]
331365

332-
version_defaults[:future] = Config[0.5].dup.update(
333-
responses_without_block: :raise,
366+
version_defaults[0.6] = Config[0.5].dup.update(
367+
responses_without_block: :frozen_dup,
334368
).freeze
369+
version_defaults[:future] = Config[0.6]
335370

336371
version_defaults.freeze
337372
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 & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ class ConfigTest < Test::Unit::TestCase
191191
assert_same Config.global, Config.new(Config.global).parent
192192
assert_same Config[0.4], Config.new(0.4).parent
193193
assert_same Config[0.5], Config.new(:next).parent
194+
assert_same Config[0.6], Config.new(:future).parent
194195
assert_equal true, Config.new({debug: true}, debug: false).parent.debug?
195196
assert_equal true, Config.new({debug: true}, debug: false).parent.frozen?
196197
end

0 commit comments

Comments
 (0)