From 42c51019571b9a0de662b9eaebd1fd01b769b771 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. (cherry picked from commit 86c20910aed48f7d5b2ebaa91fa40d47c52d7db3) --- 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 1861edaff193b..13d69c28071eb 100644 --- a/airflow/models/dag.py +++ b/airflow/models/dag.py @@ -485,7 +485,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: @@ -513,7 +513,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 939baf4931cc6..34d761d4eb410 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 f24e862cadca2..29e6ac00b0e45 100644 --- a/tests/serialization/test_dag_serialization.py +++ b/tests/serialization/test_dag_serialization.py @@ -27,6 +27,7 @@ from glob import glob from unittest import mock +import pendulum import pytest from dateutil.relativedelta import FR, relativedelta from kubernetes.client import models as k8s @@ -444,6 +445,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):