8
8
errors, across the entire CirtuitPython library ecosystem."""
9
9
10
10
import datetime
11
- from io import StringIO
12
- import json
13
11
import os
14
12
import logging
15
- import pathlib
16
13
import re
17
14
import time
18
- from tempfile import TemporaryDirectory
19
15
20
16
from packaging .version import parse as pkg_version_parse
21
17
22
- from pylint import lint
23
- from pylint .reporters import JSONReporter
24
-
25
18
import requests
26
19
27
- import sh
28
- from sh .contrib import git
29
-
30
20
import yaml
31
21
import parse
32
22
38
28
GH_INTERFACE = pygithub .Github (os .environ ["ADABOT_GITHUB_ACCESS_TOKEN" ])
39
29
40
30
41
- class CapturedJsonReporter (JSONReporter ):
42
- """Helper class to stringify PyLint JSON reports."""
43
-
44
- def __init__ (self ):
45
- self ._stringio = StringIO ()
46
- super ().__init__ (self ._stringio )
47
-
48
- def get_result (self ):
49
- """The current value."""
50
- return self ._stringio .getvalue ()
51
-
52
-
53
31
# Define constants for error strings to make checking against them more robust:
54
32
ERROR_README_DOWNLOAD_FAILED = "Failed to download README"
55
33
ERROR_README_IMAGE_MISSING_ALT = "README image missing alt text"
@@ -61,6 +39,7 @@ def get_result(self):
61
39
"README CI badge needs to be changed to GitHub Actions"
62
40
)
63
41
ERROR_PYFILE_DOWNLOAD_FAILED = "Failed to download .py code file"
42
+ ERROR_TOMLFILE_DOWNLOAD_FAILED = "Failed to download .toml file"
64
43
ERROR_PYFILE_MISSING_STRUCT = (
65
44
".py file contains reference to import ustruct"
66
45
" without reference to import struct. See issue "
@@ -99,9 +78,12 @@ def get_result(self):
99
78
ERROR_MISSING_CODE_OF_CONDUCT = "Missing CODE_OF_CONDUCT.md"
100
79
ERROR_MISSING_README_RST = "Missing README.rst"
101
80
ERROR_MISSING_READTHEDOCS = "Missing readthedocs.yaml"
102
- ERROR_MISSING_PYPROJECT_TOML = "For pypi compatibility, missing pyproject.toml"
81
+ ERROR_MISSING_PYPROJECT_TOML = "For PyPI compatibility, missing pyproject.toml"
103
82
ERROR_MISSING_PRE_COMMIT_CONFIG = "Missing .pre-commit-config.yaml"
104
- ERROR_MISSING_REQUIREMENTS_TXT = "For pypi compatibility, missing requirements.txt"
83
+ ERROR_MISSING_REQUIREMENTS_TXT = "For PyPI compatibility, missing requirements.txt"
84
+ ERROR_MISSING_OPTIONAL_REQUIREMENTS_TXT = (
85
+ "For PyPI compatibility, missing optional_requirements.txt"
86
+ )
105
87
ERROR_MISSING_BLINKA = (
106
88
"For pypi compatibility, missing Adafruit-Blinka in requirements.txt"
107
89
)
@@ -114,7 +96,7 @@ def get_result(self):
114
96
ERROR_ONLY_ALLOW_MERGES = "Only allow merges, disallow rebase and squash"
115
97
ERROR_RTD_SUBPROJECT_MISSING = "ReadTheDocs missing as a subproject on CircuitPython"
116
98
ERROR_RTD_ADABOT_MISSING = "ReadTheDocs project missing adabot as owner"
117
- ERROR_RTD_FAILED_TO_LOAD_BUILD_STATUS = "Failed to load build status"
99
+ ERROR_RTD_FAILED_TO_LOAD_BUILD_STATUS = "Failed to load RTD build status"
118
100
ERROR_RTD_SUBPROJECT_FAILED = "Failed to list CircuitPython subprojects on ReadTheDocs"
119
101
ERROR_RTD_OUTPUT_HAS_WARNINGS = "ReadTheDocs latest build has warnings and/or errors"
120
102
ERROR_GITHUB_NO_RELEASE = "Library repository has no releases"
@@ -145,7 +127,7 @@ def get_result(self):
145
127
"Missing or incorrect pre-commit version in .pre-commit-config.yaml"
146
128
)
147
129
ERROR_PYLINT_VERSION = "Missing or incorrect pylint version in .pre-commit-config.yaml"
148
- ERROR_PYLINT_FAILED_LINTING = "Failed PyLint checks "
130
+ ERROR_CI_BUILD = "Failed CI build "
149
131
ERROR_NEW_REPO_IN_WORK = "New repo(s) currently in work, and unreleased"
150
132
151
133
# Temp category for GitHub Actions migration.
@@ -308,26 +290,6 @@ def validate_repo_state(self, repo):
308
290
errors .append (ERROR_ONLY_ALLOW_MERGES )
309
291
return errors
310
292
311
- def validate_actions_state (self , repo ):
312
- """Validate if the most recent GitHub Actions run on the default branch
313
- has passed.
314
- Just returns a message stating that the most recent run failed.
315
- """
316
-
317
- if not (
318
- repo ["owner" ]["login" ] == "adafruit"
319
- and repo ["name" ].startswith ("Adafruit_CircuitPython" )
320
- ):
321
- return []
322
-
323
- try :
324
- repo_obj = GH_INTERFACE .get_repo ("Adafruit/" + repo ["full_name" ])
325
- workflow = repo_obj .get_workflow ("build.yml" )
326
- workflow_runs = workflow .get_runs (branch = "main" )
327
- return [] if workflow_runs [0 ].conclusion else [ERROR_GITHUB_FAILING_ACTIONS ]
328
- except pygithub .GithubException :
329
- return [ERROR_UNABLE_PULL_REPO_DETAILS ]
330
-
331
293
# pylint: disable=too-many-locals,too-many-return-statements,too-many-branches
332
294
def validate_release_state (self , repo ):
333
295
"""Validate if a repo 1) has a release, and 2) if there have been commits
@@ -577,17 +539,14 @@ def _validate_pre_commit_config_yaml(self, file_info):
577
539
return errors
578
540
579
541
def _validate_pyproject_toml (self , file_info ):
580
- """Check prproject .toml for pypi compatibility"""
542
+ """Check pyproject .toml for pypi compatibility"""
581
543
download_url = file_info ["download_url" ]
582
544
contents = requests .get (download_url , timeout = 30 )
583
545
if not contents .ok :
584
- return [ERROR_PYFILE_DOWNLOAD_FAILED ]
585
-
586
- errors = []
587
-
588
- return errors
546
+ return [ERROR_TOMLFILE_DOWNLOAD_FAILED ]
547
+ return []
589
548
590
- def _validate_requirements_txt (self , repo , file_info ):
549
+ def _validate_requirements_txt (self , repo , file_info , check_blinka = True ):
591
550
"""Check requirements.txt for pypi compatibility"""
592
551
download_url = file_info ["download_url" ]
593
552
contents = requests .get (download_url , timeout = 30 )
@@ -598,7 +557,11 @@ def _validate_requirements_txt(self, repo, file_info):
598
557
lines = contents .text .split ("\n " )
599
558
blinka_lines = [l for l in lines if re .match (r"[\s]*Adafruit-Blinka[\s]*" , l )]
600
559
601
- if not blinka_lines and repo ["name" ] not in LIBRARIES_DONT_NEED_BLINKA :
560
+ if (
561
+ not blinka_lines
562
+ and repo ["name" ] not in LIBRARIES_DONT_NEED_BLINKA
563
+ and check_blinka
564
+ ):
602
565
errors .append (ERROR_MISSING_BLINKA )
603
566
return errors
604
567
@@ -733,6 +696,13 @@ def validate_contents(self, repo):
733
696
errors .extend (self ._validate_requirements_txt (repo , file_info ))
734
697
else :
735
698
errors .append (ERROR_MISSING_REQUIREMENTS_TXT )
699
+ if "optional_requirements.txt" in files :
700
+ file_info = content_list [files .index ("optional_requirements.txt" )]
701
+ errors .extend (
702
+ self ._validate_requirements_txt (repo , file_info , check_blinka = False )
703
+ )
704
+ else :
705
+ errors .append (ERROR_MISSING_OPTIONAL_REQUIREMENTS_TXT )
736
706
737
707
# Check for an examples folder.
738
708
dirs = [
@@ -1167,71 +1137,38 @@ def validate_labels(self, repo):
1167
1137
1168
1138
return errors
1169
1139
1170
- def validate_passes_linting (self , repo ):
1171
- """Clones the repo and runs pylint on the Python files"""
1140
+ def validate_actions_state (self , repo ):
1141
+ """Validate if the most recent GitHub Actions run on the default branch
1142
+ has passed.
1143
+ Just returns a message stating that the most recent run failed.
1144
+ """
1145
+
1172
1146
if not repo ["name" ].startswith ("Adafruit_CircuitPython" ):
1173
1147
return []
1174
1148
1175
- ignored_py_files = ["conf.py" ]
1176
-
1177
- desination_type = TemporaryDirectory
1178
- if self .keep_repos :
1179
- desination_type = pathlib .Path ("repos" ).absolute
1180
-
1181
- with desination_type () as tempdir :
1182
- repo_dir = pathlib .Path (tempdir ) / repo ["name" ]
1149
+ while True :
1183
1150
try :
1184
- if not repo_dir .exists ():
1185
- git .clone ("--depth=1" , repo ["clone_url" ], repo_dir )
1186
- except sh .ErrorReturnCode as err :
1187
- self .output_file_data .append (
1188
- f"Failed to clone repo for linting: { repo ['full_name' ]} \n { err .stderr } "
1189
- )
1190
- return [ERROR_OUTPUT_HANDLER ]
1191
-
1192
- if self .keep_repos and (repo_dir / ".pylint-ok" ).exists ():
1193
- return []
1194
-
1195
- for file in repo_dir .rglob ("*.py" ):
1196
- if file .name in ignored_py_files or str (file .parent ).endswith (
1197
- "examples"
1198
- ):
1199
- continue
1200
-
1201
- pylint_args = [str (file )]
1202
- if (repo_dir / ".pylintrc" ).exists ():
1203
- pylint_args += [f"--rcfile={ str (repo_dir / '.pylintrc' )} " ]
1204
-
1205
- reporter = CapturedJsonReporter ()
1151
+ lib_repo = GH_INTERFACE .get_repo (repo ["full_name" ])
1206
1152
1207
- logging .debug ("Running pylint on %s" , file )
1153
+ if lib_repo .archived :
1154
+ return []
1208
1155
1209
- lint .Run (pylint_args , reporter = reporter , exit = False )
1210
- pylint_stderr = ""
1211
- pylint_stdout = reporter .get_result ()
1212
-
1213
- if pylint_stderr :
1214
- self .output_file_data .append (
1215
- f"PyLint error ({ repo ['name' ]} ): '{ pylint_stderr } '"
1216
- )
1217
- return [ERROR_OUTPUT_HANDLER ]
1156
+ arg_dict = {"branch" : lib_repo .default_branch }
1218
1157
1219
1158
try :
1220
- pylint_result = json .loads (pylint_stdout )
1221
- except json .JSONDecodeError as json_err :
1222
- self .output_file_data .append (
1223
- f"PyLint output JSONDecodeError: { json_err .msg } "
1224
- )
1225
- return [ERROR_OUTPUT_HANDLER ]
1226
-
1227
- if pylint_result :
1228
- return [ERROR_PYLINT_FAILED_LINTING ]
1229
-
1230
- if self .keep_repos :
1231
- with open (repo_dir / ".pylint-ok" , "w" ) as pylint_ok :
1232
- pylint_ok .write ("" .join (pylint_result ))
1233
-
1234
- return []
1159
+ workflow = lib_repo .get_workflow ("build.yml" )
1160
+ workflow_runs = workflow .get_runs (** arg_dict )
1161
+ except pygithub .GithubException : # This can probably be tightened later
1162
+ # No workflows or runs yet
1163
+ return []
1164
+ if not workflow_runs [0 ].conclusion :
1165
+ return [ERROR_CI_BUILD ]
1166
+ return []
1167
+ except pygithub .RateLimitExceededException :
1168
+ core_rate_limit_reset = GH_INTERFACE .get_rate_limit ().core .reset
1169
+ sleep_time = core_rate_limit_reset - datetime .datetime .now ()
1170
+ logging .warning ("Rate Limit will reset at: %s" , core_rate_limit_reset )
1171
+ time .sleep (sleep_time .seconds )
1235
1172
1236
1173
def validate_default_branch (self , repo ):
1237
1174
"""Makes sure that the default branch is main"""
0 commit comments