4848from http import HTTPStatus
4949from http .client import HTTPException
5050from string import ascii_letters
51+ from typing import List , NamedTuple
5152from urllib .request import HTTPError , URLError , urlopen
5253
5354from easybuild .base import fancylogger
9091 _log .warning ("Failed to import 'git' Python module: %s" , err )
9192
9293
94+ class CategorizedPaths (NamedTuple ):
95+ easyconfigs : List [str ]
96+ easyconfigs : List [str ]
97+ files_to_delete : List [str ]
98+ patch_files : List [str ]
99+ py_files : List [str ]
100+
101+
93102GITHUB_URL = 'https://github.com'
94103GITHUB_API_URL = 'https://api.github.com'
95104GITHUB_BRANCH_MAIN = 'main'
@@ -1052,7 +1061,8 @@ def setup_repo(git_repo, target_account, target_repo, branch_name, silent=False,
10521061
10531062
10541063@only_if_module_is_available ('git' , pkgname = 'GitPython' )
1055- def _easyconfigs_pr_common (paths , ecs , start_branch = None , pr_branch = None , start_account = None , commit_msg = None ):
1064+ def _easyconfigs_pr_common (paths : CategorizedPaths , ecs , start_branch = None , pr_branch = None ,
1065+ start_account = None , commit_msg = None ):
10561066 """
10571067 Common code for new_pr and update_pr functions:
10581068 * check whether all supplied paths point to existing files
@@ -1072,8 +1082,8 @@ def _easyconfigs_pr_common(paths, ecs, start_branch=None, pr_branch=None, start_
10721082 # we need files to create the PR with
10731083 non_existing_paths = []
10741084 ec_paths = []
1075- if paths [ ' easyconfigs' ] or paths [ ' py_files' ] :
1076- for path in paths [ ' easyconfigs' ] + paths [ ' py_files' ] :
1085+ if paths . easyconfigs or paths . py_files :
1086+ for path in paths . easyconfigs + paths . py_files :
10771087 if not os .path .exists (path ):
10781088 non_existing_paths .append (path )
10791089 else :
@@ -1085,7 +1095,7 @@ def _easyconfigs_pr_common(paths, ecs, start_branch=None, pr_branch=None, start_
10851095 exit_code = EasyBuildExit .OPTION_ERROR
10861096 )
10871097
1088- if not any (paths . values () ):
1098+ if not any (paths ):
10891099 raise EasyBuildError ("No paths specified" , exit_code = EasyBuildExit .OPTION_ERROR )
10901100
10911101 pr_target_repo = det_pr_target_repo (paths )
@@ -1139,7 +1149,7 @@ def _easyconfigs_pr_common(paths, ecs, start_branch=None, pr_branch=None, start_
11391149
11401150 # figure out commit message to use
11411151 if commit_msg :
1142- if (pr_target_repo == GITHUB_EASYCONFIGS_REPO and all (file_info ['new' ]) and not paths [ ' files_to_delete' ]
1152+ if (pr_target_repo == GITHUB_EASYCONFIGS_REPO and all (file_info ['new' ]) and not paths . files_to_delete
11431153 and is_new_pr ): # Only if opening a new PR
11441154 msg = "When only adding new easyconfigs a PR commit msg (--pr-commit-msg) should not be used, as "
11451155 msg += "the PR title will be automatically generated."
@@ -1150,11 +1160,11 @@ def _easyconfigs_pr_common(paths, ecs, start_branch=None, pr_branch=None, start_
11501160 raise EasyBuildError (msg )
11511161 cnt = len (file_info ['paths_in_repo' ])
11521162 _log .debug ("Using specified commit message for all %d new/modified files at once: %s" , cnt , commit_msg )
1153- elif pr_target_repo == GITHUB_EASYCONFIGS_REPO and all (file_info ['new' ]) and not paths [ ' files_to_delete' ] :
1163+ elif pr_target_repo == GITHUB_EASYCONFIGS_REPO and all (file_info ['new' ]) and not paths . files_to_delete :
11541164 # automagically derive meaningful commit message if all easyconfig files are new
11551165 commit_msg = "adding easyconfigs: %s" % ', ' .join (os .path .basename (p ) for p in file_info ['paths_in_repo' ])
1156- if paths [ ' patch_files' ] :
1157- commit_msg += " and patches: %s" % ', ' .join (os .path .basename (p ) for p in paths [ ' patch_files' ] )
1166+ if paths . patch_files :
1167+ commit_msg += " and patches: %s" % ', ' .join (os .path .basename (p ) for p in paths . patch_files )
11581168 elif pr_target_repo == GITHUB_EASYBLOCKS_REPO and all (file_info ['new' ]):
11591169 commit_msg = "adding easyblocks: %s" % ', ' .join (os .path .basename (p ) for p in file_info ['paths_in_repo' ])
11601170 else :
@@ -1163,22 +1173,22 @@ def _easyconfigs_pr_common(paths, ecs, start_branch=None, pr_branch=None, start_
11631173 if not new ]
11641174 if modified_files :
11651175 msg += '\n Modified: ' + ', ' .join (modified_files )
1166- if paths [ ' files_to_delete' ] :
1167- msg += '\n Deleted: ' + ', ' .join (paths [ ' files_to_delete' ] )
1176+ if paths . files_to_delete :
1177+ msg += '\n Deleted: ' + ', ' .join (paths . files_to_delete )
11681178 raise EasyBuildError ("A meaningful commit message must be specified via --pr-commit-msg when "
11691179 "modifying/deleting files or targeting the framework repo." + msg ,
11701180 exit_code = EasyBuildExit .OPTION_ERROR )
11711181
11721182 # figure out to which software name patches relate, and copy them to the right place
1173- if paths [ ' patch_files' ] :
1174- patch_specs = det_patch_specs (paths [ ' patch_files' ] , file_info , [target_dir ])
1183+ if paths . patch_files :
1184+ patch_specs = det_patch_specs (paths . patch_files , file_info , [target_dir ])
11751185
11761186 print_msg ("copying patch files to %s..." % target_dir )
11771187 patch_info = copy_patch_files (patch_specs , target_dir )
11781188
11791189 # determine path to files to delete (if any)
11801190 deleted_paths = []
1181- for fn in paths [ ' files_to_delete' ] :
1191+ for fn in paths . files_to_delete :
11821192 fullpath = os .path .join (repo_path , fn )
11831193 if os .path .exists (fullpath ):
11841194 deleted_paths .append (fullpath )
@@ -1217,8 +1227,8 @@ def _easyconfigs_pr_common(paths, ecs, start_branch=None, pr_branch=None, start_
12171227 if pr_branch is None :
12181228 if ec_paths and pr_target_repo == GITHUB_EASYCONFIGS_REPO :
12191229 label = file_info ['ecs' ][0 ].name + re .sub ('[.-]' , '' , file_info ['ecs' ][0 ].version )
1220- elif pr_target_repo == GITHUB_EASYBLOCKS_REPO and paths .get ( ' py_files' ) :
1221- label = os .path .splitext (os .path .basename (paths [ ' py_files' ] [0 ]))[0 ]
1230+ elif pr_target_repo == GITHUB_EASYBLOCKS_REPO and paths .py_files :
1231+ label = os .path .splitext (os .path .basename (paths . py_files [0 ]))[0 ]
12221232 else :
12231233 label = '' .join (random .choice (ascii_letters ) for _ in range (10 ))
12241234 pr_branch = '%s_new_pr_%s' % (time .strftime ("%Y%m%d%H%M%S" ), label )
@@ -1233,7 +1243,7 @@ def _easyconfigs_pr_common(paths, ecs, start_branch=None, pr_branch=None, start_
12331243 git_repo .index .add (file_info ['paths_in_repo' ])
12341244 git_repo .index .add (dep_info ['paths_in_repo' ])
12351245
1236- if paths [ ' patch_files' ] :
1246+ if paths . patch_files :
12371247 _log .debug ("Staging all %d new/modified patch files" , len (patch_info ['paths_in_repo' ]))
12381248 git_repo .index .add (patch_info ['paths_in_repo' ])
12391249
@@ -1913,7 +1923,7 @@ def add_pr_labels(pr, branch=GITHUB_DEVELOP_BRANCH):
19131923
19141924
19151925@only_if_module_is_available ('git' , pkgname = 'GitPython' )
1916- def new_branch_github (paths , ecs , commit_msg = None ):
1926+ def new_branch_github (paths : CategorizedPaths , ecs , commit_msg = None ):
19171927 """
19181928 Create new branch on GitHub using specified files
19191929
@@ -1971,7 +1981,8 @@ def det_pr_title(ecs):
19711981
19721982
19731983@only_if_module_is_available ('git' , pkgname = 'GitPython' )
1974- def new_pr_from_branch (branch_name , title = None , descr = None , pr_target_repo = None , pr_metadata = None , commit_msg = None ):
1984+ def new_pr_from_branch (branch_name : CategorizedPaths , title = None , descr = None ,
1985+ pr_target_repo = None , pr_metadata = None , commit_msg = None ):
19751986 """
19761987 Create new pull request from specified branch on GitHub.
19771988 """
@@ -2152,7 +2163,7 @@ def new_pr_from_branch(branch_name, title=None, descr=None, pr_target_repo=None,
21522163 print_msg ("This PR should be labelled %s" % ', ' .join (labels ), log = _log , prefix = False )
21532164
21542165
2155- def new_pr (paths , ecs , title = None , descr = None , commit_msg = None ):
2166+ def new_pr (paths : CategorizedPaths , ecs , title = None , descr = None , commit_msg = None ):
21562167 """
21572168 Open new pull request using specified files
21582169
@@ -2184,8 +2195,8 @@ def new_pr(paths, ecs, title=None, descr=None, commit_msg=None):
21842195 raise EasyBuildError (msg , exit_code = EasyBuildExit .EASYCONFIG_ERROR )
21852196 patch = patch_info ['name' ]
21862197
2187- if patch not in paths [ ' patch_files' ] and not os .path .isfile (os .path .join (os .path .dirname (ec_path ),
2188- patch )):
2198+ if patch not in paths . patch_files and not os .path .isfile (os .path .join (os .path .dirname (ec_path ),
2199+ patch )):
21892200 print_warning ("new patch file %s, referenced by %s, is not included in this PR" %
21902201 (patch , ec .filename ()))
21912202
@@ -2226,29 +2237,26 @@ def det_account_branch_for_pr(pr_id, github_user=None, pr_target_repo=None):
22262237 return account , branch
22272238
22282239
2229- def det_pr_target_repo (paths ):
2240+ def det_pr_target_repo (paths : CategorizedPaths ):
22302241 """Determine target repository for pull request from given cagetorized list of files
22312242
22322243 :param paths: paths to categorized lists of files (easyconfigs, files to delete, patches, .py files)
22332244 """
22342245 pr_target_repo = build_option ('pr_target_repo' )
22352246
22362247 # determine target repository for PR based on which files are provided
2237- # (see categorize_files_by_type function)
22382248 if pr_target_repo is None :
22392249
22402250 _log .info ("Trying to derive target repository based on specified files..." )
22412251
2242- easyconfigs , files_to_delete , patch_files , py_files = [paths [key ] for key in sorted (paths .keys ())]
2243-
22442252 # Python files provided, and no easyconfig files or patches
2245- if py_files and not (easyconfigs or patch_files ):
2253+ if paths . py_files and not (paths . easyconfigs or paths . patch_files ):
22462254
22472255 _log .info ("Only Python files provided, no easyconfig files or patches..." )
22482256
22492257 # if all Python files are easyblocks, target repo should be easyblocks;
22502258 # otherwise, target repo is assumed to be framework
2251- if all (get_easyblock_class_name (path ) for path in py_files ):
2259+ if all (get_easyblock_class_name (path ) for path in paths . py_files ):
22522260 pr_target_repo = GITHUB_EASYBLOCKS_REPO
22532261 _log .info ("All Python files are easyblocks, target repository is assumed to be %s" , pr_target_repo )
22542262 else :
@@ -2257,7 +2265,8 @@ def det_pr_target_repo(paths):
22572265
22582266 # if no Python files are provided, only easyconfigs & patches, or if files to delete are .eb files,
22592267 # then target repo is assumed to be easyconfigs
2260- elif easyconfigs or patch_files or (files_to_delete and all (x .endswith ('.eb' ) for x in files_to_delete )):
2268+ elif paths .easyconfigs or paths .patch_files or (paths .files_to_delete and all (x .endswith ('.eb' )
2269+ for x in paths .files_to_delete )):
22612270 pr_target_repo = GITHUB_EASYCONFIGS_REPO
22622271 _log .info ("Only easyconfig and patch files found, target repository is assumed to be %s" , pr_target_repo )
22632272
@@ -2268,7 +2277,7 @@ def det_pr_target_repo(paths):
22682277
22692278
22702279@only_if_module_is_available ('git' , pkgname = 'GitPython' )
2271- def update_branch (branch_name , paths , ecs , github_account = None , commit_msg = None ):
2280+ def update_branch (branch_name , paths : CategorizedPaths , ecs , github_account = None , commit_msg = None ):
22722281 """
22732282 Update specified branch in GitHub using specified files
22742283
@@ -2303,7 +2312,7 @@ def update_branch(branch_name, paths, ecs, github_account=None, commit_msg=None)
23032312
23042313
23052314@only_if_module_is_available ('git' , pkgname = 'GitPython' )
2306- def update_pr (pr_id , paths , ecs , commit_msg = None ):
2315+ def update_pr (pr_id , paths : CategorizedPaths , ecs , commit_msg = None ):
23072316 """
23082317 Update specified pull request using specified files
23092318
0 commit comments