Skip to content
This repository

safe_constantize #3105

Closed
wants to merge 3 commits into from

4 participants

rroblak Alex José Valim Damien Mathieu
rroblak

safe_constantize is similar to constantize except that it returns nil when the given String does not correspond to a defined constant (instead of raising a NameError). This was inspired by some discussion around my proposal for a String#is_a_class_name? method. José Valim suggested safe_constantize as more flexible alternative to is_a_class_name?-- the equivalent syntax would be my_string.safe_constantize.is_a?(Class).

I also refactored the ActiveSupport::Inflector.*constantize tests so that both the ActiveSupport::Inflector and the String *constantize methods are tested.

rroblak Added ActiveSupport::Inflector.safe_constantize and String#safe_const…
…antize; refactored common constantize tests into ConstantizeTestCases
9bdd28f
activesupport/lib/active_support/inflector/methods.rb
((11 lines not shown))
  234
+    #
  235
+    #   C = 'outside'
  236
+    #   module M
  237
+    #     C = 'inside'
  238
+    #     C                    # => 'inside'
  239
+    #     "C".safe_constantize # => 'outside', same as ::C
  240
+    #   end
  241
+    #
  242
+    # nil is returned when the name is not in CamelCase or the constant is
  243
+    # unknown.
  244
+    #
  245
+    #   "blargle".safe_constantize  # => nil
  246
+    def safe_constantize(camel_cased_word)
  247
+      begin
  248
+        camel_cased_word.constantize
  249
+      rescue NameError
3
José Valim Owner

Nice! But we still need to handle the NameError exception here, right?

Ah, right. I spaced that. Any thoughts on how to write a test for that?

I couldn't come up with a test for it, but the new commit handles the NameError exception. I had to modify constantize for it to work properly, because the NameError constantize raises doesn't have NameError.name set properly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
activesupport/lib/active_support/inflector/methods.rb
((9 lines not shown))
  232
+    # The name is assumed to be the one of a top-level constant, no matter whether
  233
+    # it starts with "::" or not. No lexical context is taken into account:
  234
+    #
  235
+    #   C = 'outside'
  236
+    #   module M
  237
+    #     C = 'inside'
  238
+    #     C                    # => 'inside'
  239
+    #     "C".safe_constantize # => 'outside', same as ::C
  240
+    #   end
  241
+    #
  242
+    # nil is returned when the name is not in CamelCase or the constant is
  243
+    # unknown.
  244
+    #
  245
+    #   "blargle".safe_constantize  # => nil
  246
+    def safe_constantize(camel_cased_word)
  247
+      begin
2
Damien Mathieu Collaborator

The begin is not necessary here. You can do :

def safe_constantize(camel_cased_word)
  camel_cased_word.constantize
rescue NameError
  nil
end

Good call.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
rroblak Modified ActiveSupport::Inflector#safe_constantize to handle the case…
… where a NameError is raised for something other than the specified String; modified ActiveSupport::Inflector#constantized to ensure that when it raises a NameError sets the name to be the value of the specified String (this is needed for the change to ActiveSupport::Inflector#safe_constantize to work); cleaned up multiple definitions of ActiveSupport::Inflector#constantized
69a9f89
Alex

I'm not sure of the proper way to do this, but a test like this seems to cover the new commit:

NameError.any_instance.stubs( :name ).returns( "asdba" )
assert_raise(NameError) { yield("blargle") }

rroblak

@Ninju: thanks! I incorporated your suggestion into the latest commit.

José Valim
Owner

Hey mate, I have applied your first commit. I have decided to not apply the following ones because I've decided to handle the NameError issue differently (and added a few more test cases to handle the other scenarios):

b2f34d1

Thank you very much for the patch. I applied another commit that uses safe_constantize throughout Rails:

e8987c3

And I am sure I will be able to use it in other projects as well!

José Valim josevalim closed this September 23, 2011
rroblak

Cool! I like the way you handled the errors-- it's more cautious and focused than what I had proposed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 3 unique commits by 1 author.

Sep 22, 2011
rroblak Added ActiveSupport::Inflector.safe_constantize and String#safe_const…
…antize; refactored common constantize tests into ConstantizeTestCases
9bdd28f
rroblak Modified ActiveSupport::Inflector#safe_constantize to handle the case…
… where a NameError is raised for something other than the specified String; modified ActiveSupport::Inflector#constantized to ensure that when it raises a NameError sets the name to be the value of the specified String (this is needed for the change to ActiveSupport::Inflector#safe_constantize to work); cleaned up multiple definitions of ActiveSupport::Inflector#constantized
69a9f89
rroblak Added tests to ensure correct NameError handling in constantize and s…
…afe_constantize
66fa70f
This page is out of date. Refresh to see the latest.
19  activesupport/lib/active_support/core_ext/string/inflections.rb
@@ -33,14 +33,27 @@ def singularize
33 33
 
