Skip to content

Commit f85a6f5

Browse files
Simplify mention parsing and remove text extraction
1 parent 3212976 commit f85a6f5

File tree

5 files changed

+369
-1304
lines changed

5 files changed

+369
-1304
lines changed

src/django_github_app/mentions.py

Lines changed: 18 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,11 @@
22

33
import re
44
from dataclasses import dataclass
5-
from datetime import datetime
65
from enum import Enum
76
from typing import NamedTuple
87

9-
from django.conf import settings
10-
from django.utils import timezone
118
from gidgethub import sansio
129

13-
from .conf import app_settings
14-
1510

1611
class EventAction(NamedTuple):
1712
event: str
@@ -76,8 +71,6 @@ class RawMention:
7671
CODE_BLOCK_PATTERN = re.compile(r"```[\s\S]*?```", re.MULTILINE)
7772
INLINE_CODE_PATTERN = re.compile(r"`[^`]+`")
7873
BLOCKQUOTE_PATTERN = re.compile(r"^\s*>.*$", re.MULTILINE)
79-
80-
8174
# GitHub username rules:
8275
# - 1-39 characters long
8376
# - Can only contain alphanumeric characters or hyphens
@@ -127,63 +120,47 @@ def for_mention_in_comment(cls, comment: str, mention_position: int):
127120
return cls(lineno=line_number, text=line_text)
128121

129122

130-
def extract_mention_text(
131-
body: str, current_index: int, all_mentions: list[RawMention], mention_end: int
132-
) -> str:
133-
text_start = mention_end
134-
135-
# Find next @mention (any mention, not just matched ones) to know where this text ends
136-
next_mention_index = None
137-
for j in range(current_index + 1, len(all_mentions)):
138-
next_mention_index = j
139-
break
140-
141-
if next_mention_index is not None:
142-
text_end = all_mentions[next_mention_index].position
143-
else:
144-
text_end = len(body)
145-
146-
return body[text_start:text_end].strip()
147-
148-
149123
@dataclass
150124
class ParsedMention:
151125
username: str
152-
text: str
153126
position: int
154127
line_info: LineInfo
155-
match: re.Match[str] | None = None
156128
previous_mention: ParsedMention | None = None
157129
next_mention: ParsedMention | None = None
158130

159131

132+
def matches_pattern(text: str, pattern: str | re.Pattern[str]) -> bool:
133+
match pattern:
134+
case re.Pattern():
135+
return pattern.fullmatch(text) is not None
136+
case str():
137+
return text.strip().lower() == pattern.strip().lower()
138+
139+
160140
def extract_mentions_from_event(
161141
event: sansio.Event, username_pattern: str | re.Pattern[str] | None = None
162142
) -> list[ParsedMention]:
163-
comment = event.data.get("comment", {}).get("body", "")
143+
comment_key = "comment" if event.event != "pull_request_review" else "review"
144+
comment = event.data.get(comment_key, {}).get("body", "")
164145

165146
if not comment:
166147
return []
167148

168-
if username_pattern is None:
169-
username_pattern = app_settings.SLUG
170-
171149
mentions: list[ParsedMention] = []
172150
potential_mentions = extract_all_mentions(comment)
173-
for i, raw_mention in enumerate(potential_mentions):
174-
if not matches_pattern(raw_mention.username, username_pattern):
151+
for raw_mention in potential_mentions:
152+
if username_pattern and not matches_pattern(
153+
raw_mention.username, username_pattern
154+
):
175155
continue
176156

