|
6 | 6 |
|
7 | 7 | import argparse
|
8 | 8 | import json
|
9 |
| -import sys |
10 |
| -import warnings |
11 |
| -from io import StringIO |
12 |
| -from itertools import chain |
13 | 9 | from pathlib import Path
|
14 |
| -from typing import Dict, List, Union |
15 | 10 |
|
16 |
| -import git |
17 |
| - |
18 |
| -from pylint.lint import Run |
19 |
| -from pylint.reporters import JSONReporter |
20 | 11 | from pylint.testutils._primer import PackageToLint
|
21 |
| - |
22 |
| -MAX_GITHUB_COMMENT_LENGTH = 65536 |
23 |
| - |
24 |
| -PackageMessages = Dict[str, List[Dict[str, Union[str, int]]]] |
25 |
| - |
26 |
| -GITHUB_CRASH_TEMPLATE_LOCATION = "/home/runner/.cache" |
27 |
| -CRASH_TEMPLATE_INTRO = "There is a pre-filled template" |
| 12 | +from pylint.testutils._primer.primer_command import PrimerCommand |
| 13 | +from pylint.testutils._primer.primer_compare_command import CompareCommand |
| 14 | +from pylint.testutils._primer.primer_prepare_command import PrepareCommand |
| 15 | +from pylint.testutils._primer.primer_run_command import RunCommand |
28 | 16 |
|
29 | 17 |
|
30 | 18 | class Primer:
|
@@ -90,212 +78,16 @@ def __init__(self, primer_directory: Path, json_path: Path) -> None:
|
90 | 78 | self.packages = self._get_packages_to_lint_from_json(json_path)
|
91 | 79 | """All packages to prime."""
|
92 | 80 |
|
93 |
| - def run(self) -> None: |
94 | 81 | if self.config.command == "prepare":
|
95 |
| - self._handle_prepare_command() |
| 82 | + command_class: type[PrimerCommand] = PrepareCommand |
96 | 83 | if self.config.command == "run":
|
97 |
| - self._handle_run_command() |
| 84 | + command_class = RunCommand |
98 | 85 | if self.config.command == "compare":
|
99 |
| - self._handle_compare_command() |
100 |
| - |
101 |
| - def _handle_prepare_command(self) -> None: |
102 |
| - commit_string = "" |
103 |
| - if self.config.clone: |
104 |
| - for package, data in self.packages.items(): |
105 |
| - local_commit = data.lazy_clone() |
106 |
| - print(f"Cloned '{package}' at commit '{local_commit}'.") |
107 |
| - commit_string += local_commit + "_" |
108 |
| - elif self.config.check: |
109 |
| - for package, data in self.packages.items(): |
110 |
| - local_commit = git.Repo(data.clone_directory).head.object.hexsha |
111 |
| - print(f"Found '{package}' at commit '{local_commit}'.") |
112 |
| - commit_string += local_commit + "_" |
113 |
| - elif self.config.make_commit_string: |
114 |
| - for package, data in self.packages.items(): |
115 |
| - remote_sha1_commit = ( |
116 |
| - git.cmd.Git().ls_remote(data.url, data.branch).split("\t")[0] |
117 |
| - ) |
118 |
| - print(f"'{package}' remote is at commit '{remote_sha1_commit}'.") |
119 |
| - commit_string += remote_sha1_commit + "_" |
120 |
| - elif self.config.read_commit_string: |
121 |
| - with open( |
122 |
| - self.primer_directory / "commit_string.txt", encoding="utf-8" |
123 |
| - ) as f: |
124 |
| - print(f.read()) |
125 |
| - |
126 |
| - if commit_string: |
127 |
| - with open( |
128 |
| - self.primer_directory / "commit_string.txt", "w", encoding="utf-8" |
129 |
| - ) as f: |
130 |
| - f.write(commit_string) |
131 |
| - |
132 |
| - def _handle_run_command(self) -> None: |
133 |
| - packages: PackageMessages = {} |
134 |
| - |
135 |
| - for package, data in self.packages.items(): |
136 |
| - output = self._lint_package(data) |
137 |
| - packages[package] = output |
138 |
| - print(f"Successfully primed {package}.") |
139 |
| - |
140 |
| - astroid_errors = [] |
141 |
| - other_fatal_msgs = [] |
142 |
| - for msg in chain.from_iterable(packages.values()): |
143 |
| - if msg["type"] == "fatal": |
144 |
| - # Remove the crash template location if we're running on GitHub. |
145 |
| - # We were falsely getting "new" errors when the timestamp changed. |
146 |
| - assert isinstance(msg["message"], str) |
147 |
| - if GITHUB_CRASH_TEMPLATE_LOCATION in msg["message"]: |
148 |
| - msg["message"] = msg["message"].rsplit(CRASH_TEMPLATE_INTRO)[0] |
149 |
| - if msg["symbol"] == "astroid-error": |
150 |
| - astroid_errors.append(msg) |
151 |
| - else: |
152 |
| - other_fatal_msgs.append(msg) |
153 |
| - |
154 |
| - with open( |
155 |
| - self.primer_directory |
156 |
| - / f"output_{'.'.join(str(i) for i in sys.version_info[:3])}_{self.config.type}.txt", |
157 |
| - "w", |
158 |
| - encoding="utf-8", |
159 |
| - ) as f: |
160 |
| - json.dump(packages, f) |
161 |
| - |
162 |
| - # Fail loudly (and fail CI pipelines) if any fatal errors are found, |
163 |
| - # unless they are astroid-errors, in which case just warn. |
164 |
| - # This is to avoid introducing a dependency on bleeding-edge astroid |
165 |
| - # for pylint CI pipelines generally, even though we want to use astroid main |
166 |
| - # for the purpose of diffing emitted messages and generating PR comments. |
167 |
| - if astroid_errors: |
168 |
| - warnings.warn(f"Fatal errors traced to astroid: {astroid_errors}") |
169 |
| - assert not other_fatal_msgs, other_fatal_msgs |
170 |
| - |
171 |
| - def _handle_compare_command(self) -> None: |
172 |
| - with open(self.config.base_file, encoding="utf-8") as f: |
173 |
| - main_dict: PackageMessages = json.load(f) |
174 |
| - with open(self.config.new_file, encoding="utf-8") as f: |
175 |
| - new_dict: PackageMessages = json.load(f) |
| 86 | + command_class = CompareCommand |
| 87 | + self.command = command_class(self.primer_directory, self.packages, self.config) |
176 | 88 |
|
177 |
| - final_main_dict: PackageMessages = {} |
178 |
| - for package, messages in main_dict.items(): |
179 |
| - final_main_dict[package] = [] |
180 |
| - for message in messages: |
181 |
| - try: |
182 |
| - new_dict[package].remove(message) |
183 |
| - except ValueError: |
184 |
| - final_main_dict[package].append(message) |
185 |
| - |
186 |
| - self._create_comment(final_main_dict, new_dict) |
187 |
| - |
188 |
| - def _create_comment( |
189 |
| - self, all_missing_messages: PackageMessages, all_new_messages: PackageMessages |
190 |
| - ) -> None: |
191 |
| - comment = "" |
192 |
| - for package, missing_messages in all_missing_messages.items(): |
193 |
| - if len(comment) >= MAX_GITHUB_COMMENT_LENGTH: |
194 |
| - break |
195 |
| - |
196 |
| - new_messages = all_new_messages[package] |
197 |
| - package_data = self.packages[package] |
198 |
| - |
199 |
| - if not missing_messages and not new_messages: |
200 |
| - continue |
201 |
| - |
202 |
| - comment += f"\n\n**Effect on [{package}]({self.packages[package].url}):**\n" |
203 |
| - |
204 |
| - # Create comment for new messages |
205 |
| - count = 1 |
206 |
| - astroid_errors = 0 |
207 |
| - new_non_astroid_messages = "" |
208 |
| - if new_messages: |
209 |
| - print("Now emitted:") |
210 |
| - for message in new_messages: |
211 |
| - filepath = str(message["path"]).replace( |
212 |
| - str(package_data.clone_directory), "" |
213 |
| - ) |
214 |
| - # Existing astroid errors may still show up as "new" because the timestamp |
215 |
| - # in the message is slightly different. |
216 |
| - if message["symbol"] == "astroid-error": |
217 |
| - astroid_errors += 1 |
218 |
| - else: |
219 |
| - new_non_astroid_messages += ( |
220 |
| - f"{count}) {message['symbol']}:\n*{message['message']}*\n" |
221 |
| - f"{package_data.url}/blob/{package_data.branch}{filepath}#L{message['line']}\n" |
222 |
| - ) |
223 |
| - print(message) |
224 |
| - count += 1 |
225 |
| - |
226 |
| - if astroid_errors: |
227 |
| - comment += ( |
228 |
| - f"{astroid_errors} error(s) were found stemming from the `astroid` library. " |
229 |
| - "This is unlikely to have been caused by your changes. " |
230 |
| - "A GitHub Actions warning links directly to the crash report template. " |
231 |
| - "Please open an issue against `astroid` if one does not exist already. \n\n" |
232 |
| - ) |
233 |
| - if new_non_astroid_messages: |
234 |
| - comment += ( |
235 |
| - "The following messages are now emitted:\n\n<details>\n\n" |
236 |
| - + new_non_astroid_messages |
237 |
| - + "\n</details>\n\n" |
238 |
| - ) |
239 |
| - |
240 |
| - # Create comment for missing messages |
241 |
| - count = 1 |
242 |
| - if missing_messages: |
243 |
| - comment += ( |
244 |
| - "The following messages are no longer emitted:\n\n<details>\n\n" |
245 |
| - ) |
246 |
| - print("No longer emitted:") |
247 |
| - for message in missing_messages: |
248 |
| - comment += f"{count}) {message['symbol']}:\n*{message['message']}*\n" |
249 |
| - filepath = str(message["path"]).replace( |
250 |
| - str(package_data.clone_directory), "" |
251 |
| - ) |
252 |
| - assert not package_data.url.endswith( |
253 |
| - ".git" |
254 |
| - ), "You don't need the .git at the end of the github url." |
255 |
| - comment += f"{package_data.url}/blob/{package_data.branch}{filepath}#L{message['line']}\n" |
256 |
| - count += 1 |
257 |
| - print(message) |
258 |
| - if missing_messages: |
259 |
| - comment += "\n</details>\n\n" |
260 |
| - |
261 |
| - if comment == "": |
262 |
| - comment = ( |
263 |
| - "🤖 According to the primer, this change has **no effect** on the" |
264 |
| - " checked open source code. 🤖🎉\n\n" |
265 |
| - ) |
266 |
| - else: |
267 |
| - comment = ( |
268 |
| - f"🤖 **Effect of this PR on checked open source code:** 🤖\n\n{comment}" |
269 |
| - ) |
270 |
| - hash_information = ( |
271 |
| - f"*This comment was generated for commit {self.config.commit}*" |
272 |
| - ) |
273 |
| - if len(comment) + len(hash_information) >= MAX_GITHUB_COMMENT_LENGTH: |
274 |
| - truncation_information = ( |
275 |
| - f"*This comment was truncated because GitHub allows only" |
276 |
| - f" {MAX_GITHUB_COMMENT_LENGTH} characters in a comment.*" |
277 |
| - ) |
278 |
| - max_len = ( |
279 |
| - MAX_GITHUB_COMMENT_LENGTH |
280 |
| - - len(hash_information) |
281 |
| - - len(truncation_information) |
282 |
| - ) |
283 |
| - comment = f"{comment[:max_len - 10]}...\n\n{truncation_information}\n\n" |
284 |
| - comment += hash_information |
285 |
| - with open(self.primer_directory / "comment.txt", "w", encoding="utf-8") as f: |
286 |
| - f.write(comment) |
287 |
| - |
288 |
| - def _lint_package(self, data: PackageToLint) -> list[dict[str, str | int]]: |
289 |
| - # We want to test all the code we can |
290 |
| - enables = ["--enable-all-extensions", "--enable=all"] |
291 |
| - # Duplicate code takes too long and is relatively safe |
292 |
| - # TODO: Find a way to allow cyclic-import and compare output correctly |
293 |
| - disables = ["--disable=duplicate-code,cyclic-import"] |
294 |
| - arguments = data.pylint_args + enables + disables |
295 |
| - output = StringIO() |
296 |
| - reporter = JSONReporter(output) |
297 |
| - Run(arguments, reporter=reporter, exit=False) |
298 |
| - return json.loads(output.getvalue()) |
| 89 | + def run(self) -> None: |
| 90 | + self.command.run() |
299 | 91 |
|
300 | 92 | @staticmethod
|
301 | 93 | def _get_packages_to_lint_from_json(json_path: Path) -> dict[str, PackageToLint]:
|
|
0 commit comments