34 34
   # +constantize+ tries to find a declared constant with the name specified
35 35
   # in the string. It raises a NameError when the name is not in CamelCase
36  
-  # or is not initialized.
  36
+  # or is not initialized.  See ActiveSupport::Inflector.constantize
37 37
   #
38 38
   # Examples
39  
-  #   "Module".constantize # => Module
40  
-  #   "Class".constantize  # => Class
  39
+  #   "Module".constantize       # => Module
  40
+  #   "Class".constantize        # => Class
  41
+  #   "blargle".safe_constantize # => NameError: wrong constant name blargle
41 42
   def constantize
42 43
     ActiveSupport::Inflector.constantize(self)
43 44
   end
  45
+  
  46
+  # +safe_constantize+ tries to find a declared constant with the name specified
  47
+  # in the string. It returns nil when the name is not in CamelCase
  48
+  # or is not initialized.  See ActiveSupport::Inflector.safe_constantize
  49
+  #
  50
+  # Examples
  51
+  #   "Module".safe_constantize  # => Module
  52
+  #   "Class".safe_constantize   # => Class
  53
+  #   "blargle".safe_constantize # => nil
  54
+  def safe_constantize
  55
+    ActiveSupport::Inflector.safe_constantize(self)
  56
+  end
44 57
 
45 58
   # By default, +camelize+ converts strings to UpperCamelCase. If the argument to camelize
46 59
   # is set to <tt>:lower</tt> then camelize produces lowerCamelCase.
107  activesupport/lib/active_support/inflector/methods.rb
@@ -182,48 +182,85 @@ def foreign_key(class_name, separate_class_name_and_id_with_underscore = true)
182 182
     end
183 183
 
184 184
     # Ruby 1.9 introduces an inherit argument for Module#const_get and
185  
-    # #const_defined? and changes their default behavior.
  185
+    # #const_defined? and changes their default behavior.  Used in
  186
+    # constantize below.
186 187
     if Module.method(:const_get).arity == 1
187  
-      # Tries to find a constant with the name specified in the argument string:
188  
-      #
189  
-      #   "Module".constantize     # => Module
190  
-      #   "Test::Unit".constantize # => Test::Unit
191  
-      #
192  
-      # The name is assumed to be the one of a top-level constant, no matter whether
193  
-      # it starts with "::" or not. No lexical context is taken into account:
194  
-      #
195  
-      #   C = 'outside'
196  
-      #   module M
197  
-      #     C = 'inside'
198  
-      #     C               # => 'inside'
199  
-      #     "C".constantize # => 'outside', same as ::C
200  
-      #   end
201  
-      #
202  
-      # NameError is raised when the name is not in CamelCase or the constant is
203  
-      # unknown.
204  
-      def constantize(camel_cased_word)
205  
-        names = camel_cased_word.split('::')
206  
-        names.shift if names.empty? || names.first.empty?
207  
-
208  
-        constant = Object
209  
-        names.each do |name|
210  
-          constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
211  
-        end
212  
-        constant
  188
+      def const_defined?(constant, name)
  189
+        constant.const_defined?(name)
213 190
       end
214 191
     else
215  
-      def constantize(camel_cased_word) #:nodoc:
216  
-        names = camel_cased_word.split('::')
217  
-        names.shift if names.empty? || names.first.empty?
  192
+      def const_defined?(constant, name)
  193
+        constant.const_defined?(name, false)
  194
+      end
  195
+    end
218 196
 
219  
-        constant = Object
220  
-        names.each do |name|
221  
-          constant = constant.const_defined?(name, false) ? constant.const_get(name) : constant.const_missing(name)
222  
-        end
223  
-        constant
  197
+    private :const_defined?
  198
+
  199
+    # Tries to find a constant with the name specified in the argument string:
  200
+    #
  201
+    #   "Module".constantize     # => Module
  202
+    #   "Test::Unit".constantize # => Test::Unit
  203
+    #
  204
+    # The name is assumed to be the one of a top-level constant, no matter whether
  205
+    # it starts with "::" or not. No lexical context is taken into account:
  206
+    #
  207
