Skip to content

Commit 4df3dc6

Browse files
Refactor mention system for cleaner API and better encapsulation
- Add explicit kwargs to mention() decorator (pattern, username, scope) - Pass context as explicit parameter instead of mutating kwargs - Create MentionEvent.from_event() generator to encapsulate all mention processing logic (parsing, filtering, context creation) - Rename MentionContext to MentionEvent for clarity - Simplify router code from ~30 lines to 4 lines per wrapper This creates a much cleaner API where the mention decorator's wrappers simply iterate over MentionEvent instances yielded by the generator. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 30be1b7 commit 4df3dc6

File tree

3 files changed

+63
-63
lines changed

3 files changed

+63
-63
lines changed

src/django_github_app/mentions.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,53 @@ def from_event(cls, event: sansio.Event) -> Comment:
133133

134134

135135
@dataclass
136-
class MentionContext:
136+
class MentionEvent:
137137
comment: Comment
138138
triggered_by: Mention
139139
scope: MentionScope | None
140140

141+
@classmethod
142+
def from_event(
143+
cls,
144+
event: sansio.Event,
145+
*,
146+
username: str | re.Pattern[str] | None = None,
147+
pattern: str | re.Pattern[str] | None = None,
148+
scope: MentionScope | None = None,
149+
):
150+
"""Generate MentionEvent instances from a GitHub event.
151+
152+
Yields MentionEvent for each mention that matches the given criteria.
153+
"""
154+
# Check scope match first
155+
event_scope = MentionScope.from_event(event)
156+
if scope is not None and event_scope != scope:
157+
return
158+
159+
# Parse mentions
160+
mentions = parse_mentions_for_username(event, username)
161+
if not mentions:
162+
return
163+
164+
# Create comment
165+
comment = Comment.from_event(event)
166+
comment.mentions = mentions
167+
168+
# Yield contexts for matching mentions
169+
for mention in mentions:
170+
# Check pattern match if specified
171+
if pattern is not None:
172+
match = check_pattern_match(mention.text, pattern)
173+
if not match:
174+
continue
175+
mention.match = match
176+
177+
yield cls(
178+
comment=comment,
179+
triggered_by=mention,
180+
scope=event_scope,
181+
)
182+
141183

142184
CODE_BLOCK_PATTERN = re.compile(r"```[\s\S]*?```", re.MULTILINE)
143185
INLINE_CODE_PATTERN = re.compile(r"`[^`]+`")

src/django_github_app/routing.py

Lines changed: 17 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,8 @@
1717
from ._typing import override
1818
from .github import AsyncGitHubAPI
1919
from .github import SyncGitHubAPI
20-
from .mentions import Comment
21-
from .mentions import MentionContext
20+
from .mentions import MentionEvent
2221
from .mentions import MentionScope
23-
from .mentions import check_pattern_match
24-
from .mentions import parse_mentions_for_username
2522

2623
AsyncCallback = Callable[..., Awaitable[None]]
2724
SyncCallback = Callable[..., None]
@@ -73,71 +70,32 @@ def decorator(func: CB) -> CB:
7370

7471
return decorator
7572

76-
def mention(self, **kwargs: Any) -> Callable[[CB], CB]:
73+
def mention(
74+
self,
75+
*,
76+
pattern: str | re.Pattern[str] | None = None,
77+
username: str | re.Pattern[str] | None = None,
78+
scope: MentionScope | None = None,
79+
**kwargs: Any,
80+
) -> Callable[[CB], CB]:
7781
def decorator(func: CB) -> CB:
78-
pattern = kwargs.pop("pattern", None)
79-
username = kwargs.pop("username", None)
80-
scope = kwargs.pop("scope", None)
81-
8282
@wraps(func)
8383
async def async_wrapper(
8484
event: sansio.Event, gh: AsyncGitHubAPI, *args: Any, **kwargs: Any
8585
) -> None:
86-
event_scope = MentionScope.from_event(event)
87-
if scope is not None and event_scope != scope:
88-
return
89-
90-
mentions = parse_mentions_for_username(event, username)
91-
if not mentions:
92-
return
93-
94-
comment = Comment.from_event(event)
95-
comment.mentions = mentions
96-
97-
for mention in mentions:
98-
if pattern is not None:
99-
match = check_pattern_match(mention.text, pattern)
100-
if not match:
101-
continue
102-
mention.match = match
103-
104-
kwargs["context"] = MentionContext(
105-
comment=comment,
106-
triggered_by=mention,
107-
scope=event_scope,
108-
)
109-
110-
await func(event, gh, *args, **kwargs) # type: ignore[func-returns-value]
86+
for context in MentionEvent.from_event(
87+
event, username=username, pattern=pattern, scope=scope
88+
):
89+
await func(event, gh, *args, context=context, **kwargs) # type: ignore[func-returns-value]
11190

11291
@wraps(func)
11392
def sync_wrapper(
11493
event: sansio.Event, gh: SyncGitHubAPI, *args: Any, **kwargs: Any
11594
) -> None:
116-
event_scope = MentionScope.from_event(event)
117-
if scope is not None and event_scope != scope:
118-
return
119-
120-
mentions = parse_mentions_for_username(event, username)
121-
if not mentions:
122-
return
123-
124-
comment = Comment.from_event(event)
125-
comment.mentions = mentions
126-
127-
for mention in mentions:
128-
if pattern is not None:
129-
match = check_pattern_match(mention.text, pattern)
130-
if not match:
131-
continue
132-
mention.match = match
133-
134-
kwargs["context"] = MentionContext(
135-
comment=comment,
136-
triggered_by=mention,
137-
scope=event_scope,
138-
)
139-
140-
func(event, gh, *args, **kwargs)
95+
for context in MentionEvent.from_event(
96+
event, username=username, pattern=pattern, scope=scope
97+
):
98+
func(event, gh, *args, context=context, **kwargs)
14199

142100
wrapper: MentionHandler
143101
if iscoroutinefunction(func):

tests/test_routing.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -582,10 +582,10 @@ def deploy_handler(event, *args, **kwargs):
582582

583583

584584
class TestUpdatedMentionContext:
585-
"""Test the updated MentionContext structure with comment and triggered_by fields."""
585+
"""Test the updated MentionEvent structure with comment and triggered_by fields."""
586586

587587
def test_mention_context_structure(self, test_router, get_mock_github_api_sync):
588-
"""Test that MentionContext has the new structure with comment and triggered_by."""
588+
"""Test that MentionEvent has the new structure with comment and triggered_by."""
589589
handler_called = False
590590
captured_mention = None
591591

@@ -725,7 +725,7 @@ def general_handler(event, *args, **kwargs):
725725
async def test_async_mention_context_structure(
726726
self, test_router, get_mock_github_api
727727
):
728-
"""Test async handlers get the same updated MentionContext structure."""
728+
"""Test async handlers get the same updated MentionEvent structure."""
729729
handler_called = False
730730
captured_mention = None
731731

0 commit comments

Comments
 (0)