|
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