+    #   C = 'outside'
  208
+    #   module M
  209
+    #     C = 'inside'
  210
+    #     C               # => 'inside'
  211
+    #     "C".constantize # => 'outside', same as ::C
  212
+    #   end
  213
+    #
  214
+    # NameError is raised when the name is not in CamelCase or the constant is
  215
+    # unknown.
  216
+    def constantize(camel_cased_word)
  217
+      names = camel_cased_word.split('::')
  218
+      names.shift if names.empty? || names.first.empty?
  219
+
  220
+      constant = Object
  221
+      names.each do |name|
  222
+        constant = begin
  223
+                     if const_defined?(constant, name)
  224
+                       constant.const_get(name)
  225
+                     else
  226
+                       constant.const_missing(name)
  227
+                     end
  228
+                   rescue NameError => e
  229
+                     raise NameError.new(e.message, camel_cased_word)
  230
+                   end
224 231
       end
  232
+      constant
225 233
     end
226 234
 
  235
+    # Tries to find a constant with the name specified in the argument string:
  236
+    #
  237
+    #   "Module".safe_constantize     # => Module
  238
+    #   "Test::Unit".safe_constantize # => Test::Unit
  239
+    #
  240
+    # The name is assumed to be the one of a top-level constant, no matter whether
  241
+    # it starts with "::" or not. No lexical context is taken into account:
  242
+    #
  243
+    #   C = 'outside'
  244
+    #   module M
  245
+    #     C = 'inside'
  246
+    #     C                    # => 'inside'
  247
+    #     "C".safe_constantize # => 'outside', same as ::C
  248
+    #   end
  249
+    #
  250
+    # nil is returned when the name is not in CamelCase or the constant is
  251
+    # unknown.
  252
+    #
  253
+    #   "blargle".safe_constantize  # => nil
  254
+    def safe_constantize(camel_cased_word)
  255
+      camel_cased_word.constantize
  256
+    rescue NameError => e
  257
+      if e.name == camel_cased_word
  258
+        nil
  259
+      else
  260
+        raise e
  261
+      end
  262
+    end
  263
+    
227 264
     # Turns a number into an ordinal string used to denote the position in an
228 265
     # ordered sequence such as 1st, 2nd, 3rd, 4th.
229 266
     #
48  activesupport/test/constantize_test_cases.rb
... ...
@@ -0,0 +1,48 @@
  1
+module Ace
  2
+  module Base
  3
+    class Case
  4
+    end
  5
+  end
  6
+end
  7
+
  8
+module ConstantizeTestCases
  9
+  def run_constantize_tests_on
  10
+    assert_nothing_raised { assert_equal Ace::Base::Case, yield("Ace::Base::Case") }
  11
+    assert_nothing_raised { assert_equal Ace::Base::Case, yield("::Ace::Base::Case") }
  12
+    assert_nothing_raised { assert_equal ConstantizeTestCases, yield("ConstantizeTestCases") }
  13
+    assert_nothing_raised { assert_equal ConstantizeTestCases, yield("::ConstantizeTestCases") }
  14
+    assert_raise(NameError) { yield("UnknownClass") }
  15
+    assert_raise(NameError) { yield("An invalid string") }
  16
+    assert_raise(NameError) { yield("InvalidClass\n") }
  17
+    assert_raise(NameError) { yield("Ace::Base::ConstantizeTestCases") }
  18
+    
  19
+    # any NameError it raises should have name set to the full specified String (not just part of it)
  20
+    begin
  21
+      yield("Ace::Base::Blargle")
  22
+      assert false
  23
+    rescue NameError => e
  24
+      assert_equal "Ace::Base::Blargle", e.name
  25
+    end
  26
+  end
  27
+  
  28
+  def run_safe_constantize_tests_on
  29
+    assert_nothing_raised { assert_equal Ace::Base::Case, yield("Ace::Base::Case") }
  30
+    assert_nothing_raised { assert_equal Ace::Base::Case, yield("::Ace::Base::Case") }
  31
+    assert_nothing_raised { assert_equal ConstantizeTestCases, yield("ConstantizeTestCases") }
  32
+    assert_nothing_raised { assert_equal ConstantizeTestCases, yield("::ConstantizeTestCases") }
  33
+    assert_nothing_raised { assert_equal nil, yield("UnknownClass") }
  34
+    assert_nothing_raised { assert_equal nil, yield("An invalid string") }
  35
+    assert_nothing_raised { assert_equal nil, yield("InvalidClass\n") }
  36
