From 3dd4f8f4c9fa1711215285eac6f5f36d8ea9ef26 Mon Sep 17 00:00:00 2001 From: Ruben Laguna Date: Wed, 23 Jun 2021 16:31:45 +0200 Subject: [PATCH] Fix ``AttributeError``: ``datetime.timezone`` object has no attribute ``name`` (#16599) closes: #16551 Previous implementation tried to force / coerce the provided timezone (from the dag's `start_date`) into a `pendulum.tz.timezone.*` that only worked if the provided timezone was already a pendulum's timezone and it specifically failed when with `datetime.timezone.utc` as timezone. --- airflow/models/dag.py | 4 +- tests/models/test_dag.py | 40 +++++++++++++++++++ tests/serialization/test_dag_serialization.py | 2 + 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/airflow/models/dag.py b/airflow/models/dag.py index 88f79fb1de6e1..708f8561b1053 100644 --- a/airflow/models/dag.py +++ b/airflow/models/dag.py @@ -499,7 +499,7 @@ def following_schedule(self, dttm): else: # absolute (e.g. 3 AM) naive = cron.get_next(datetime) - tz = pendulum.timezone(self.timezone.name) + tz = self.timezone following = timezone.make_aware(naive, tz) return timezone.convert_to_utc(following) elif self.normalized_schedule_interval is not None: @@ -527,7 +527,7 @@ def previous_schedule(self, dttm): else: # absolute (e.g. 3 AM) naive = cron.get_prev(datetime) - tz = pendulum.timezone(self.timezone.name) + tz = self.timezone previous = timezone.make_aware(naive, tz) return timezone.convert_to_utc(previous) elif self.normalized_schedule_interval is not None: diff --git a/tests/models/test_dag.py b/tests/models/test_dag.py index 4d953247de5de..d8714da84e77a 100644 --- a/tests/models/test_dag.py +++ b/tests/models/test_dag.py @@ -621,6 +621,46 @@ def test_following_schedule_relativedelta(self): _next = dag.following_schedule(_next) assert _next.isoformat() == "2015-01-02T02:00:00+00:00" + def test_previous_schedule_datetime_timezone(self): + # Check that we don't get an AttributeError 'name' for self.timezone + + start = datetime.datetime(2018, 3, 25, 2, tzinfo=datetime.timezone.utc) + dag = DAG('tz_dag', start_date=start, schedule_interval='@hourly') + when = dag.previous_schedule(start) + assert when.isoformat() == "2018-03-25T01:00:00+00:00" + + def test_following_schedule_datetime_timezone(self): + # Check that we don't get an AttributeError 'name' for self.timezone + + start = datetime.datetime(2018, 3, 25, 2, tzinfo=datetime.timezone.utc) + dag = DAG('tz_dag', start_date=start, schedule_interval='@hourly') + when = dag.following_schedule(start) + assert when.isoformat() == "2018-03-25T03:00:00+00:00" + + def test_following_schedule_datetime_timezone_utc0530(self): + # Check that we don't get an AttributeError 'name' for self.timezone + class UTC0530(datetime.tzinfo): + """tzinfo derived concrete class named "+0530" with offset of 19800""" + + # can be configured here + _offset = datetime.timedelta(seconds=19800) + _dst = datetime.timedelta(0) + _name = "+0530" + + def utcoffset(self, dt): + return self.__class__._offset + + def dst(self, dt): + return self.__class__._dst + + def tzname(self, dt): + return self.__class__._name + + start = datetime.datetime(2018, 3, 25, 10, tzinfo=UTC0530()) + dag = DAG('tz_dag', start_date=start, schedule_interval='@hourly') + when = dag.following_schedule(start) + assert when.isoformat() == "2018-03-25T05:30:00+00:00" + def test_dagtag_repr(self): clear_db_dags() dag = DAG('dag-test-dagtag', start_date=DEFAULT_DATE, tags=['tag-1', 'tag-2']) diff --git a/tests/serialization/test_dag_serialization.py b/tests/serialization/test_dag_serialization.py index 1c92e36aa96e2..353a01fc4eefb 100644 --- a/tests/serialization/test_dag_serialization.py +++ b/tests/serialization/test_dag_serialization.py @@ -28,6 +28,7 @@ from typing import Dict, Optional, Tuple from unittest import mock +import pendulum import pytest from dateutil.relativedelta import FR, relativedelta from kubernetes.client import models as k8s @@ -445,6 +446,7 @@ def validate_deserialized_task( datetime(2019, 7, 30, tzinfo=timezone.utc), datetime(2019, 8, 1, tzinfo=timezone.utc), ), + (pendulum.datetime(2019, 8, 1, tz='UTC'), None, pendulum.datetime(2019, 8, 1, tz='UTC')), ] ) def test_deserialization_start_date(self, dag_start_date, task_start_date, expected_task_start_date):