github2gerrit 1.3.1__tar.gz → 1.3.3__tar.gz

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.
Files changed (134) hide show
  1. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/PKG-INFO +1 -1
  2. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/sitecustomize.py +6 -0
  3. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/cli.py +16 -4
  4. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/core.py +0 -17
  5. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/duplicate_detection.py +0 -9
  6. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/external_api.py +2 -2
  7. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/gerrit_rest.py +0 -13
  8. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/github_api.py +22 -12
  9. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/gitreview.py +0 -5
  10. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/gitutils.py +2 -1
  11. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/ssh_discovery.py +0 -1
  12. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_dependency_supersession.py +68 -41
  13. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_external_api_framework.py +2 -2
  14. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_gitutils_helpers.py +79 -31
  15. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/.editorconfig +0 -0
  16. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/.gitignore +0 -0
  17. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/.gitlint +0 -0
  18. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/.markdownlint.yaml +0 -0
  19. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/.pre-commit-config.yaml +0 -0
  20. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/.readthedocs.yml +0 -0
  21. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/.yamllint +0 -0
  22. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/LICENSE +0 -0
  23. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/LICENSES/Apache-2.0.txt +0 -0
  24. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/README.md +0 -0
  25. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/REUSE.toml +0 -0
  26. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/SECURITY.md +0 -0
  27. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/action.yaml +0 -0
  28. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/docs/COMMIT_RULES.md +0 -0
  29. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/docs/COMPOSITE_ACTION_TESTING.md +0 -0
  30. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/docs/PR_UPDATE_IMPLEMENTATION.md +0 -0
  31. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/docs/RELEASE-v0.2.0.md +0 -0
  32. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/docs/github2gerrit_token_permissions_classic.png +0 -0
  33. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/pyproject.toml +0 -0
  34. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/__init__.py +0 -0
  35. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/commit_normalization.py +0 -0
  36. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/commit_rules.py +0 -0
  37. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/config.py +0 -0
  38. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/constants.py +0 -0
  39. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/error_codes.py +0 -0
  40. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/gerrit_pr_closer.py +0 -0
  41. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/gerrit_query.py +0 -0
  42. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/gerrit_ssh.py +0 -0
  43. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/gerrit_urls.py +0 -0
  44. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/mapping_comment.py +0 -0
  45. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/models.py +0 -0
  46. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/netrc.py +0 -0
  47. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/orchestrator/__init__.py +0 -0
  48. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/orchestrator/reconciliation.py +0 -0
  49. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/pr_commands.py +0 -0
  50. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/pr_content_filter.py +0 -0
  51. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/reconcile_matcher.py +0 -0
  52. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/rich_display.py +0 -0
  53. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/rich_logging.py +0 -0
  54. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/similarity.py +0 -0
  55. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/ssh_agent_setup.py +0 -0
  56. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/ssh_common.py +0 -0
  57. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/ssh_config_parser.py +0 -0
  58. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/trailers.py +0 -0
  59. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/utils.py +0 -0
  60. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/conftest.py +0 -0
  61. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/fixtures/__init__.py +0 -0
  62. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/fixtures/make_repo.py +0 -0
  63. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/fixtures/ssh_config_samples.py +0 -0
  64. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_action_environment_mapping.py +0 -0
  65. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_action_outputs.py +0 -0
  66. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_action_pr_number_handling.py +0 -0
  67. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_action_step_validation.py +0 -0
  68. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_automation_only.py +0 -0
  69. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_change_id_deduplication.py +0 -0
  70. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_clean_squash_title.py +0 -0
  71. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_cli.py +0 -0
  72. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_cli_helpers.py +0 -0
  73. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_cli_netrc_options.py +0 -0
  74. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_cli_outputs_file.py +0 -0
  75. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_cli_url_and_dryrun.py +0 -0
  76. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_commit_normalization.py +0 -0
  77. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_commit_rules.py +0 -0
  78. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_composite_action_coverage.py +0 -0
  79. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_config_and_reviewers.py +0 -0
  80. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_config_helpers.py +0 -0
  81. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_core_close_pr_policy.py +0 -0
  82. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_core_config_and_errors.py +0 -0
  83. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_core_gerrit_backref_comment.py +0 -0
  84. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_core_gerrit_push_errors.py +0 -0
  85. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_core_gerrit_rest_results.py +0 -0
  86. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_core_integration_fixture_repo.py +0 -0
  87. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_core_prepare_commits.py +0 -0
  88. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_core_shallow_clone.py +0 -0
  89. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_core_ssh_setup.py +0 -0
  90. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_core_ssrf_protection.py +0 -0
  91. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_dns_validation_and_no_gerrit.py +0 -0
  92. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_duplicate_detection.py +0 -0
  93. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_email_case_normalization.py +0 -0
  94. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_error_codes.py +0 -0
  95. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_force_flag_cli.py +0 -0
  96. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_gerrit_change_id_footer.py +0 -0
  97. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_gerrit_change_status_checks.py +0 -0
  98. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_gerrit_pr_closer.py +0 -0
  99. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_gerrit_rest_client.py +0 -0
  100. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_gerrit_ssh_abandon.py +0 -0
  101. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_gerrit_urls.py +0 -0
  102. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_gerrit_urls_more.py +0 -0
  103. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_ghe_and_gitreview_args.py +0 -0
  104. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_github_api_error_handling.py +0 -0
  105. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_github_api_helpers.py +0 -0
  106. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_github_api_retry_and_helpers.py +0 -0
  107. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_gitreview.py +0 -0
  108. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_issue_157_regressions.py +0 -0
  109. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_mapping_comment_additional.py +0 -0
  110. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_mapping_comment_digest_and_backref.py +0 -0
  111. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_metadata_and_reconciliation.py +0 -0
  112. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_metadata_trailer_separation_bug.py +0 -0
  113. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_misc_small_coverage.py +0 -0
  114. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_netrc.py +0 -0
  115. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_orphan_rest_side_effects.py +0 -0
  116. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_pr_commands.py +0 -0
  117. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_pr_content_filter.py +0 -0
  118. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_pr_content_filter_integration.py +0 -0
  119. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_pr_update_detection.py +0 -0
  120. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_reconciliation_extracted_module.py +0 -0
  121. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_reconciliation_plan_and_orphans.py +0 -0
  122. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_reconciliation_scenarios.py +0 -0
  123. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_ssh_agent.py +0 -0
  124. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_ssh_agent_ownership.py +0 -0
  125. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_ssh_artifact_prevention.py +0 -0
  126. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_ssh_common.py +0 -0
  127. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_ssh_discovery.py +0 -0
  128. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_ssh_discovery_dry_run.py +0 -0
  129. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_trailers_additional.py +0 -0
  130. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_url_parser.py +0 -0
  131. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_utils.py +0 -0
  132. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/unit/test_config_integration.py +0 -0
  133. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/unit/test_ssh_config_parser.py +0 -0
  134. {github2gerrit-1.3.1 → github2gerrit-1.3.3}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github2gerrit
