Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deprecate ActiveSupport::Deprecation singleton usage #47354

Merged
39 changes: 39 additions & 0 deletions activesupport/CHANGELOG.md
@@ -1,3 +1,39 @@
* Deprecate usage of the singleton `ActiveSupport::Deprecation`.

All usage of `ActiveSupport::Deprecation` as a singleton is deprecated, the most common one being
`ActiveSupport::Deprecation.warn`. Gem authors should now create their own deprecator (`ActiveSupport::Deprecation`
object), and use it to emit deprecation warnings.

Calling any of the following without specifying a deprecator argument is also deprecated:
* Module.deprecate
* deprecate_constant
* DeprecatedObjectProxy
* DeprecatedInstanceVariableProxy
* DeprecatedConstantProxy
* deprecation-related test assertions

Use of `ActiveSupport::Deprecation.silence` and configuration methods like `behavior=`, `disallowed_behavior=`,
`disallowed_warnings=` should now be aimed at the [application's deprecators](https://api.rubyonrails.org/classes/Rails/Application.html#method-i-deprecators).
etiennebarrie marked this conversation as resolved.
Show resolved Hide resolved

```ruby
Rails.application.deprecators.silence do
# code that emits deprecation warnings
end
```

If your gem has a Railtie or Engine, it's encouraged to add your deprecator to the application's deprecators, that
way the deprecation related configuration options will apply to it as well, e.g.
`config.active_support.report_deprecations` set to `false` in the production environment will also disable your
deprecator.
etiennebarrie marked this conversation as resolved.
Show resolved Hide resolved

```ruby
initializer "my_gem.deprecator" do |app|
app.deprecators[:my_gem] = MyGem.deprecator
end
```

*Étienne Barrié*
etiennebarrie marked this conversation as resolved.
Show resolved Hide resolved

* Add `Object#with` to set and restore public attributes around a block

```ruby
Expand Down Expand Up @@ -459,6 +495,9 @@
deprecator.warn("bar") # => raise ActiveSupport::DeprecationException
```

Note that global `ActiveSupport::Deprecation` methods such as `ActiveSupport::Deprecation.warn`
and `ActiveSupport::Deprecation.disallowed_warnings` have been deprecated.

*Jonathan Hefner*

* Add italic and underline support to `ActiveSupport::LogSubscriber#color`
Expand Down
27 changes: 15 additions & 12 deletions activesupport/lib/active_support/core_ext/module/deprecation.rb
@@ -1,25 +1,28 @@
# frozen_string_literal: true

class Module
# deprecate :foo
# deprecate bar: 'message'
# deprecate :foo, :bar, baz: 'warning!', qux: 'gone!'
# deprecate :foo, deprecator: MyLib.deprecator
# deprecate :foo, bar: "warning!", deprecator: MyLib.deprecator
#
# You can also use custom deprecator instance:
#
# deprecate :foo, deprecator: MyLib::Deprecator.new
# deprecate :foo, bar: "warning!", deprecator: MyLib::Deprecator.new
#
# \Custom deprecators must respond to <tt>deprecation_warning(deprecated_method_name, message, caller_backtrace)</tt>
# method where you can implement your custom warning behavior.
# A deprecator is typically an instance of ActiveSupport::Deprecation, but you can also pass any object that responds
# to <tt>deprecation_warning(deprecated_method_name, message, caller_backtrace)</tt> where you can implement your
# custom warning behavior.
#
# class MyLib::Deprecator
# def deprecation_warning(deprecated_method_name, message, caller_backtrace = nil)
# message = "#{deprecated_method_name} is deprecated and will be removed from MyLibrary | #{message}"
# Kernel.warn message
# end
# end
def deprecate(*method_names)
ActiveSupport::Deprecation.deprecate_methods(self, *method_names)
def deprecate(*method_names, deprecator: nil, **options)
if deprecator.is_a?(ActiveSupport::Deprecation)
deprecator.deprecate_methods(self, *method_names, **options)
elsif deprecator
# we just need any instance to call deprecate_methods, but the deprecation will be emitted by deprecator
ActiveSupport.deprecator.deprecate_methods(self, *method_names, **options, deprecator: deprecator)
else
ActiveSupport.deprecator.warn("Module.deprecate without a deprecator is deprecated")
ActiveSupport::Deprecation._instance.deprecate_methods(self, *method_names, **options)
end
end
end
31 changes: 28 additions & 3 deletions activesupport/lib/active_support/deprecation.rb
Expand Up @@ -3,8 +3,33 @@
require "singleton"

module ActiveSupport
# \Deprecation specifies the API used by Rails to deprecate methods, instance
# variables, objects, and constants.
# \Deprecation specifies the API used by Rails to deprecate methods, instance variables, objects, and constants. It's
# also available for gems or applications.
Comment on lines -6 to +7
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the prose parts, let's keep the method doc wrapped at 80-ish characters.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a blocker, but keeping lines like this (not code examples) wrapped at 80-ish characters would be consistent with other method docs (for example, further down).

#
# For a gem, use Deprecation.new to create a Deprecation object and store it in your module or class (in order for
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# For a gem, use Deprecation.new to create a Deprecation object and store it in your module or class (in order for
# For a gem, use Deprecation.new to create a deprecator object and store it in your module or class (in order for

If we could go back in time, I would rename ActiveSupport::Deprecation to ActiveSupport::Deprecator, but it may not worth the effort now. 🤷‍♂️

# users to be able to configure it).
#
# module MyLibrary
# def self.deprecator
# @deprecator ||= ActiveSupport::Deprecation.new("2.0", "MyLibrary")
# end
# end
#
# For a Railtie or Engine, you may also want to add it to the application's deprecators, so that the application's
# configuration can be applied to it.
#
# module MyLibrary
# class Railtie < Rails::Railtie
# initializer "deprecator" do |app|
# app.deprecators[:my_library] = MyLibrary.deprecator
# end
# end
# end
#
# With the above initializer, configuration settings like the following will affect +MyLibrary.deprecator+:
#
# # in config/environments/test.rb
# config.active_support.deprecation = :raise
class Deprecation
# active_support.rb sets an autoload for ActiveSupport::Deprecation.
#
Expand All @@ -25,7 +50,7 @@ class Deprecation
require "active_support/core_ext/module/deprecation"
require "concurrent/atomic/thread_local_var"

include Singleton
include Singleton # :nodoc:
include InstanceDelegator
include Behavior
include Reporting
Expand Down
11 changes: 6 additions & 5 deletions activesupport/lib/active_support/deprecation/behaviors.rb
Expand Up @@ -88,10 +88,11 @@ def disallowed_behavior
# Deprecation warnings raised by gems are not affected by this setting
# because they happen before Rails boots up.
#
# ActiveSupport::Deprecation.behavior = :stderr
# ActiveSupport::Deprecation.behavior = [:stderr, :log]
# ActiveSupport::Deprecation.behavior = MyCustomHandler
# ActiveSupport::Deprecation.behavior = ->(message, callstack, deprecation_horizon, gem_name) {
# deprecator = ActiveSupport::Deprecation.new
# deprecator.behavior = :stderr
# deprecator.behavior = [:stderr, :log]
# deprecator.behavior = MyCustomHandler
# deprecator.behavior = ->(message, callstack, deprecation_horizon, gem_name) {
# # custom stuff
# }
#
Expand All @@ -102,7 +103,7 @@ def behavior=(behavior)
end

# Sets the behavior for disallowed deprecations (those configured by
# ActiveSupport::Deprecation.disallowed_warnings=) to the specified
# ActiveSupport::Deprecation#disallowed_warnings=) to the specified
# value. As with +behavior=+, this can be a single value, array, or an
# object that responds to +call+.
def disallowed_behavior=(behavior)
Expand Down
Expand Up @@ -6,8 +6,7 @@ class Deprecation
# hooking +const_missing+.
#
# It takes the names of an old (deprecated) constant and of a new constant
# (both in string form) and optionally a deprecator. The deprecator defaults
# to +ActiveSupport::Deprecator+ if none is specified.
# (both in string form) and a deprecator.
#
# The deprecated constant now returns the same object as the new one rather
# than a proxy object, so it can be used transparently in +rescue+ blocks
Expand All @@ -19,7 +18,7 @@ class Deprecation
#
# PLANETS_POST_2006 = %w(mercury venus earth mars jupiter saturn uranus neptune)
# include ActiveSupport::Deprecation::DeprecatedConstantAccessor
# deprecate_constant 'PLANETS', 'PLANETS_POST_2006'
# deprecate_constant 'PLANETS', 'PLANETS_POST_2006', deprecator: ActiveSupport::Deprecation.new
#
# PLANETS.map { |planet| planet.capitalize }
# # => DEPRECATION WARNING: PLANETS is deprecated! Use PLANETS_POST_2006 instead.
Expand All @@ -40,7 +39,9 @@ def const_missing(missing_const_name)
super
end

def deprecate_constant(const_name, new_constant, message: nil, deprecator: ActiveSupport::Deprecation.instance)
def deprecate_constant(const_name, new_constant, message: nil, deprecator: nil)
ActiveSupport.deprecator.warn("DeprecatedConstantAccessor.deprecate_constant without a deprecator is deprecated") unless deprecator
deprecator ||= ActiveSupport::Deprecation._instance
class_variable_set(:@@_deprecated_constants, {}) unless class_variable_defined?(:@@_deprecated_constants)
class_variable_get(:@@_deprecated_constants)[const_name.to_s] = { new: new_constant, message: message, deprecator: deprecator }
end
Expand Down
28 changes: 25 additions & 3 deletions activesupport/lib/active_support/deprecation/instance_delegator.rb
@@ -1,11 +1,10 @@
# frozen_string_literal: true

require "active_support/core_ext/module/delegation"

module ActiveSupport
class Deprecation
module InstanceDelegator # :nodoc:
def self.included(base)
base.singleton_class.alias_method(:_instance, :instance)
base.extend(ClassMethods)
base.singleton_class.prepend(OverrideDelegators)
base.public_class_method :new
Expand All @@ -18,7 +17,30 @@ def include(included_module)
end

def method_added(method_name)
singleton_class.delegate(method_name, to: :instance)
use_instead =
case method_name
when :silence, :behavior=, :disallowed_behavior=, :disallowed_warnings=, :silenced=, :debug=
target = "(defined?(Rails.application.deprecators) ? Rails.application.deprecators : ActiveSupport::Deprecation._instance)"
"Rails.application.deprecators.#{method_name}"
when :warn, :deprecate_methods, :gem_name, :gem_name=, :deprecation_horizon, :deprecation_horizon=
"your own Deprecation object"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we give an example here? Consider this code in an app (not in a gem/engine), which I think is pretty common and there is lots of internet recommendations to use:

ActiveSupport::Deprecation.warn "foo"

You get this warning:

DEPRECATION WARNING: Calling warn on ActiveSupport::Deprecation is deprecated and will be removed from Rails (use your own Deprecation object instead)

I think a more useful warning would be:

DEPRECATION WARNING: Calling warn on ActiveSupport::Deprecation is deprecated and will be removed from Rails (use ActiveSupport.deprecator.warn instead)

Unless we are recommending that apps define their own deprecators as well? (Which to me seems excessive for most apps.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that we could give a better example of how to fix this deprecation. We absolutely do not want to point to ActiveSupport.deprecator though. The whole point of this deprecation cycle is to stop third-party code from generating deprecations that seem to come from Rails.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But what about first party code? What should it do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant third-party from the point of view of Rails, i.e. libraries and the application.

Regarding application deprecations, at Shopify, we've added an application deprecator in config/application.rb:

module Shopify
  class Application < Rails::Application

    # ...

    initializer :deprecator do |app|
      app.deprecators[:shopify] = ActiveSupport::Deprecation.new("soon", "Shopify/shopify")
    end
  end
end

And then we use it directly:

Rails.application.deprecators[:shopify].warn("some warning")

We've debated whether to do like a gem would typically do and have an API like Shopify.deprecator but ultimately felt like the out-of-the-box API from Rails was good enough.

I also thought that maybe Rails could always define an application deprecator to be used directly but didn't propose it because ultimately I don't think most apps need deprecations (it's possible to just change callers and callees at once, unlike in a library/framework), the deprecation horizon doesn't make much sense, etc.

If an application needs a deprecator, it can do what we did above, or do like in a gem, define a singleton method on the application module, set in the application deprecators in an initializer.

I'm not sure what we should write in the deprecation to make that clearer, there's some setup involved… Yes the deprecated API was clean and nice, but in the end it was misguided as separate gems and applications in addition to Rails were all sharing the same Deprecation instance.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. I don't think the fact that setup is required should be celebrated. I understand Shopify doing this but for the long tail of small apps I think this is overly complicated vs. what Rails 7 and before offered.

else
"Rails.application.deprecators[framework].#{method_name} where framework is for example :active_record"
end
args = /[^\]]=\z/.match?(method_name) ? "arg" : "..."
target ||= "ActiveSupport::Deprecation._instance"
singleton_class.module_eval <<~RUBY, __FILE__, __LINE__ + 1
def #{method_name}(#{args})
#{target}.#{method_name}(#{args})
ensure
ActiveSupport.deprecator.warn("Calling #{method_name} on ActiveSupport::Deprecation is deprecated and will be removed from Rails (use #{use_instead} instead)")
end
RUBY
end

def instance
ActiveSupport.deprecator.warn("ActiveSupport::Deprecation.instance is deprecated (use your own Deprecation object)")
super
Comment on lines +42 to +43
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, to cover e.g. ActiveSupport::Deprecation.instance.behavior = ...:

Suggested change
ActiveSupport.deprecator.warn("ActiveSupport::Deprecation.instance is deprecated (use your own Deprecation object)")
super
super
ensure
ActiveSupport::Deprecation._instance.warn("ActiveSupport::Deprecation.instance is deprecated (use your own Deprecation object)")

Or, if we make ActiveSupport.deprecator == ActiveSupport::Deprecation._instance:

Suggested change
ActiveSupport.deprecator.warn("ActiveSupport::Deprecation.instance is deprecated (use your own Deprecation object)")
super
super
ensure
ActiveSupport.deprecator.warn("ActiveSupport::Deprecation.instance is deprecated (use your own Deprecation object)")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is not necessary/possible, as calling ActiveSupport::Deprecation.instance will first emit a deprecation, before we ever have a chance to set the behavior on it, whether we emit the deprecation before calling super or not.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right — swapping the order here is nonsensical. 🤦‍♂️ 🤦‍♂️

Using ActiveSupport::Deprecation._instance as the underlying deprecator will benefit subsequent calls though. e.g.:

ActiveSupport::Deprecation.instance.behavior = ...
ActiveSupport::Deprecation.instance.disallowed_behavior = ...

(Which is now covered since ActiveSupport.deprecator is ActiveSupport::Deprecation._instance.)

end
end

Expand Down
29 changes: 6 additions & 23 deletions activesupport/lib/active_support/deprecation/method_wrappers.rb
Expand Up @@ -16,38 +16,21 @@ module MethodWrapper
# def eee; end
# end
#
# Using the default deprecator:
# ActiveSupport::Deprecation.deprecate_methods(Fred, :aaa, bbb: :zzz, ccc: 'use Bar#ccc instead')
# deprecator = ActiveSupport::Deprecation.new('next-release', 'MyGem')
#
# deprecator.deprecate_methods(Fred, :aaa, bbb: :zzz, ccc: 'use Bar#ccc instead')
# # => Fred
#
# Fred.new.aaa
# # DEPRECATION WARNING: aaa is deprecated and will be removed from Rails 5.1. (called from irb_binding at (irb):10)
# # DEPRECATION WARNING: aaa is deprecated and will be removed from MyGem next-release. (called from irb_binding at (irb):10)
# # => nil
#
# Fred.new.bbb
# # DEPRECATION WARNING: bbb is deprecated and will be removed from Rails 5.1 (use zzz instead). (called from irb_binding at (irb):11)
# # DEPRECATION WARNING: bbb is deprecated and will be removed from MyGem next-release (use zzz instead). (called from irb_binding at (irb):11)
# # => nil
#
# Fred.new.ccc
# # DEPRECATION WARNING: ccc is deprecated and will be removed from Rails 5.1 (use Bar#ccc instead). (called from irb_binding at (irb):12)
# # => nil
#
# Passing in a custom deprecator:
# custom_deprecator = ActiveSupport::Deprecation.new('next-release', 'MyGem')
# ActiveSupport::Deprecation.deprecate_methods(Fred, ddd: :zzz, deprecator: custom_deprecator)
# # => [:ddd]
#
# Fred.new.ddd
# DEPRECATION WARNING: ddd is deprecated and will be removed from MyGem next-release (use zzz instead). (called from irb_binding at (irb):15)
# # => nil
#
# Using a custom deprecator directly:
# custom_deprecator = ActiveSupport::Deprecation.new('next-release', 'MyGem')
# custom_deprecator.deprecate_methods(Fred, eee: :zzz)
# # => [:eee]
#
# Fred.new.eee
# DEPRECATION WARNING: eee is deprecated and will be removed from MyGem next-release (use zzz instead). (called from irb_binding at (irb):18)
# # DEPRECATION WARNING: ccc is deprecated and will be removed from MyGem next-release (use Bar#ccc instead). (called from irb_binding at (irb):12)
# # => nil
def deprecate_methods(target_module, *method_names)
options = method_names.extract_options!
Expand Down
44 changes: 22 additions & 22 deletions activesupport/lib/active_support/deprecation/proxy_wrappers.rb
Expand Up @@ -25,22 +25,22 @@ def method_missing(called, *args, &block)
end
end

# DeprecatedObjectProxy transforms an object into a deprecated one. It
# takes an object, a deprecation message, and optionally a deprecator. The
# deprecator defaults to +ActiveSupport::Deprecator+ if none is specified.
# DeprecatedObjectProxy transforms an object into a deprecated one. It takes an object, a deprecation message, and
# a deprecator.
#
# deprecated_object = ActiveSupport::Deprecation::DeprecatedObjectProxy.new(Object.new, "This object is now deprecated")
# deprecated_object = ActiveSupport::Deprecation::DeprecatedObjectProxy.new(Object.new, "This object is now deprecated", ActiveSupport::Deprecation.new)
# # => #<Object:0x007fb9b34c34b0>
#
# deprecated_object.to_s
# DEPRECATION WARNING: This object is now deprecated.
# (Backtrace)
# # => "#<Object:0x007fb9b34c34b0>"
class DeprecatedObjectProxy < DeprecationProxy
def initialize(object, message, deprecator = ActiveSupport::Deprecation.instance)
def initialize(object, message, deprecator = nil)
@object = object
@message = message
@deprecator = deprecator
ActiveSupport.deprecator.warn("DeprecatedObjectProxy without a deprecator is deprecated") unless deprecator
@deprecator = deprecator || ActiveSupport::Deprecation._instance
end

private
Expand All @@ -53,15 +53,15 @@ def warn(callstack, called, args)
end
end

# DeprecatedInstanceVariableProxy transforms an instance variable into a
# deprecated one. It takes an instance of a class, a method on that class
# and an instance variable. It optionally takes a deprecator as the last
# argument. The deprecator defaults to +ActiveSupport::Deprecator+ if none
# is specified.
# DeprecatedInstanceVariableProxy transforms an instance variable into a deprecated one. It takes an instance of a
# class, a method on that class, an instance variable, and a deprecator as the last argument.
#
# Trying to use the deprecated instance variable will result in a deprecation warning, pointing to the method as a
# replacement.
#
# class Example
# def initialize
# @request = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(self, :request, :@request)
# @request = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(self, :request, :@request, ActiveSupport::Deprecation.new)
# @_request = :special_request
# end
#
Expand All @@ -86,11 +86,12 @@ def warn(callstack, called, args)
# example.request.to_s
# # => "special_request"
class DeprecatedInstanceVariableProxy < DeprecationProxy
def initialize(instance, method, var = "@#{method}", deprecator = ActiveSupport::Deprecation.instance)
def initialize(instance, method, var = "@#{method}", deprecator = nil)
@instance = instance
@method = method
@var = var
@deprecator = deprecator
ActiveSupport.deprecator.warn("DeprecatedInstanceVariableProxy without a deprecator is deprecated") unless deprecator
@deprecator = deprecator || ActiveSupport::Deprecation._instance
end

private
Expand All @@ -103,18 +104,16 @@ def warn(callstack, called, args)
end
end

# DeprecatedConstantProxy transforms a constant into a deprecated one. It
# takes the full names of an old (deprecated) constant and of a new constant
# (both in string form) and optionally a deprecator. The deprecator defaults
# to +ActiveSupport::Deprecator+ if none is specified. The deprecated constant
# now returns the value of the new one.
# DeprecatedConstantProxy transforms a constant into a deprecated one. It takes the full names of an old
# (deprecated) constant and of a new constant (both in string form) and a deprecator. The deprecated constant now
# returns the value of the new one.
#
# PLANETS = %w(mercury venus earth mars jupiter saturn uranus neptune pluto)
#
# # (In a later update, the original implementation of `PLANETS` has been removed.)
#
# PLANETS_POST_2006 = %w(mercury venus earth mars jupiter saturn uranus neptune)
# PLANETS = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('PLANETS', 'PLANETS_POST_2006')
# PLANETS = ActiveSupport::Deprecation::DeprecatedConstantProxy.new("PLANETS", "PLANETS_POST_2006", ActiveSupport::Deprecation.new)
#
# PLANETS.map { |planet| planet.capitalize }
# # => DEPRECATION WARNING: PLANETS is deprecated! Use PLANETS_POST_2006 instead.
Expand All @@ -128,12 +127,13 @@ def self.new(*args, **options, &block)
super
end

def initialize(old_const, new_const, deprecator = ActiveSupport::Deprecation.instance, message: "#{old_const} is deprecated! Use #{new_const} instead.")
def initialize(old_const, new_const, deprecator = nil, message: "#{old_const} is deprecated! Use #{new_const} instead.")
Kernel.require "active_support/inflector/methods"

@old_const = old_const
@new_const = new_const
@deprecator = deprecator
ActiveSupport.deprecator.warn("DeprecatedConstantProxy without a deprecator is deprecated") unless deprecator
@deprecator = deprecator || ActiveSupport::Deprecation._instance
@message = message
end

Expand Down