github2gerrit 0.1.10__py3-none-any.whl → 0.1.11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,9 +3,10 @@
3
3
  #
4
4
  # PR Content Filtering Module
5
5
  #
6
- # This module provides an extensible, rule-based system for filtering and cleaning
7
- # GitHub pull request content for Gerrit consumption. It supports multiple automation
8
- # tools (Dependabot, pre-commit.ci, etc.) with author-specific filtering rules.
6
+ # This module provides an extensible, rule-based system for filtering and
7
+ # cleaning GitHub pull request content for Gerrit consumption. It supports
8
+ # multiple automation tools (Dependabot, pre-commit.ci, etc.) with
9
+ # author-specific filtering rules.
9
10
  #
10
11
  # Key features:
11
12
  # - Author-specific filtering rules
@@ -29,7 +30,8 @@ log = logging.getLogger("github2gerrit.pr_content_filter")
29
30
 
30
31
  # Common patterns used across filters
31
32
  _HTML_DETAILS_PATTERN = re.compile(
32
- r"<details[^>]*>\s*<summary[^>]*>(.*?)</summary>\s*(.*?)</details>", re.IGNORECASE | re.DOTALL
33
+ r"<details[^>]*>\s*<summary[^>]*>(.*?)</summary>\s*(.*?)</details>",
34
+ re.IGNORECASE | re.DOTALL,
33
35
  )
34
36
  _MARKDOWN_LINK_PATTERN = re.compile(r"\[([^\]]+)\]\([^)]+\)")
35
37
  _HTML_TAG_PATTERN = re.compile(r"<[^>]+>")
@@ -50,8 +52,12 @@ class FilterConfig:
50
52
  author_rules: dict[str, str] = field(default_factory=dict)
51
53
 
52
54
  # Rule-specific configurations
53
- dependabot_config: DependabotConfig = field(default_factory=lambda: DependabotConfig())
54
- precommit_config: PrecommitConfig = field(default_factory=lambda: PrecommitConfig())
55
+ dependabot_config: DependabotConfig = field(
56
+ default_factory=lambda: DependabotConfig()
57
+ )
58
+ precommit_config: PrecommitConfig = field(
59
+ default_factory=lambda: PrecommitConfig()
60
+ )
55
61
 
56
62
 
57
63
  @dataclass
@@ -149,12 +155,18 @@ class DependabotRule(FilterRule):
149
155
 
150
156
  def _remove_compatibility_images(self, content: str) -> str:
151
157
  """Remove Dependabot compatibility score images."""
152
- pattern = re.compile(r"!\[.*?\]\(https://camo\.githubusercontent\.com/[^)]*\)", re.IGNORECASE | re.DOTALL)
158
+ pattern = re.compile(
159
+ r"!\[.*?\]\(https://camo\.githubusercontent\.com/[^)]*\)",
160
+ re.IGNORECASE | re.DOTALL,
161
+ )
153
162
  return pattern.sub("", content)
154
163
 
155
164
  def _truncate_at_dependabot_commands(self, content: str) -> str:
156
165
  """Truncate at Dependabot command instructions."""
157
- pattern = re.compile(r"Dependabot will resolve any conflicts", re.IGNORECASE | re.MULTILINE)
166
+ pattern = re.compile(
167
+ r"Dependabot will resolve any conflicts",
168
+ re.IGNORECASE | re.MULTILINE,
169
+ )
158
170
  match = pattern.search(content)
159
171
  if match:
160
172
  return content[: match.start()].rstrip()
@@ -210,7 +222,10 @@ class PRContentFilter:
210
222
  # Check author-specific rules first
211
223
  if author in self.config.author_rules:
212
224
  rule_name = self.config.author_rules[author]
213
- return any(rule.__class__.__name__.lower().startswith(rule_name.lower()) for rule in self.rules)
225
+ return any(
226
+ rule.__class__.__name__.lower().startswith(rule_name.lower())
227
+ for rule in self.rules
228
+ )
214
229
 
215
230
  # Check if any rule matches
216
231
  return any(rule.matches(title, body, author) for rule in self.rules)
@@ -232,7 +247,9 @@ class PRContentFilter:
232
247
  config_key = rule.get_config_key()
233
248
  rule_config = getattr(self.config, config_key, None)
234
249
  if rule_config:
235
- filtered_body = rule.apply(title, filtered_body, rule_config)
250
+ filtered_body = rule.apply(
251
+ title, filtered_body, rule_config
252
+ )
236
253
 
237
254
  # Apply global post-processing
