Skip to content

Commit dd9df35

Browse files
authored
Add back missing [sources] link in generated documentation's includes (apache#49978)
* Add back missing `[sources]` link in generated documentation's includes The exampleinclude of ours used to have generated "sources" link to link to sources of the examples. This was however gone after recent doc build refactoring. The problem was the example directive include import and lack of tests folder available on the python path. Both issues fixed in this PR. Fixes: apache#49893 * fixup! Add back missing `[sources]` link in generated documentation's includes
1 parent 5f4bcc1 commit dd9df35

File tree

55 files changed

+341
-97
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+341
-97
lines changed

Dockerfile.ci

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,12 @@ function environment_initialization() {
949949

950950
cd "${AIRFLOW_SOURCES}"
951951

952+
# Temporarily add /opt/airflow/providers/standard/tests to PYTHONPATH in order to see example dags
953+
# in the UI when testing in Breeze. This might be solved differently in the future
954+
if [[ -d /opt/airflow/providers/standard/tests ]]; then
955+
export PYTHONPATH=${PYTHONPATH=}:/opt/airflow/providers/standard/tests
956+
fi
957+
952958
if [[ ${START_AIRFLOW:="false"} == "true" || ${START_AIRFLOW} == "True" ]]; then
953959
export AIRFLOW__CORE__LOAD_EXAMPLES=${LOAD_EXAMPLES}
954960
wait_for_asset_compilation

airflow-core/docs/tutorial/taskflow.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ system-level packages. TaskFlow supports multiple execution environments to isol
269269
Creates a temporary virtualenv at task runtime. Great for experimental or dynamic tasks, but may have cold start
270270
overhead.
271271

272-
.. exampleinclude:: /../src/airflow/example_dags/example_python_decorator.py
272+
.. exampleinclude:: /../../providers/standard/tests/system/standard/example_python_decorator.py
273273
:language: python
274274
:dedent: 4
275275
:start-after: [START howto_operator_python_venv]
@@ -283,7 +283,7 @@ overhead.
283283

284284
Executes the task using a pre-installed Python interpreter — ideal for consistent environments or shared virtualenvs.
285285

286-
.. exampleinclude:: /../src/airflow/example_dags/example_python_decorator.py
286+
.. exampleinclude:: /../../providers/standard/tests/system/standard/example_python_decorator.py
287287
:language: python
288288
:dedent: 4
289289
:start-after: [START howto_operator_external_python]
@@ -333,7 +333,7 @@ Using Sensors
333333
Use ``@task.sensor`` to build lightweight, reusable sensors using Python functions. These support both poke and reschedule
334334
modes.
335335

336-
.. exampleinclude:: /../src/airflow/example_dags/example_sensor_decorator.py
336+
.. exampleinclude:: /../../providers/standard/tests/system/standard/example_sensor_decorator.py
337337
:language: python
338338
:start-after: [START tutorial]
339339
:end-before: [END tutorial]

airflow-core/src/airflow/dag_processing/bundles/manager.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
# under the License.
1717
from __future__ import annotations
1818

19+
import os
1920
from typing import TYPE_CHECKING
2021

2122
from airflow.configuration import conf
@@ -34,6 +35,7 @@
3435
from airflow.dag_processing.bundles.base import BaseDagBundle
3536

3637
_example_dag_bundle_name = "example_dags"
38+
_example_standard_dag_bundle_name = "example_standard_dags"
3739

3840

3941
def _bundle_item_exc(msg):
@@ -80,6 +82,25 @@ def _add_example_dag_bundle(config_list):
8082
)
8183

8284

85+
def _add_example_standard_dag_bundle(config_list):
86+
# TODO(potiuk): make it more generic - for now we only add standard example_dags if they are locally available
87+
try:
88+
from system import standard
89+
except ImportError:
90+
return
91+
92+
example_dag_folder = next(iter(standard.__path__))
93+
config_list.append(
94+
{
95+
"name": _example_standard_dag_bundle_name,
96+
"classpath": "airflow.dag_processing.bundles.local.LocalDagBundle",
97+
"kwargs": {
98+
"path": example_dag_folder,
99+
},
100+
}
101+
)
102+
103+
83104
class DagBundlesManager(LoggingMixin):
84105
"""Manager for DAG bundles."""
85106

@@ -112,6 +133,11 @@ def parse_config(self) -> None:
112133
_validate_bundle_config(config_list)
113134
if conf.getboolean("core", "LOAD_EXAMPLES"):
114135
_add_example_dag_bundle(config_list)
136+
if (
137+
os.environ.get("BREEZE", "").lower() == "true"
138+
or os.environ.get("_IN_UNIT_TESTS", "").lower() == "true"
139+
):
140+
_add_example_standard_dag_bundle(config_list)
115141

