From fe413084c53642b0de0728afbd78f6856d359bef Mon Sep 17 00:00:00 2001 From: jonathan vanasco Date: Tue, 1 Sep 2020 16:56:53 -0400 Subject: [PATCH] Rename Core expression isnot, not_in_ Several operators are renamed to achieve more consistent naming across SQLAlchemy. The operator changes are: * `isnot` is now `is_not` * `not_in_` is now `not_in` Because these are core operators, the internal migration strategy for this change is to support legacy terms for an extended period of time -- if not indefinitely -- but update all documentation, tutorials, and internal usage to the new terms. The new terms are used to define the functions, and the legacy terms have been deprecated into aliases of the new terms. Fixes: #5429 Change-Id: Ia1e66e7a50ac35d3f6260d8bf6ba3ce8087cbad2 --- doc/build/changelog/unreleased_14/5429.rst | 17 +++++ doc/build/core/tutorial.rst | 6 +- doc/build/orm/tutorial.rst | 6 +- lib/sqlalchemy/orm/evaluator.py | 4 +- lib/sqlalchemy/sql/compiler.py | 4 +- lib/sqlalchemy/sql/default_comparator.py | 14 ++--- lib/sqlalchemy/sql/operators.py | 47 ++++++++++---- lib/sqlalchemy/testing/suite/test_select.py | 4 +- test/orm/test_evaluator.py | 4 +- test/sql/test_deprecations.py | 69 +++++++++++++++++++++ test/sql/test_operators.py | 20 +++--- 11 files changed, 154 insertions(+), 41 deletions(-) create mode 100644 doc/build/changelog/unreleased_14/5429.rst diff --git a/doc/build/changelog/unreleased_14/5429.rst b/doc/build/changelog/unreleased_14/5429.rst new file mode 100644 index 0000000000..2ff7e2416c --- /dev/null +++ b/doc/build/changelog/unreleased_14/5429.rst @@ -0,0 +1,17 @@ +.. change:: + :tags: change, sql + :tickets: 5429 + + Several operators are renamed to achieve more consistent naming across + SQLAlchemy. + + The operator changes are: + + * `isnot` is now `is_not` + * `not_in_` is now `not_in` + + Because these are core operators, the internal migration strategy for this + change is to support legacy terms for an extended period of time -- if not + indefinitely -- but update all documentation, tutorials, and internal usage + to the new terms. The new terms are used to define the functions, and + the legacy terms have been deprecated into aliases of the new terms. diff --git a/doc/build/core/tutorial.rst b/doc/build/core/tutorial.rst index 738d4d74ee..395144ed1d 100644 --- a/doc/build/core/tutorial.rst +++ b/doc/build/core/tutorial.rst @@ -775,7 +775,7 @@ objects is at :class:`.ColumnOperators`. in_([('ed', 'edsnickname'), ('wendy', 'windy')]) ) -* :meth:`NOT IN <.ColumnOperators.notin_>`:: +* :meth:`NOT IN <.ColumnOperators.not_in>`:: statement.where(~users.c.name.in_(['ed', 'wendy', 'jack'])) @@ -786,12 +786,12 @@ objects is at :class:`.ColumnOperators`. # alternatively, if pep8/linters are a concern statement.where(users.c.name.is_(None)) -* :meth:`IS NOT NULL <.ColumnOperators.isnot>`:: +* :meth:`IS NOT NULL <.ColumnOperators.is_not>`:: statement.where(users.c.name != None) # alternatively, if pep8/linters are a concern - statement.where(users.c.name.isnot(None)) + statement.where(users.c.name.is_not(None)) * :func:`AND <.sql.expression.and_>`:: diff --git a/doc/build/orm/tutorial.rst b/doc/build/orm/tutorial.rst index c04caf9e6f..5ed42449ac 100644 --- a/doc/build/orm/tutorial.rst +++ b/doc/build/orm/tutorial.rst @@ -789,7 +789,7 @@ Here's a rundown of some of the most common operators used in in_([('ed', 'edsnickname'), ('wendy', 'windy')]) ) -* :meth:`NOT IN <.ColumnOperators.notin_>`:: +* :meth:`NOT IN <.ColumnOperators.not_in>`:: query.filter(~User.name.in_(['ed', 'wendy', 'jack'])) @@ -800,12 +800,12 @@ Here's a rundown of some of the most common operators used in # alternatively, if pep8/linters are a concern query.filter(User.name.is_(None)) -* :meth:`IS NOT NULL <.ColumnOperators.isnot>`:: +* :meth:`IS NOT NULL <.ColumnOperators.is_not>`:: query.filter(User.name != None) # alternatively, if pep8/linters are a concern - query.filter(User.name.isnot(None)) + query.filter(User.name.is_not(None)) * :func:`AND <.sql.expression.and_>`:: diff --git a/lib/sqlalchemy/orm/evaluator.py b/lib/sqlalchemy/orm/evaluator.py index 21d5e72d40..f7f12ce127 100644 --- a/lib/sqlalchemy/orm/evaluator.py +++ b/lib/sqlalchemy/orm/evaluator.py @@ -37,7 +37,7 @@ _straight_ops = set( _extended_ops = { operators.in_op: (lambda a, b: a in b), - operators.notin_op: (lambda a, b: a not in b), + operators.not_in_op: (lambda a, b: a not in b), } _notimplemented_ops = set( @@ -170,7 +170,7 @@ class EvaluatorCompiler(object): def evaluate(obj): return eval_left(obj) == eval_right(obj) - elif operator is operators.isnot: + elif operator is operators.is_not: def evaluate(obj): return eval_left(obj) != eval_right(obj) diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index ec1a579355..9254415392 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -190,12 +190,12 @@ OPERATORS = { operators.match_op: " MATCH ", operators.notmatch_op: " NOT MATCH ", operators.in_op: " IN ", - operators.notin_op: " NOT IN ", + operators.not_in_op: " NOT IN ", operators.comma_op: ", ", operators.from_: " FROM ", operators.as_: " AS ", operators.is_: " IS ", - operators.isnot: " IS NOT ", + operators.is_not: " IS NOT ", operators.collate: " COLLATE ", # unary operators.exists: "EXISTS ", diff --git a/lib/sqlalchemy/sql/default_comparator.py b/lib/sqlalchemy/sql/default_comparator.py index eec174e8b5..d5762ff1f3 100644 --- a/lib/sqlalchemy/sql/default_comparator.py +++ b/lib/sqlalchemy/sql/default_comparator.py @@ -73,20 +73,20 @@ def _boolean_compare( expr, coercions.expect(roles.ConstExprRole, obj), operators.is_, - negate=operators.isnot, + negate=operators.is_not, type_=result_type, ) - elif op in (operators.ne, operators.isnot): + elif op in (operators.ne, operators.is_not): return BinaryExpression( expr, coercions.expect(roles.ConstExprRole, obj), - operators.isnot, + operators.is_not, negate=operators.is_, type_=result_type, ) else: raise exc.ArgumentError( - "Only '=', '!=', 'is_()', 'isnot()', " + "Only '=', '!=', 'is_()', 'is_not()', " "'is_distinct_from()', 'isnot_distinct_from()' " "operators can be used with None/True/False" ) @@ -328,10 +328,10 @@ operator_lookup = { "asc_op": (_scalar, UnaryExpression._create_asc), "nullsfirst_op": (_scalar, UnaryExpression._create_nullsfirst), "nullslast_op": (_scalar, UnaryExpression._create_nullslast), - "in_op": (_in_impl, operators.notin_op), - "notin_op": (_in_impl, operators.in_op), + "in_op": (_in_impl, operators.not_in_op), + "not_in_op": (_in_impl, operators.in_op), "is_": (_boolean_compare, operators.is_), - "isnot": (_boolean_compare, operators.isnot), + "is_not": (_boolean_compare, operators.is_not), "collate": (_collate_impl,), "match_op": (_match_impl,), "notmatch_op": (_match_impl,), diff --git a/lib/sqlalchemy/sql/operators.py b/lib/sqlalchemy/sql/operators.py index ba03a69347..5f5052c286 100644 --- a/lib/sqlalchemy/sql/operators.py +++ b/lib/sqlalchemy/sql/operators.py @@ -596,7 +596,7 @@ class ColumnOperators(Operators): """ return self.operate(in_op, other) - def notin_(self, other): + def not_in(self, other): """implement the ``NOT IN`` operator. This is equivalent to using negation with @@ -608,8 +608,12 @@ class ColumnOperators(Operators): :paramref:`_sa.create_engine.empty_in_strategy` may be used to alter this behavior. + .. versionchanged:: 1.4 The ``not_in()`` operator is renamed from + ``notin_()`` in previous releases. The previous name remains + available for backwards compatibility. + .. versionchanged:: 1.2 The :meth:`.ColumnOperators.in_` and - :meth:`.ColumnOperators.notin_` operators + :meth:`.ColumnOperators.not_in` operators now produce a "static" expression for an empty IN sequence by default. @@ -618,7 +622,10 @@ class ColumnOperators(Operators): :meth:`.ColumnOperators.in_` """ - return self.operate(notin_op, other) + return self.operate(not_in_op, other) + + # deprecated 1.4; see #5429 + notin_ = not_in def notlike(self, other, escape=None): """implement the ``NOT LIKE`` operator. @@ -654,12 +661,12 @@ class ColumnOperators(Operators): usage of ``IS`` may be desirable if comparing to boolean values on certain platforms. - .. seealso:: :meth:`.ColumnOperators.isnot` + .. seealso:: :meth:`.ColumnOperators.is_not` """ return self.operate(is_, other) - def isnot(self, other): + def is_not(self, other): """Implement the ``IS NOT`` operator. Normally, ``IS NOT`` is generated automatically when comparing to a @@ -667,10 +674,18 @@ class ColumnOperators(Operators): usage of ``IS NOT`` may be desirable if comparing to boolean values on certain platforms. + .. versionchanged:: 1.4 The ``is_not()`` operator is renamed from + ``isnot()`` in previous releases. The previous name remains + available for backwards compatibility. + + .. seealso:: :meth:`.ColumnOperators.is_` """ - return self.operate(isnot, other) + return self.operate(is_not, other) + + # deprecated 1.4; see #5429 + isnot = is_not def startswith(self, other, **kwargs): r"""Implement the ``startswith`` operator. @@ -1269,8 +1284,12 @@ def is_(a, b): @comparison_op -def isnot(a, b): - return a.isnot(b) +def is_not(a, b): + return a.is_not(b) + + +# 1.4 deprecated; see #5429 +isnot = is_not def collate(a, b): @@ -1317,8 +1336,12 @@ def in_op(a, b): @comparison_op -def notin_op(a, b): - return a.notin_(b) +def not_in_op(a, b): + return a.not_in(b) + + +# 1.4 deprecated; see #5429 +notin_op = not_in_op def distinct_op(a): @@ -1529,9 +1552,9 @@ _PRECEDENCE = { like_op: 5, notlike_op: 5, in_op: 5, - notin_op: 5, + not_in_op: 5, is_: 5, - isnot: 5, + is_not: 5, eq: 5, ne: 5, is_distinct_from: 5, diff --git a/lib/sqlalchemy/testing/suite/test_select.py b/lib/sqlalchemy/testing/suite/test_select.py index adcd7d8b90..c199929a72 100644 --- a/lib/sqlalchemy/testing/suite/test_select.py +++ b/lib/sqlalchemy/testing/suite/test_select.py @@ -821,7 +821,7 @@ class ExpandingBoundInTest(fixtures.TablesTest): stmt = ( select(table.c.id) - .where(table.c.x.notin_(bindparam("q", expanding=True))) + .where(table.c.x.not_in(bindparam("q", expanding=True))) .order_by(table.c.id) ) @@ -843,7 +843,7 @@ class ExpandingBoundInTest(fixtures.TablesTest): stmt = ( select(table.c.id) - .where(table.c.z.notin_(bindparam("q", expanding=True))) + .where(table.c.z.not_in(bindparam("q", expanding=True))) .order_by(table.c.id) ) diff --git a/test/orm/test_evaluator.py b/test/orm/test_evaluator.py index 20577d8e62..a6c889aa74 100644 --- a/test/orm/test_evaluator.py +++ b/test/orm/test_evaluator.py @@ -205,7 +205,7 @@ class EvaluateTest(fixtures.MappedTest): ) eval_eq( - User.name.notin_(["foo", "bar"]), + User.name.not_in(["foo", "bar"]), testcases=[ (User(id=1, name="foo"), False), (User(id=2, name="bat"), True), @@ -229,7 +229,7 @@ class EvaluateTest(fixtures.MappedTest): ) eval_eq( - tuple_(User.id, User.name).notin_([(1, "foo"), (2, "bar")]), + tuple_(User.id, User.name).not_in([(1, "foo"), (2, "bar")]), testcases=[ (User(id=1, name="foo"), False), (User(id=2, name="bat"), True), diff --git a/test/sql/test_deprecations.py b/test/sql/test_deprecations.py index edaa951e15..a72974dbbe 100644 --- a/test/sql/test_deprecations.py +++ b/test/sql/test_deprecations.py @@ -28,12 +28,14 @@ from sqlalchemy import util from sqlalchemy import VARCHAR from sqlalchemy.engine import default from sqlalchemy.sql import coercions +from sqlalchemy.sql import operators from sqlalchemy.sql import quoted_name from sqlalchemy.sql import roles from sqlalchemy.sql import visitors from sqlalchemy.sql.selectable import SelectStatementGrouping from sqlalchemy.testing import assert_raises from sqlalchemy.testing import assert_raises_message +from sqlalchemy.testing import assertions from sqlalchemy.testing import AssertsCompiledSQL from sqlalchemy.testing import engines from sqlalchemy.testing import eq_ @@ -1777,3 +1779,70 @@ class TableDeprecationTest(fixtures.TestBase): exc.InvalidRequestError, "Table 'foo' not defined" ): Table("foo", MetaData(), mustexist=True) + + +class LegacyOperatorTest(AssertsCompiledSQL, fixtures.TestBase): + __dialect__ = "default" + + def test_issue_5429_compile(self): + self.assert_compile(column("x").isnot("foo"), "x IS NOT :x_1") + + self.assert_compile( + column("x").notin_(["foo", "bar"]), "x NOT IN ([POSTCOMPILE_x_1])" + ) + + def test_issue_5429_operators(self): + # functions + # is_not + assert hasattr(operators, "is_not") # modern + assert hasattr(operators, "isnot") # legacy + assert operators.is_not is operators.isnot + # not_in + assert hasattr(operators, "not_in_op") # modern + assert hasattr(operators, "notin_op") # legacy + assert operators.not_in_op is operators.notin_op + + # precedence mapping + # is_not + assert operators.is_not in operators._PRECEDENCE # modern + assert operators.isnot in operators._PRECEDENCE # legacy + assert ( + operators._PRECEDENCE[operators.is_not] + == operators._PRECEDENCE[operators.isnot] + ) + # not_in_op + assert operators.not_in_op in operators._PRECEDENCE # modern + assert operators.notin_op in operators._PRECEDENCE # legacy + assert ( + operators._PRECEDENCE[operators.not_in_op] + == operators._PRECEDENCE[operators.notin_op] + ) + + # ColumnOperators + # is_not + assert hasattr(operators.ColumnOperators, "is_not") # modern + assert hasattr(operators.ColumnOperators, "isnot") # legacy + assert ( + operators.ColumnOperators.is_not == operators.ColumnOperators.isnot + ) + # not_in + assert hasattr(operators.ColumnOperators, "not_in") # modern + assert hasattr(operators.ColumnOperators, "notin_") # legacy + assert ( + operators.ColumnOperators.not_in + == operators.ColumnOperators.notin_ + ) + + def test_issue_5429_assertions(self): + """ + 2) ensure compatibility across sqlalchemy.testing.assertions + """ + # functions + # is_not + assert hasattr(assertions, "is_not") # modern + assert hasattr(assertions, "is_not_") # legacy + assert assertions.is_not is assertions.is_not_ + # not_in + assert hasattr(assertions, "not_in") # modern + assert hasattr(assertions, "not_in_") # legacy + assert assertions.not_in is assertions.not_in_ diff --git a/test/sql/test_operators.py b/test/sql/test_operators.py index c011b961c8..3eb0c449f7 100644 --- a/test/sql/test_operators.py +++ b/test/sql/test_operators.py @@ -87,7 +87,8 @@ class DefaultColumnComparatorTest(fixtures.TestBase): @testing.combinations( (operators.add, right_column), (operators.is_, None), - (operators.isnot, None), + (operators.is_not, None), + (operators.isnot, None), # deprecated 1.4; See #5429 (operators.is_, null()), (operators.is_, true()), (operators.is_, false()), @@ -98,15 +99,18 @@ class DefaultColumnComparatorTest(fixtures.TestBase): (operators.is_distinct_from, None), (operators.isnot_distinct_from, True), (operators.is_, True), - (operators.isnot, True), + (operators.is_not, True), + (operators.isnot, True), # deprecated 1.4; See #5429 (operators.is_, False), - (operators.isnot, False), + (operators.is_not, False), + (operators.isnot, False), # deprecated 1.4; See #5429 (operators.like_op, right_column), (operators.notlike_op, right_column), (operators.ilike_op, right_column), (operators.notilike_op, right_column), (operators.is_, right_column), - (operators.isnot, right_column), + (operators.is_not, right_column), + (operators.isnot, right_column), # deprecated 1.4; See #5429 (operators.concat_op, right_column), id_="ns", ) @@ -179,19 +183,19 @@ class DefaultColumnComparatorTest(fixtures.TestBase): ) self._loop_test(operators.in_op, [1, 2, 3]) - def test_notin(self): + def test_not_in(self): left = column("left") - assert left.comparator.operate(operators.notin_op, [1, 2, 3]).compare( + assert left.comparator.operate(operators.not_in_op, [1, 2, 3]).compare( BinaryExpression( left, BindParameter( "left", value=[1, 2, 3], unique=True, expanding=True ), - operators.notin_op, + operators.not_in_op, type_=sqltypes.BOOLEANTYPE, ) ) - self._loop_test(operators.notin_op, [1, 2, 3]) + self._loop_test(operators.not_in_op, [1, 2, 3]) def test_in_no_accept_list_of_non_column_element(self): left = column("left") -- 2.47.3