177-
text = extract_mention_text(comment, i, potential_mentions, raw_mention.end)
178-
line_info = LineInfo.for_mention_in_comment(comment, raw_mention.position)
179-
180157
mentions.append(
181158
ParsedMention(
182159
username=raw_mention.username,
183-
text=text,
184160
position=raw_mention.position,
185-
line_info=line_info,
186-
match=None,
161+
line_info=LineInfo.for_mention_in_comment(
162+
comment, raw_mention.position
163+
),
187164
previous_mention=None,
188165
next_mention=None,
189166
)
@@ -198,63 +175,8 @@ def extract_mentions_from_event(
198175
return mentions
199176

200177

201-
@dataclass
202-
class Comment:
203-
body: str
204-
author: str
205-
created_at: datetime
206-
url: str
207-
mentions: list[ParsedMention]
208-
209-
@property
210-
def line_count(self) -> int:
211-
if not self.body:
212-
return 0
213-
return len(self.body.splitlines())
214-
215-
@classmethod
216-
def from_event(cls, event: sansio.Event) -> Comment:
217-
match event.event:
218-
case "issue_comment" | "pull_request_review_comment" | "commit_comment":
219-
comment_data = event.data.get("comment")
220-
case "pull_request_review":
221-
comment_data = event.data.get("review")
222-
case _:
223-
comment_data = None
224-
225-
if not comment_data:
226-
raise ValueError(f"Cannot extract comment from event type: {event.event}")
227-
228-
if created_at_str := comment_data.get("created_at", ""):
229-
# GitHub timestamps are in ISO format: 2024-01-01T12:00:00Z
230-
created_at_aware = datetime.fromisoformat(
231-
created_at_str.replace("Z", "+00:00")
232-
)
233-
if settings.USE_TZ:
234-
created_at = created_at_aware
235-
else:
236-
created_at = timezone.make_naive(
237-
created_at_aware, timezone.get_default_timezone()
238-
)
239-
else:
240-
created_at = timezone.now()
241-
242-
author = comment_data.get("user", {}).get("login", "")
243-
if not author and "sender" in event.data:
244-
author = event.data.get("sender", {}).get("login", "")
245-
246-
return cls(
247-
body=comment_data.get("body", ""),
248-
author=author,
249-
created_at=created_at,
250-
url=comment_data.get("html_url", ""),
251-
mentions=[],
252-
)
253-
254-
255178
@dataclass
256179
class Mention:
257-
comment: Comment
258180
mention: ParsedMention
259181
scope: MentionScope | None
260182

@@ -264,50 +186,8 @@ def from_event(
264186
event: sansio.Event,
265187
*,
266188
username: str | re.Pattern[str] | None = None,
267-
pattern: str | re.Pattern[str] | None = None,
268189
scope: MentionScope | None = None,
269190
):
270-
event_scope = MentionScope.from_event(event)
271-
if scope is not None and event_scope != scope:
272-
return
273-
274191
mentions = extract_mentions_from_event(event, username)
275-
if not mentions:
276-
return
277-
278-
comment = Comment.from_event(event)
279-
comment.mentions = mentions
280-
281192
for mention in mentions:
282-
if pattern is not None:
283-
match = get_match(mention.text, pattern)
284-
if not match:
285-
continue
286-
mention.match = match
287-
288-
yield cls(
289-
comment=comment,
290-
mention=mention,
291-
scope=event_scope,
292-
)
293-
294-
295-
def matches_pattern(text: str, pattern: str | re.Pattern[str]) -> bool:
296-
match pattern:
297-
case re.Pattern():
298-
return pattern.fullmatch(text) is not None
299-
case str():
300-
return text.strip().lower() == pattern.strip().lower()
301-
302-
303-
def get_match(text: str, pattern: str | re.Pattern[str] | None) -> re.Match[str] | None:
304-
match pattern:
305-
case None:
306-
return re.match(r"(.*)", text, re.IGNORECASE | re.DOTALL)
307-
case re.Pattern():
308-
# Use the pattern directly, preserving its flags
309-
return pattern.match(text)
310-
case str():
311-
# For strings, do exact match (case-insensitive)
312-
# Escape the string to treat it literally
313-
return re.match(re.escape(pattern), text, re.IGNORECASE)
193+
yield cls(mention=mention, scope=scope)

src/django_github_app/routing.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ def decorator(func: CB) -> CB:
6767
def mention(
6868
self,
6969
*,
70-
pattern: str | re.Pattern[str] | None = None,
7170
username: str | re.Pattern[str] | None = None,
7271
scope: MentionScope | None = None,
7372
**kwargs: Any,
@@ -77,17 +76,25 @@ def decorator(func: CB) -> CB:
7776
async def async_wrapper(
7877
event: sansio.Event, gh: AsyncGitHubAPI, *args: Any, **kwargs: Any
7978
) -> None:
79+
event_scope = MentionScope.from_event(event)
80+
if scope is not None and event_scope != scope:
81+
return
82+
8083
for mention in Mention.from_event(
81-
event, username=username, pattern=pattern, scope=scope
84+
event, username=username, scope=event_scope
8285
):
8386
await func(event, gh, *args, context=mention, **kwargs) # type: ignore[func-returns-value]
8487

8588
@wraps(func)
8689
def sync_wrapper(
8790
event: sansio.Event, gh: SyncGitHubAPI, *args: Any, **kwargs: Any
8891
) -> None:
92+
event_scope = MentionScope.from_event(event)
93+
if scope is not None and event_scope != scope:
94+
return
95+
8996
for mention in Mention.from_event(
90-
event, username=username, pattern=pattern, scope=scope
97+
event, username=username, scope=event_scope
9198
):
9299
func(event, gh, *args, context=mention, **kwargs)
93100

tests/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,11 +280,11 @@ def _create_event(event_type, delivery_id=None, **data):
280280
in ["issue_comment", "pull_request_review_comment", "commit_comment"]
281281
and "comment" not in data
282282
):
283-
data["comment"] = {"body": faker.sentence()}
283+
data["comment"] = {"body": f"@{faker.user_name()} {faker.sentence()}"}
284284

285285
# Auto-create review field for pull request review events
286286
if event_type == "pull_request_review" and "review" not in data:
287-
data["review"] = {"body": faker.sentence()}
287+
data["review"] = {"body": f"@{faker.user_name()} {faker.sentence()}"}
288288

289289
# Add user to comment if not present
290290
if "comment" in data and "user" not in data["comment"]:

0 commit comments

Comments
 (0)