+    assert_nothing_raised { assert_equal nil, yield("blargle") }
  37
+    assert_nothing_raised { assert_equal nil, yield("Ace::Base::ConstantizeTestCases") }
  38
+    
  39
+    # should re-raise any NameError it encounters that doesn't correspond to the specified String
  40
+    NameError.any_instance.stubs(:name).returns("not-blargle")
  41
+    begin
  42
+      yield("blargle")
  43
+      assert false
  44
+    rescue NameError => e
  45
+      assert_equal "not-blargle", e.name
  46
+    end
  47
+  end
  48
+end
23  activesupport/test/core_ext/string_ext_test.rb
@@ -2,6 +2,7 @@
2 2
 require 'date'
3 3
 require 'abstract_unit'
4 4
 require 'inflector_test_cases'
  5
+require 'constantize_test_cases'
5 6
 
6 7
 require 'active_support/inflector'
7 8
 require 'active_support/core_ext/string'
@@ -9,9 +10,17 @@
9 10
 require 'active_support/core_ext/string/strip'
10 11
 require 'active_support/core_ext/string/output_safety'
11 12
 
  13
+module Ace
  14
+  module Base
  15
+    class Case
  16
+    end
  17
+  end
  18
+end
  19
+
12 20
 class StringInflectionsTest < Test::Unit::TestCase
13 21
   include InflectorTestCases
14  
-
  22
+  include ConstantizeTestCases
  23
+  
15 24
   def test_erb_escape
16 25
     string = [192, 60].pack('CC')
17 26
     expected = 192.chr + "&lt;"
@@ -292,6 +301,18 @@ def test_truncate_multibyte
292 301
         "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244".force_encoding('UTF-8').truncate(10)
293 302
     end
294 303
   end
  304
+  
  305
+  def test_constantize
  306
+    run_constantize_tests_on do |string|
  307
+      string.constantize
  308
+    end
  309
+  end
  310
+  
  311
+  def test_safe_constantize
  312
+    run_safe_constantize_tests_on do |string|
  313
+      string.safe_constantize
  314
+    end
  315
+  end
295 316
 end
296 317
 
297 318
 class StringBehaviourTest < Test::Unit::TestCase
27  activesupport/test/inflector_test.rb
@@ -2,16 +2,11 @@
2 2
 require 'active_support/inflector'
3 3
 
4 4
 require 'inflector_test_cases'
5  
-
6  
-module Ace
7  
-  module Base
8  
-    class Case
9  
-    end
10  
-  end
11  
-end
  5
+require 'constantize_test_cases'
12 6
 
13 7
 class InflectorTest < Test::Unit::TestCase
14 8
   include InflectorTestCases
  9
+  include ConstantizeTestCases
15 10
 
16 11
   def test_pluralize_plurals
17 12
     assert_equal "plurals", ActiveSupport::Inflector.pluralize("plurals")
@@ -282,17 +277,15 @@ def test_humanize_by_string
282 277
   end
283 278
 
284 279
   def test_constantize
285  
-    assert_nothing_raised { assert_equal Ace::Base::Case, ActiveSupport::Inflector.constantize("Ace::Base::Case") }
286  
-    assert_nothing_raised { assert_equal Ace::Base::Case, ActiveSupport::Inflector.constantize("::Ace::Base::Case") }
287  
-    assert_nothing_raised { assert_equal InflectorTest, ActiveSupport::Inflector.constantize("InflectorTest") }
288  
-    assert_nothing_raised { assert_equal InflectorTest, ActiveSupport::Inflector.constantize("::InflectorTest") }
289  
-    assert_raise(NameError) { ActiveSupport::Inflector.constantize("UnknownClass") }
290  
-    assert_raise(NameError) { ActiveSupport::Inflector.constantize("An invalid string") }
291  
-    assert_raise(NameError) { ActiveSupport::Inflector.constantize("InvalidClass\n") }
  280
+    run_constantize_tests_on do |string|
  281
+      ActiveSupport::Inflector.constantize(string)
  282
+    end
292 283
   end
293  
-
294  
-  def test_constantize_does_lexical_lookup
295  
-    assert_raise(NameError) { ActiveSupport::Inflector.constantize("Ace::Base::InflectorTest") }
  284
+  
  285
+  def test_safe_constantize
  286
+    run_safe_constantize_tests_on do |string|
  287
+      ActiveSupport::Inflector.safe_constantize(string)
  288
+    end
296 289
   end
297 290
 
298 291
   def test_ordinal
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.