Skip to content
Merged
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
7 changes: 7 additions & 0 deletions airflow-core/docs/howto/deadline-alerts.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -420,6 +420,10 @@ references for specific integrations like calendars or other data sources. To do
a class that inherits from BaseDeadlineReference, add the ``@deadline_reference`` decorator, and
implement an ``_evaluate_with()`` method.

The decorator may be used with or without parentheses. Used bare, or with empty parentheses, the
reference is evaluated when a new Dag run is created; pass a ``DeadlineReference.TYPES`` value to
choose a different time.


**Creating a Custom Reference**

Expand DownExpand Up@@ -520,6 +524,9 @@ followed by a more urgent escalation if the Dag is still running.
**Important Notes:**

* **Timezone Awareness**: Always return timezone-aware datetime objects.
* **No-argument Construction**: Custom references are instantiated during registration, so they must be
constructible with no arguments. If your reference takes parameters, decorate it with ``@dataclass``
and give every field a default value.
* **Plugin Placement**: One convenient place for custom references is in the plugins directory.
* **API Server Restart**: Restart the Airflow API Server after adding or modifying custom references.
* **Required Parameters**: Use ``required_kwargs`` to specify parameters your reference needs.
Expand Down
1 change: 1 addition & 0 deletions airflow-core/newsfragments/70708.bugfix.rst
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
The ``@deadline_reference`` decorator can now be used without parentheses. Previously, using it that way silently skipped registration and rebound the decorated class to the decorator's inner function, which surfaced later as an unrelated ``TypeError``.
Comment thread
ferruzzi marked this conversation as resolved.
39 changes: 39 additions & 0 deletions airflow-core/tests/unit/models/test_deadline.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,7 @@
from __future__ import annotations

import re
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import TYPE_CHECKING
from unittest import mock
Expand DownExpand Up@@ -913,6 +914,44 @@ def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:

mock_register.assert_called_once_with(DecoratedCustomRef, timing)

def test_deadline_reference_decorator_without_parentheses(self):
@deadline_reference
class BareDecoratedRef(BaseDeadlineReference):
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
return timezone.datetime(DEFAULT_DATE)

# The decorated name must still be the class, not the inner decorator function.
assert isinstance(BareDecoratedRef, type)
assert issubclass(BareDecoratedRef, BaseDeadlineReference)

assert hasattr(DeadlineReference, BareDecoratedRef.__name__)
assert getattr(DeadlineReference, BareDecoratedRef.__name__).__class__ is BareDecoratedRef

assert_correct_timing(BareDecoratedRef, DeadlineReference.TYPES.DAGRUN_CREATED)
assert_builtin_types_unchanged(
DeadlineReference.TYPES.DAGRUN_QUEUED, DeadlineReference.TYPES.DAGRUN_CREATED
)

def test_deadline_reference_decorator_without_parentheses_invalid_class(self):
"""Test that the bare form must inherit the base class."""
with pytest.raises(ValueError, match="InvalidBareRef must inherit from BaseDeadlineReference"):

@deadline_reference
class InvalidBareRef:
pass

def test_deadline_reference_requiring_arguments_raises_helpful_error(self):
"""Test that a reference which cannot be instantiated with no arguments explains itself."""
with pytest.raises(TypeError, match="must be constructible with no arguments"):

@deadline_reference()
@dataclass
class RefWithRequiredField(BaseDeadlineReference):
required_field: str

def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
return timezone.datetime(DEFAULT_DATE)


@pytest.mark.db_test
class TestDeadlineMetricsTeamName:
Expand Down
46 changes: 40 additions & 6 deletions task-sdk/src/airflow/sdk/definitions/deadline.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,7 @@
from abc import ABC
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, overload

import attrs

