From 0156bfd88a27bc5a740407987271126d70b4afaf Mon Sep 17 00:00:00 2001 From: Adrien Berchet Date: Thu, 11 Apr 2019 18:46:10 +0200 Subject: [PATCH] Use a symbol to mark function that are in case-sensitive mode. --- lib/sqlalchemy/sql/functions.py | 46 ++++++++++++++++++++------------- test/sql/test_deprecations.py | 45 +++++++++++++++++++++++++------- test/sql/test_functions.py | 30 +++++++++++---------- 3 files changed, 81 insertions(+), 40 deletions(-) diff --git a/lib/sqlalchemy/sql/functions.py b/lib/sqlalchemy/sql/functions.py index f5720408bc..32c9149ad7 100644 --- a/lib/sqlalchemy/sql/functions.py +++ b/lib/sqlalchemy/sql/functions.py @@ -8,8 +8,6 @@ """SQL function API, factories, and built-in functions. """ -import warnings - from . import annotation from . import operators from . import schema @@ -35,7 +33,6 @@ from .selectable import Alias from .selectable import FromClause from .selectable import Select from .visitors import VisitableType -from .. import exc as sa_exc from .. import util @@ -43,6 +40,10 @@ _registry = util.defaultdict(dict) _case_sensitive_registry = util.defaultdict( lambda: util.defaultdict(dict) ) +_CASE_SENSITIVE = util.symbol( + name="case_sensitive_function", + doc="Symbol to mark the functions that are switched into case-sensitive " + "mode.") def register_function(identifier, fn, package="_default"): @@ -60,18 +61,26 @@ def register_function(identifier, fn, package="_default"): identifier = identifier.lower() # Check if a function with the same lowercase identifier is registered. - if identifier in reg: + if identifier in reg and reg[identifier] is not _CASE_SENSITIVE: if raw_identifier in case_sensitive_reg[identifier]: - warnings.warn( + util.warn( "The GenericFunction '{}' is already registered and " - "is going to be overriden.".format(identifier), - sa_exc.SAWarning) + "is going to be overriden.".format(identifier)) reg[identifier] = fn else: # If a function with the same lowercase identifier is registered, # then these 2 functions are considered as case-sensitive. # Note: This case should raise an error in a later release. - reg.pop(identifier) + util.warn_deprecated( + "GenericFunction '{}' is already registered with " + "different letter case, so the previously registered function " + "'{}' is switched into case-sensitive mode. " + "GenericFunction objects will be fully case-insensitive in a " + "future release.".format( + raw_identifier, + list(case_sensitive_reg[identifier].keys())[0], + )) + reg[identifier] = _CASE_SENSITIVE # Check if a function with different letter case identifier is registered. elif identifier in case_sensitive_reg: @@ -80,16 +89,17 @@ def register_function(identifier, fn, package="_default"): raw_identifier not in case_sensitive_reg[identifier] ): util.warn_deprecated( - "GenericFunction(s) {} are already registered with " - "different letter cases and might interact with {}.".format( + "GenericFunction(s) '{}' are already registered with " + "different letter cases and might interact with '{}'. " + "GenericFunction objects will be fully case-insensitive in a " + "future release.".format( list(case_sensitive_reg[identifier].keys()), raw_identifier)) else: - warnings.warn( + util.warn( "The GenericFunction '{}' is already registered and " - "is going to be overriden.".format(identifier), - sa_exc.SAWarning) + "is going to be overriden.".format(raw_identifier)) # Register by default else: @@ -487,11 +497,11 @@ class _FunctionGenerator(object): package = None if package is not None: - reg = _registry[package] - case_sensitive_reg = _case_sensitive_registry[package] - func = reg.get(fname.lower()) - if func is None and fname.lower() in case_sensitive_reg: - func = case_sensitive_reg[fname.lower()].get(fname) + func = _registry[package].get(fname.lower()) + if func is _CASE_SENSITIVE: + case_sensitive_reg = _case_sensitive_registry[package] + func = case_sensitive_reg.get(fname.lower()).get(fname) + if func is not None: return func(*c, **o) diff --git a/test/sql/test_deprecations.py b/test/sql/test_deprecations.py index b1a61d9c9d..05698c78a8 100644 --- a/test/sql/test_deprecations.py +++ b/test/sql/test_deprecations.py @@ -36,7 +36,6 @@ from sqlalchemy.testing import fixtures from sqlalchemy.testing import in_ from sqlalchemy.testing import mock from sqlalchemy.testing import not_in_ -from sqlalchemy.testing.assertions import expect_warnings class DeprecationWarningsTest(fixtures.TestBase): @@ -174,7 +173,14 @@ class DeprecationWarningsTest(fixtures.TestBase): in_("myfunc", cs_reg) eq_(set(cs_reg['myfunc'].keys()), set(['MYFUNC'])) - with testing.expect_deprecated(): + with testing.expect_deprecated( + "GenericFunction 'MyFunc' is already registered with" + " different letter case, so the previously registered function " + "'MYFUNC' is switched into case-sensitive mode. " + "GenericFunction objects will be fully case-insensitive in a " + "future release.", + regex=False + ): class MyFunc(GenericFunction): type = Integer @@ -185,7 +191,7 @@ class DeprecationWarningsTest(fixtures.TestBase): with pytest.raises(AssertionError): assert isinstance(func.myfunc().type, Integer) - not_in_("myfunc", reg) + eq_(reg["myfunc"], functions._CASE_SENSITIVE) not_in_("MYFUNC", reg) not_in_("MyFunc", reg) in_("myfunc", cs_reg) @@ -210,7 +216,14 @@ class DeprecationWarningsTest(fixtures.TestBase): in_("replaceable_func", cs_reg) eq_(set(cs_reg['replaceable_func'].keys()), set(['REPLACEABLE_FUNC'])) - with testing.expect_deprecated(): + with testing.expect_deprecated( + "GenericFunction 'Replaceable_Func' is already registered with" + " different letter case, so the previously registered function " + "'REPLACEABLE_FUNC' is switched into case-sensitive mode. " + "GenericFunction objects will be fully case-insensitive in a " + "future release.", + regex=False + ): class Replaceable_Func(GenericFunction): type = DateTime identifier = 'Replaceable_Func' @@ -220,24 +233,38 @@ class DeprecationWarningsTest(fixtures.TestBase): assert isinstance(func.RePlAcEaBlE_fUnC().type, NullType) assert isinstance(func.replaceable_func().type, NullType) - not_in_("replaceable_func", reg) + eq_(reg["replaceable_func"], functions._CASE_SENSITIVE) not_in_("REPLACEABLE_FUNC", reg) not_in_("Replaceable_Func", reg) in_("replaceable_func", cs_reg) eq_(set(cs_reg['replaceable_func'].keys()), set(['REPLACEABLE_FUNC', 'Replaceable_Func'])) - with expect_warnings(): + with testing.expect_warnings( + "The GenericFunction 'REPLACEABLE_FUNC' is already registered and " + "is going to be overriden.", + regex=False + ): class replaceable_func_override(GenericFunction): type = DateTime identifier = 'REPLACEABLE_FUNC' - with testing.expect_deprecated(): + with testing.expect_deprecated( + "GenericFunction(s) '['REPLACEABLE_FUNC', 'Replaceable_Func']' " + "are already registered with different letter cases and might " + "interact with 'replaceable_func'. GenericFunction objects will " + "be fully case-insensitive in a future release.", + regex=False + ): class replaceable_func_lowercase(GenericFunction): type = String identifier = 'replaceable_func' - with expect_warnings(): + with testing.expect_warnings( + "The GenericFunction 'Replaceable_Func' is already registered and " + "is going to be overriden.", + regex=False + ): class Replaceable_Func_override(GenericFunction): type = Integer identifier = 'Replaceable_Func' @@ -247,7 +274,7 @@ class DeprecationWarningsTest(fixtures.TestBase): assert isinstance(func.RePlAcEaBlE_fUnC().type, NullType) assert isinstance(func.replaceable_func().type, String) - not_in_("replaceable_func", reg) + eq_(reg["replaceable_func"], functions._CASE_SENSITIVE) not_in_("REPLACEABLE_FUNC", reg) not_in_("Replaceable_Func", reg) in_("replaceable_func", cs_reg) diff --git a/test/sql/test_functions.py b/test/sql/test_functions.py index ae2bdd47ac..9f80727212 100644 --- a/test/sql/test_functions.py +++ b/test/sql/test_functions.py @@ -241,22 +241,26 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL): def test_replace_function(self): - class replacable_func(GenericFunction): + class replaceable_func(GenericFunction): type = Integer - identifier = 'replacable_func' - - assert isinstance(func.Replacable_Func().type, Integer) - assert isinstance(func.RePlAcaBlE_fUnC().type, Integer) - assert isinstance(func.replacable_func().type, Integer) - - with expect_warnings(): - class replacable_func_override(GenericFunction): + identifier = 'replaceable_func' + + assert isinstance(func.Replaceable_Func().type, Integer) + assert isinstance(func.RePlAcEaBlE_fUnC().type, Integer) + assert isinstance(func.replaceable_func().type, Integer) + + with expect_warnings( + "The GenericFunction 'replaceable_func' is already registered and " + "is going to be overriden.", + regex=False + ): + class replaceable_func_override(GenericFunction): type = DateTime - identifier = 'replacable_func' + identifier = 'replaceable_func' - assert isinstance(func.Replacable_Func().type, DateTime) - assert isinstance(func.RePlAcaBlE_fUnC().type, DateTime) - assert isinstance(func.replacable_func().type, DateTime) + assert isinstance(func.Replaceable_Func().type, DateTime) + assert isinstance(func.RePlAcEaBlE_fUnC().type, DateTime) + assert isinstance(func.replaceable_func().type, DateTime) def test_custom_w_custom_name(self): class myfunc(GenericFunction): -- 2.47.3