238
255
  filtered_body = self._post_process(title, filtered_body)
@@ -263,6 +280,9 @@ class PRContentFilter:
263
280
  # Clean up whitespace
264
281
  processed = self._clean_whitespace(processed)
265
282
 
283
+ # Remove trailing ellipses
284
+ processed = self._remove_trailing_ellipses(processed)
285
+
266
286
  return processed
267
287
 
268
288
  def _remove_emoji_codes(self, content: str) -> str:
@@ -283,14 +303,18 @@ class PRContentFilter:
283
303
 
284
304
  # Fix lines that started with emoji codes and now have leading space
285
305
  if cleaned_line.startswith(" ") and not line.startswith(" "):
286
- # This line originally started with an emoji, remove the leading space
306
+ # This line originally started with an emoji, remove the
307
+ # leading space
287
308
  cleaned_line = cleaned_line.lstrip()
288
309
 
289
310
  # Fix markdown headers that lost their emoji but kept leading space
290
- # e.g., "### :sparkles: New features" -> "### New features" not "### New features"
311
+ # e.g., "### :sparkles: New features" -> "### New features"
312
+ # not "### New features"
291
313
  if cleaned_line.startswith("### "):
292
314
  # Ensure exactly one space after ###
293
- header_text = cleaned_line[4:].lstrip() # Remove ### and any spaces
315
+ header_text = cleaned_line[
316
+ 4:
317
+ ].lstrip() # Remove ### and any spaces
294
318
  cleaned_line = f"### {header_text}" if header_text else "###"
295
319
  elif cleaned_line.startswith("## "):
296
320
  # Same for ## headers
@@ -309,7 +333,8 @@ class PRContentFilter:
309
333
  for i, line in enumerate(cleaned_lines):
310
334
  final_lines.append(line)
311
335
 
312
- # If this line looks like a heading (not starting with #) and should have
336
+ # If this line looks like a heading (not starting with #) and should
337
+ # have
313
338
  # a blank line after it, add one
314
339
  if (
315
340
  i < len(cleaned_lines) - 1
@@ -380,7 +405,8 @@ class PRContentFilter:
380
405
  )
381
406
 
382
407
  if is_duplicate:
383
- # Remove the duplicate line and any immediately following empty lines
408
+ # Remove the duplicate line and any immediately following empty
409
+ # lines
384
410
  remaining_lines = lines[first_content_line_idx + 1 :]
385
411
  while remaining_lines and not remaining_lines[0].strip():
386
412
  remaining_lines = remaining_lines[1:]
@@ -407,6 +433,24 @@ class PRContentFilter:
407
433
 
408
434
  return cleaned
409
435
 
436
+ def _remove_trailing_ellipses(self, content: str) -> str:
437
+ """Remove trailing ellipses that are often left by truncated content."""
438
+ lines = content.splitlines()
439
+ cleaned_lines = []
440
+
441
+ for line in lines:
442
+ # Remove lines that are just "..." or whitespace + "..."
443
+ stripped = line.strip()
444
+ if stripped == "..." or stripped == "…":
445
+ continue
446
+
447
+ # Remove trailing ellipses from lines
448
+ cleaned_line = re.sub(r"\s*\.{3,}\s*$", "", line)
449
+ cleaned_line = re.sub(r"\s*…\s*$", "", cleaned_line)
450
+ cleaned_lines.append(cleaned_line)
451
+
452
+ return "\n".join(cleaned_lines)
453
+
410
454
  def add_rule(self, rule: FilterRule) -> None:
411
455
  """Add a custom filtering rule."""
412
456
  self.rules.append(rule)
@@ -434,7 +478,9 @@ def create_default_filter() -> PRContentFilter:
434
478
  return PRContentFilter(config)
435
479
 
436
480
 
437
- def filter_pr_body(title: str, body: str | None, author: str | None = None) -> str:
481
+ def filter_pr_body(
482
+ title: str, body: str | None, author: str | None = None
483
+ ) -> str:
438
484
  """
439
485
  Main entry point for PR body filtering with default configuration.
440
486
 
@@ -454,7 +500,9 @@ def filter_pr_body(title: str, body: str | None, author: str | None = None) -> s
454
500
 
455
501
 
456
502
  # Legacy compatibility functions
457
- def should_filter_pr_body(title: str, body: str | None, author: str | None = None) -> bool:
503
+ def should_filter_pr_body(
504
+ title: str, body: str | None, author: str | None = None
505
+ ) -> bool:
458
506
  """Legacy function for checking if filtering should be applied."""
459
507
  if not body:
460
508
  return False