Expand DownExpand Up@@ -290,7 +290,15 @@ def register_custom_reference(
raise ValueError(f"{reference_class.__name__} must inherit from BaseDeadlineReference")

# Register the new reference with DeadlineReference for discoverability
setattr(cls, reference_class.__name__, reference_class())
try:
reference_instance = reference_class()
except TypeError as e:
raise TypeError(
f"{reference_class.__name__} must be constructible with no arguments in order to be "
f"registered as a deadline reference. If it takes parameters, decorate it with "
f"@dataclass and give every field a default value. Original error: {e}"
) from e
setattr(cls, reference_class.__name__, reference_instance)
logger.info("Registered DeadlineReference %s", reference_class.__name__)

# Add to appropriate deadline_reference_type classification
Expand All@@ -310,34 +318,60 @@ def register_custom_reference(
return reference_class


@overload
def deadline_reference(
deadline_reference_type: type[BaseDeadlineReference],
) -> type[BaseDeadlineReference]: ...


@overload
def deadline_reference(
deadline_reference_type: DeadlineReferenceTypes | None = None,
) -> Callable[[type[BaseDeadlineReference]], type[BaseDeadlineReference]]:
) -> Callable[[type[BaseDeadlineReference]], type[BaseDeadlineReference]]: ...


def deadline_reference(deadline_reference_type=None):
"""
Decorate a class to register a custom deadline reference.

Usage:
May be used with or without parentheses. Without parentheses the reference is evaluated when a
new dagrun is created; pass a ``DeadlineReference.TYPES`` value to choose a different time.

.. code-block:: python

@deadline_reference
class MyBareReference(BaseDeadlineReference):
# Equivalent to @deadline_reference(); evaluated when a new dagrun is created.
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
return some_datetime


@deadline_reference()
class MyCustomReference(BaseDeadlineReference):
# By default, evaluate_with will be called when a new dagrun is created.
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
# Put your business logic here (use deferred imports for Core types)
from airflow.models import DagRun

return some_datetime

def serialize_reference(self) -> dict:
return {"reference_type": self.reference_name}


# Optionally, specify when it is calculated by providing a DeadlineReference.TYPES value.
@deadline_reference(DeadlineReference.TYPES.DAGRUN_QUEUED)
class MyQueuedRef(BaseDeadlineReference):
# Optionally, you can specify when you want it calculated by providing a DeadlineReference.TYPES
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
# Put your business logic here
# Put your business logic here
return some_datetime

def serialize_reference(self) -> dict:
return {"reference_type": self.reference_name}
"""
# Used bare, without parentheses: the decorated class is passed in directly.
if isinstance(deadline_reference_type, type):
return DeadlineReference.register_custom_reference(deadline_reference_type)

def decorator(
reference_class: type[BaseDeadlineReference],
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Fix the deadline_reference decorator's no-parentheses form by ferruzzi · Pull Request #70708 · apache/airflow · GitHub
Skip to content
Merged
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
7 changes: 7 additions & 0 deletions airflow-core/docs/howto/deadline-alerts.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -420,6 +420,10 @@ references for specific integrations like calendars or other data sources. To do
a class that inherits from BaseDeadlineReference, add the ``@deadline_reference`` decorator, and
implement an ``_evaluate_with()`` method.

The decorator may be used with or without parentheses. Used bare, or with empty parentheses, the
reference is evaluated when a new Dag run is created; pass a ``DeadlineReference.TYPES`` value to
choose a different time.


**Creating a Custom Reference**

Expand DownExpand Up@@ -520,6 +524,9 @@ followed by a more urgent escalation if the Dag is still running.
**Important Notes:**

* **Timezone Awareness**: Always return timezone-aware datetime objects.
* **No-argument Construction**: Custom references are instantiated during registration, so they must be
constructible with no arguments. If your reference takes parameters, decorate it with ``@dataclass``
and give every field a default value.
* **Plugin Placement**: One convenient place for custom references is in the plugins directory.
* **API Server Restart**: Restart the Airflow API Server after adding or modifying custom references.
* **Required Parameters**: Use ``required_kwargs`` to specify parameters your reference needs.
Expand Down
1 change: 1 addition & 0 deletions airflow-core/newsfragments/70708.bugfix.rst
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
The ``@deadline_reference`` decorator can now be used without parentheses. Previously, using it that way silently skipped registration and rebound the decorated class to the decorator's inner function, which surfaced later as an unrelated ``TypeError``.
Comment thread
ferruzzi marked this conversation as resolved.
39 changes: 39 additions & 0 deletions airflow-core/tests/unit/models/test_deadline.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,7 @@
from __future__ import annotations

import re
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import TYPE_CHECKING
from unittest import mock
Expand DownExpand Up@@ -913,6 +914,44 @@ def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:

mock_register.assert_called_once_with(DecoratedCustomRef, timing)

def test_deadline_reference_decorator_without_parentheses(self):
@deadline_reference
class BareDecoratedRef(BaseDeadlineReference):
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
return timezone.datetime(DEFAULT_DATE)

# The decorated name must still be the class, not the inner decorator function.
assert isinstance(BareDecoratedRef, type)
assert issubclass(BareDecoratedRef, BaseDeadlineReference)

assert hasattr(DeadlineReference, BareDecoratedRef.__name__)
assert getattr(DeadlineReference, BareDecoratedRef.__name__).__class__ is BareDecoratedRef

assert_correct_timing(BareDecoratedRef, DeadlineReference.TYPES.DAGRUN_CREATED)
assert_builtin_types_unchanged(
DeadlineReference.TYPES.DAGRUN_QUEUED, DeadlineReference.TYPES.DAGRUN_CREATED
)

def test_deadline_reference_decorator_without_parentheses_invalid_class(self):
"""Test that the bare form must inherit the base class."""
with pytest.raises(ValueError, match="InvalidBareRef must inherit from BaseDeadlineReference"):

@deadline_reference
class InvalidBareRef:
pass

def test_deadline_reference_requiring_arguments_raises_helpful_error(self):
"""Test that a reference which cannot be instantiated with no arguments explains itself."""
with pytest.raises(TypeError, match="must be constructible with no arguments"):

@deadline_reference()
@dataclass
class RefWithRequiredField(BaseDeadlineReference):
required_field: str

def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
return timezone.datetime(DEFAULT_DATE)


@pytest.mark.db_test
class TestDeadlineMetricsTeamName:
Expand Down
46 changes: 40 additions & 6 deletions task-sdk/src/airflow/sdk/definitions/deadline.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,7 @@
from abc import ABC
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, overload

import attrs

Expand DownExpand Up@@ -290,7 +290,15 @@ def register_custom_reference(
raise ValueError(f"{reference_class.__name__} must inherit from BaseDeadlineReference")

# Register the new reference with DeadlineReference for discoverability
setattr(cls, reference_class.__name__, reference_class())
try:
reference_instance = reference_class()
except TypeError as e:
raise TypeError(
f"{reference_class.__name__} must be constructible with no arguments in order to be "
f"registered as a deadline reference. If it takes parameters, decorate it with "
f"@dataclass and give every field a default value. Original error: {e}"
) from e
setattr(cls, reference_class.__name__, reference_instance)
logger.info("Registered DeadlineReference %s", reference_class.__name__)

# Add to appropriate deadline_reference_type classification
Expand All@@ -310,34 +318,60 @@ def register_custom_reference(
return reference_class


@overload
def deadline_reference(
deadline_reference_type: type[BaseDeadlineReference],
) -> type[BaseDeadlineReference]: ...


@overload
def deadline_reference(
deadline_reference_type: DeadlineReferenceTypes | None = None,
) -> Callable[[type[BaseDeadlineReference]], type[BaseDeadlineReference]]:
) -> Callable[[type[BaseDeadlineReference]], type[BaseDeadlineReference]]: ...


def deadline_reference(deadline_reference_type=None):
"""
Decorate a class to register a custom deadline reference.

Usage:
May be used with or without parentheses. Without parentheses the reference is evaluated when a
new dagrun is created; pass a ``DeadlineReference.TYPES`` value to choose a different time.

.. code-block:: python

@deadline_reference
class MyBareReference(BaseDeadlineReference):
# Equivalent to @deadline_reference(); evaluated when a new dagrun is created.
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
return some_datetime


@deadline_reference()
class MyCustomReference(BaseDeadlineReference):
# By default, evaluate_with will be called when a new dagrun is created.
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
# Put your business logic here (use deferred imports for Core types)
from airflow.models import DagRun

return some_datetime

def serialize_reference(self) -> dict:
return {"reference_type": self.reference_name}


# Optionally, specify when it is calculated by providing a DeadlineReference.TYPES value.
@deadline_reference(DeadlineReference.TYPES.DAGRUN_QUEUED)
class MyQueuedRef(BaseDeadlineReference):
# Optionally, you can specify when you want it calculated by providing a DeadlineReference.TYPES
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
# Put your business logic here
# Put your business logic here
return some_datetime

def serialize_reference(self) -> dict:
return {"reference_type": self.reference_name}
"""
# Used bare, without parentheses: the decorated class is passed in directly.
if isinstance(deadline_reference_type, type):
return DeadlineReference.register_custom_reference(deadline_reference_type)

def decorator(
reference_class: type[BaseDeadlineReference],
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Fix the deadline_reference decorator's no-parentheses form by ferruzzi · Pull Request #70708 · apache/airflow · GitHub
Skip to content
Merged
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
7 changes: 7 additions & 0 deletions airflow-core/docs/howto/deadline-alerts.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -420,6 +420,10 @@ references for specific integrations like calendars or other data sources. To do
a class that inherits from BaseDeadlineReference, add the ``@deadline_reference`` decorator, and
implement an ``_evaluate_with()`` method.

The decorator may be used with or without parentheses. Used bare, or with empty parentheses, the
reference is evaluated when a new Dag run is created; pass a ``DeadlineReference.TYPES`` value to
choose a different time.


**Creating a Custom Reference**

Expand DownExpand Up@@ -520,6 +524,9 @@ followed by a more urgent escalation if the Dag is still running.
**Important Notes:**

* **Timezone Awareness**: Always return timezone-aware datetime objects.
* **No-argument Construction**: Custom references are instantiated during registration, so they must be
constructible with no arguments. If your reference takes parameters, decorate it with ``@dataclass``
and give every field a default value.
* **Plugin Placement**: One convenient place for custom references is in the plugins directory.
* **API Server Restart**: Restart the Airflow API Server after adding or modifying custom references.
* **Required Parameters**: Use ``required_kwargs`` to specify parameters your reference needs.
Expand Down
1 change: 1 addition & 0 deletions airflow-core/newsfragments/70708.bugfix.rst
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
The ``@deadline_reference`` decorator can now be used without parentheses. Previously, using it that way silently skipped registration and rebound the decorated class to the decorator's inner function, which surfaced later as an unrelated ``TypeError``.
Comment thread
ferruzzi marked this conversation as resolved.
39 changes: 39 additions & 0 deletions airflow-core/tests/unit/models/test_deadline.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,7 @@
from __future__ import annotations

import re
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import TYPE_CHECKING
from unittest import mock
Expand DownExpand Up@@ -913,6 +914,44 @@ def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:

mock_register.assert_called_once_with(DecoratedCustomRef, timing)

def test_deadline_reference_decorator_without_parentheses(self):
@deadline_reference
class BareDecoratedRef(BaseDeadlineReference):
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
return timezone.datetime(DEFAULT_DATE)

# The decorated name must still be the class, not the inner decorator function.
assert isinstance(BareDecoratedRef, type)
assert issubclass(BareDecoratedRef, BaseDeadlineReference)

assert hasattr(DeadlineReference, BareDecoratedRef.__name__)
assert getattr(DeadlineReference, BareDecoratedRef.__name__).__class__ is BareDecoratedRef

assert_correct_timing(BareDecoratedRef, DeadlineReference.TYPES.DAGRUN_CREATED)
assert_builtin_types_unchanged(
DeadlineReference.TYPES.DAGRUN_QUEUED, DeadlineReference.TYPES.DAGRUN_CREATED
)

def test_deadline_reference_decorator_without_parentheses_invalid_class(self):
"""Test that the bare form must inherit the base class."""
with pytest.raises(ValueError, match="InvalidBareRef must inherit from BaseDeadlineReference"):

@deadline_reference
class InvalidBareRef:
pass

def test_deadline_reference_requiring_arguments_raises_helpful_error(self):
"""Test that a reference which cannot be instantiated with no arguments explains itself."""
with pytest.raises(TypeError, match="must be constructible with no arguments"):

@deadline_reference()
@dataclass
class RefWithRequiredField(BaseDeadlineReference):
required_field: str

def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
return timezone.datetime(DEFAULT_DATE)


@pytest.mark.db_test
class TestDeadlineMetricsTeamName:
Expand Down
46 changes: 40 additions & 6 deletions task-sdk/src/airflow/sdk/definitions/deadline.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,7 @@
from abc import ABC
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, overload

import attrs

Expand DownExpand Up@@ -290,7 +290,15 @@ def register_custom_reference(
raise ValueError(f"{reference_class.__name__} must inherit from BaseDeadlineReference")

# Register the new reference with DeadlineReference for discoverability
setattr(cls, reference_class.__name__, reference_class())
try:
reference_instance = reference_class()
except TypeError as e:
raise TypeError(
f"{reference_class.__name__} must be constructible with no arguments in order to be "
f"registered as a deadline reference. If it takes parameters, decorate it with "
f"@dataclass and give every field a default value. Original error: {e}"
) from e
setattr(cls, reference_class.__name__, reference_instance)
logger.info("Registered DeadlineReference %s", reference_class.__name__)

# Add to appropriate deadline_reference_type classification
Expand All@@ -310,34 +318,60 @@ def register_custom_reference(
return reference_class


@overload
def deadline_reference(
deadline_reference_type: type[BaseDeadlineReference],
) -> type[BaseDeadlineReference]: ...


@overload
def deadline_reference(
deadline_reference_type: DeadlineReferenceTypes | None = None,
) -> Callable[[type[BaseDeadlineReference]], type[BaseDeadlineReference]]:
) -> Callable[[type[BaseDeadlineReference]], type[BaseDeadlineReference]]: ...


def deadline_reference(deadline_reference_type=None):
"""
Decorate a class to register a custom deadline reference.

Usage:
May be used with or without parentheses. Without parentheses the reference is evaluated when a
new dagrun is created; pass a ``DeadlineReference.TYPES`` value to choose a different time.

.. code-block:: python

@deadline_reference
class MyBareReference(BaseDeadlineReference):
# Equivalent to @deadline_reference(); evaluated when a new dagrun is created.
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
return some_datetime


@deadline_reference()
class MyCustomReference(BaseDeadlineReference):
# By default, evaluate_with will be called when a new dagrun is created.
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
# Put your business logic here (use deferred imports for Core types)
from airflow.models import DagRun

return some_datetime

def serialize_reference(self) -> dict:
return {"reference_type": self.reference_name}


# Optionally, specify when it is calculated by providing a DeadlineReference.TYPES value.
@deadline_reference(DeadlineReference.TYPES.DAGRUN_QUEUED)
class MyQueuedRef(BaseDeadlineReference):
# Optionally, you can specify when you want it calculated by providing a DeadlineReference.TYPES
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
# Put your business logic here
# Put your business logic here
return some_datetime

def serialize_reference(self) -> dict:
return {"reference_type": self.reference_name}
"""
# Used bare, without parentheses: the decorated class is passed in directly.
if isinstance(deadline_reference_type, type):
return DeadlineReference.register_custom_reference(deadline_reference_type)

def decorator(
reference_class: type[BaseDeadlineReference],
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Fix the deadline_reference decorator's no-parentheses form by ferruzzi · Pull Request #70708 · apache/airflow · GitHub
Skip to content
Merged
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
7 changes: 7 additions & 0 deletions airflow-core/docs/howto/deadline-alerts.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -420,6 +420,10 @@ references for specific integrations like calendars or other data sources. To do
a class that inherits from BaseDeadlineReference, add the ``@deadline_reference`` decorator, and
implement an ``_evaluate_with()`` method.

The decorator may be used with or without parentheses. Used bare, or with empty parentheses, the
reference is evaluated when a new Dag run is created; pass a ``DeadlineReference.TYPES`` value to
choose a different time.


**Creating a Custom Reference**

Expand DownExpand Up@@ -520,6 +524,9 @@ followed by a more urgent escalation if the Dag is still running.
**Important Notes:**

* **Timezone Awareness**: Always return timezone-aware datetime objects.
* **No-argument Construction**: Custom references are instantiated during registration, so they must be
constructible with no arguments. If your reference takes parameters, decorate it with ``@dataclass``
and give every field a default value.
* **Plugin Placement**: One convenient place for custom references is in the plugins directory.
* **API Server Restart**: Restart the Airflow API Server after adding or modifying custom references.
* **Required Parameters**: Use ``required_kwargs`` to specify parameters your reference needs.
Expand Down
1 change: 1 addition & 0 deletions airflow-core/newsfragments/70708.bugfix.rst
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
The ``@deadline_reference`` decorator can now be used without parentheses. Previously, using it that way silently skipped registration and rebound the decorated class to the decorator's inner function, which surfaced later as an unrelated ``TypeError``.
Comment thread
ferruzzi marked this conversation as resolved.
39 changes: 39 additions & 0 deletions airflow-core/tests/unit/models/test_deadline.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,7 @@
from __future__ import annotations

import re
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import TYPE_CHECKING
from unittest import mock
Expand DownExpand Up@@ -913,6 +914,44 @@ def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:

mock_register.assert_called_once_with(DecoratedCustomRef, timing)

def test_deadline_reference_decorator_without_parentheses(self):
@deadline_reference
class BareDecoratedRef(BaseDeadlineReference):
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
return timezone.datetime(DEFAULT_DATE)

# The decorated name must still be the class, not the inner decorator function.
assert isinstance(BareDecoratedRef, type)
assert issubclass(BareDecoratedRef, BaseDeadlineReference)

assert hasattr(DeadlineReference, BareDecoratedRef.__name__)
assert getattr(DeadlineReference, BareDecoratedRef.__name__).__class__ is BareDecoratedRef

assert_correct_timing(BareDecoratedRef, DeadlineReference.TYPES.DAGRUN_CREATED)
assert_builtin_types_unchanged(
DeadlineReference.TYPES.DAGRUN_QUEUED, DeadlineReference.TYPES.DAGRUN_CREATED
)

def test_deadline_reference_decorator_without_parentheses_invalid_class(self):
"""Test that the bare form must inherit the base class."""
with pytest.raises(ValueError, match="InvalidBareRef must inherit from BaseDeadlineReference"):

@deadline_reference
class InvalidBareRef:
pass

def test_deadline_reference_requiring_arguments_raises_helpful_error(self):
"""Test that a reference which cannot be instantiated with no arguments explains itself."""
with pytest.raises(TypeError, match="must be constructible with no arguments"):

@deadline_reference()
@dataclass
class RefWithRequiredField(BaseDeadlineReference):
required_field: str

def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
return timezone.datetime(DEFAULT_DATE)


@pytest.mark.db_test
class TestDeadlineMetricsTeamName:
Expand Down
46 changes: 40 additions & 6 deletions task-sdk/src/airflow/sdk/definitions/deadline.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,7 @@
from abc import ABC
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, overload

import attrs

Expand DownExpand Up@@ -290,7 +290,15 @@ def register_custom_reference(
raise ValueError(f"{reference_class.__name__} must inherit from BaseDeadlineReference")

# Register the new reference with DeadlineReference for discoverability
setattr(cls, reference_class.__name__, reference_class())
try:
reference_instance = reference_class()
except TypeError as e:
raise TypeError(
f"{reference_class.__name__} must be constructible with no arguments in order to be "
f"registered as a deadline reference. If it takes parameters, decorate it with "
f"@dataclass and give every field a default value. Original error: {e}"
) from e
setattr(cls, reference_class.__name__, reference_instance)
logger.info("Registered DeadlineReference %s", reference_class.__name__)

# Add to appropriate deadline_reference_type classification
Expand All@@ -310,34 +318,60 @@ def register_custom_reference(
return reference_class


@overload
def deadline_reference(
deadline_reference_type: type[BaseDeadlineReference],
) -> type[BaseDeadlineReference]: ...


@overload
def deadline_reference(
deadline_reference_type: DeadlineReferenceTypes | None = None,
) -> Callable[[type[BaseDeadlineReference]], type[BaseDeadlineReference]]:
) -> Callable[[type[BaseDeadlineReference]], type[BaseDeadlineReference]]: ...


def deadline_reference(deadline_reference_type=None):
"""
Decorate a class to register a custom deadline reference.

Usage:
May be used with or without parentheses. Without parentheses the reference is evaluated when a
new dagrun is created; pass a ``DeadlineReference.TYPES`` value to choose a different time.

.. code-block:: python

@deadline_reference
class MyBareReference(BaseDeadlineReference):
# Equivalent to @deadline_reference(); evaluated when a new dagrun is created.
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
return some_datetime


@deadline_reference()
class MyCustomReference(BaseDeadlineReference):
# By default, evaluate_with will be called when a new dagrun is created.
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
# Put your business logic here (use deferred imports for Core types)
from airflow.models import DagRun

return some_datetime

def serialize_reference(self) -> dict:
return {"reference_type": self.reference_name}


# Optionally, specify when it is calculated by providing a DeadlineReference.TYPES value.
@deadline_reference(DeadlineReference.TYPES.DAGRUN_QUEUED)
class MyQueuedRef(BaseDeadlineReference):
# Optionally, you can specify when you want it calculated by providing a DeadlineReference.TYPES
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
# Put your business logic here
# Put your business logic here
return some_datetime

def serialize_reference(self) -> dict:
return {"reference_type": self.reference_name}
"""
# Used bare, without parentheses: the decorated class is passed in directly.
if isinstance(deadline_reference_type, type):
return DeadlineReference.register_custom_reference(deadline_reference_type)

def decorator(
reference_class: type[BaseDeadlineReference],
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Fix the deadline_reference decorator's no-parentheses form by ferruzzi · Pull Request #70708 · apache/airflow · GitHub
Skip to content
Merged
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
7 changes: 7 additions & 0 deletions airflow-core/docs/howto/deadline-alerts.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -420,6 +420,10 @@ references for specific integrations like calendars or other data sources. To do
a class that inherits from BaseDeadlineReference, add the ``@deadline_reference`` decorator, and
implement an ``_evaluate_with()`` method.

The decorator may be used with or without parentheses. Used bare, or with empty parentheses, the
reference is evaluated when a new Dag run is created; pass a ``DeadlineReference.TYPES`` value to
choose a different time.


**Creating a Custom Reference**

Expand DownExpand Up@@ -520,6 +524,9 @@ followed by a more urgent escalation if the Dag is still running.
**Important Notes:**

* **Timezone Awareness**: Always return timezone-aware datetime objects.
* **No-argument Construction**: Custom references are instantiated during registration, so they must be
constructible with no arguments. If your reference takes parameters, decorate it with ``@dataclass``
and give every field a default value.
* **Plugin Placement**: One convenient place for custom references is in the plugins directory.
* **API Server Restart**: Restart the Airflow API Server after adding or modifying custom references.
* **Required Parameters**: Use ``required_kwargs`` to specify parameters your reference needs.
Expand Down
1 change: 1 addition & 0 deletions airflow-core/newsfragments/70708.bugfix.rst
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
The ``@deadline_reference`` decorator can now be used without parentheses. Previously, using it that way silently skipped registration and rebound the decorated class to the decorator's inner function, which surfaced later as an unrelated ``TypeError``.
Comment thread
ferruzzi marked this conversation as resolved.
39 changes: 39 additions & 0 deletions airflow-core/tests/unit/models/test_deadline.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,7 @@
from __future__ import annotations

import re
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import TYPE_CHECKING
from unittest import mock
Expand DownExpand Up@@ -913,6 +914,44 @@ def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:

mock_register.assert_called_once_with(DecoratedCustomRef, timing)

def test_deadline_reference_decorator_without_parentheses(self):
@deadline_reference
class BareDecoratedRef(BaseDeadlineReference):
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
return timezone.datetime(DEFAULT_DATE)

# The decorated name must still be the class, not the inner decorator function.
assert isinstance(BareDecoratedRef, type)
assert issubclass(BareDecoratedRef, BaseDeadlineReference)

assert hasattr(DeadlineReference, BareDecoratedRef.__name__)
assert getattr(DeadlineReference, BareDecoratedRef.__name__).__class__ is BareDecoratedRef

assert_correct_timing(BareDecoratedRef, DeadlineReference.TYPES.DAGRUN_CREATED)
assert_builtin_types_unchanged(
DeadlineReference.TYPES.DAGRUN_QUEUED, DeadlineReference.TYPES.DAGRUN_CREATED
)

def test_deadline_reference_decorator_without_parentheses_invalid_class(self):
"""Test that the bare form must inherit the base class."""
with pytest.raises(ValueError, match="InvalidBareRef must inherit from BaseDeadlineReference"):

@deadline_reference
class InvalidBareRef:
pass

def test_deadline_reference_requiring_arguments_raises_helpful_error(self):
"""Test that a reference which cannot be instantiated with no arguments explains itself."""
with pytest.raises(TypeError, match="must be constructible with no arguments"):

@deadline_reference()
@dataclass
class RefWithRequiredField(BaseDeadlineReference):
required_field: str

def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
return timezone.datetime(DEFAULT_DATE)


@pytest.mark.db_test
class TestDeadlineMetricsTeamName:
Expand Down
46 changes: 40 additions & 6 deletions task-sdk/src/airflow/sdk/definitions/deadline.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,7 @@
from abc import ABC
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, overload

import attrs

Expand DownExpand Up@@ -290,7 +290,15 @@ def register_custom_reference(
raise ValueError(f"{reference_class.__name__} must inherit from BaseDeadlineReference")

# Register the new reference with DeadlineReference for discoverability
setattr(cls, reference_class.__name__, reference_class())
try:
reference_instance = reference_class()
except TypeError as e:
raise TypeError(
f"{reference_class.__name__} must be constructible with no arguments in order to be "
f"registered as a deadline reference. If it takes parameters, decorate it with "
f"@dataclass and give every field a default value. Original error: {e}"
) from e
setattr(cls, reference_class.__name__, reference_instance)
logger.info("Registered DeadlineReference %s", reference_class.__name__)

# Add to appropriate deadline_reference_type classification
Expand All@@ -310,34 +318,60 @@ def register_custom_reference(
return reference_class


@overload
def deadline_reference(
deadline_reference_type: type[BaseDeadlineReference],
) -> type[BaseDeadlineReference]: ...


@overload
def deadline_reference(
deadline_reference_type: DeadlineReferenceTypes | None = None,
) -> Callable[[type[BaseDeadlineReference]], type[BaseDeadlineReference]]:
) -> Callable[[type[BaseDeadlineReference]], type[BaseDeadlineReference]]: ...


def deadline_reference(deadline_reference_type=None):
"""
Decorate a class to register a custom deadline reference.

Usage:
May be used with or without parentheses. Without parentheses the reference is evaluated when a
new dagrun is created; pass a ``DeadlineReference.TYPES`` value to choose a different time.

.. code-block:: python

@deadline_reference
class MyBareReference(BaseDeadlineReference):
# Equivalent to @deadline_reference(); evaluated when a new dagrun is created.
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
return some_datetime


@deadline_reference()
class MyCustomReference(BaseDeadlineReference):
# By default, evaluate_with will be called when a new dagrun is created.
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
# Put your business logic here (use deferred imports for Core types)
from airflow.models import DagRun

return some_datetime

def serialize_reference(self) -> dict:
return {"reference_type": self.reference_name}


# Optionally, specify when it is calculated by providing a DeadlineReference.TYPES value.
@deadline_reference(DeadlineReference.TYPES.DAGRUN_QUEUED)
class MyQueuedRef(BaseDeadlineReference):
# Optionally, you can specify when you want it calculated by providing a DeadlineReference.TYPES
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
# Put your business logic here
# Put your business logic here
return some_datetime

def serialize_reference(self) -> dict:
return {"reference_type": self.reference_name}
"""
# Used bare, without parentheses: the decorated class is passed in directly.
if isinstance(deadline_reference_type, type):
return DeadlineReference.register_custom_reference(deadline_reference_type)

def decorator(
reference_class: type[BaseDeadlineReference],
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Fix the deadline_reference decorator's no-parentheses form by ferruzzi · Pull Request #70708 · apache/airflow · GitHub
Skip to content
Merged
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
7 changes: 7 additions & 0 deletions airflow-core/docs/howto/deadline-alerts.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -420,6 +420,10 @@ references for specific integrations like calendars or other data sources. To do
a class that inherits from BaseDeadlineReference, add the ``@deadline_reference`` decorator, and
implement an ``_evaluate_with()`` method.

The decorator may be used with or without parentheses. Used bare, or with empty parentheses, the
reference is evaluated when a new Dag run is created; pass a ``DeadlineReference.TYPES`` value to
choose a different time.


**Creating a Custom Reference**

Expand DownExpand Up@@ -520,6 +524,9 @@ followed by a more urgent escalation if the Dag is still running.
**Important Notes:**

* **Timezone Awareness**: Always return timezone-aware datetime objects.
* **No-argument Construction**: Custom references are instantiated during registration, so they must be
constructible with no arguments. If your reference takes parameters, decorate it with ``@dataclass``
and give every field a default value.
* **Plugin Placement**: One convenient place for custom references is in the plugins directory.
* **API Server Restart**: Restart the Airflow API Server after adding or modifying custom references.
* **Required Parameters**: Use ``required_kwargs`` to specify parameters your reference needs.
Expand Down
1 change: 1 addition & 0 deletions airflow-core/newsfragments/70708.bugfix.rst
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
The ``@deadline_reference`` decorator can now be used without parentheses. Previously, using it that way silently skipped registration and rebound the decorated class to the decorator's inner function, which surfaced later as an unrelated ``TypeError``.
Comment thread
ferruzzi marked this conversation as resolved.
39 changes: 39 additions & 0 deletions airflow-core/tests/unit/models/test_deadline.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,7 @@
from __future__ import annotations

import re
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import TYPE_CHECKING
from unittest import mock
Expand DownExpand Up@@ -913,6 +914,44 @@ def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:

mock_register.assert_called_once_with(DecoratedCustomRef, timing)

def test_deadline_reference_decorator_without_parentheses(self):
@deadline_reference
class BareDecoratedRef(BaseDeadlineReference):
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
return timezone.datetime(DEFAULT_DATE)

# The decorated name must still be the class, not the inner decorator function.
assert isinstance(BareDecoratedRef, type)
assert issubclass(BareDecoratedRef, BaseDeadlineReference)

assert hasattr(DeadlineReference, BareDecoratedRef.__name__)
assert getattr(DeadlineReference, BareDecoratedRef.__name__).__class__ is BareDecoratedRef

assert_correct_timing(BareDecoratedRef, DeadlineReference.TYPES.DAGRUN_CREATED)
assert_builtin_types_unchanged(
DeadlineReference.TYPES.DAGRUN_QUEUED, DeadlineReference.TYPES.DAGRUN_CREATED
)

def test_deadline_reference_decorator_without_parentheses_invalid_class(self):
"""Test that the bare form must inherit the base class."""
with pytest.raises(ValueError, match="InvalidBareRef must inherit from BaseDeadlineReference"):

@deadline_reference
class InvalidBareRef:
pass

def test_deadline_reference_requiring_arguments_raises_helpful_error(self):
"""Test that a reference which cannot be instantiated with no arguments explains itself."""
with pytest.raises(TypeError, match="must be constructible with no arguments"):

@deadline_reference()
@dataclass
class RefWithRequiredField(BaseDeadlineReference):
required_field: str

def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
return timezone.datetime(DEFAULT_DATE)


@pytest.mark.db_test
class TestDeadlineMetricsTeamName:
Expand Down
46 changes: 40 additions & 6 deletions task-sdk/src/airflow/sdk/definitions/deadline.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,7 @@
from abc import ABC
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, overload

import attrs

Expand DownExpand Up@@ -290,7 +290,15 @@ def register_custom_reference(
raise ValueError(f"{reference_class.__name__} must inherit from BaseDeadlineReference")

# Register the new reference with DeadlineReference for discoverability
setattr(cls, reference_class.__name__, reference_class())
try:
reference_instance = reference_class()
except TypeError as e:
raise TypeError(
f"{reference_class.__name__} must be constructible with no arguments in order to be "
f"registered as a deadline reference. If it takes parameters, decorate it with "
f"@dataclass and give every field a default value. Original error: {e}"
) from e
setattr(cls, reference_class.__name__, reference_instance)
logger.info("Registered DeadlineReference %s", reference_class.__name__)

# Add to appropriate deadline_reference_type classification
Expand All@@ -310,34 +318,60 @@ def register_custom_reference(
return reference_class


@overload
def deadline_reference(
deadline_reference_type: type[BaseDeadlineReference],
) -> type[BaseDeadlineReference]: ...


@overload
def deadline_reference(
deadline_reference_type: DeadlineReferenceTypes | None = None,
) -> Callable[[type[BaseDeadlineReference]], type[BaseDeadlineReference]]:
) -> Callable[[type[BaseDeadlineReference]], type[BaseDeadlineReference]]: ...


def deadline_reference(deadline_reference_type=None):
"""
Decorate a class to register a custom deadline reference.

Usage:
May be used with or without parentheses. Without parentheses the reference is evaluated when a
new dagrun is created; pass a ``DeadlineReference.TYPES`` value to choose a different time.

.. code-block:: python

@deadline_reference
class MyBareReference(BaseDeadlineReference):
# Equivalent to @deadline_reference(); evaluated when a new dagrun is created.
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
return some_datetime


@deadline_reference()
class MyCustomReference(BaseDeadlineReference):
# By default, evaluate_with will be called when a new dagrun is created.
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
# Put your business logic here (use deferred imports for Core types)
from airflow.models import DagRun

return some_datetime

def serialize_reference(self) -> dict:
return {"reference_type": self.reference_name}


# Optionally, specify when it is calculated by providing a DeadlineReference.TYPES value.
@deadline_reference(DeadlineReference.TYPES.DAGRUN_QUEUED)
class MyQueuedRef(BaseDeadlineReference):
# Optionally, you can specify when you want it calculated by providing a DeadlineReference.TYPES
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
# Put your business logic here
# Put your business logic here
return some_datetime

def serialize_reference(self) -> dict:
return {"reference_type": self.reference_name}
"""
# Used bare, without parentheses: the decorated class is passed in directly.
if isinstance(deadline_reference_type, type):
return DeadlineReference.register_custom_reference(deadline_reference_type)

def decorator(
reference_class: type[BaseDeadlineReference],
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Fix the deadline_reference decorator's no-parentheses form by ferruzzi · Pull Request #70708 · apache/airflow · GitHub
Skip to content
Merged
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
7 changes: 7 additions & 0 deletions airflow-core/docs/howto/deadline-alerts.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -420,6 +420,10 @@ references for specific integrations like calendars or other data sources. To do
a class that inherits from BaseDeadlineReference, add the ``@deadline_reference`` decorator, and
implement an ``_evaluate_with()`` method.

The decorator may be used with or without parentheses. Used bare, or with empty parentheses, the
reference is evaluated when a new Dag run is created; pass a ``DeadlineReference.TYPES`` value to
choose a different time.


**Creating a Custom Reference**

Expand DownExpand Up@@ -520,6 +524,9 @@ followed by a more urgent escalation if the Dag is still running.
**Important Notes:**

* **Timezone Awareness**: Always return timezone-aware datetime objects.
* **No-argument Construction**: Custom references are instantiated during registration, so they must be
constructible with no arguments. If your reference takes parameters, decorate it with ``@dataclass``
and give every field a default value.
* **Plugin Placement**: One convenient place for custom references is in the plugins directory.
* **API Server Restart**: Restart the Airflow API Server after adding or modifying custom references.
* **Required Parameters**: Use ``required_kwargs`` to specify parameters your reference needs.
Expand Down
1 change: 1 addition & 0 deletions airflow-core/newsfragments/70708.bugfix.rst
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
The ``@deadline_reference`` decorator can now be used without parentheses. Previously, using it that way silently skipped registration and rebound the decorated class to the decorator's inner function, which surfaced later as an unrelated ``TypeError``.
Comment thread
ferruzzi marked this conversation as resolved.
39 changes: 39 additions & 0 deletions airflow-core/tests/unit/models/test_deadline.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,7 @@
from __future__ import annotations

import re
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import TYPE_CHECKING
from unittest import mock
Expand DownExpand Up@@ -913,6 +914,44 @@ def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:

mock_register.assert_called_once_with(DecoratedCustomRef, timing)

def test_deadline_reference_decorator_without_parentheses(self):
@deadline_reference
class BareDecoratedRef(BaseDeadlineReference):
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
return timezone.datetime(DEFAULT_DATE)

# The decorated name must still be the class, not the inner decorator function.
assert isinstance(BareDecoratedRef, type)
assert issubclass(BareDecoratedRef, BaseDeadlineReference)

assert hasattr(DeadlineReference, BareDecoratedRef.__name__)
assert getattr(DeadlineReference, BareDecoratedRef.__name__).__class__ is BareDecoratedRef

assert_correct_timing(BareDecoratedRef, DeadlineReference.TYPES.DAGRUN_CREATED)
assert_builtin_types_unchanged(
DeadlineReference.TYPES.DAGRUN_QUEUED, DeadlineReference.TYPES.DAGRUN_CREATED
)

def test_deadline_reference_decorator_without_parentheses_invalid_class(self):
"""Test that the bare form must inherit the base class."""
with pytest.raises(ValueError, match="InvalidBareRef must inherit from BaseDeadlineReference"):

@deadline_reference
class InvalidBareRef:
pass

def test_deadline_reference_requiring_arguments_raises_helpful_error(self):
"""Test that a reference which cannot be instantiated with no arguments explains itself."""
with pytest.raises(TypeError, match="must be constructible with no arguments"):

@deadline_reference()
@dataclass
class RefWithRequiredField(BaseDeadlineReference):
required_field: str

def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
return timezone.datetime(DEFAULT_DATE)


@pytest.mark.db_test
class TestDeadlineMetricsTeamName:
Expand Down
46 changes: 40 additions & 6 deletions task-sdk/src/airflow/sdk/definitions/deadline.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,7 @@
from abc import ABC
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, overload

import attrs

Expand DownExpand Up@@ -290,7 +290,15 @@ def register_custom_reference(
raise ValueError(f"{reference_class.__name__} must inherit from BaseDeadlineReference")

# Register the new reference with DeadlineReference for discoverability
setattr(cls, reference_class.__name__, reference_class())
try:
reference_instance = reference_class()
except TypeError as e:
raise TypeError(
f"{reference_class.__name__} must be constructible with no arguments in order to be "
f"registered as a deadline reference. If it takes parameters, decorate it with "
f"@dataclass and give every field a default value. Original error: {e}"
) from e
setattr(cls, reference_class.__name__, reference_instance)
logger.info("Registered DeadlineReference %s", reference_class.__name__)

# Add to appropriate deadline_reference_type classification
Expand All@@ -310,34 +318,60 @@ def register_custom_reference(
return reference_class


@overload
def deadline_reference(
deadline_reference_type: type[BaseDeadlineReference],
) -> type[BaseDeadlineReference]: ...


@overload
def deadline_reference(
deadline_reference_type: DeadlineReferenceTypes | None = None,
) -> Callable[[type[BaseDeadlineReference]], type[BaseDeadlineReference]]:
) -> Callable[[type[BaseDeadlineReference]], type[BaseDeadlineReference]]: ...


def deadline_reference(deadline_reference_type=None):
"""
Decorate a class to register a custom deadline reference.

Usage:
May be used with or without parentheses. Without parentheses the reference is evaluated when a
new dagrun is created; pass a ``DeadlineReference.TYPES`` value to choose a different time.

.. code-block:: python

@deadline_reference
class MyBareReference(BaseDeadlineReference):
# Equivalent to @deadline_reference(); evaluated when a new dagrun is created.
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
return some_datetime


@deadline_reference()
class MyCustomReference(BaseDeadlineReference):
# By default, evaluate_with will be called when a new dagrun is created.
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
# Put your business logic here (use deferred imports for Core types)
from airflow.models import DagRun

return some_datetime

def serialize_reference(self) -> dict:
return {"reference_type": self.reference_name}


# Optionally, specify when it is calculated by providing a DeadlineReference.TYPES value.
@deadline_reference(DeadlineReference.TYPES.DAGRUN_QUEUED)
class MyQueuedRef(BaseDeadlineReference):
# Optionally, you can specify when you want it calculated by providing a DeadlineReference.TYPES
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
# Put your business logic here
# Put your business logic here
return some_datetime

def serialize_reference(self) -> dict:
return {"reference_type": self.reference_name}
"""
# Used bare, without parentheses: the decorated class is passed in directly.
if isinstance(deadline_reference_type, type):
return DeadlineReference.register_custom_reference(deadline_reference_type)

def decorator(
reference_class: type[BaseDeadlineReference],
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Fix the deadline_reference decorator's no-parentheses form by ferruzzi · Pull Request #70708 · apache/airflow · GitHub
Skip to content
Merged
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
7 changes: 7 additions & 0 deletions airflow-core/docs/howto/deadline-alerts.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -420,6 +420,10 @@ references for specific integrations like calendars or other data sources. To do
a class that inherits from BaseDeadlineReference, add the ``@deadline_reference`` decorator, and
implement an ``_evaluate_with()`` method.

The decorator may be used with or without parentheses. Used bare, or with empty parentheses, the
reference is evaluated when a new Dag run is created; pass a ``DeadlineReference.TYPES`` value to
choose a different time.


**Creating a Custom Reference**

Expand DownExpand Up@@ -520,6 +524,9 @@ followed by a more urgent escalation if the Dag is still running.
**Important Notes:**

* **Timezone Awareness**: Always return timezone-aware datetime objects.
* **No-argument Construction**: Custom references are instantiated during registration, so they must be
constructible with no arguments. If your reference takes parameters, decorate it with ``@dataclass``
and give every field a default value.
* **Plugin Placement**: One convenient place for custom references is in the plugins directory.
* **API Server Restart**: Restart the Airflow API Server after adding or modifying custom references.
* **Required Parameters**: Use ``required_kwargs`` to specify parameters your reference needs.
Expand Down
1 change: 1 addition & 0 deletions airflow-core/newsfragments/70708.bugfix.rst
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
The ``@deadline_reference`` decorator can now be used without parentheses. Previously, using it that way silently skipped registration and rebound the decorated class to the decorator's inner function, which surfaced later as an unrelated ``TypeError``.
Comment thread
ferruzzi marked this conversation as resolved.
39 changes: 39 additions & 0 deletions airflow-core/tests/unit/models/test_deadline.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,7 @@
from __future__ import annotations

import re
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import TYPE_CHECKING
from unittest import mock
Expand DownExpand Up@@ -913,6 +914,44 @@ def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:

mock_register.assert_called_once_with(DecoratedCustomRef, timing)

def test_deadline_reference_decorator_without_parentheses(self):
@deadline_reference
class BareDecoratedRef(BaseDeadlineReference):
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
return timezone.datetime(DEFAULT_DATE)

# The decorated name must still be the class, not the inner decorator function.
assert isinstance(BareDecoratedRef, type)
assert issubclass(BareDecoratedRef, BaseDeadlineReference)

assert hasattr(DeadlineReference, BareDecoratedRef.__name__)
assert getattr(DeadlineReference, BareDecoratedRef.__name__).__class__ is BareDecoratedRef

assert_correct_timing(BareDecoratedRef, DeadlineReference.TYPES.DAGRUN_CREATED)
assert_builtin_types_unchanged(
DeadlineReference.TYPES.DAGRUN_QUEUED, DeadlineReference.TYPES.DAGRUN_CREATED
)

def test_deadline_reference_decorator_without_parentheses_invalid_class(self):
"""Test that the bare form must inherit the base class."""
with pytest.raises(ValueError, match="InvalidBareRef must inherit from BaseDeadlineReference"):

@deadline_reference
class InvalidBareRef:
pass

def test_deadline_reference_requiring_arguments_raises_helpful_error(self):
"""Test that a reference which cannot be instantiated with no arguments explains itself."""
with pytest.raises(TypeError, match="must be constructible with no arguments"):

@deadline_reference()
@dataclass
class RefWithRequiredField(BaseDeadlineReference):
required_field: str

def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
return timezone.datetime(DEFAULT_DATE)


@pytest.mark.db_test
class TestDeadlineMetricsTeamName:
Expand Down
46 changes: 40 additions & 6 deletions task-sdk/src/airflow/sdk/definitions/deadline.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,7 @@
from abc import ABC
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, overload

import attrs

Expand DownExpand Up@@ -290,7 +290,15 @@ def register_custom_reference(
raise ValueError(f"{reference_class.__name__} must inherit from BaseDeadlineReference")

# Register the new reference with DeadlineReference for discoverability
setattr(cls, reference_class.__name__, reference_class())
try:
reference_instance = reference_class()
except TypeError as e:
raise TypeError(
f"{reference_class.__name__} must be constructible with no arguments in order to be "
f"registered as a deadline reference. If it takes parameters, decorate it with "
f"@dataclass and give every field a default value. Original error: {e}"
) from e
setattr(cls, reference_class.__name__, reference_instance)
logger.info("Registered DeadlineReference %s", reference_class.__name__)

# Add to appropriate deadline_reference_type classification
Expand All@@ -310,34 +318,60 @@ def register_custom_reference(
return reference_class


@overload
def deadline_reference(
deadline_reference_type: type[BaseDeadlineReference],
) -> type[BaseDeadlineReference]: ...


@overload
def deadline_reference(
deadline_reference_type: DeadlineReferenceTypes | None = None,
) -> Callable[[type[BaseDeadlineReference]], type[BaseDeadlineReference]]:
) -> Callable[[type[BaseDeadlineReference]], type[BaseDeadlineReference]]: ...


def deadline_reference(deadline_reference_type=None):
"""
Decorate a class to register a custom deadline reference.

Usage:
May be used with or without parentheses. Without parentheses the reference is evaluated when a
new dagrun is created; pass a ``DeadlineReference.TYPES`` value to choose a different time.

.. code-block:: python

@deadline_reference
class MyBareReference(BaseDeadlineReference):
# Equivalent to @deadline_reference(); evaluated when a new dagrun is created.
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
return some_datetime


@deadline_reference()
class MyCustomReference(BaseDeadlineReference):
# By default, evaluate_with will be called when a new dagrun is created.
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
# Put your business logic here (use deferred imports for Core types)
from airflow.models import DagRun

return some_datetime

def serialize_reference(self) -> dict:
return {"reference_type": self.reference_name}


# Optionally, specify when it is calculated by providing a DeadlineReference.TYPES value.
@deadline_reference(DeadlineReference.TYPES.DAGRUN_QUEUED)
class MyQueuedRef(BaseDeadlineReference):
# Optionally, you can specify when you want it calculated by providing a DeadlineReference.TYPES
def _evaluate_with(self, *, session: Session, **kwargs) -> datetime:
# Put your business logic here
# Put your business logic here
return some_datetime

def serialize_reference(self) -> dict:
return {"reference_type": self.reference_name}
"""
# Used bare, without parentheses: the decorated class is passed in directly.
if isinstance(deadline_reference_type, type):
return DeadlineReference.register_custom_reference(deadline_reference_type)

def decorator(
reference_class: type[BaseDeadlineReference],
Expand Down