116142
for cfg in config_list:
117143
name = cfg["name"]

airflow-core/src/airflow/models/dagbag.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,14 @@ def collect_dags(
586586
example_dag_folder = next(iter(example_dags.__path__))
587587

588588
files_to_parse.extend(list_py_file_paths(example_dag_folder, safe_mode=safe_mode))
589+
try:
590+
from system import standard
591+
592+
example_dag_folder_standard = next(iter(standard.__path__))
593+
files_to_parse.extend(list_py_file_paths(example_dag_folder_standard, safe_mode=safe_mode))
594+
except ImportError:
595+
# Nothing happens - this should only work during tests
596+
pass
589597

590598
for filepath in files_to_parse:
591599
try:

airflow-core/src/airflow/utils/cli.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,6 @@ def get_dag(bundle_names: list | None, dag_id: str, from_db: bool = False) -> DA
270270

271271
bundle_names = bundle_names or []
272272
dag: DAG | None = None
273-
274273
if from_db:
275274
dagbag = DagBag(read_dags_from_db=True)
276275
dag = dagbag.get_dag(dag_id) # get_dag loads from the DB as requested

airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_parsing.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828

2929
pytestmark = pytest.mark.db_test
3030

31-
EXAMPLE_DAG_FILE = AIRFLOW_CORE_SOURCES_PATH / "airflow" / "example_dags" / "example_bash_operator.py"
32-
TEST_DAG_ID = "example_bash_operator"
31+
EXAMPLE_DAG_FILE = AIRFLOW_CORE_SOURCES_PATH / "airflow" / "example_dags" / "example_simplest_dag.py"
32+
TEST_DAG_ID = "example_simplest_dag"
3333
NOT_READABLE_DAG_ID = "latest_only_with_trigger"
3434
TEST_MULTIPLE_DAGS_ID = "asset_produces_1"
3535

airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_report.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
from airflow.utils.file import list_py_file_paths
2525

26+
import system.standard
2627
from tests_common.test_utils.config import conf_vars
2728
from tests_common.test_utils.db import clear_db_dags, parse_and_sync_to_db
2829

@@ -37,8 +38,10 @@
3738
def get_corresponding_dag_file_count(dir: str, include_examples: bool = True) -> int:
3839
from airflow import example_dags
3940

40-
return len(list_py_file_paths(directory=dir)) + (
41-
len(list_py_file_paths(next(iter(example_dags.__path__)))) if include_examples else 0
41+
return (
42+
len(list_py_file_paths(directory=dir))
43+
+ (len(list_py_file_paths(next(iter(example_dags.__path__)))) if include_examples else 0)
44+
+ (len(list_py_file_paths(next(iter(system.standard.__path__)))) if include_examples else 0)
4245
)
4346

4447

airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_sources.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@
3939

4040
# Example bash operator located here: airflow/example_dags/example_bash_operator.py
4141
EXAMPLE_DAG_FILE = (
42-
AIRFLOW_REPO_ROOT_PATH / "airflow-core" / "src" / "airflow" / "example_dags" / "example_bash_operator.py"
42+
AIRFLOW_REPO_ROOT_PATH / "airflow-core" / "src" / "airflow" / "example_dags" / "example_simplest_dag.py"
4343
)
44-
TEST_DAG_ID = "example_bash_operator"
44+
TEST_DAG_ID = "example_simplest_dag"
4545

4646

4747
@pytest.fixture

airflow-core/tests/unit/cli/commands/test_task_command.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
from airflow.utils.state import State, TaskInstanceState
4848
from airflow.utils.types import DagRunTriggeredByType, DagRunType
4949

50+
from tests_common.test_utils.config import conf_vars
5051
from tests_common.test_utils.db import clear_db_runs, parse_and_sync_to_db
5152

5253
pytestmark = pytest.mark.db_test
@@ -74,7 +75,6 @@ def move_back(old_path, new_path):
7475
shutil.move(new_path, old_path)
7576

7677

77-
# TODO: Check if tests needs side effects - locally there's missing DAG
7878
class TestCliTasks:
7979
run_id = "TEST_RUN_ID"
8080
dag_id = "example_python_operator"
@@ -91,7 +91,7 @@ def setup_class(cls):
9191
cls.parser = cli_parser.get_parser()
9292
clear_db_runs()
9393

94-
cls.dagbag = DagBag(read_dags_from_db=True)
94+
cls.dagbag = DagBag(read_dags_from_db=True, include_examples=True)
9595
cls.dag = cls.dagbag.get_dag(cls.dag_id)
9696
data_interval = cls.dag.timetable.infer_manual_data_interval(run_after=DEFAULT_DATE)
9797
cls.dag_run = cls.dag.create_dagrun(
@@ -108,6 +108,7 @@ def setup_class(cls):
108108
def teardown_class(cls) -> None:
109109
clear_db_runs()
110110

111+
@conf_vars({("core", "load_examples"): "true"})
111112
@pytest.mark.execution_timeout(120)
112113
def test_cli_list_tasks(self):
113114
for dag_id in self.dagbag.dags:

airflow-core/tests/unit/jobs/test_scheduler_job.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
from sqlalchemy import func, select, update
3838
from sqlalchemy.orm import joinedload
3939

40-
import airflow.example_dags
4140
from airflow import settings
4241
from airflow.assets.manager import AssetManager
4342
from airflow.callbacks.callback_requests import DagCallbackRequest, TaskCallbackRequest
@@ -75,6 +74,7 @@
7574
from airflow.utils.thread_safe_dict import ThreadSafeDict
7675
from airflow.utils.types import DagRunTriggeredByType, DagRunType
7776

77+
from system import standard
7878
from tests_common.test_utils.asserts import assert_queries_count
7979
from tests_common.test_utils.config import conf_vars, env_vars
8080
from tests_common.test_utils.db import (
@@ -104,7 +104,7 @@
104104
ELASTIC_DAG_FILE = os.path.join(PERF_DAGS_FOLDER, "elastic_dag.py")
105105

106106
TEST_DAG_FOLDER = os.environ["AIRFLOW__CORE__DAGS_FOLDER"]
107-
EXAMPLE_DAGS_FOLDER = airflow.example_dags.__path__[0]
107+
EXAMPLE_STANDARD_DAGS_FOLDER = standard.__path__[0]
108108
DEFAULT_DATE = timezone.datetime(2016, 1, 1)
109109
DEFAULT_LOGICAL_DATE = timezone.coerce_datetime(DEFAULT_DATE)
110110
TRY_NUMBER = 1
@@ -5707,7 +5707,7 @@ def test_find_and_purge_task_instances_without_heartbeats_nothing(self):
57075707

57085708
@pytest.mark.usefixtures("testing_dag_bundle")
57095709
def test_find_and_purge_task_instances_without_heartbeats(self, session, create_dagrun):
5710-
dagfile = os.path.join(EXAMPLE_DAGS_FOLDER, "example_branch_operator.py")
5710+
dagfile = os.path.join(EXAMPLE_STANDARD_DAGS_FOLDER, "example_branch_operator.py")
57115711
dagbag = DagBag(dagfile)
57125712
dag = dagbag.get_dag("example_branch_operator")
57135713
dm = LazyDeserializedDAG(data=SerializedDAG.to_dict(dag))
@@ -5773,7 +5773,7 @@ def test_task_instance_heartbeat_timeout_message(self, session, create_dagrun):
57735773
"""
57745774
Check that the task instance heartbeat timeout message comes out as expected
57755775
"""
5776-
dagfile = os.path.join(EXAMPLE_DAGS_FOLDER, "example_branch_operator.py")
5776+
dagfile = os.path.join(EXAMPLE_STANDARD_DAGS_FOLDER, "example_branch_operator.py")
57775777
dagbag = DagBag(dagfile)
57785778
dag = dagbag.get_dag("example_branch_operator")
57795779
dm = LazyDeserializedDAG(data=SerializedDAG.to_dict(dag))

airflow-core/tests/unit/models/test_dagbag.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from airflow.serialization.serialized_objects import SerializedDAG
4343
from airflow.utils import timezone as tz
4444
from airflow.utils.session import create_session
45+
from scripts.ci.pre_commit.common_precommit_utils import AIRFLOW_ROOT_PATH
4546

4647
from tests_common.test_utils import db
4748
from tests_common.test_utils.asserts import assert_queries_count
@@ -52,6 +53,12 @@
5253
pytestmark = pytest.mark.db_test
5354

5455
example_dags_folder = pathlib.Path(airflow.example_dags.__path__[0]) # type: ignore[attr-defined]
56+
try:
57+
import system.standard
58+
59+
example_standard_dags_folder = pathlib.Path(system.standard.__path__[0]) # type: ignore[attr-defined]
60+
except ImportError:
61+
example_standard_dags_folder = pathlib.Path(airflow.example_dags.__path__[0]) # type: ignore[attr-defined]
5562

5663
PY311 = sys.version_info >= (3, 11)
5764

@@ -359,13 +366,16 @@ def process_file(self, filepath, only_if_updated=True, safe_mode=True):
359366
("file_to_load", "expected"),
360367
(
361368
pytest.param(
362-
pathlib.Path(example_dags_folder) / "example_bash_operator.py",
363-
{"example_bash_operator": "airflow/example_dags/example_bash_operator.py"},
369+
pathlib.Path(example_standard_dags_folder) / "example_bash_operator.py",
370+
{
371+
"example_bash_operator": f"{example_standard_dags_folder.relative_to(AIRFLOW_ROOT_PATH) / 'example_bash_operator.py'}"
372+
},
364373
id="example_bash_operator",
365374
),
366375
),
367376
)
368377
def test_get_dag_registration(self, file_to_load, expected):
378+
pytest.importorskip("system.standard")
369379
dagbag = DagBag(dag_folder=os.devnull, include_examples=False)
370380
dagbag.process_file(os.fspath(file_to_load))
371381
for dag_id, path in expected.items():
@@ -420,7 +430,7 @@ def test_refresh_py_dag(self, mock_dagmodel, tmp_path):
420430
Test that we can refresh an ordinary .py DAG
421431
"""
422432
dag_id = "example_bash_operator"
423-
fileloc = str(example_dags_folder / "example_bash_operator.py")
433+
fileloc = str(example_standard_dags_folder / "example_bash_operator.py")
424434

425435
mock_dagmodel.return_value = DagModel()
426436
mock_dagmodel.return_value.last_expired = datetime.max.replace(tzinfo=timezone.utc)

clients/python/test_python_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969

7070
# Make sure in the [core] section, the `load_examples` config is set to True in your airflow.cfg
7171
# or AIRFLOW__CORE__LOAD_EXAMPLES environment variable set to True
72-
DAG_ID = "example_bash_operator"
72+
DAG_ID = "example_simplest_dag"
7373

7474

7575
# Enter a context with an instance of the API client

devel-common/src/docs/provider_conf.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -156,15 +156,8 @@
156156
"operators/_partials",
157157
"_api/airflow/index.rst",
158158
"_api/airflow/providers/index.rst",
159-
"_api/airflow/providers/apache/index.rst",
160-
"_api/airflow/providers/atlassian/index.rst",
161-
"_api/airflow/providers/cncf/index.rst",
162-
"_api/airflow/providers/common/index.rst",
163-
"_api/airflow/providers/common/messaging/providers/base_provider/index.rst",
164-
"_api/airflow/providers/common/messaging/providers/sqs/index.rst",
165-
"_api/airflow/providers/dbt/index.rst",
166-
"_api/airflow/providers/microsoft/index.rst",
167159
"_api/docs/conf",
160+
*[f"_api/airflow/providers/{subpackage}/index.rst" for subpackage in empty_subpackages],
168161
*[f"_api/system/{subpackage}/index.rst" for subpackage in empty_subpackages],
169162
*[f"_api/tests/system/{subpackage}/index.rst" for subpackage in empty_subpackages],
170163
]

devel-common/src/sphinx_exts/docs_build/docs_builder.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,13 @@ def _src_dir(self) -> Path:
120120
console.print(f"[red]Unknown package name: {self.package_name}")
121121
sys.exit(1)
122122

123+
@property
124+
def pythonpath(self) -> list[Path]:
125+
path = []
126+
if (self._src_dir.parent / "tests").exists():
127+
path.append(self._src_dir.parent.joinpath("tests").resolve())
128+
return path
129+
123130
@property
124131
def _generated_api_dir(self) -> Path:
125132
return self._build_dir.resolve() / "_api"
@@ -167,6 +174,8 @@ def check_spelling(self, verbose: bool) -> tuple[list[SpellingError], list[DocBu
167174
console.print("[yellow]Command to run:[/] ", " ".join([shlex.quote(arg) for arg in build_cmd]))
168175
env = os.environ.copy()
169176
env["AIRFLOW_PACKAGE_NAME"] = self.package_name
177+
if self.pythonpath:
178+
env["PYTHONPATH"] = ":".join([path.as_posix() for path in self.pythonpath])
170179
if verbose:
171180
console.print(
172181
f"[bright_blue]{self.package_name:60}:[/] The output is hidden until an error occurs."
@@ -246,6 +255,8 @@ def build_sphinx_docs(self, verbose: bool) -> list[DocBuildError]:
246255
console.print("[yellow]Command to run:[/] ", " ".join([shlex.quote(arg) for arg in build_cmd]))
247256
env = os.environ.copy()
248257
env["AIRFLOW_PACKAGE_NAME"] = self.package_name
258+
if self.pythonpath:
259+
env["PYTHONPATH"] = ":".join([path.as_posix() for path in self.pythonpath])
249260
if verbose:
250261
console.print(
251262
f"[bright_blue]{self.package_name:60}:[/] Running sphinx. "

0 commit comments

Comments
 (0)