3
- Version: 1.3.1
3
+ Version: 1.3.3
4
4
  Summary: Submit a GitHub pull request to a Gerrit repository.
5
5
  Project-URL: Homepage, https://github.com/lfreleng-actions/github2gerrit-action
6
6
  Project-URL: Repository, https://github.com/lfreleng-actions/github2gerrit-action
@@ -57,6 +57,8 @@ def _dbg(msg: str) -> None:
57
57
  try:
58
58
  print(f"[sitecustomize] {msg}", file=sys.stderr)
59
59
  except Exception:
60
+ # Debug logging must never disrupt interpreter startup; if
61
+ # writing to stderr fails, silently drop the message.
60
62
  pass
61
63
 
62
64
 
@@ -125,6 +127,8 @@ def _collect_base_dirs(repo_root: Path) -> set[Path]:
125
127
  try:
126
128
  bases.add(Path.cwd())
127
129
  except Exception:
130
+ # CWD may be unavailable (e.g. deleted); skip it and continue
131
+ # collecting the remaining base directories.
128
132
  pass
129
133
  # Repository root (directory containing this file)
130
134
  bases.add(repo_root)
@@ -134,6 +138,8 @@ def _collect_base_dirs(repo_root: Path) -> set[Path]:
134
138
  try:
135
139
  bases.add(Path(gw))
136
140
  except Exception:
141
+ # A malformed GITHUB_WORKSPACE value should not abort cleanup;
142
+ # skip it and continue with the other base directories.
137
143
  pass
138
144
  return bases
139
145
 
@@ -385,12 +385,15 @@ else:
385
385
 
386
386
 
387
387
  class _FormatterProto(Protocol):
388
- def write_usage(self, prog: str, args: str, prefix: str = ...) -> None: ...
388
+ def write_usage(self, prog: str, args: str, prefix: str = ...) -> None:
389
+ """Write the program usage line."""
389
390
 
390
391
 
391
392
  class _ContextProto(Protocol):
392
393
  @property
393
- def command_path(self) -> str: ...
394
+ def command_path(self) -> str:
395
+ """Return the full command path."""
396
+ raise NotImplementedError
394
397
 
395
398
 
396
399
  class _SingleUsageGroup(BaseGroup):
@@ -471,14 +474,23 @@ def _resolve_issue_id_from_json(json_str: str, github_actor: str) -> str:
471
474
  return ""
472
475
 
473
476
 
474
- # Show version information when --help is used or in GitHub Actions mode
475
- if "--help" in sys.argv or _is_github_actions_context():
477
+ def _print_version_banner() -> None:
478
+ """Print the resolved package version, or a fallback notice.
479
+
480
+ Defined as a function so the ``print`` calls do not execute at module
481
+ scope during import; the banner is only emitted when explicitly invoked.
482
+ """
476
483
  try:
477
484
  app_version = get_version("github2gerrit")
478
485
  print(f"🏷️ github2gerrit version {app_version}")
479
486
  except Exception:
480
487
  print("⚠️ github2gerrit version information not available")
