Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion airflow-core/src/airflow/cli/commands/dag_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,17 @@ def iter_next_dagrun_info() -> Iterator[DagRunInfo | None]:
else:
columns = ["logical_date", "data_interval.start", "data_interval.end", "run_after"]
getters = [(c, operator.attrgetter(c)) for c in columns]
AirflowConsole().print_as_table([{n: f(o) for n, f in getters} for o in iter_next_dagrun_info()])
rows = []
for o in iter_next_dagrun_info():
if o is None:
print(
"[WARN] No following schedule can be found. "
"This DAG may have schedule interval '@once' or `None`.",
file=sys.stderr,
)
else:
rows.append({n: f(o) for n, f in getters})
AirflowConsole().print_as_table(rows)
return

if args.field:
Expand Down
30 changes: 30 additions & 0 deletions airflow-core/tests/unit/cli/commands/test_dag_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,36 @@ def test_next_execution(self, dag_id, delta, schedule, catchup, first, second, t
clear_db_dags()
parse_and_sync_to_db(os.devnull, include_examples=True)

def test_next_execution_table_none_schedule(self, tmp_path, stdout_capture, capsys):
"""--table must not crash when schedule=None yields a None DagRunInfo."""
dag_id = "no_schedule_table_test"
file_content = os.linesep.join(
[
"from airflow import DAG",
"from airflow.providers.standard.operators.empty import EmptyOperator",
"from pendulum import today",
f"dag = DAG('{dag_id}', start_date=today(tz='UTC'), schedule=None)",
"task = EmptyOperator(task_id='empty_task', dag=dag)",
]
)
dag_file = tmp_path / f"{dag_id}.py"
dag_file.write_text(file_content)

with time_machine.travel(DEFAULT_DATE):
clear_db_dags()
parse_and_sync_to_db(tmp_path, include_examples=False)

args = self.parser.parse_args(["dags", "next-execution", dag_id, "--table"])
# Must not raise AttributeError when DagRunInfo is None
dag_command.dag_next_execution(args)

captured = capsys.readouterr()
assert "No following schedule can be found" in captured.err

# Rebuild Test DB for other tests
clear_db_dags()
parse_and_sync_to_db(os.devnull, include_examples=True)

@conf_vars({("core", "load_examples"): "true"})
def test_cli_report(self, stdout_capture):
args = self.parser.parse_args(["dags", "report", "--output", "json"])
Expand Down
Loading