22
33import re
44from dataclasses import dataclass
5- from datetime import datetime
65from enum import Enum
76from typing import NamedTuple
87
9- from django .conf import settings
10- from django .utils import timezone
118from gidgethub import sansio
129
13- from .conf import app_settings
14-
1510
1611class EventAction (NamedTuple ):
1712 event : str
@@ -76,8 +71,6 @@ class RawMention:
7671CODE_BLOCK_PATTERN = re .compile (r"```[\s\S]*?```" , re .MULTILINE )
7772INLINE_CODE_PATTERN = re .compile (r"`[^`]+`" )
7873BLOCKQUOTE_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
150124class 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+
160140def 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
256179class 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 )
0 commit comments