rails/activesupport/CHANGELOG.md

459 lines
15 KiB
Markdown
Raw Normal View History

* Add `Rails.env.local?` shorthand for `Rails.env.development? || Rails.env.test?`.
*DHH*
* `ActiveSupport::Testing::TimeHelpers` now accepts named `with_usec` argument
to `freeze_time`, `travel`, and `travel_to` methods. Passing true prevents
truncating the destination time with `change(usec: 0)`.
*KevSlashNull*, and *serprex*
* `ActiveSupport::CurrentAttributes.resets` now accepts a method name
The block API is still the recommended approach, but now both APIs are supported:
```ruby
class Current < ActiveSupport::CurrentAttributes
resets { Time.zone = nil }
resets :clear_time_zone
end
```
*Alex Ghiculescu*
* Ensure `ActiveSupport::Testing::Isolation::Forking` closes pipes
Previously, `Forking.run_in_isolation` opened two ends of a pipe. The fork
process closed the read end, wrote to it, and then terminated (which
presumably closed the file descriptors on its end). The parent process
closed the write end, read from it, and returned, never closing the read
end.
This resulted in an accumulation of open file descriptors, which could
cause errors if the limit is reached.
*Sam Bostock*
Fix Time#change and #advance around the end of DST Prior to this commit, when `Time#change` or `Time#advance` constructed a time inside the final stretch of Daylight Saving Time (DST), the non-DST offset would always be chosen for local times: ```ruby # DST ended just before 2021-11-07 2:00:00 AM in US/Eastern. ENV["TZ"] = "US/Eastern" time = Time.local(2021, 11, 07, 00, 59, 59) + 1 # => 2021-11-07 01:00:00 -0400 time.change(day: 07) # => 2021-11-07 01:00:00 -0500 time.advance(seconds: 0) # => 2021-11-07 01:00:00 -0500 time = Time.local(2021, 11, 06, 01, 00, 00) # => 2021-11-06 01:00:00 -0400 time.change(day: 07) # => 2021-11-07 01:00:00 -0500 time.advance(days: 1) # => 2021-11-07 01:00:00 -0500 ``` And the DST offset would always be chosen for times with a `TimeZone` object: ```ruby Time.zone = "US/Eastern" time = Time.new(2021, 11, 07, 02, 00, 00, Time.zone) - 3600 # => 2021-11-07 01:00:00 -0500 time.change(day: 07) # => 2021-11-07 01:00:00 -0400 time.advance(seconds: 0) # => 2021-11-07 01:00:00 -0400 time = Time.new(2021, 11, 8, 01, 00, 00, Time.zone) # => 2021-11-08 01:00:00 -0500 time.change(day: 07) # => 2021-11-07 01:00:00 -0400 time.advance(days: -1) # => 2021-11-07 01:00:00 -0400 ``` This commit fixes `Time#change` and `Time#advance` to choose the offset that matches the original time's offset when possible: ```ruby ENV["TZ"] = "US/Eastern" time = Time.local(2021, 11, 07, 00, 59, 59) + 1 # => 2021-11-07 01:00:00 -0400 time.change(day: 07) # => 2021-11-07 01:00:00 -0400 time.advance(seconds: 0) # => 2021-11-07 01:00:00 -0400 time = Time.local(2021, 11, 06, 01, 00, 00) # => 2021-11-06 01:00:00 -0400 time.change(day: 07) # => 2021-11-07 01:00:00 -0400 time.advance(days: 1) # => 2021-11-07 01:00:00 -0400 Time.zone = "US/Eastern" time = Time.new(2021, 11, 07, 02, 00, 00, Time.zone) - 3600 # => 2021-11-07 01:00:00 -0500 time.change(day: 07) # => 2021-11-07 01:00:00 -0500 time.advance(seconds: 0) # => 2021-11-07 01:00:00 -0500 time = Time.new(2021, 11, 8, 01, 00, 00, Time.zone) # => 2021-11-08 01:00:00 -0500 time.change(day: 07) # => 2021-11-07 01:00:00 -0500 time.advance(days: -1) # => 2021-11-07 01:00:00 -0500 ``` It's worth noting that there is a somewhat dubious case when calling `advance` with a mix of calendar and clock units (e.g. months + hours). `advance` adds calendar units first, then adds clock units. So a non-DST time may first be advanced to a date within DST before any clock units are applied. For example: ```ruby # DST began on 2021-03-14 in US/Eastern. Time.zone = "US/Eastern" non_dst = Time.new(2021, 03, 07, 00, 00, 00, Time.zone) # => 2021-03-07 00:00:00 -0500 non_dst.advance(months: 8, hours: 1) # => 2021-11-07 01:00:00 -0400 ``` One could argue that the expected result is `2021-11-07 01:00:00 -0500`. However, the difference between that and the result for `hours: 0` is greater than 1 hour: ```ruby adjusted_result = non_dst.advance(months: 8, hours: 1) + 3600 # => 2021-11-07 01:00:00 -0500 adjusted_result - non_dst.advance(months: 8, hours: 0) # => 7200.0 ``` Which might also be unexpected. Furthermore, it may be difficult (or expensive) to apply such an adjustment in a consistent way. For example, the result for `hours: 2` does have the expected `-0500` offset, so it might seem no adjustment is necessary, but if we didn't adjust it too, it would conflict with the adjusted `hours: 1` result: ```ruby non_dst.advance(months: 8, hours: 2) # => 2021-11-07 01:00:00 -0500 ``` Therefore, the approach in this commit (which produces a `-0400` offset for `hours: 1`) seems like a reasonable compromise. Fixes #45055. Closes #45556. Closes #46248. Co-authored-by: Kevin Hall <bigtoe416@yahoo.com> Co-authored-by: Takayoshi Nishida <takayoshi.nishida@gmail.com>
2022-10-15 19:55:40 +00:00
* Fix `Time#change` and `Time#advance` for times around the end of Daylight
Saving Time.
Previously, when `Time#change` or `Time#advance` constructed a time inside
the final stretch of Daylight Saving Time (DST), the non-DST offset would
always be chosen for local times:
```ruby
# DST ended just before 2021-11-07 2:00:00 AM in US/Eastern.
ENV["TZ"] = "US/Eastern"
time = Time.local(2021, 11, 07, 00, 59, 59) + 1
# => 2021-11-07 01:00:00 -0400
time.change(day: 07)
# => 2021-11-07 01:00:00 -0500
time.advance(seconds: 0)
# => 2021-11-07 01:00:00 -0500
time = Time.local(2021, 11, 06, 01, 00, 00)
# => 2021-11-06 01:00:00 -0400
time.change(day: 07)
# => 2021-11-07 01:00:00 -0500
time.advance(days: 1)
# => 2021-11-07 01:00:00 -0500
```
And the DST offset would always be chosen for times with a `TimeZone`
object:
```ruby
Time.zone = "US/Eastern"
time = Time.new(2021, 11, 07, 02, 00, 00, Time.zone) - 3600
# => 2021-11-07 01:00:00 -0500
time.change(day: 07)
# => 2021-11-07 01:00:00 -0400
time.advance(seconds: 0)
# => 2021-11-07 01:00:00 -0400
time = Time.new(2021, 11, 8, 01, 00, 00, Time.zone)
# => 2021-11-08 01:00:00 -0500
time.change(day: 07)
# => 2021-11-07 01:00:00 -0400
time.advance(days: -1)
# => 2021-11-07 01:00:00 -0400
```
Now, `Time#change` and `Time#advance` will choose the offset that matches
the original time's offset when possible:
```ruby
ENV["TZ"] = "US/Eastern"
time = Time.local(2021, 11, 07, 00, 59, 59) + 1
# => 2021-11-07 01:00:00 -0400
time.change(day: 07)
# => 2021-11-07 01:00:00 -0400
time.advance(seconds: 0)
# => 2021-11-07 01:00:00 -0400
time = Time.local(2021, 11, 06, 01, 00, 00)
# => 2021-11-06 01:00:00 -0400
time.change(day: 07)
# => 2021-11-07 01:00:00 -0400
time.advance(days: 1)
# => 2021-11-07 01:00:00 -0400
Time.zone = "US/Eastern"
time = Time.new(2021, 11, 07, 02, 00, 00, Time.zone) - 3600
# => 2021-11-07 01:00:00 -0500
time.change(day: 07)
# => 2021-11-07 01:00:00 -0500
time.advance(seconds: 0)
# => 2021-11-07 01:00:00 -0500
time = Time.new(2021, 11, 8, 01, 00, 00, Time.zone)
# => 2021-11-08 01:00:00 -0500
time.change(day: 07)
# => 2021-11-07 01:00:00 -0500
time.advance(days: -1)
# => 2021-11-07 01:00:00 -0500
```
*Kevin Hall*, *Takayoshi Nishida*, and *Jonathan Hefner*
* Fix MemoryStore to preserve entries TTL when incrementing or decrementing
This is to be more consistent with how MemCachedStore and RedisCacheStore behaves.
*Jean Boussier*
* `Rails.error.handle` and `Rails.error.record` filter now by multiple error classes.
```ruby
Rails.error.handle(IOError, ArgumentError) do
1 + '1' # raises TypeError
end
1 + 1 # TypeErrors are not IOErrors or ArgumentError, so this will *not* be handled
```
*Martin Spickermann*
* `Class#subclasses` and `Class#descendants` now automatically filter reloaded classes.
Previously they could return old implementations of reloadable classes that have been
dereferenced but not yet garbage collected.
They now automatically filter such classes like `DescendantTracker#subclasses` and
`DescendantTracker#descendants`.
*Jean Boussier*
* `Rails.error.report` now marks errors as reported to avoid reporting them twice.
In some cases, users might want to report errors explicitly with some extra context
before letting it bubble up.
This also allows to safely catch and report errors outside of the execution context.
*Jean Boussier*
* Add `assert_error_reported` and `assert_no_error_reported`
Allows to easily asserts an error happened but was handled
```ruby
report = assert_error_reported(IOError) do
# ...
end
assert_equal "Oops", report.error.message
assert_equal "admin", report.context[:section]
assert_equal :warning, report.severity
assert_predicate report, :handled?
```
*Jean Boussier*
* `ActiveSupport::Deprecation` behavior callbacks can now receive the
deprecator instance as an argument. This makes it easier for such callbacks
to change their behavior based on the deprecator's state. For example,
based on the deprecator's `debug` flag.
3-arity and splat-args callbacks such as the following will now be passed
the deprecator instance as their third argument:
* `->(message, callstack, deprecator) { ... }`
* `->(*args) { ... }`
* `->(message, *other_args) { ... }`
2-arity and 4-arity callbacks such as the following will continue to behave
the same as before:
* `->(message, callstack) { ... }`
* `->(message, callstack, deprecation_horizon, gem_name) { ... }`
* `->(message, callstack, *deprecation_details) { ... }`
*Jonathan Hefner*
* `ActiveSupport::Deprecation#disallowed_warnings` now affects the instance on
which it is configured.
This means that individual `ActiveSupport::Deprecation` instances can be
configured with their own disallowed warnings, and the global
`ActiveSupport::Deprecation.disallowed_warnings` now only affects the global
`ActiveSupport::Deprecation.warn`.
**Before**
```ruby
ActiveSupport::Deprecation.disallowed_warnings = ["foo"]
deprecator = ActiveSupport::Deprecation.new("2.0", "MyCoolGem")
deprecator.disallowed_warnings = ["bar"]
ActiveSupport::Deprecation.warn("foo") # => raise ActiveSupport::DeprecationException
ActiveSupport::Deprecation.warn("bar") # => print "DEPRECATION WARNING: bar"
deprecator.warn("foo") # => raise ActiveSupport::DeprecationException
deprecator.warn("bar") # => print "DEPRECATION WARNING: bar"
```
**After**
```ruby
ActiveSupport::Deprecation.disallowed_warnings = ["foo"]
deprecator = ActiveSupport::Deprecation.new("2.0", "MyCoolGem")
deprecator.disallowed_warnings = ["bar"]
ActiveSupport::Deprecation.warn("foo") # => raise ActiveSupport::DeprecationException
ActiveSupport::Deprecation.warn("bar") # => print "DEPRECATION WARNING: bar"
deprecator.warn("foo") # => print "DEPRECATION WARNING: foo"
deprecator.warn("bar") # => raise ActiveSupport::DeprecationException
```
*Jonathan Hefner*
* Add italic and underline support to `ActiveSupport::LogSubscriber#color`
Previously, only bold text was supported via a positional argument.
This allows for bold, italic, and underline options to be specified
for colored logs.
```ruby
info color("Hello world!", :red, bold: true, underline: true)
```
*Gannon McGibbon*
* Add `String#downcase_first` method.
This method is the corollary of `String#upcase_first`.
*Mark Schneider*
* `thread_mattr_accessor` will call `.dup.freeze` on non-frozen default values.
This provides a basic level of protection against different threads trying
to mutate a shared default object.
*Jonathan Hefner*
* Add `raise_on_invalid_cache_expiration_time` config to `ActiveSupport::Cache::Store`
Specifies if an `ArgumentError` should be raised if `Rails.cache` `fetch` or
`write` are given an invalid `expires_at` or `expires_in` time.
Options are `true`, and `false`. If `false`, the exception will be reported
as `handled` and logged instead. Defaults to `true` if `config.load_defaults >= 7.1`.
*Trevor Turk*
* `ActiveSupport::Cache:Store#fetch` now passes an options accessor to the block.
It makes possible to override cache options:
Rails.cache.fetch("3rd-party-token") do |name, options|
token = fetch_token_from_remote
# set cache's TTL to match token's TTL
options.expires_in = token.expires_in
token
end
*Andrii Gladkyi*, *Jean Boussier*
Fix `thread_mattr_accessor` `default` option behavior This makes the value supplied to the `default` option of `thread_mattr_accessor` to be set in descendant classes as well as in any new Thread that starts. Previously, the `default` value provided was set only at the moment of defining the attribute writer, which would cause the attribute to be uninitialized in descendants and in other threads. For instance: ```ruby class Processor thread_mattr_accessor :mode, default: :smart end class SubProcessor < Processor end SubProcessor.mode # => :smart (was `nil` prior to this commit) Thread.new do Processor.mode # => :smart (was `nil` prior to this commit) end.join ``` If a non-`nil` default has been specified, there is a small (~7%) performance decrease when reading non-`nil` values, and a larger (~45%) performance decrease when reading `nil` values. Benchmark script: ```ruby # frozen_string_literal: true require "benchmark/ips" require "active_support" require "active_support/core_ext/module/attribute_accessors_per_thread" class MyClass thread_mattr_accessor :default_value, default: "default" thread_mattr_accessor :string_value, default: "default" thread_mattr_accessor :nil_value, default: "default" end MyClass.string_value = "string" MyClass.nil_value = nil Benchmark.ips do |x| x.report("default_value") { MyClass.default_value } x.report("string_value") { MyClass.string_value } x.report("nil_value") { MyClass.nil_value } end ``` Before this commit: ``` default_value 2.075M (± 0.7%) i/s - 10.396M in 5.010585s string_value 2.103M (± 0.7%) i/s - 10.672M in 5.074624s nil_value 1.777M (± 0.9%) i/s - 8.924M in 5.023058s ``` After this commit: ``` default_value 2.008M (± 0.7%) i/s - 10.187M in 5.072990s string_value 1.967M (± 0.7%) i/s - 9.891M in 5.028570s nil_value 1.144M (± 0.5%) i/s - 5.770M in 5.041630s ``` If no default or a `nil` default is specified, there is no performance impact. Fixes #43312. Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
2021-09-30 07:44:58 +00:00
* `default` option of `thread_mattr_accessor` now applies through inheritance and
also across new threads.
Previously, the `default` value provided was set only at the moment of defining
the attribute writer, which would cause the attribute to be uninitialized in
descendants and in other threads.
Fixes #43312.
*Thierry Deo*
* Redis cache store is now compatible with redis-rb 5.0.
*Jean Boussier*
* Add `skip_nil:` support to `ActiveSupport::Cache::Store#fetch_multi`.
*Daniel Alfaro*
* Add `quarter` method to date/time
*Matt Swanson*
* Fix `NoMethodError` on custom `ActiveSupport::Deprecation` behavior.
`ActiveSupport::Deprecation.behavior=` was supposed to accept any object
that responds to `call`, but in fact its internal implementation assumed that
this object could respond to `arity`, so it was restricted to only `Proc` objects.
This change removes this `arity` restriction of custom behaviors.
*Ryo Nakamura*
* Support `:url_safe` option for `MessageEncryptor`.
The `MessageEncryptor` constructor now accepts a `:url_safe` option, similar
to the `MessageVerifier` constructor. When enabled, this option ensures
that messages use a URL-safe encoding.
*Jonathan Hefner*
* Add `url_safe` option to `ActiveSupport::MessageVerifier` initializer
`ActiveSupport::MessageVerifier.new` now takes optional `url_safe` argument.
It can generate URL-safe strings by passing `url_safe: true`.
```ruby
verifier = ActiveSupport::MessageVerifier.new(url_safe: true)
message = verifier.generate(data) # => URL-safe string
```
This option is `false` by default to be backwards compatible.
*Shouichi Kamiya*
* Enable connection pooling by default for `MemCacheStore` and `RedisCacheStore`.
If you want to disable connection pooling, set `:pool` option to `false` when configuring the cache store:
```ruby
config.cache_store = :mem_cache_store, "cache.example.com", pool: false
```
*fatkodima*
* Add `force:` support to `ActiveSupport::Cache::Store#fetch_multi`.
*fatkodima*
* Deprecated `:pool_size` and `:pool_timeout` options for configuring connection pooling in cache stores.
Use `pool: true` to enable pooling with default settings:
```ruby
config.cache_store = :redis_cache_store, pool: true
```
Or pass individual options via `:pool` option:
```ruby
config.cache_store = :redis_cache_store, pool: { size: 10, timeout: 2 }
```
*fatkodima*
* Allow #increment and #decrement methods of `ActiveSupport::Cache::Store`
subclasses to set new values.
Previously incrementing or decrementing an unset key would fail and return
nil. A default will now be assumed and the key will be created.
*Andrej Blagojević*, *Eugene Kenny*
2022-05-15 10:45:22 +00:00
* Add `skip_nil:` support to `RedisCacheStore`
*Joey Paris*
* `ActiveSupport::Cache::MemoryStore#write(name, val, unless_exist:true)` now
correctly writes expired keys.
*Alan Savage*
* `ActiveSupport::ErrorReporter` now accepts and forward a `source:` parameter.
This allow libraries to signal the origin of the errors, and reporters
to easily ignore some sources.
*Jean Boussier*
* Fix and add protections for XSS in `ActionView::Helpers` and `ERB::Util`.
Add the method `ERB::Util.xml_name_escape` to escape dangerous characters
in names of tags and names of attributes, following the specification of XML.
*Álvaro Martín Fraguas*
* Respect `ActiveSupport::Logger.new`'s `:formatter` keyword argument
The stdlib `Logger::new` allows passing a `:formatter` keyword argument to
set the logger's formatter. Previously `ActiveSupport::Logger.new` ignored
that argument by always setting the formatter to an instance of
`ActiveSupport::Logger::SimpleFormatter`.
*Steven Harman*
* Deprecate preserving the pre-Ruby 2.4 behavior of `to_time`
With Ruby 2.4+ the default for +to_time+ changed from converting to the
local system time to preserving the offset of the receiver. At the time Rails
supported older versions of Ruby so a compatibility layer was added to assist
in the migration process. From Rails 5.0 new applications have defaulted to
the Ruby 2.4+ behavior and since Rails 7.0 now only supports Ruby 2.7+
this compatibility layer can be safely removed.
To minimize any noise generated the deprecation warning only appears when the
setting is configured to `false` as that is the only scenario where the
removal of the compatibility layer has any effect.
*Andrew White*
* `Pathname.blank?` only returns true for `Pathname.new("")`
Previously it would end up calling `Pathname#empty?` which returned true
if the path existed and was an empty directory or file.
That behavior was unlikely to be expected.
*Jean Boussier*
2022-01-17 18:49:31 +00:00
* Deprecate `Notification::Event`'s `#children` and `#parent_of?`
*John Hawthorn*
* Change default serialization format of `MessageEncryptor` from `Marshal` to `JSON` for Rails 7.1.
Existing apps are provided with an upgrade path to migrate to `JSON` as described in `guides/source/upgrading_ruby_on_rails.md`
*Zack Deveau* and *Martin Gingras*
* Add `ActiveSupport::TestCase#stub_const` to stub a constant for the duration of a yield.
*DHH*
* Fix `ActiveSupport::EncryptedConfiguration` to be compatible with Psych 4
*Stephen Sugden*
2022-01-12 00:47:01 +00:00
* Improve `File.atomic_write` error handling
*Daniel Pepper*
* Fix `Class#descendants` and `DescendantsTracker#descendants` compatibility with Ruby 3.1.
[The native `Class#descendants` was reverted prior to Ruby 3.1 release](https://bugs.ruby-lang.org/issues/14394#note-33),
but `Class#subclasses` was kept, breaking the feature detection.
*Jean Boussier*
2021-12-07 15:52:30 +00:00
Please check [7-0-stable](https://github.com/rails/rails/blob/7-0-stable/activesupport/CHANGELOG.md) for previous changes.