481
488
 
489
+
490
+ # Show version information when --help is used or in GitHub Actions mode
491
+ if "--help" in sys.argv or _is_github_actions_context():
492
+ _print_version_banner()
493
+
482
494
  app: typer.Typer = typer.Typer(
483
495
  add_completion=False,
484
496
  no_args_is_help=False,
@@ -80,19 +80,10 @@ from .utils import is_verbose_mode
80
80
 
81
81
 
82
82
  try:
83
- from pygerrit2 import GerritRestAPI
84
- from pygerrit2 import HTTPBasicAuth
85
- except ImportError:
86
- GerritRestAPI = None
87
- HTTPBasicAuth = None
88
-
89
- try:
90
- from .ssh_discovery import SSHDiscoveryError
91
83
  from .ssh_discovery import auto_discover_gerrit_host_keys
92
84
  except ImportError:
93
85
  # Fallback if ssh_discovery module is not available
94
86
  auto_discover_gerrit_host_keys = None # type: ignore[assignment]
95
- SSHDiscoveryError = Exception # type: ignore[misc,assignment]
96
87
 
97
88
  try:
98
89
  from .ssh_agent_setup import SSHAgentManager
@@ -113,7 +104,6 @@ log = logging.getLogger("github2gerrit.core")
113
104
 
114
105
 
115
106
  # Error message constants to comply with TRY003
116
- _MSG_ISSUE_ID_MULTILINE = "Issue ID must be single line"
117
107
  _MSG_MISSING_PR_CONTEXT = "missing PR context"
118
108
  _MSG_BAD_REPOSITORY_CONTEXT = "bad repository context"
119
109
  _MSG_MISSING_GERRIT_SERVER = (
@@ -121,10 +111,6 @@ _MSG_MISSING_GERRIT_SERVER = (
121
111
  "input/environment variable, .gitreview file, or action configuration."
122
112
  )
123
113
  _MSG_MISSING_GERRIT_PROJECT = "missing GERRIT_PROJECT"
124
- _MSG_PYGERRIT2_REQUIRED_REST = "pygerrit2 is required to query Gerrit REST API"
125
- _MSG_PYGERRIT2_REQUIRED_AUTH = "pygerrit2 is required for HTTP authentication"
126
- _MSG_PYGERRIT2_MISSING = "pygerrit2 missing"
127
- _MSG_PYGERRIT2_AUTH_MISSING = "pygerrit2 auth missing"
128
114
  _MSG_DNS_RESOLUTION_FAILED = "DNS resolution failed for '%s'"
129
115
 
130
116
 
@@ -4429,7 +4415,6 @@ class Orchestrator:
4429
4415
  List of email addresses that were not found in Gerrit
4430
4416
  """
4431
4417
  combined_output = f"{exc.stdout}\n{exc.stderr}"
4432
- import re
4433
4418
 
4434
4419
  # Pattern to match: Account 'email@domain.com' not found
4435
4420
  pattern = r"Account\s+'([^']+)'\s+not\s+found"
@@ -4903,7 +4888,6 @@ class Orchestrator:
4903
4888
  # Extract specific rejection reason from output
4904
4889
  # Handle multiline rejection messages by looking in normalized
4905
4890
  # output
4906
- import re
4907
4891
 
4908
4892
  # Look for the rejection pattern in the normalized output
4909
4893
  rejection_match = re.search(
@@ -5299,7 +5283,6 @@ class Orchestrator:
5299
5283
  self, workspace: Path, gh: GitHubContext, inputs: Inputs
5300
5284
  ) -> None:
5301
5285
  """Fallback to GitHub API archive download (ported from CLI)."""
5302
- import json
5303
5286
  import zipfile
5304
5287
 
5305
5288
  repo_full = gh.repository.strip() if gh.repository else ""
@@ -32,15 +32,6 @@ from .rich_display import safe_console_print
32
32
  from .trailers import extract_github_metadata
33
33
 
34
34
 
35
- # Optional Gerrit REST API support
36
- try:
37
- from pygerrit2 import GerritRestAPI
38
- from pygerrit2 import HTTPBasicAuth
39
- except ImportError:
40
- GerritRestAPI = None
41
- HTTPBasicAuth = None
42
-
43
-
44
35
  log = logging.getLogger(__name__)
45
36
 
46
37
  __all__ = [
@@ -327,7 +327,7 @@ def external_api_call(
327
327
  policy=policy,
328
328
  )
329
329
 
330
- last_exc: BaseException | None = None
330
+ last_exc: Exception | None = None
331
331
 
332
332
  for attempt in range(1, policy.max_attempts + 1):
333
333
  context.attempt = attempt
@@ -347,7 +347,7 @@ def external_api_call(
347
347
 
348
348
  # Call the actual function
349
349
  result = func(*args, **kwargs)
350
- except BaseException as exc:
350
+ except Exception as exc:
351
351
  last_exc = exc
352
352
  duration = time.time() - context.start_time
353
353
 
@@ -68,19 +68,6 @@ _MSG_PYGERRIT2_REQUIRED_AUTH: Final[str] = (
68
68
  "pygerrit2 is required for HTTP authentication"
69
69
  )
70
70
 
71
- _TRANSIENT_ERR_SUBSTRINGS: Final[tuple[str, ...]] = (
72
- "timed out",
73
- "temporarily unavailable",
74
- "temporary failure",
75
- "connection reset",
76
- "connection aborted",
77
- "broken pipe",
78
- "connection refused",
79
- "bad gateway",
80
- "service unavailable",
81
- "gateway timeout",
82
- )
83
-
84
71
  # HTTP status codes that indicate an authentication/authorization problem
85
72
  # rather than a transient fault or a bug. These are expected when no
86
73
  # Gerrit REST credentials are available (or they are insufficient for the
@@ -23,7 +23,6 @@ from collections.abc import Iterable
23
23
  from importlib import import_module
24
24
  from typing import Any
25
25
  from typing import Protocol
26
- from typing import TypeVar
27
26
  from typing import cast
28
27
 
29
28
  from .error_codes import ExitCode
@@ -35,8 +34,6 @@ from .external_api import external_api_call
35
34
 
36
35
  # Error message constants to comply with TRY003
37
36
  _MSG_PYGITHUB_REQUIRED = "PyGithub required"
38
- _MSG_MISSING_GITHUB_TOKEN = "missing GITHUB_TOKEN" # noqa: S105
39
- _MSG_BAD_GITHUB_REPOSITORY = "bad GITHUB_REPOSITORY"
40
37
 
41
38
 
42
39
  class GithubExceptionType(Exception):
@@ -82,8 +79,12 @@ class GhIssueComment(Protocol):
82
79
 
83
80
 
84
81
  class GhIssue(Protocol):
85
- def get_comments(self) -> Iterable[GhIssueComment]: ...
86
- def create_comment(self, body: str) -> None: ...
82
+ def get_comments(self) -> Iterable[GhIssueComment]:
83
+ """Return the comments on the issue."""
84
+ raise NotImplementedError
85
+
86
+ def create_comment(self, body: str) -> None:
87
+ """Create a new comment on the issue."""
87
88
 
88
89
 
89
90
  class GhPullRequest(Protocol):
@@ -91,17 +92,28 @@ class GhPullRequest(Protocol):
91
92
  title: str | None
92
93
  body: str | None
93
94
 
94
- def as_issue(self) -> GhIssue: ...
95
- def edit(self, *, state: str) -> None: ...
95
+ def as_issue(self) -> GhIssue:
96
+ """Return the issue view of this pull request."""
97
+ raise NotImplementedError
98
+
99
+ def edit(self, *, state: str) -> None:
100
+ """Edit the pull request state."""
96
101
 
97
102
 
98
103
  class GhRepository(Protocol):
99
- def get_pull(self, number: int) -> GhPullRequest: ...
100
- def get_pulls(self, state: str) -> Iterable[GhPullRequest]: ...
104
+ def get_pull(self, number: int) -> GhPullRequest:
105
+ """Return the pull request with the given number."""
106
+ raise NotImplementedError
107
+
108
+ def get_pulls(self, state: str) -> Iterable[GhPullRequest]:
109
+ """Return the pull requests with the given state."""
110
+ raise NotImplementedError
101
111
 
102
112
 
103
113
  class GhClient(Protocol):
104
- def get_repo(self, full: str) -> GhRepository: ...
114
+ def get_repo(self, full: str) -> GhRepository:
115
+ """Return the repository for the given full name."""
116
+ raise NotImplementedError
105
117
 
106
118
 
107
119
  __all__ = [
@@ -120,8 +132,6 @@ __all__ = [
120
132
 
121
133
  log = logging.getLogger("github2gerrit.github_api")
122
134
 
123
- _T = TypeVar("_T")
124
-
125
135
 
126
136
  def _getenv_str(name: str) -> str:
127
137
  val = os.getenv(name, "")
@@ -151,11 +151,6 @@ def derive_base_path(host: str) -> str | None:
151
151
  return _KNOWN_BASE_PATHS.get(host.lower().strip())
152
152
 
153
153
 
154
- # Keep a private alias so internal callers don't break if they were
155
- # referencing the underscore-prefixed name during development.
156
- _derive_base_path = derive_base_path
157
-
158
-
159
154
  # ───────────────────────────────────────────────────────────────────────
160
155
  # Pure parser
161
156
  # ───────────────────────────────────────────────────────────────────────
@@ -555,6 +555,8 @@ def git_commit_amend(
555
555
  if committer_email and _has_committer_signoff(body):
556
556
  effective_signoff = False
557
557
  except GitError:
558
+ # Unable to read the current commit body (e.g. no HEAD yet);
559
+ # fall back to honoring the requested signoff setting.
558
560
  pass
559
561
  except Exception:
560
562
  # Best effort only; default to requested signoff
@@ -613,7 +615,6 @@ def git_commit_new(
613
615
  _tf.flush()
614
616
  tmp_path = Path(_tf.name)
615
617
  message_file = tmp_path
616
- message = None
617
618
 
618
619
  # Determine whether to add -s; only suppress if message already has a
619
620
  # sign-off for current committer
@@ -379,7 +379,6 @@ def save_host_keys_to_config(
379
379
  new_lines.append(f"[{organization}]")
380
380
  escaped_keys = host_keys.replace("\n", "\\n")
381
381
  new_lines.append(f'GERRIT_KNOWN_HOSTS = "{escaped_keys}"')
382
- gerrit_known_hosts_updated = True
383
382
 
384
383
  # If section existed but didn't have GERRIT_KNOWN_HOSTS, add it
385
384
  elif not gerrit_known_hosts_updated:
@@ -17,6 +17,7 @@ Covers two mechanisms:
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
+ import hashlib
20
21
  from dataclasses import dataclass
21
22
  from typing import Any
22
23
  from unittest.mock import MagicMock
@@ -31,6 +32,7 @@ from github2gerrit.gerrit_query import query_open_changes_by_project
31
32
  from github2gerrit.gitreview import GerritInfo
32
33
  from github2gerrit.models import GitHubContext
33
34
  from github2gerrit.similarity import extract_dependency_package_from_subject
35
+ from github2gerrit.utils import reset_warning_once
34
36
 
35
37
 
36
38
  # -------------------------------------------------------------------
@@ -38,6 +40,46 @@ from github2gerrit.similarity import extract_dependency_package_from_subject
38
40
  # -------------------------------------------------------------------
39
41
 
40
42
 
43
+ # Dependency fixture values reused across the supersession tests.
44
+ # Centralising them documents which data is significant to the logic under
45
+ # test (the *package name* must match for a change to be superseded) versus
46
+ # which is arbitrary (the exact version strings).
47
+ _DEP_PACKAGE = "requests"
48
+ _DEP_VERSION_OLD = "2.31.0"
49
+ _DEP_VERSION_NEW = "2.32.0"
50
+ _DEP_VERSION_INTERMEDIATE = "2.31.5"
51
+ _DEP_BUMP_TITLE = (
52
+ f"Bump {_DEP_PACKAGE} from {_DEP_VERSION_OLD} to {_DEP_VERSION_NEW}"
53
+ )
54
+
55
+
56
+ @pytest.fixture(autouse=True)
57
+ def _reset_warning_once_state() -> None:
58
+ """Reset the one-time warning cache before each test.
59
+
60
+ Several tests assert warn-once behaviour (for example the truncation
61
+ warning emitted by the anonymous fallback), which relies on process-wide
62
+ global state. Resetting it automatically keeps every test isolated so
63
+ individual tests no longer need to call ``reset_warning_once`` by hand.
64
+ """
65
+ reset_warning_once()
66
+
67
+
68
+ def _make_change_id(seed: str) -> str:
69
+ """Return a valid-looking Gerrit Change-Id derived from ``seed``.
70
+
71
+ A real Gerrit Change-Id is the letter ``I`` followed by 40 hexadecimal
72
+ characters (a SHA-1 digest). Tests previously used mnemonic but invalid
73
+ values such as ``Iexist00...`` that contain non-hex characters. This
74
+ helper instead derives a truncated SHA-256 digest from a human-readable
75
+ seed: the exact hash algorithm is unimportant, it only needs to match
76
+ the ``I`` + 40-hex-character format while keeping failures easy to
77
+ trace.
78
+ """
79
+ digest = hashlib.sha256(seed.encode("utf-8")).hexdigest()[:40]
80
+ return f"I{digest}"
81
+
82
+
41
83
  def _gerrit_change(
42
84
  *,
43
85
  change_id: str = "Iaaaa000000000000000000000000000000000000",
@@ -202,10 +244,6 @@ class TestQueryOpenChangesByProject:
202
244
  to drive the anonymous fallback, the helper must short-circuit and
203
245
  never issue the request, returning an empty list.
204
246
  """
205
- from github2gerrit.utils import reset_warning_once
206
-
207
- reset_warning_once()
208
-
209
247
  fake_client = MagicMock()
210
248
  fake_client.is_authenticated = False
211
249
 
@@ -232,10 +270,6 @@ class TestQueryOpenChangesByProject:
232
270
  The query must NOT use owner:self, and only changes whose
233
271
  GitHub-PR trailer targets the current repository are returned.
234
272
  """
235
- from github2gerrit.utils import reset_warning_once
236
-
237
- reset_warning_once()
238
-
239
273
  ours = _raw_change(
240
274
  100,
241
275
  "chore: bump requests from 2.31.0 to 2.32.0",
@@ -262,10 +296,6 @@ class TestQueryOpenChangesByProject:
262
296
 
263
297
  def test_anonymous_fallback_repo_match_is_case_insensitive(self) -> None:
264
298
  """Repo trailer matching ignores case differences."""
265
- from github2gerrit.utils import reset_warning_once
266
-
267
- reset_warning_once()
268
-
269
299
  ours = _raw_change(
270
300
  200,
271
301
  "chore: bump requests from 2.31.0 to 2.32.0",
@@ -283,10 +313,6 @@ class TestQueryOpenChangesByProject:
283
313
 
284
314
  def test_anonymous_fallback_matches_ghes_host(self) -> None:
285
315
  """The host is ignored, so GitHub Enterprise URLs still match."""
286
- from github2gerrit.utils import reset_warning_once
287
-
288
- reset_warning_once()
289
-
290
316
  ours = _raw_change(
291
317
  210,
292
318
  "chore: bump requests from 2.31.0 to 2.32.0",
@@ -310,10 +336,6 @@ class TestQueryOpenChangesByProject:
310
336
  repo in its query string, a non-URL value, and a relative path
311
337
  (e.g. ``/org/repo/pull/1``) are all correctly excluded.
312
338
  """
313
- from github2gerrit.utils import reset_warning_once
314
-
315
- reset_warning_once()
316
-
317
339
  other = _raw_change(
318
340
  220,
319
341
  "chore: bump requests from 2.31.0 to 2.32.0",
@@ -346,10 +368,6 @@ class TestQueryOpenChangesByProject:
346
368
  GitHub2Gerrit changes (which carry the GitHub-PR trailer) so busy
347
369
  projects do not truncate before the relevant changes are seen.
348
370
  """
349
- from github2gerrit.utils import reset_warning_once
350
-
351
- reset_warning_once()
352
-
353
371
  fake_client = MagicMock()
354
372
  fake_client.is_authenticated = False
355
373
  fake_client.get.return_value = []
@@ -369,10 +387,6 @@ class TestQueryOpenChangesByProject:
369
387
  """Hitting the result cap emits a one-time truncation warning."""
370
388
  import logging
371
389
 
372
- from github2gerrit.utils import reset_warning_once
373
-
374
- reset_warning_once()
375
-
376
390
  c1 = _raw_change(
377
391
  300,
378
392
  "chore: bump x from 1 to 2",
@@ -406,9 +420,6 @@ class TestQueryOpenChangesByProject:
406
420
  self, monkeypatch: pytest.MonkeyPatch
407
421
  ) -> None:
408
422
  """Disabling the fallback flag skips even when repo is known."""
409
- from github2gerrit.utils import reset_warning_once
410
-
411
- reset_warning_once()
412
423
  monkeypatch.setenv("G2G_ANON_SUPERSEDE_FALLBACK", "false")
413
424
 
414
425
  fake_client = MagicMock()
@@ -422,10 +433,6 @@ class TestQueryOpenChangesByProject:
422
433
 
423
434
  def test_anonymous_fallback_whitespace_repo_skips(self) -> None:
424
435
  """A blank/whitespace-only repository must not trigger a query."""
425
- from github2gerrit.utils import reset_warning_once
426
-
427
- reset_warning_once()
428
-
429
436
  fake_client = MagicMock()
430
437
  fake_client.is_authenticated = False
431
438
 
@@ -446,7 +453,16 @@ class TestQueryOpenChangesByProject:
446
453
  new=lambda *_a, **_kw: "",
447
454
  )
448
455
  class TestAbandonSupersededDependencyChanges:
449
- """Tests for the post-push abandon sweep."""
456
+ """Tests for the post-push abandon sweep.
457
+
458
+ The class-level ``@patch`` stubs
459
+ ``gerrit_urls._discover_base_path_for_host`` to return an empty base
460
+ path for *every* test method below, so URL construction does not attempt
461
+ host discovery (a network call). The patch is intentionally applied at
462
+ the class level because all methods exercise URL building; it is not
463
+ injected as a method argument because the stubbed value is never
464
+ inspected by the tests.
465
+ """
450
466
 
451
467
  @patch("github2gerrit.gerrit_rest.build_client_for_host")
452
468
  @patch("github2gerrit.gerrit_query.query_open_changes_by_project")
@@ -789,7 +805,7 @@ class TestStrategy5Integration:
789
805
  # Set up GitHub mocks — strategies 1-4 should all fail
790
806
  mock_get_pull.return_value = _FakePullRequest(
791
807
  number=42,
792
- title="Bump requests from 2.31.0 to 2.32.0",
808
+ title=_DEP_BUMP_TITLE,
793
809
  user=_FakeGitHubUser(),
794
810
  )
795
811
  mock_get_repo.return_value = MagicMock()
@@ -803,10 +819,14 @@ class TestStrategy5Integration:
803
819
  mock_gerrit_client.return_value = fake_gerrit
804
820
 
805
821
  # Strategy 5 — return an open change bumping the same package
822
+ existing_change_id = _make_change_id("strategy5-existing")
806
823
  existing_change = _gerrit_change(
807
- change_id="Iexist00000000000000000000000000000000000",
824
+ change_id=existing_change_id,
808
825
  number="999",
809
- subject="chore: bump requests from 2.31.0 to 2.31.5",
826
+ subject=(
827
+ f"chore: bump {_DEP_PACKAGE} from "
828
+ f"{_DEP_VERSION_OLD} to {_DEP_VERSION_INTERMEDIATE}"
829
+ ),
810
830
  )
811
831
  mock_query_open.return_value = [existing_change]
812
832
 
@@ -832,7 +852,7 @@ class TestStrategy5Integration:
832
852
  )
833
853
 
834
854
  result = orch._find_existing_change_for_pr(gh, gerrit)
835
- assert result == ["Iexist00000000000000000000000000000000000"]
855
+ assert result == [existing_change_id]
836
856
 
837
857
  @patch("github2gerrit.core.build_client")
838
858
  @patch("github2gerrit.core.get_repo_from_env")
@@ -956,7 +976,14 @@ class TestStrategy5Integration:
956
976
  new=lambda *_a, **_kw: "",
957
977
  )
958
978
  class TestRealWorldScenarios:
959
- """Scenarios taken directly from the issue report."""
979
+ """Scenarios taken directly from the issue report.
980
+
981
+ As with :class:`TestAbandonSupersededDependencyChanges`, the class-level
982
+ ``@patch`` stubs ``gerrit_urls._discover_base_path_for_host`` for every
983
+ method so URL construction avoids host discovery. The stub uses ``new=``
984
+ rather than a mock argument because its return value is fixed and never
985
+ asserted on.
986
+ """
960
987
 
961
988
  @patch("github2gerrit.gerrit_rest.build_client_for_host")
962
989
  @patch("github2gerrit.gerrit_query.query_open_changes_by_project")
@@ -222,6 +222,8 @@ def test_curl_download_success(
222
222
  output_path = Path(cmd[o_index + 1])
223
223
  output_path.write_text("downloaded content")
224
224
  except (ValueError, IndexError):
225
+ # No "-o" flag (or no value after it) in the mocked curl
226
+ # command; nothing to write, so behave like a no-op download.
225
227
  pass
226
228
  return MockCompletedProcess()
227
229
 
@@ -278,8 +280,6 @@ def test_log_api_metrics_summary(
278
280
  monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
279
281
  ) -> None:
280
282
  """Test API metrics summary logging."""
281
- import logging
282
-
283
283
  # Set up some metrics
284
284
  reset_api_metrics()
285
285
 
@@ -16,19 +16,40 @@ from github2gerrit.gitutils import run_cmd
16
16
  from github2gerrit.gitutils import run_cmd_with_retries
17
17
 
18
18
 
19
+ # Environment variables that pytest, pytest-cov, and pytest-xdist inject so
20
+ # that coverage measurement and test metadata propagate into subprocesses.
21
+ # When a test spawns the current Python interpreter via ``run_cmd`` we clear
22
+ # these so the child does not start its own coverage run (which can emit
23
+ # warnings or write stray data files) and so its output stays deterministic
24
+ # regardless of how the outer pytest session was invoked.
25
+ #
26
+ # COV_CORE_SOURCE/CONFIG/DATAFILE pytest-cov subprocess coverage hooks
27
+ # COVERAGE_PROCESS_START triggers coverage startup in children
28
+ # COVERAGE_FILE / COVERAGE_RCFILE coverage data / config file locations
29
+ # PYTEST_ADDOPTS extra pytest args inherited by children
30
+ # PYTEST_CURRENT_TEST current test id (set per test by pytest)
31
+ # PYTEST_XDIST_WORKER xdist worker id when running in parallel
32
+ _COVERAGE_ENV_KEYS = (
33
+ "COV_CORE_SOURCE",
34
+ "COV_CORE_CONFIG",
35
+ "COV_CORE_DATAFILE",
36
+ "COVERAGE_PROCESS_START",
37
+ "COVERAGE_FILE",
38
+ "COVERAGE_RCFILE",
39
+ "PYTEST_ADDOPTS",
40
+ "PYTEST_CURRENT_TEST",
41
+ "PYTEST_XDIST_WORKER",
42
+ )
43
+
44
+
19
45
  def _no_cov_env() -> dict[str, str]:
20
- keys = [
21
- "COV_CORE_SOURCE",
22
- "COV_CORE_CONFIG",
23
- "COV_CORE_DATAFILE",
24
- "COVERAGE_PROCESS_START",
25
- "COVERAGE_FILE",
26
- "COVERAGE_RCFILE",
27
- "PYTEST_ADDOPTS",
28
- "PYTEST_CURRENT_TEST",
29
- "PYTEST_XDIST_WORKER",
30
- ]
31
- return dict.fromkeys(keys, "")
46
+ """Return an env mapping that disables coverage in subprocesses.
47
+
48
+ Every key in :data:`_COVERAGE_ENV_KEYS` is mapped to an empty string so
49
+ a spawned child process inherits a clean, coverage-free environment. See
50
+ that constant for the rationale behind each cleared variable.
51
+ """
52
+ return dict.fromkeys(_COVERAGE_ENV_KEYS, "")
32
53
 
33
54
 
34
55
  def test_mask_text_replaces_tokens_and_ignores_empty() -> None:
@@ -93,24 +114,31 @@ def test_run_cmd_with_retries_retries_on_transient_error_then_succeeds(
93
114
 
94
115
  monkeypatch.setattr("time.sleep", _fake_sleep)
95
116
 
96
- # Script: on first run create a marker and exit non-zero with a transient
97
- # error in stderr.
98
- # On subsequent runs (marker exists) print success and exit 0.
117
+ # The helper script creates a marker file and fails with a transient
118
+ # error on its first run, then succeeds once the marker exists. Writing
119
+ # the behaviour to a real script file (taking the marker path as an
120
+ # argument) keeps the test readable and avoids embedding Python source
121
+ # inside an f-string.
99
122
  marker = tmp_path / "attempted"
100
-
101
- code = f"""
102
- import os, sys, pathlib
103
- p = pathlib.Path({str(marker)!r})
104
- if not p.exists():
105
- p.write_text("1", encoding="utf-8")
106
- print("could not resolve host: example.com", file=sys.stderr)
107
- sys.exit(128)
108
- else:
109
- print("ok")
110
- """
123
+ script = tmp_path / "transient_then_succeed.py"
124
+ script.write_text(
125
+ "import pathlib\n"
126
+ "import sys\n"
127
+ "\n"
128
+ "marker = pathlib.Path(sys.argv[1])\n"
129
+ "if not marker.exists():\n"
130
+ ' marker.write_text("1", encoding="utf-8")\n'
131
+ ' print("could not resolve host: example.com", file=sys.stderr)\n'
132
+ " sys.exit(128)\n"
133
+ "else:\n"
134
+ ' print("ok")\n',
135
+ encoding="utf-8",
136
+ )
111
137
 
112
138
  res = run_cmd_with_retries(
113
- [sys.executable, "-c", code], cwd=tmp_path, env=_no_cov_env()
139
+ [sys.executable, str(script), str(marker)],
140
+ cwd=tmp_path,
141
+ env=_no_cov_env(),
114
142
  )
115
143
  # Should succeed after retry, marker must exist, and we should have slept at
116
144
  # least once
@@ -222,7 +250,7 @@ def test_git_last_commit_trailers_parsing_edge_cases(
222
250
  ) -> None:
223
251
  # Patch git_show to return a crafted commit message body with various
224
252
  # trailer forms.
225
- import github2gerrit.gitutils as gitutils
253
+ from github2gerrit.gitutils import git_last_commit_trailers
226
254
 
227
255
  body = (
228
256
  "Subject line\n\n"
@@ -235,10 +263,12 @@ def test_git_last_commit_trailers_parsing_edge_cases(
235
263
  "Change-Id: Iabc123abc123abc123abc123abc123abc123ab\n"
236
264
  )
237
265
 
238
- monkeypatch.setattr(gitutils, "git_show", lambda rev, **kw: body)
266
+ monkeypatch.setattr(
267
+ "github2gerrit.gitutils.git_show", lambda rev, **kw: body
268
+ )
239
269
 
240
270
  # Without filter: expect only proper footer trailers collected.
241
- trailers_all = gitutils.git_last_commit_trailers()
271
+ trailers_all = git_last_commit_trailers()
242
272
  # Body "Key:" lines should not be extracted as trailers
243
273
  assert "Key" not in trailers_all
244
274
  # 'Empty' should be ignored due to empty value
@@ -254,7 +284,7 @@ def test_git_last_commit_trailers_parsing_edge_cases(
254
284
  ]
255
285
 
256
286
  # With filter keys: only Change-Id should be returned.
257
- trailers_change = gitutils.git_last_commit_trailers(keys=["Change-Id"])
287
+ trailers_change = git_last_commit_trailers(keys=["Change-Id"])
258
288
  assert list(trailers_change.keys()) == ["Change-Id"]
259
289
  assert trailers_change["Change-Id"] == [
260
290
  "Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
@@ -262,6 +292,24 @@ def test_git_last_commit_trailers_parsing_edge_cases(
262
292
  ]
263
293
 
264
294
 
295
+ def test_git_last_commit_trailers_returns_empty_on_git_error(
296
+ monkeypatch: pytest.MonkeyPatch,
297
+ ) -> None:
298
+ """A failing ``git show`` (e.g. fresh repo) yields an empty mapping."""
299
+ from github2gerrit.gitutils import GitError
300
+ from github2gerrit.gitutils import git_last_commit_trailers
301
+
302
+ def _raise_git_error(rev: str, **_kw: object) -> str:
303
+ raise GitError("git show HEAD failed")
304
+
305
+ monkeypatch.setattr("github2gerrit.gitutils.git_show", _raise_git_error)
306
+
307
+ # The implementation catches GitError and returns an empty dict, both
308
+ # without and with a key filter.
309
+ assert git_last_commit_trailers() == {}
310
+ assert git_last_commit_trailers(keys=["Change-Id"]) == {}
311
+
312
+
265
313
  def test_git_quiet_suppresses_failure_logging(
266
314
  caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch
267
315
  ) -> None:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes