diff options
| -rw-r--r-- | doc/build/changelog/unreleased_14/5429.rst | 17 | ||||
| -rw-r--r-- | doc/build/core/tutorial.rst | 6 | ||||
| -rw-r--r-- | doc/build/orm/tutorial.rst | 6 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/evaluator.py | 4 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/compiler.py | 4 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/default_comparator.py | 14 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/operators.py | 47 | ||||
| -rw-r--r-- | lib/sqlalchemy/testing/suite/test_select.py | 4 | ||||
| -rw-r--r-- | test/orm/test_evaluator.py | 4 | ||||
| -rw-r--r-- | test/sql/test_deprecations.py | 69 | ||||
| -rw-r--r-- | test/sql/test_operators.py | 20 |
11 files changed, 154 insertions, 41 deletions
diff --git a/doc/build/changelog/unreleased_14/5429.rst b/doc/build/changelog/unreleased_14/5429.rst new file mode 100644 index 000000000..2ff7e2416 --- /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 738d4d74e..395144ed1 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 c04caf9e6..5ed42449a 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 21d5e72d4..f7f12ce12 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 ec1a57935..925441539 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 eec174e8b..d5762ff1f 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 ba03a6934..5f5052c28 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 adcd7d8b9..c199929a7 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 20577d8e6..a6c889aa7 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 edaa951e1..a72974dbb 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 c011b961c..3eb0c449f 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") |
