From a8a87bc9316432f9dca3d7db669f324d374f0b60 Mon Sep 17 00:00:00 2001 From: Ihor Solodrai Date: Wed, 15 Oct 2025 16:10:38 -0700 Subject: [PATCH 1/2] branch_worker: minor typecheck fixes Internal Meta typechecker reported a couple of issues in recent PR comments forwardning feature [1]. Fix them with additional checks and local variables. [1] https://github.com/kernel-patches/kernel-patches-daemon/pull/25 Signed-off-by: Ihor Solodrai --- kernel_patches_daemon/branch_worker.py | 37 +++++++++++++++++--------- kernel_patches_daemon/config.py | 6 ----- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/kernel_patches_daemon/branch_worker.py b/kernel_patches_daemon/branch_worker.py index a8965ce..44950cf 100644 --- a/kernel_patches_daemon/branch_worker.py +++ b/kernel_patches_daemon/branch_worker.py @@ -37,7 +37,11 @@ from github.Repository import Repository from github.WorkflowJob import WorkflowJob -from kernel_patches_daemon.config import EmailConfig, SERIES_TARGET_SEPARATOR +from kernel_patches_daemon.config import ( + EmailConfig, + PRCommentsForwardingConfig, + SERIES_TARGET_SEPARATOR, +) from kernel_patches_daemon.github_connector import GithubConnector from kernel_patches_daemon.github_logs import GithubLogExtractor from kernel_patches_daemon.patchwork import Patchwork, Series, Subject @@ -405,7 +409,7 @@ def reply_email_recipients( async def send_pr_comment_email( - config: EmailConfig, msg: EmailMessage, body: str + email_config: EmailConfig, msg: EmailMessage, body: str ) -> Optional[str]: """ This function forwards a pull request comment (`body`) as an email reply to the original @@ -413,17 +417,19 @@ async def send_pr_comment_email( and always_cc as configured, and sets Subject and In-Reply-To based on the `msg` Args: - config: EmailConfig with PRCommentsForwardingConfig + email_config: EmailConfig with PRCommentsForwardingConfig msg: the original EmailMessage we are replying to (the patch submission) body: the content of the reply we are sending Returns: Message-Id of the sent email, or None if it wasn't sent """ - if config is None or not config.is_pr_comment_forwarding_enabled(): + if email_config.pr_comments_forwarding is None: + return + cfg: PRCommentsForwardingConfig = email_config.pr_comments_forwarding + if not cfg.enabled: return - cfg = config.pr_comments_forwarding (to_list, cc_list) = reply_email_recipients( msg, allowlist=cfg.recipient_allowlist, denylist=cfg.recipient_denylist ) @@ -435,7 +441,7 @@ async def send_pr_comment_email( subject = "Re: " + msg.get("Subject") in_reply_to = msg.get("Message-Id") - return await send_email(config, to_list, cc_list, subject, body, in_reply_to) + return await send_email(email_config, to_list, cc_list, subject, body, in_reply_to) def pr_has_label(pr: PullRequest, label: str) -> bool: @@ -1293,6 +1299,8 @@ async def evaluate_ci_result( logger.info("No email configuration present; skipping sending...") return + email_cfg: EmailConfig = self.email_config + if status in (Status.PENDING, Status.SKIPPED): return @@ -1329,7 +1337,7 @@ async def evaluate_ci_result( subject = await get_ci_email_subject(series) ctx = build_email_body_context(self.repo, pr, status, series, inline_logs) body = furnish_ci_email_body(ctx) - await send_ci_results_email(self.email_config, series, subject, body) + await send_ci_results_email(email_cfg, series, subject, body) bump_email_status_counters(status) def expire_branches(self) -> None: @@ -1379,12 +1387,15 @@ async def submit_pr_summary( pr_summary_report.add(1) async def forward_pr_comments(self, pr: PullRequest, series: Series): - if ( - self.email_config is None - or not self.email_config.is_pr_comment_forwarding_enabled() - ): + # The checks and local variables are needed to make type checker happy + if self.email_config is None: + return + email_cfg: EmailConfig = self.email_config + if email_cfg.pr_comments_forwarding is None: + return + cfg: PRCommentsForwardingConfig = email_cfg.pr_comments_forwarding + if not cfg.enabled: return - cfg = self.email_config.pr_comments_forwarding comments = pr.get_issue_comments() @@ -1441,7 +1452,7 @@ async def forward_pr_comments(self, pr: PullRequest, series: Series): # and forward the target comment via email sent_msg_id = await send_pr_comment_email( - self.email_config, patch_msg, comment.body + email_cfg, patch_msg, comment.body ) if sent_msg_id is not None: logger.info( diff --git a/kernel_patches_daemon/config.py b/kernel_patches_daemon/config.py index b845f6e..142a9ac 100644 --- a/kernel_patches_daemon/config.py +++ b/kernel_patches_daemon/config.py @@ -168,12 +168,6 @@ def from_json(cls, json: Dict) -> "EmailConfig": ), ) - def is_pr_comment_forwarding_enabled(self) -> bool: - if self.pr_comments_forwarding is not None: - return self.pr_comments_forwarding.enabled - else: - return False - @dataclass class PatchworksConfig: From 20155c37352d68e10deea0f587437cf7f53c9fda Mon Sep 17 00:00:00 2001 From: Ihor Solodrai Date: Wed, 15 Oct 2025 16:17:52 -0700 Subject: [PATCH 2/2] ci: typecheck with pyrefly pyrefly [1] is a modern type checker, suggested as a successor of pyre-check [2]. Add pyrefly configuration to pyproject.toml Add pyrefly check run in main ci workflow. Following the recommendation from the docs, annotate existing errors detected by pyrefly [3]. We'll be removing them incrementally with new code changes. [1] https://github.com/facebook/pyrefly [2] https://github.com/facebook/pyre-check [3] https://pyrefly.org/en/docs/error-suppressions/#upgrading-pyrefly-and-other-changes-that-introduce-new-type-errors Signed-off-by: Ihor Solodrai --- .github/workflows/ci.yml | 4 +++ kernel_patches_daemon/branch_worker.py | 43 +++++++++++++++++++++++ kernel_patches_daemon/daemon.py | 3 ++ kernel_patches_daemon/github_connector.py | 2 ++ kernel_patches_daemon/github_logs.py | 1 + kernel_patches_daemon/github_sync.py | 9 +++++ kernel_patches_daemon/patchwork.py | 14 ++++++++ pyproject.toml | 10 ++++++ tests/test_branch_worker.py | 15 ++++++++ tests/test_daemon.py | 2 ++ tests/test_github_connector.py | 15 +++++++- tests/test_github_sync.py | 6 ++++ tests/test_patchwork.py | 23 ++++++++++-- 13 files changed, 143 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4dafcc6..0ffe55a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,6 +45,10 @@ jobs: run: | python -m poetry install + - name: Run typecheck + run: | + python -m poetry run pyrefly check + - name: Run tests run: | python -m poetry run python -m unittest diff --git a/kernel_patches_daemon/branch_worker.py b/kernel_patches_daemon/branch_worker.py index 44950cf..2d6743b 100644 --- a/kernel_patches_daemon/branch_worker.py +++ b/kernel_patches_daemon/branch_worker.py @@ -392,8 +392,10 @@ def reply_email_recipients( """ tos = msg.get_all("To", []) ccs = msg.get_all("Cc", []) + # pyrefly: ignore # implicit-import cc_list = [a for (_, a) in email.utils.getaddresses(tos + ccs)] + # pyrefly: ignore # implicit-import, bad-argument-type (_, sender_address) = email.utils.parseaddr(msg.get("From")) to_list = [sender_address] @@ -438,6 +440,7 @@ async def send_pr_comment_email( if not to_list and not cc_list: return + # pyrefly: ignore # unsupported-operation subject = "Re: " + msg.get("Subject") in_reply_to = msg.get("Message-Id") @@ -465,6 +468,7 @@ def _uniq_tmp_folder( sha.update(f"{url}/{branch}".encode("utf-8")) # pyre-fixme[6]: For 1st argument expected `PathLike[Variable[AnyStr <: [str, # bytes]]]` but got `Optional[str]`. + # pyrefly: ignore # no-matching-overload repo_name = remove_unsafe_chars(os.path.basename(url)) return os.path.join(base_directory, f"pw_sync_{repo_name}_{sha.hexdigest()}") @@ -510,6 +514,7 @@ def parse_pr_ref(ref: str) -> Dict[str, Any]: tmp = res["series"].split("/", maxsplit=1) if len(tmp) >= 2 and tmp[1].isdigit(): + # pyrefly: ignore # unsupported-operation res["series_id"] = int(tmp[1]) return res @@ -610,6 +615,7 @@ def _is_outdated_pr(pr: PullRequest) -> bool: # that have it as a parent will also be changed. commit = commits[commits.totalCount - 1] last_modified = dateutil.parser.parse(commit.stats.last_modified) + # pyrefly: ignore # unsupported-operation age = datetime.now(timezone.utc) - last_modified logger.info(f"Pull request {pr} has age {age}") return age > PULL_REQUEST_TTL @@ -733,16 +739,20 @@ def update_e2e_test_branch_and_update_pr( # Now that we have an updated base branch, create a dummy commit on top # so that we can actually create a pull request (this path tests # upstream directly, without any mailbox patches applied). + # pyrefly: ignore # missing-attribute self.repo_local.git.checkout("-B", branch_name) + # pyrefly: ignore # missing-attribute self.repo_local.git.commit("--allow-empty", "--message", "Dummy commit") title = f"[test] {branch_name}" # Force push only if there is no branch or code changes. pushed = False + # pyrefly: ignore # missing-attribute if branch_name not in self.branches or self.repo_local.git.diff( branch_name, f"remotes/origin/{branch_name}" ): + # pyrefly: ignore # missing-attribute self.repo_local.git.push("--force", "origin", branch_name) pushed = True @@ -756,18 +766,25 @@ def can_do_sync(self) -> bool: def do_sync(self) -> None: # fetch most recent upstream + # pyrefly: ignore # missing-attribute if UPSTREAM_REMOTE_NAME in [x.name for x in self.repo_local.remotes]: + # pyrefly: ignore # missing-attribute urls = list(self.repo_local.remote(UPSTREAM_REMOTE_NAME).urls) if urls != [self.upstream_url]: logger.warning(f"remote upstream set to track {urls}, re-creating") + # pyrefly: ignore # missing-attribute self.repo_local.delete_remote(UPSTREAM_REMOTE_NAME) + # pyrefly: ignore # missing-attribute self.repo_local.create_remote(UPSTREAM_REMOTE_NAME, self.upstream_url) else: + # pyrefly: ignore # missing-attribute self.repo_local.create_remote(UPSTREAM_REMOTE_NAME, self.upstream_url) + # pyrefly: ignore # missing-attribute upstream_repo = self.repo_local.remote(UPSTREAM_REMOTE_NAME) upstream_repo.fetch(self.upstream_branch) upstream_branch = getattr(upstream_repo.refs, self.upstream_branch) _reset_repo(self.repo_local, f"{UPSTREAM_REMOTE_NAME}/{self.upstream_branch}") + # pyrefly: ignore # missing-attribute self.repo_local.git.push( "--force", "origin", f"{upstream_branch}:refs/heads/{self.repo_branch}" ) @@ -807,6 +824,7 @@ def fetch_repo_branch(self) -> None: """ Fetch the repository branch of interest only once """ + # pyrefly: ignore # bad-assignment self.repo_local = self.fetch_repo( self.repo_dir, self.repo_url, self.repo_branch ) @@ -830,15 +848,19 @@ def _update_pr_base_branch(self, base_branch: str): self._add_ci_files() try: + # pyrefly: ignore # missing-attribute diff = self.repo_local.git.diff(f"remotes/origin/{base_branch}") except git.exc.GitCommandError: # The remote may not exist, in which case we want to push. diff = True if diff: + # pyrefly: ignore # missing-attribute self.repo_local.git.checkout("-B", base_branch) + # pyrefly: ignore # missing-attribute self.repo_local.git.push("--force", "origin", f"refs/heads/{base_branch}") else: + # pyrefly: ignore # missing-attribute self.repo_local.git.checkout("-B", base_branch, f"origin/{base_branch}") def _create_dummy_commit(self, branch_name: str) -> None: @@ -846,8 +868,11 @@ def _create_dummy_commit(self, branch_name: str) -> None: Reset branch, create dummy commit """ _reset_repo(self.repo_local, f"{UPSTREAM_REMOTE_NAME}/{self.upstream_branch}") + # pyrefly: ignore # missing-attribute self.repo_local.git.checkout("-B", branch_name) + # pyrefly: ignore # missing-attribute self.repo_local.git.commit("--allow-empty", "--message", "Dummy commit") + # pyrefly: ignore # missing-attribute self.repo_local.git.push("--force", "origin", branch_name) def _close_pr(self, pr: PullRequest) -> None: @@ -920,6 +945,7 @@ async def _comment_series_pr( if not pr and can_create and not close: # If there is no merge conflict and no change, ignore the series + # pyrefly: ignore # missing-attribute if not has_merge_conflict and not self.repo_local.git.diff( self.repo_pr_base_branch, branch_name ): @@ -1016,9 +1042,12 @@ def _add_ci_files(self) -> None: """ if Path(f"{self.ci_repo_dir}/.github").exists(): execute_command(f"cp --archive {self.ci_repo_dir}/.github {self.repo_dir}") + # pyrefly: ignore # missing-attribute self.repo_local.git.add("--force", ".github") execute_command(f"cp --archive {self.ci_repo_dir}/* {self.repo_dir}") + # pyrefly: ignore # missing-attribute self.repo_local.git.add("--all", "--force") + # pyrefly: ignore # missing-attribute self.repo_local.git.commit("--all", "--message", "adding ci files") async def try_apply_mailbox_series( @@ -1028,17 +1057,20 @@ async def try_apply_mailbox_series( # The pull request will be created against `repo_pr_base_branch`. So # prepare it for that. self._update_pr_base_branch(self.repo_pr_base_branch) + # pyrefly: ignore # missing-attribute self.repo_local.git.checkout("-B", branch_name) # Apply series patch_content = await series.get_patch_binary_content() with temporary_patch_file(patch_content) as tmp_patch_file: try: + # pyrefly: ignore # missing-attribute self.repo_local.git.am("--3way", istream=tmp_patch_file) except git.exc.GitCommandError as e: logger.warning( f"Failed complete 3-way merge series {series.id} patch into {branch_name} branch: {e}" ) + # pyrefly: ignore # missing-attribute conflict = self.repo_local.git.diff() return (False, e, conflict) return (True, None, None) @@ -1057,6 +1089,7 @@ async def apply_push_comment( # In other words, patchwork could be reporting a relevant # status (ie. !accepted) while the series has already been # merged and pushed. + # pyrefly: ignore # bad-argument-type if await _series_already_applied(self.repo_local, series): logger.info(f"Series {series.url} already applied to tree") raise NewPRWithNoChangeException(self.repo_pr_base_branch, branch_name) @@ -1080,6 +1113,7 @@ async def apply_push_comment( if branch_name in self.branches and ( branch_name not in self.all_prs # NO PR yet or _is_branch_changed( + # pyrefly: ignore # bad-argument-type self.repo_local, f"remotes/origin/{self.repo_pr_base_branch}", f"remotes/origin/{branch_name}", @@ -1095,10 +1129,12 @@ async def apply_push_comment( can_create=True, ) assert pr + # pyrefly: ignore # missing-attribute self.repo_local.git.push("--force", "origin", branch_name) # Metadata inside `pr` may be stale from the force push; refresh it pr.update() + # pyrefly: ignore # missing-attribute wanted_sha = self.repo_local.head.commit.hexsha for _ in range(30): if pr.head.sha == wanted_sha: @@ -1112,9 +1148,11 @@ async def apply_push_comment( return pr # we don't have a branch, also means no PR, push first then create PR elif branch_name not in self.branches: + # pyrefly: ignore # missing-attribute if not self.repo_local.git.diff(self.repo_pr_base_branch, branch_name): # raise an exception so it bubbles up to the caller. raise NewPRWithNoChangeException(self.repo_pr_base_branch, branch_name) + # pyrefly: ignore # missing-attribute self.repo_local.git.push("--force", "origin", branch_name) return await self._comment_series_pr( series, @@ -1185,9 +1223,11 @@ def closed_prs(self) -> List[Any]: # closed prs are last resort to re-open expired PRs # and also required for branch expiration if not self._closed_prs: + # pyrefly: ignore # bad-assignment self._closed_prs = list( self.repo.get_pulls(state="closed", base=self.repo_pr_base_branch) ) + # pyrefly: ignore # bad-return return self._closed_prs def filter_closed_pr(self, head: str) -> Optional[PullRequest]: @@ -1229,6 +1269,7 @@ async def sync_checks(self, pr: PullRequest, series: Series) -> None: # completed ones. The reason being that the information that pending # ones are present is very much relevant for status reporting. for run in self.repo.get_workflow_runs( + # pyrefly: ignore # bad-argument-type actor=self.user_login, head_sha=pr.head.sha, ): @@ -1432,6 +1473,7 @@ async def forward_pr_comments(self, pr: PullRequest, series: Series): subject = match.group(1) patch = series.patch_by_subject(subject) if not patch: + # pyrefly: ignore # deprecated logger.warn( f"Ignoring PR comment {comment.html_url}, could not find relevant patch on patchwork" ) @@ -1459,6 +1501,7 @@ async def forward_pr_comments(self, pr: PullRequest, series: Series): f"Forwarded PR comment {comment.html_url} via email, Message-Id: {sent_msg_id}" ) else: + # pyrefly: ignore # deprecated logger.warn( f"Failed to forward PR comment in reply to {msg_id}, no recipients" ) diff --git a/kernel_patches_daemon/daemon.py b/kernel_patches_daemon/daemon.py index 868c330..1407a39 100755 --- a/kernel_patches_daemon/daemon.py +++ b/kernel_patches_daemon/daemon.py @@ -53,6 +53,7 @@ def reset_github_sync(self) -> bool: async def submit_metrics(self) -> None: if self.metrics_logger is None: + # pyrefly: ignore # deprecated logger.warn( "Not submitting run metrics because metrics logger is not configured" ) @@ -119,7 +120,9 @@ async def start_async(self) -> None: loop = asyncio.get_event_loop() + # pyrefly: ignore # bad-argument-type loop.add_signal_handler(signal.SIGTERM, self.stop) + # pyrefly: ignore # bad-argument-type loop.add_signal_handler(signal.SIGINT, self.stop) self._task = asyncio.create_task(self.worker.run()) diff --git a/kernel_patches_daemon/github_connector.py b/kernel_patches_daemon/github_connector.py index daff129..f77f386 100644 --- a/kernel_patches_daemon/github_connector.py +++ b/kernel_patches_daemon/github_connector.py @@ -89,6 +89,7 @@ def __init__( self.github_account_name = gh_user.login else: self.auth_type = AuthType.APP_AUTH + # pyrefly: ignore # missing-attribute app = GithubIntegration( auth=Auth.AppAuth( app_id=app_auth.app_id, private_key=app_auth.private_key @@ -144,6 +145,7 @@ def __init__( def __get_new_auth_token(self) -> str: # refresh token if needed # pyre-fixme[16]: `github.MainClass.Github` has no attribute `__requester`. + # pyrefly: ignore # missing-attribute gh_requester = self.git._Github__requester return gh_requester.auth.token diff --git a/kernel_patches_daemon/github_logs.py b/kernel_patches_daemon/github_logs.py index 69786af..479f39d 100644 --- a/kernel_patches_daemon/github_logs.py +++ b/kernel_patches_daemon/github_logs.py @@ -186,6 +186,7 @@ def _parse_out_test_progs_failure(self, log: str) -> str: if not line: continue + # pyrefly: ignore # bad-argument-type error_log.append(line) return "\n".join(error_log) diff --git a/kernel_patches_daemon/github_sync.py b/kernel_patches_daemon/github_sync.py index e2e216c..f0fe471 100644 --- a/kernel_patches_daemon/github_sync.py +++ b/kernel_patches_daemon/github_sync.py @@ -292,6 +292,7 @@ async def sync_patches(self) -> None: if worker.can_do_sync() ] if not sync_workers: + # pyrefly: ignore # deprecated logger.warn("No branch workers that can_do_sync(), skipping sync_patches()") return @@ -303,11 +304,15 @@ async def sync_patches(self) -> None: for branch, worker in sync_workers: logging.info(f"Refreshing repo info for {branch}.") + # pyrefly: ignore # bad-argument-type await loop.run_in_executor(None, worker.fetch_repo_branch) + # pyrefly: ignore # bad-argument-type await loop.run_in_executor(None, worker.get_pulls) + # pyrefly: ignore # bad-argument-type await loop.run_in_executor(None, worker.do_sync) worker._closed_prs = None branches = worker.repo.get_branches() + # pyrefly: ignore # bad-assignment worker.branches = [b.name for b in branches] mirror_done = time.time() @@ -329,10 +334,12 @@ async def sync_patches(self) -> None: # sync old subjects subject_names = {x.subject for x in self.subjects} for _, worker in sync_workers: + # pyrefly: ignore # bad-assignment for subject_name, pr in worker.prs.items(): if subject_name in subject_names: continue + # pyrefly: ignore # bad-argument-type if worker._is_relevant_pr(pr): parsed_ref = parse_pr_ref(pr.head.ref) # ignore unknown format branch/PRs. @@ -373,7 +380,9 @@ async def sync_patches(self) -> None: continue await worker.sync_checks(pr, latest_series) + # pyrefly: ignore # bad-argument-type await loop.run_in_executor(None, worker.expire_branches) + # pyrefly: ignore # bad-argument-type await loop.run_in_executor(None, worker.expire_user_prs) rate_limit = worker.git.get_rate_limit() diff --git a/kernel_patches_daemon/patchwork.py b/kernel_patches_daemon/patchwork.py index 6c962e8..d5c172f 100644 --- a/kernel_patches_daemon/patchwork.py +++ b/kernel_patches_daemon/patchwork.py @@ -204,6 +204,7 @@ async def on_request_end( def time_since_secs(date: str) -> float: parsed_datetime = dateparser.parse(date) + # pyrefly: ignore # deprecated, unsupported-operation duration = datetime.datetime.utcnow() - parsed_datetime return duration.total_seconds() @@ -267,12 +268,14 @@ async def wrapper(*args, **kwargs): async def wrapper(*args, **kwargs): k = key(*args, **kwargs) try: + # pyrefly: ignore # bad-context-manager with lock: return cache[k] except KeyError: pass # key not found v = await func(*args, **kwargs) try: + # pyrefly: ignore # bad-context-manager with lock: cache[k] = v except ValueError: @@ -398,6 +401,7 @@ async def get_patches(self) -> Tuple[Dict]: for the most recent relevant series """ tasks = [self.pw_client.get_patch_by_id(patch["id"]) for patch in self.patches] + # pyrefly: ignore # bad-return return await asyncio.gather(*tasks) async def is_closed(self) -> bool: @@ -540,8 +544,10 @@ async def get_http_session(self) -> aiohttp.ClientSession: # https://docs.aiohttp.org/en/stable/client_advanced.html#aiohttp-client-tracing trace_config = aiohttp.TraceConfig(trace_config_ctx_factory=TraceContext) # pyre-fixme[6]: In call `typing.MutableSequence.append`, for 1st positional argument, expected `_SignalCallback[TraceRequestStartParams]` but got `typing.Callable(on_request_start)[[Named(session, ClientSession), Named(trace_ctx, TraceContext), Named(params, TraceRequestStartParams)], Coroutine[typing.Any, typing.Any, None]]`. + # pyrefly: ignore # bad-argument-type trace_config.on_request_start.append(on_request_start) # pyre-fixme[6]: In call `typing.MutableSequence.append`, for 1st positional argument, expected `_SignalCallback[TraceRequestEndParams]` but got `typing.Callable(on_request_end)[[Named(session, ClientSession), Named(trace_ctx, TraceContext), Named(params, TraceRequestEndParams)], Coroutine[typing.Any, typing.Any, None]]`. + # pyrefly: ignore # bad-argument-type trace_config.on_request_end.append(on_request_end) client_session = aiohttp.ClientSession( trace_configs=[trace_config], @@ -551,12 +557,15 @@ async def get_http_session(self) -> aiohttp.ClientSession: # Work around intermittent issues by adding some retry logic. retry_options = ExponentialRetry(attempts=self.http_retries) + # pyrefly: ignore # bad-assignment self.http_session = RetryClient( client_session=client_session, retry_options=retry_options ) + # pyrefly: ignore # bad-return return self.http_session def format_since(self, pw_lookback: int) -> str: + # pyrefly: ignore # deprecated today = datetime.datetime.utcnow().date() lookback = today - datetime.timedelta(days=pw_lookback) return lookback.strftime("%Y-%m-%dT%H:%M:%S") @@ -567,8 +576,10 @@ async def __get( http_session = await self.get_http_session() resp = await http_session.get( # pyre-ignore + # pyrefly: ignore # bad-argument-type urljoin(self.api_url, path), # pyre-ignore + # pyrefly: ignore # bad-argument-type **kwargs, ) return resp @@ -602,6 +613,7 @@ async def __post(self, path: AnyStr, data: Dict) -> aiohttp.ClientResponse: http_session = await self.get_http_session() resp = await http_session.post( # pyre-ignore + # pyrefly: ignore # bad-argument-type urljoin(self.api_url, path), headers={"Authorization": f"Token {self.auth_token}"}, data=data, @@ -723,6 +735,7 @@ async def get_relevant_subjects(self) -> Sequence[Subject]: for pattern in self.search_patterns: patch_filters = MultiDict( + # pyrefly: ignore # bad-argument-type [ ("archived", str(False)), *[("state", val) for val in RELEVANT_STATES.values()], @@ -737,6 +750,7 @@ async def get_relevant_subjects(self) -> Sequence[Subject]: all_patches = await self.__get_objects_recursive( "patches", # pyre-ignore + # pyrefly: ignore # bad-argument-type params=patch_filters, ) diff --git a/pyproject.toml b/pyproject.toml index c0930d8..ff7414a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ munch = "^3.0.0" # urllib3 2.0 and later cause poetry to fail with error: # > __init__() got an unexpected keyword argument 'strict' # https://github.com/python-poetry/poetry/issues/7936 +pyrefly = "^0.37.0" [tool.poetry.group.poetry_fix.dependencies] urllib3 = "^1.26.0" @@ -59,3 +60,12 @@ build-backend = "poetry.core.masonry.api" [tool.black] target-version = ["py310", "py311", "py312"] + +[tool.pyrefly] +project-includes = ["**/*"] +project-excludes = [ + "**/node_modules", + "**/__pycache__", + "**/*venv/**/*", +] + diff --git a/tests/test_branch_worker.py b/tests/test_branch_worker.py index 8cc2644..bf1c55d 100644 --- a/tests/test_branch_worker.py +++ b/tests/test_branch_worker.py @@ -165,6 +165,7 @@ def test_fetch_repo_branch(self) -> None: Fetch master fetches upstream repo, the CI repo, and check out the right branch. """ with patch.object(BranchWorker, "fetch_repo") as fr: + # pyrefly: ignore # implicit-import git_mock = unittest.mock.Mock() fr.return_value = git_mock self._bw.fetch_repo_branch() @@ -235,7 +236,9 @@ class TestCase: for case in test_cases: with self.subTest(msg=case.name): + # pyrefly: ignore # missing-attribute self._bw.repo.get_pulls.reset_mock() + # pyrefly: ignore # missing-attribute self._bw.repo.get_pulls.return_value = case.prs with ( patch.object(BranchWorker, "add_pr") as ap, @@ -246,6 +249,7 @@ class TestCase: self._bw.get_pulls() # We get pull requests from upstream repo through Github API, only looking for opened PR against our branch of interest. + # pyrefly: ignore # missing-attribute self._bw.repo.get_pulls.assert_called_once_with( state="open", base=TEST_REPO_PR_BASE_BRANCH ) @@ -346,6 +350,7 @@ def test_do_sync_reset_repo(self) -> None: # Create a mock suitable to mock a git.RemoteReference remote_ref = "a/b/c/d" m = MagicMock() + # pyrefly: ignore # missing-attribute m.__str__.return_value = remote_ref lr.remote.return_value.refs = MagicMock(**{TEST_UPSTREAM_BRANCH: m}) self._bw.do_sync() @@ -376,6 +381,7 @@ def make_munch( state: str = "open", ) -> Munch: """Helper to make a Munch that can be consumed as a PR (e.g accessing nested attributes)""" + # pyrefly: ignore # bad-return return munchify( { "user": {"login": user}, @@ -433,6 +439,7 @@ class TestCase: with self.subTest(msg=case.name): # pyre-fixme: Incompatible parameter type [6]: In call `BranchWorker._is_relevant_pr`, # for 1st positional argument, expected `PullRequest` but got `Munch`. + # pyrefly: ignore # bad-argument-type self.assertEqual(self._bw._is_relevant_pr(case.pr), case.relevant) def test_fetch_repo_path_doesnt_exist_full_sync(self) -> None: @@ -533,6 +540,7 @@ class TestCase: for case in test_cases: with self.subTest(msg=case.name): + # pyrefly: ignore # bad-assignment self._bw.branches = case.branches self._bw.all_prs = {p: {} for p in case.all_prs} with ( @@ -566,6 +574,7 @@ def make_munch( title: str = "title", ) -> Munch: """Helper to make a Munch that can be consumed as a PR (e.g accessing nested attributes)""" + # pyrefly: ignore # bad-return return munchify( { "head": {"ref": head_ref}, @@ -717,6 +726,7 @@ async def test_guess_pr_not_in_cache_no_specified_branch_has_remote_branch_v1( series = Series(self._pw, {**SERIES_DATA, "version": 1}) mybranch = await self._bw.subject_to_branch(Subject(series.subject, self._pw)) + # pyrefly: ignore # bad-assignment self._bw.branches = ["aaa"] pr = await self._bw._guess_pr(series, mybranch) @@ -744,6 +754,7 @@ async def test_guess_pr_not_in_cache_no_specified_branch_has_remote_branch_v2_fi series = Series(self._pw, {**SERIES_DATA, "name": "code", "version": 2}) mybranch = await self._bw.subject_to_branch(Subject(series.subject, self._pw)) + # pyrefly: ignore # bad-assignment self._bw.branches = [mybranch] pr = await self._bw._guess_pr(series, mybranch) @@ -772,6 +783,7 @@ async def test_guess_pr_not_in_cache_no_specified_branch_is_remote_branch_v2_mul # DEFAULT_TEST_RESPONSES will return series 6 and 9, 6 being the first one mybranch = f"series/6=>{TEST_REPO_BRANCH}" + # pyrefly: ignore # bad-assignment self._bw.branches = [mybranch] # Calling without specifying `branch` so we force looking up series in @@ -816,6 +828,7 @@ async def test_guess_pr_not_in_cache_no_specified_branch_is_remote_branch_v2_mul series = Series(self._pw, {**SERIES_DATA, "name": "[v2] barv2", "version": 2}) + # pyrefly: ignore # bad-assignment self._bw.branches = [mybranch] # Calling without specifying `branch` so we force looking up series in @@ -1474,9 +1487,11 @@ def test_reply_email_recipients(self): mbox = read_test_data_file( "test_sync_patches_pr_summary_success/series-970926.mbox" ) + # pyrefly: ignore # implicit-import parser = email.parser.BytesParser(policy=email.policy.default) msg = parser.parsebytes(mbox.encode("utf-8"), headersonly=True) self.assertIsNotNone(mbox) + # pyrefly: ignore # missing-attribute denylist = kpd_config.email.pr_comments_forwarding.recipient_denylist (to_list, cc_list) = reply_email_recipients(msg, denylist=denylist) diff --git a/tests/test_daemon.py b/tests/test_daemon.py index 2feae4e..0a8339b 100644 --- a/tests/test_daemon.py +++ b/tests/test_daemon.py @@ -81,7 +81,9 @@ async def test_run_ok(self) -> None: await self.worker.run() gh_sync = self.worker.github_sync_worker + # pyrefly: ignore # missing-attribute gh_sync.sync_patches.assert_called_once() + # pyrefly: ignore # missing-attribute self.worker.reset_github_sync.assert_called_once() self.assertEqual(len(LOGGED_METRICS), 1) stats = LOGGED_METRICS[0][self.worker.project] diff --git a/tests/test_github_connector.py b/tests/test_github_connector.py index 8927c5f..8f33a1c 100644 --- a/tests/test_github_connector.py +++ b/tests/test_github_connector.py @@ -110,10 +110,12 @@ def test_oauth_get_repo_no_fallback(self) -> None: gc = get_default_gc_oauth_client() # We do auth first + # pyrefly: ignore # missing-attribute gc.git.get_user.assert_called_once() # then try to get the repo as the user user_mock.get_repo.assert_called_once_with(TEST_REPO) # and we do not fallback to getting the org + # pyrefly: ignore # missing-attribute gc.git.get_organization.assert_not_called() # user_or_org is derived from the Github auth user.... @@ -131,7 +133,11 @@ def test_oauth_get_repo_fallback_to_org_repo(self) -> None: # Force throwing an exception user_mock.get_repo.side_effect = GithubException( - "gh exception", "data", "headers" + # pyrefly: ignore # bad-argument-type + "gh exception", + "data", + # pyrefly: ignore # bad-argument-type + "headers", ) m = MagicMock() self._gh_mock.return_value.get_organization.return_value = m @@ -140,10 +146,12 @@ def test_oauth_get_repo_fallback_to_org_repo(self) -> None: gc = get_default_gc_oauth_client() # We do auth first + # pyrefly: ignore # missing-attribute gc.git.get_user.assert_called_once() # then try to get the repo as the user user_mock.get_repo.assert_called_once_with(TEST_REPO) # and fallback to getting the org + # pyrefly: ignore # missing-attribute gc.git.get_organization.assert_called_once_with(TEST_ORG) m.get_repo.assert_called_once_with(TEST_REPO) @@ -265,6 +273,7 @@ def test_renew_expired_token(self) -> None: gc = get_default_gc_app_auth_client() # Force generating a first token # pyre-fixme[16]: `github.MainClass.Github` has no attribute `__requester`. + # pyrefly: ignore # missing-attribute gc.git._Github__requester.auth.token self.assertEqual(p.call_count, 1) # set time to 1 second after expiration so that we renew the token. @@ -274,6 +283,7 @@ def test_renew_expired_token(self) -> None: ) - TOKEN_REFRESH_THRESHOLD_TIMEDELTA ) + # pyrefly: ignore # missing-attribute gc.git._Github__requester.auth.token self.assertEqual(p.call_count, 2) @@ -300,6 +310,7 @@ def test_donot_renew_non_expired_token(self) -> None: gc = get_default_gc_app_auth_client() # Force generating a first token # pyre-fixme[16]: `github.MainClass.Github` has no attribute `__requester`. + # pyrefly: ignore # missing-attribute gc.git._Github__requester.auth.token self.assertEqual(p.call_count, 1) # Set time to 1 seconds before expiration so that we do not renew the token. @@ -309,6 +320,7 @@ def test_donot_renew_non_expired_token(self) -> None: ) - TOKEN_REFRESH_THRESHOLD_TIMEDELTA ) + # pyrefly: ignore # missing-attribute gc.git._Github__requester.auth.token self.assertEqual(p.call_count, 1) @@ -337,6 +349,7 @@ def test_repo_url(self) -> None: gc_oauth = get_default_gc_oauth_client() # Force generating a first token # pyre-fixme[16]: `github.MainClass.Github` has no attribute `__requester`. + # pyrefly: ignore # missing-attribute gc_app_auth.git._Github__requester.auth.token self.assertEqual(p.call_count, 1) diff --git a/tests/test_github_sync.py b/tests/test_github_sync.py index 818b588..a9f36d0 100644 --- a/tests/test_github_sync.py +++ b/tests/test_github_sync.py @@ -354,10 +354,13 @@ async def test_sync_patches_pr_summary_success(self, m: aioresponses) -> None: self._gh.pw = patchwork worker = self._gh.workers[TEST_BRANCH] + # pyrefly: ignore # missing-attribute worker.repo.get_branches.return_value = [MagicMock(name=TEST_BRANCH)] worker = self._gh.workers[TEST_BPF_NEXT_BRANCH] + # pyrefly: ignore # missing-attribute worker.repo.get_branches.return_value = [MagicMock(name=TEST_BPF_NEXT_BRANCH)] + # pyrefly: ignore # missing-attribute worker.repo.create_pull.return_value = MagicMock( html_url="https://github.com/org/repo/pull/98765" ) @@ -368,6 +371,7 @@ async def test_sync_patches_pr_summary_success(self, m: aioresponses) -> None: await self._gh.sync_patches() # Verify expected patches and series fetched from patchwork + # pyrefly: ignore # missing-attribute get_requests = [key for key in m.requests.keys() if key[0] == "GET"] touched_urls = set() for req in get_requests: @@ -383,8 +387,10 @@ async def test_sync_patches_pr_summary_success(self, m: aioresponses) -> None: # Verify that a single POST request was made to patchwork # Updating state of a patch 14114605 + # pyrefly: ignore # missing-attribute post_requests = [key for key in m.requests.keys() if key[0] == "POST"] self.assertEqual(1, len(post_requests)) + # pyrefly: ignore # unsupported-operation post_calls = m.requests[post_requests[0]] url = str(post_requests[0][1]) self.assertEqual("https://patchwork.test/api/1.1/patches/14114605/checks/", url) diff --git a/tests/test_patchwork.py b/tests/test_patchwork.py index 8ddff5e..af8ddda 100644 --- a/tests/test_patchwork.py +++ b/tests/test_patchwork.py @@ -56,6 +56,7 @@ async def test_get_wrapper(self, m: aioresponses) -> None: pattern = re.compile(r"^.*$") m.get(pattern, status=200, body=b"""{"key1": "value1", "key2": 2}""") + # pyrefly: ignore # missing-attribute resp = await self._pw._Patchwork__get("object") m.assert_called_once() @@ -70,8 +71,11 @@ async def test_post_wrapper(self, m: aioresponses) -> None: pattern = re.compile(r"^.*$") m.post(pattern, status=200, body=b"""{"key1": "value1", "key2": 2}""") # Make sure user and token are set so the requests is actually posted. + # pyrefly: ignore # missing-attribute self._pw.pw_token = "1234567890" + # pyrefly: ignore # missing-attribute self._pw.pw_user = "somerandomuser" + # pyrefly: ignore # missing-attribute resp = await self._pw._Patchwork__post("some/random/url", "somerandomdata") m.assert_called_once() @@ -168,12 +172,15 @@ class TestCase: body=resp.body, ) + # pyrefly: ignore # missing-attribute resp = await self._pw._Patchwork__get_objects_recursive( "projects", params=case.filters ) self.assertEqual(resp, case.expected) self.assertEqual( - sum([len(x) for x in m.requests.values()]), case.get_calls + # pyrefly: ignore # missing-attribute + sum([len(x) for x in m.requests.values()]), + case.get_calls, ) @aioresponses() @@ -184,12 +191,14 @@ async def test_try_post_nocred_nomutation(self, m: aioresponses) -> None: pattern = re.compile(r"^.*$") m.post(pattern, status=200) self._pw.auth_token = None + # pyrefly: ignore # missing-attribute await self._pw._Patchwork__try_post( "https://127.0.0.1/some/random/url", "somerandomdata" ) m.assert_not_called() self._pw.auth_token = "" + # pyrefly: ignore # missing-attribute await self._pw._Patchwork__try_post( "https://127.0.0.1/some/random/url", "somerandomdata" ) @@ -272,6 +281,7 @@ async def _test_lookback( self._pw = get_default_pw_client(lookback_in_days=lookback) m.get(re.compile(r"^.*$"), status=200, body=b"[]") await self._pw.get_relevant_subjects() + # pyrefly: ignore # missing-attribute for request in m.requests.keys(): url = str(request[1]) assert_func(url) @@ -483,6 +493,7 @@ async def test_series_checks_update_all_diffs(self, m: aioresponses) -> None: # Hack... aioresponses stores the requests that were made in a dictionary whose's key is a tuple, # of the form (method, url). # https://github.com/pnuckowski/aioresponses/blob/56b843319d5d0ae8a405f188e68d7ba8c7573bc8/aioresponses/core.py#L507 + # pyrefly: ignore # missing-attribute self.assertEqual(len([x for x in m.requests.keys() if x[0] == "POST"]), 3) @aioresponses() @@ -522,6 +533,7 @@ async def test_series_checks_no_update_same_state_target( ) # First patch is not updates self.assertEqual( + # pyrefly: ignore # missing-attribute len([x for x in m.requests.keys() if x[0] == "POST"]), len(series.patches) - 1, ) @@ -564,7 +576,9 @@ async def test_series_checks_update_same_state_diff_target( ) # First patch is not updates self.assertEqual( - len([x for x in m.requests.keys() if x[0] == "POST"]), len(series.patches) + # pyrefly: ignore # missing-attribute + len([x for x in m.requests.keys() if x[0] == "POST"]), + len(series.patches), ) @aioresponses() @@ -608,7 +622,9 @@ async def test_series_checks_update_diff_state_same_target( ) # First patch is not updates self.assertEqual( - len([x for x in m.requests.keys() if x[0] == "POST"]), len(series.patches) + # pyrefly: ignore # missing-attribute + len([x for x in m.requests.keys() if x[0] == "POST"]), + len(series.patches), ) @aioresponses() @@ -649,6 +665,7 @@ async def test_series_checks_no_update_diff_pending_state( ) # First patch is not updates self.assertEqual( + # pyrefly: ignore # missing-attribute len([x for x in m.requests.keys() if x[0] == "POST"]), len(series.patches) - 1, )