Skip to content

Commit cc05b62

Browse files
shreyaskj-0710RNHTTR
authored andcommitted
[v3-1-test] Fix cron expression display for Day-of-Month and Day-of-Week conflicts (#54644)
* Fix cron expression display for Day-of-Month and Day-of-Week conflicts * Add test case for CronMixin description attribute * Add test case for CronMixin description attribute * Add test case for CronMixin description attribute * Add test case for CronMixin description attribute * Add test case for CronMixin description attribute --------- (cherry picked from commit c6531bb) Co-authored-by: shreyaskj-0710 <[email protected]> Co-authored-by: Ryan Hatter <[email protected]>
1 parent ac730ee commit cc05b62

File tree

2 files changed

+82
-6
lines changed

2 files changed

+82
-6
lines changed

airflow-core/src/airflow/timetables/_cron.py

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,52 @@ def __init__(self, cron: str, timezone: str | Timezone | FixedTimezone) -> None:
7171
self._timezone = timezone
7272

7373
try:
74-
descriptor = ExpressionDescriptor(
75-
expression=self._expression, casing_type=CasingTypeEnum.Sentence, use_24hour_time_format=True
76-
)
7774
# checking for more than 5 parameters in Cron and avoiding evaluation for now,
7875
# as Croniter has inconsistent evaluation with other libraries
7976
if len(croniter(self._expression).expanded) > 5:
8077
raise FormatException()
81-
interval_description: str = descriptor.get_description()
78+
79+
self.description = self._describe_with_dom_dow_fix(self._expression)
80+
8281
except (CroniterBadCronError, FormatException, MissingFieldException):
83-
interval_description = ""
84-
self.description: str = interval_description
82+
self.description = ""
83+
84+
def _describe_with_dom_dow_fix(self, expression: str) -> str:
85+
"""
86+
Return cron description with fix for DOM+DOW conflicts.
87+
88+
If both DOM and DOW are restricted, explain them as OR.
89+
"""
90+
cron_fields = expression.split()
91+
92+
if len(cron_fields) < 5:
93+
return ExpressionDescriptor(
94+
expression, casing_type=CasingTypeEnum.Sentence, use_24hour_time_format=True
95+
).get_description()
96+
97+
dom = cron_fields[2]
98+
dow = cron_fields[4]
99+
100+
if dom != "*" and dow != "*":
101+
# Case: conflict → DOM OR DOW
102+
cron_fields_dom = cron_fields.copy()
103+
cron_fields_dom[4] = "*"
104+
day_of_month_desc = ExpressionDescriptor(
105+
" ".join(cron_fields_dom), casing_type=CasingTypeEnum.Sentence, use_24hour_time_format=True
106+
).get_description()
107+
108+
cron_fields_dow = cron_fields.copy()
109+
cron_fields_dow[2] = "*"
110+
day_of_week_desc = ExpressionDescriptor(
111+
" ".join(cron_fields_dow), casing_type=CasingTypeEnum.Sentence, use_24hour_time_format=True
112+
).get_description()
113+
114+
return f"{day_of_month_desc} (or) {day_of_week_desc}"
115+
116+
# no conflict → return normal description
117+
return ExpressionDescriptor(
118+
expression, casing_type=CasingTypeEnum.Sentence, use_24hour_time_format=True
119+
).get_description()
85120

86121
def __eq__(self, other: object) -> bool:
87122
"""
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
from __future__ import annotations
18+
19+
from airflow.timetables._cron import CronMixin
20+
21+
SAMPLE_TZ = "UTC"
22+
23+
24+
def test_valid_cron_expression():
25+
cm = CronMixin("* * 1 * *", SAMPLE_TZ) # every day at midnight
26+
assert isinstance(cm.description, str)
27+
assert "Every minute" in cm.description or "month" in cm.description
28+
29+
30+
def test_invalid_cron_expression():
31+
cm = CronMixin("invalid cron", SAMPLE_TZ)
32+
assert cm.description == ""
33+
34+
35+
def test_dom_and_dow_conflict():
36+
cm = CronMixin("* * 1 * 1", SAMPLE_TZ) # 1st of month or Monday
37+
desc = cm.description
38+
39+
assert "(or)" in desc
40+
assert "Every minute, on day 1 of the month" in desc
41+
assert "Every minute, only on Monday" in desc

0 commit comments

Comments
 (0)