88errors, across the entire CirtuitPython library ecosystem."""
99
1010import datetime
11- from io import StringIO
12- import json
1311import os
1412import logging
15- import pathlib
1613import re
1714import time
18- from tempfile import TemporaryDirectory
1915
2016from packaging .version import parse as pkg_version_parse
2117
22- from pylint import lint
23- from pylint .reporters import JSONReporter
24-
2518import requests
2619
27- import sh
28- from sh .contrib import git
29-
3020import yaml
3121import parse
3222
3828GH_INTERFACE = pygithub .Github (os .environ ["ADABOT_GITHUB_ACCESS_TOKEN" ])
3929
4030
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-
5331# Define constants for error strings to make checking against them more robust:
5432ERROR_README_DOWNLOAD_FAILED = "Failed to download README"
5533ERROR_README_IMAGE_MISSING_ALT = "README image missing alt text"
@@ -61,6 +39,7 @@ def get_result(self):
6139 "README CI badge needs to be changed to GitHub Actions"
6240)
6341ERROR_PYFILE_DOWNLOAD_FAILED = "Failed to download .py code file"
42+ ERROR_TOMLFILE_DOWNLOAD_FAILED = "Failed to download .toml file"
6443ERROR_PYFILE_MISSING_STRUCT = (
6544 ".py file contains reference to import ustruct"
6645 " without reference to import struct. See issue "
@@ -99,9 +78,12 @@ def get_result(self):
9978ERROR_MISSING_CODE_OF_CONDUCT = "Missing CODE_OF_CONDUCT.md"
10079ERROR_MISSING_README_RST = "Missing README.rst"
10180ERROR_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"
10382ERROR_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+ )
10587ERROR_MISSING_BLINKA = (
10688 "For pypi compatibility, missing Adafruit-Blinka in requirements.txt"
10789)
@@ -114,7 +96,7 @@ def get_result(self):
11496ERROR_ONLY_ALLOW_MERGES = "Only allow merges, disallow rebase and squash"
11597ERROR_RTD_SUBPROJECT_MISSING = "ReadTheDocs missing as a subproject on CircuitPython"
11698ERROR_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"
118100ERROR_RTD_SUBPROJECT_FAILED = "Failed to list CircuitPython subprojects on ReadTheDocs"
119101ERROR_RTD_OUTPUT_HAS_WARNINGS = "ReadTheDocs latest build has warnings and/or errors"
120102ERROR_GITHUB_NO_RELEASE = "Library repository has no releases"
@@ -145,7 +127,7 @@ def get_result(self):
145127 "Missing or incorrect pre-commit version in .pre-commit-config.yaml"
146128)
147129ERROR_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 "
149131ERROR_NEW_REPO_IN_WORK = "New repo(s) currently in work, and unreleased"
150132
151133# Temp category for GitHub Actions migration.
@@ -308,26 +290,6 @@ def validate_repo_state(self, repo):
308290 errors .append (ERROR_ONLY_ALLOW_MERGES )
309291 return errors
310292
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-
331293 # pylint: disable=too-many-locals,too-many-return-statements,too-many-branches
332294 def validate_release_state (self , repo ):
333295 """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):
577539 return errors
578540
579541 def _validate_pyproject_toml (self , file_info ):
580- """Check prproject .toml for pypi compatibility"""
542+ """Check pyproject .toml for pypi compatibility"""
581543 download_url = file_info ["download_url" ]
582544 contents = requests .get (download_url , timeout = 30 )
583545 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 []
589548
590- def _validate_requirements_txt (self , repo , file_info ):
549+ def _validate_requirements_txt (self , repo , file_info , check_blinka = True ):
591550 """Check requirements.txt for pypi compatibility"""
592551 download_url = file_info ["download_url" ]
593552 contents = requests .get (download_url , timeout = 30 )
@@ -598,7 +557,11 @@ def _validate_requirements_txt(self, repo, file_info):
598557 lines = contents .text .split ("\n " )
599558 blinka_lines = [l for l in lines if re .match (r"[\s]*Adafruit-Blinka[\s]*" , l )]
600559
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+ ):
602565 errors .append (ERROR_MISSING_BLINKA )
603566 return errors
604567
@@ -733,6 +696,13 @@ def validate_contents(self, repo):
733696 errors .extend (self ._validate_requirements_txt (repo , file_info ))
734697 else :
735698 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 )
736706
737707 # Check for an examples folder.
738708 dirs = [
@@ -1167,71 +1137,38 @@ def validate_labels(self, repo):
11671137
11681138 return errors
11691139
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+
11721146 if not repo ["name" ].startswith ("Adafruit_CircuitPython" ):
11731147 return []
11741148
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 :
11831150 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" ])
12061152
1207- logging .debug ("Running pylint on %s" , file )
1153+ if lib_repo .archived :
1154+ return []
12081155
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 }
12181157
12191158 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 )
12351172
12361173 def validate_default_branch (self , repo ):
12371174 """Makes sure that the default branch is main"""
0 commit comments