This is an automated email from the ASF dual-hosted git repository. ephraimanierobi pushed a commit to branch v3-1-test in repository https://gitbox.apache.org/repos/asf/airflow.git
commit 861521631dda11be34f0c0146fce77c20d6d9dd2 Author: Jens Scheffler <[email protected]> AuthorDate: Mon Nov 3 06:45:49 2025 +0100 [v3-1-test] Enable ruff PLW1641 rule (#57679) (#57701) (cherry picked from commit 58fc8609d543676921a88e426793db83120460cb) --- airflow-core/src/airflow/serialization/serialized_objects.py | 3 +++ airflow-core/src/airflow/task/priority_strategy.py | 3 +++ airflow-core/src/airflow/timetables/_cron.py | 3 +++ airflow-core/src/airflow/timetables/interval.py | 3 +++ airflow-core/src/airflow/timetables/simple.py | 3 +++ airflow-core/src/airflow/utils/db.py | 3 +++ airflow-core/tests/unit/models/test_renderedtifields.py | 3 +++ airflow-core/tests/unit/serialization/test_dag_serialization.py | 3 +++ airflow-core/tests/unit/serialization/test_serde.py | 3 +++ airflow-core/tests/unit/utils/test_json.py | 3 +++ airflow-core/tests/unit/utils/test_sqlalchemy.py | 2 +- dev/airflow_perf/sql_queries.py | 3 +++ devel-common/src/sphinx_exts/docs_build/errors.py | 3 +++ devel-common/src/sphinx_exts/docs_build/spelling_checks.py | 3 +++ devel-common/src/tests_common/test_utils/timetables.py | 3 +++ kubernetes-tests/tests/kubernetes_tests/test_base.py | 3 +++ .../celery/tests/integration/celery/test_celery_executor.py | 3 +++ .../kubernetes/src/airflow/providers/cncf/kubernetes/secret.py | 3 +++ .../src/airflow/providers/databricks/hooks/databricks.py | 9 +++++++++ .../src/airflow/providers/fab/auth_manager/models/__init__.py | 3 +++ .../tests/unit/snowflake/operators/test_snowflake_sql.py | 3 +++ providers/weaviate/tests/unit/weaviate/hooks/test_weaviate.py | 3 +++ pyproject.toml | 1 + task-sdk/src/airflow/sdk/definitions/asset/__init__.py | 4 ++++ task-sdk/src/airflow/sdk/definitions/operator_resources.py | 6 ++++++ task-sdk/src/airflow/sdk/definitions/param.py | 3 +++ task-sdk/src/airflow/sdk/execution_time/context.py | 9 +++++++++ task-sdk/src/airflow/sdk/execution_time/lazy_sequence.py | 3 +++ task-sdk/src/airflow/sdk/io/store.py | 3 +++ task-sdk/tests/task_sdk/bases/test_operator.py | 3 +++ 30 files changed, 102 insertions(+), 1 deletion(-) diff --git a/airflow-core/src/airflow/serialization/serialized_objects.py b/airflow-core/src/airflow/serialization/serialized_objects.py index 626ead30132..727e2685daa 100644 --- a/airflow-core/src/airflow/serialization/serialized_objects.py +++ b/airflow-core/src/airflow/serialization/serialized_objects.py @@ -1345,6 +1345,9 @@ class SerializedBaseOperator(DAGNode, BaseSerialization): getattr(self, c, None) == getattr(other, c, None) for c in BaseOperator._comps ) + def __hash__(self): + return hash((self.task_type, *[getattr(self, c, None) for c in BaseOperator._comps])) + def __repr__(self) -> str: return f"<SerializedTask({self.task_type}): {self.task_id}>" diff --git a/airflow-core/src/airflow/task/priority_strategy.py b/airflow-core/src/airflow/task/priority_strategy.py index 05209fec8a3..90abf3aa0a3 100644 --- a/airflow-core/src/airflow/task/priority_strategy.py +++ b/airflow-core/src/airflow/task/priority_strategy.py @@ -73,6 +73,9 @@ class PriorityWeightStrategy(ABC): return False return self.serialize() == other.serialize() + def __hash__(self): + return hash(self.serialize()) + class _AbsolutePriorityWeightStrategy(PriorityWeightStrategy): """Priority weight strategy that uses the task's priority weight directly.""" diff --git a/airflow-core/src/airflow/timetables/_cron.py b/airflow-core/src/airflow/timetables/_cron.py index 632c00ae2dc..b8bc22921ba 100644 --- a/airflow-core/src/airflow/timetables/_cron.py +++ b/airflow-core/src/airflow/timetables/_cron.py @@ -128,6 +128,9 @@ class CronMixin: return NotImplemented return self._expression == other._expression and self._timezone == other._timezone + def __hash__(self): + return hash((self._expression, self._timezone)) + @property def summary(self) -> str: return self._expression diff --git a/airflow-core/src/airflow/timetables/interval.py b/airflow-core/src/airflow/timetables/interval.py index e248dd67da2..c116b2343fb 100644 --- a/airflow-core/src/airflow/timetables/interval.py +++ b/airflow-core/src/airflow/timetables/interval.py @@ -201,6 +201,9 @@ class DeltaDataIntervalTimetable(DeltaMixin, _DataIntervalTimetable): return NotImplemented return self._delta == other._delta + def __hash__(self): + return hash(self._delta) + def serialize(self) -> dict[str, Any]: from airflow.serialization.serialized_objects import encode_relativedelta diff --git a/airflow-core/src/airflow/timetables/simple.py b/airflow-core/src/airflow/timetables/simple.py index b5b6f2468f3..3723d7321ee 100644 --- a/airflow-core/src/airflow/timetables/simple.py +++ b/airflow-core/src/airflow/timetables/simple.py @@ -50,6 +50,9 @@ class _TrivialTimetable(Timetable): return NotImplemented return True + def __hash__(self): + return hash(self.__class__.__name__) + def serialize(self) -> dict[str, Any]: return {} diff --git a/airflow-core/src/airflow/utils/db.py b/airflow-core/src/airflow/utils/db.py index 11bafc01c25..9e99f53c8c5 100644 --- a/airflow-core/src/airflow/utils/db.py +++ b/airflow-core/src/airflow/utils/db.py @@ -1596,6 +1596,9 @@ class LazySelectSequence(Sequence[T]): z = itertools.zip_longest(iter(self), iter(other), fillvalue=object()) return all(x == y for x, y in z) + def __hash__(self): + return hash(tuple(x for x in iter(self))) + def __reversed__(self) -> Iterator[T]: return iter(self._process_row(r) for r in self._session.execute(self._select_desc)) diff --git a/airflow-core/tests/unit/models/test_renderedtifields.py b/airflow-core/tests/unit/models/test_renderedtifields.py index ca97b8456b7..47aa8829aa8 100644 --- a/airflow-core/tests/unit/models/test_renderedtifields.py +++ b/airflow-core/tests/unit/models/test_renderedtifields.py @@ -70,6 +70,9 @@ class ClassWithCustomAttributes: def __eq__(self, other): return self.__dict__ == other.__dict__ + def __hash__(self): + return hash(self.__dict__) + def __ne__(self, other): return not self.__eq__(other) diff --git a/airflow-core/tests/unit/serialization/test_dag_serialization.py b/airflow-core/tests/unit/serialization/test_dag_serialization.py index be27d4a8c7b..3373f238dd5 100644 --- a/airflow-core/tests/unit/serialization/test_dag_serialization.py +++ b/airflow-core/tests/unit/serialization/test_dag_serialization.py @@ -1339,6 +1339,9 @@ class TestStringifiedDAGs: def __eq__(self, other): return self.__dict__ == other.__dict__ + def __hash__(self): + return hash(self.__dict__) + def __ne__(self, other): return not self.__eq__(other) diff --git a/airflow-core/tests/unit/serialization/test_serde.py b/airflow-core/tests/unit/serialization/test_serde.py index 0dadfc7e9ee..6130437f1b9 100644 --- a/airflow-core/tests/unit/serialization/test_serde.py +++ b/airflow-core/tests/unit/serialization/test_serde.py @@ -142,6 +142,9 @@ class Z: def __eq__(self, other): return self.x == other.x + def __hash__(self): + return hash(self.x) + @attr.define class Y: diff --git a/airflow-core/tests/unit/utils/test_json.py b/airflow-core/tests/unit/utils/test_json.py index 2148830bd43..052b8118bad 100644 --- a/airflow-core/tests/unit/utils/test_json.py +++ b/airflow-core/tests/unit/utils/test_json.py @@ -45,6 +45,9 @@ class Z: def __eq__(self, other): return self.x == other.x + def __hash__(self): + return hash(self.x) + @dataclass class U: diff --git a/airflow-core/tests/unit/utils/test_sqlalchemy.py b/airflow-core/tests/unit/utils/test_sqlalchemy.py index 051c91f05b1..a7765086461 100644 --- a/airflow-core/tests/unit/utils/test_sqlalchemy.py +++ b/airflow-core/tests/unit/utils/test_sqlalchemy.py @@ -264,7 +264,7 @@ class TestExecutorConfigType: under older kubernetes library version. """ - class MockAttrError: + class MockAttrError: # noqa: PLW1641 def __eq__(self, other): raise AttributeError("hello") diff --git a/dev/airflow_perf/sql_queries.py b/dev/airflow_perf/sql_queries.py index 60f4a9e0cbf..a0164309d98 100644 --- a/dev/airflow_perf/sql_queries.py +++ b/dev/airflow_perf/sql_queries.py @@ -101,6 +101,9 @@ class Query(NamedTuple): and self.file == other.file ) + def __hash__(self): + return hash((self.function, self.sql, self.location, self.file)) + def to_dict(self): """ Convert selected attributes of the instance into a dictionary. diff --git a/devel-common/src/sphinx_exts/docs_build/errors.py b/devel-common/src/sphinx_exts/docs_build/errors.py index 823815fa5c3..d3dded809e9 100644 --- a/devel-common/src/sphinx_exts/docs_build/errors.py +++ b/devel-common/src/sphinx_exts/docs_build/errors.py @@ -45,6 +45,9 @@ class DocBuildError(NamedTuple): right = (other.file_path, other.line_no, other.message) return left == right + def __hash__(self): + return hash((self.file_path, self.line_no, self.message)) + def __ne__(self, other): return not self == other diff --git a/devel-common/src/sphinx_exts/docs_build/spelling_checks.py b/devel-common/src/sphinx_exts/docs_build/spelling_checks.py index 58197f13409..94a2de20500 100644 --- a/devel-common/src/sphinx_exts/docs_build/spelling_checks.py +++ b/devel-common/src/sphinx_exts/docs_build/spelling_checks.py @@ -61,6 +61,9 @@ class SpellingError(NamedTuple): ) return left == right + def __hash__(self): + return hash((self.file_path, self.line_no, self.spelling, self.context_line, self.message)) + def __ne__(self, other): return not self == other diff --git a/devel-common/src/tests_common/test_utils/timetables.py b/devel-common/src/tests_common/test_utils/timetables.py index f2053d83b24..3d48921aa56 100644 --- a/devel-common/src/tests_common/test_utils/timetables.py +++ b/devel-common/src/tests_common/test_utils/timetables.py @@ -45,6 +45,9 @@ class CustomSerializationTimetable(Timetable): return False return self.value == other.value + def __hash__(self): + return hash(self.value) + def serialize(self): return {"value": self.value} diff --git a/kubernetes-tests/tests/kubernetes_tests/test_base.py b/kubernetes-tests/tests/kubernetes_tests/test_base.py index 0ee8d49c727..20b807954f0 100644 --- a/kubernetes-tests/tests/kubernetes_tests/test_base.py +++ b/kubernetes-tests/tests/kubernetes_tests/test_base.py @@ -53,6 +53,9 @@ class StringContainingId(str): def __eq__(self, other): return self in other.strip() or self in other + def __hash__(self): + return hash(self) + class BaseK8STest: """Base class for K8S Tests.""" diff --git a/providers/celery/tests/integration/celery/test_celery_executor.py b/providers/celery/tests/integration/celery/test_celery_executor.py index 874fbce3cbc..a333d454404 100644 --- a/providers/celery/tests/integration/celery/test_celery_executor.py +++ b/providers/celery/tests/integration/celery/test_celery_executor.py @@ -312,6 +312,9 @@ class ClassWithCustomAttributes: def __eq__(self, other): return self.__dict__ == other.__dict__ + def __hash__(self): + return hash(self.__dict__) + def __ne__(self, other): return not self.__eq__(other) diff --git a/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/secret.py b/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/secret.py index 692777de447..bc063e75307 100644 --- a/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/secret.py +++ b/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/secret.py @@ -121,5 +121,8 @@ class Secret(K8SModel): and self.key == other.key ) + def __hash__(self): + return hash((self.deploy_type, self.deploy_target, self.secret, self.key)) + def __repr__(self): return f"Secret({self.deploy_type}, {self.deploy_target}, {self.secret}, {self.key})" diff --git a/providers/databricks/src/airflow/providers/databricks/hooks/databricks.py b/providers/databricks/src/airflow/providers/databricks/hooks/databricks.py index aaa9fece60c..9d7b1bf417c 100644 --- a/providers/databricks/src/airflow/providers/databricks/hooks/databricks.py +++ b/providers/databricks/src/airflow/providers/databricks/hooks/databricks.py @@ -134,6 +134,9 @@ class RunState: and self.state_message == other.state_message ) + def __hash__(self): + return hash((self.life_cycle_state, self.result_state, self.state_message)) + def __repr__(self) -> str: return str(self.__dict__) @@ -183,6 +186,9 @@ class ClusterState: def __eq__(self, other) -> bool: return self.state == other.state and self.state_message == other.state_message + def __hash__(self): + return hash((self.state, self.state_message)) + def __repr__(self) -> str: return str(self.__dict__) @@ -244,6 +250,9 @@ class SQLStatementState: and self.error_message == other.error_message ) + def __hash__(self): + return hash((self.state, self.error_code, self.error_message)) + def __repr__(self) -> str: return str(self.__dict__) diff --git a/providers/fab/src/airflow/providers/fab/auth_manager/models/__init__.py b/providers/fab/src/airflow/providers/fab/auth_manager/models/__init__.py index dd895e1c0b7..6817ba53fde 100644 --- a/providers/fab/src/airflow/providers/fab/auth_manager/models/__init__.py +++ b/providers/fab/src/airflow/providers/fab/auth_manager/models/__init__.py @@ -91,6 +91,9 @@ class Resource(Model): def __eq__(self, other): return (isinstance(other, self.__class__)) and (self.name == other.name) + def __hash__(self): + return hash((self.id, self.name)) + def __neq__(self, other): return self.name != other.name diff --git a/providers/snowflake/tests/unit/snowflake/operators/test_snowflake_sql.py b/providers/snowflake/tests/unit/snowflake/operators/test_snowflake_sql.py index 9f3c92ab376..1c47a022c51 100644 --- a/providers/snowflake/tests/unit/snowflake/operators/test_snowflake_sql.py +++ b/providers/snowflake/tests/unit/snowflake/operators/test_snowflake_sql.py @@ -42,6 +42,9 @@ class MockRow: def __eq__(self, other): return isinstance(other, MockRow) and self.__dict__ == other.__dict__ + def __hash__(self): + return hash(self.__dict__) + def __repr__(self): return f"MockRow({self.__dict__})" diff --git a/providers/weaviate/tests/unit/weaviate/hooks/test_weaviate.py b/providers/weaviate/tests/unit/weaviate/hooks/test_weaviate.py index 00d4d3f0b44..7cc1f7b794a 100644 --- a/providers/weaviate/tests/unit/weaviate/hooks/test_weaviate.py +++ b/providers/weaviate/tests/unit/weaviate/hooks/test_weaviate.py @@ -100,6 +100,9 @@ class MockObject: return False return self.properties == other.properties and self.uuid == other.uuid + def __hash__(self): + return hash((self.properties, self.uuid)) + class TestWeaviateHook: """ diff --git a/pyproject.toml b/pyproject.toml index feae5bdd786..fd232dc0a68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -605,6 +605,7 @@ extend-select = [ "PLW1507", # Shallow copy of os.environ via copy.copy(os.environ) "PLW1508", # Invalid type for environment variable default; expected str or None "PLW1510", # subprocess.run without explicit check argument + "PLW1641", # Object does not implement __hash__ method # Per rule enables "RUF006", # Checks for asyncio dangling task "RUF015", # Checks for unnecessary iterable allocation for first element diff --git a/task-sdk/src/airflow/sdk/definitions/asset/__init__.py b/task-sdk/src/airflow/sdk/definitions/asset/__init__.py index 3597f016f8b..4f986efa6bb 100644 --- a/task-sdk/src/airflow/sdk/definitions/asset/__init__.py +++ b/task-sdk/src/airflow/sdk/definitions/asset/__init__.py @@ -426,6 +426,10 @@ class Asset(os.PathLike, BaseAsset): f = attrs.filters.include(*attrs.fields_dict(Asset)) return attrs.asdict(self, filter=f) == attrs.asdict(other, filter=f) + def __hash__(self): + f = attrs.filters.include(*attrs.fields_dict(Asset)) + return hash(attrs.asdict(self, filter=f)) + @property def normalized_uri(self) -> str | None: """ diff --git a/task-sdk/src/airflow/sdk/definitions/operator_resources.py b/task-sdk/src/airflow/sdk/definitions/operator_resources.py index 6cb1b551cd3..a00cc84f08a 100644 --- a/task-sdk/src/airflow/sdk/definitions/operator_resources.py +++ b/task-sdk/src/airflow/sdk/definitions/operator_resources.py @@ -55,6 +55,9 @@ class Resource: return NotImplemented return self.__dict__ == other.__dict__ + def __hash__(self): + return hash(self.__dict__) + def __repr__(self): return str(self.__dict__) @@ -138,6 +141,9 @@ class Resources: return NotImplemented return self.__dict__ == other.__dict__ + def __hash__(self): + return hash(self.__dict__) + def __repr__(self): return str(self.__dict__) diff --git a/task-sdk/src/airflow/sdk/definitions/param.py b/task-sdk/src/airflow/sdk/definitions/param.py index 6c61fa88a70..5da589d79e9 100644 --- a/task-sdk/src/airflow/sdk/definitions/param.py +++ b/task-sdk/src/airflow/sdk/definitions/param.py @@ -159,6 +159,9 @@ class ParamsDict(MutableMapping[str, Any]): return self.dump() == other return NotImplemented + def __hash__(self): + return hash(self.dump()) + def __copy__(self) -> ParamsDict: return ParamsDict(self.__dict, self.suppress_exception) diff --git a/task-sdk/src/airflow/sdk/execution_time/context.py b/task-sdk/src/airflow/sdk/execution_time/context.py index 58bd2521b02..e4a647bab2f 100644 --- a/task-sdk/src/airflow/sdk/execution_time/context.py +++ b/task-sdk/src/airflow/sdk/execution_time/context.py @@ -351,6 +351,9 @@ class ConnectionAccessor: # All instances of ConnectionAccessor are equal since it is a stateless dynamic accessor return True + def __hash__(self): + return hash(self.__class__.__name__) + def get(self, conn_id: str, default_conn: Any = None) -> Any: from airflow.exceptions import AirflowNotFoundException @@ -376,6 +379,9 @@ class VariableAccessor: # All instances of VariableAccessor are equal since it is a stateless dynamic accessor return True + def __hash__(self): + return hash(self.__class__.__name__) + def __repr__(self) -> str: return "<VariableAccessor (dynamic access)>" @@ -412,6 +418,9 @@ class MacrosAccessor: return False return True + def __hash__(self): + return hash(self.__class__.__name__) + class _AssetRefResolutionMixin: _asset_ref_cache: dict[AssetRef, AssetUniqueKey] = {} diff --git a/task-sdk/src/airflow/sdk/execution_time/lazy_sequence.py b/task-sdk/src/airflow/sdk/execution_time/lazy_sequence.py index d8b356870c7..4efb0b71368 100644 --- a/task-sdk/src/airflow/sdk/execution_time/lazy_sequence.py +++ b/task-sdk/src/airflow/sdk/execution_time/lazy_sequence.py @@ -81,6 +81,9 @@ class LazyXComSequence(Sequence[T]): z = itertools.zip_longest(iter(self), iter(other), fillvalue=object()) return all(x == y for x, y in z) + def __hash__(self): + return hash((*[item for item in iter(self)],)) + def __iter__(self) -> Iterator[T]: return LazyXComIterator(seq=self) diff --git a/task-sdk/src/airflow/sdk/io/store.py b/task-sdk/src/airflow/sdk/io/store.py index c38fb81bb82..30a606f3832 100644 --- a/task-sdk/src/airflow/sdk/io/store.py +++ b/task-sdk/src/airflow/sdk/io/store.py @@ -120,6 +120,9 @@ class ObjectStore: except ValueError: return False + def __hash__(self): + return hash((self.conn_id, self.fsid)) + _STORE_CACHE: dict[str, ObjectStore] = {} diff --git a/task-sdk/tests/task_sdk/bases/test_operator.py b/task-sdk/tests/task_sdk/bases/test_operator.py index 62302ce4888..683bca1d574 100644 --- a/task-sdk/tests/task_sdk/bases/test_operator.py +++ b/task-sdk/tests/task_sdk/bases/test_operator.py @@ -63,6 +63,9 @@ class ClassWithCustomAttributes: def __eq__(self, other): return self.__dict__ == other.__dict__ + def __hash__(self): + return hash(self.__dict__) + def __ne__(self, other): return not self.__eq__(other)
