77from tempfile import TemporaryDirectory
88from typing import Dict , List , Optional , Tuple
99
10+ import tomli
11+
1012from . import __version__
1113
1214__all__ = ["main" ]
1315
14- # The source of the skeleton module to pull from
15- SKELETON = "https://github.com/DiamondLightSource/python3-pip-skeleton"
16+ # The url of the default skeleton module to pull from, a different skeleton
17+ # url can be passed in with --skeleton-git-url
18+ SKELETON_URL = "https://github.com/%s/python3-pip-skeleton"
1619# The name of the merge branch that will be created
1720MERGE_BRANCH = "skeleton-merge-branch"
1821# Extensions to change
@@ -63,6 +66,7 @@ def merge_skeleton(
6366 full_name : str ,
6467 email : str ,
6568 from_branch : str ,
69+ skeleton_org : str ,
6670 package ,
6771):
6872 path = path .resolve ()
@@ -77,6 +81,10 @@ def replace_text(text: str) -> str:
7781 text = text .replace ("main" , from_branch )
7882 return text
7983
84+ def replace_in_file (file_path : Path , text_from : str , text_to : str ):
85+ file_contents = file_path .read_text ()
86+ file_path .write_text (file_contents .replace (text_from , text_to ))
87+
8088 branches = list_branches (path )
8189 assert MERGE_BRANCH not in branches , (
8290 f"{ MERGE_BRANCH } already exists. "
@@ -94,7 +102,7 @@ def replace_text(text: str) -> str:
94102 # will do the wrong thing
95103 shutil .rmtree (git_tmp / "src" , ignore_errors = True )
96104 # Merge in the skeleton commits
97- git_tmp ("pull" , "--rebase=false" , SKELETON , from_branch )
105+ git_tmp ("pull" , "--rebase=false" , SKELETON_URL % skeleton_org , from_branch )
98106 # Move things around
99107 if package != "python3_pip_skeleton" :
100108 git_tmp ("mv" , "src/python3_pip_skeleton" , f"src/{ package } " )
@@ -135,6 +143,13 @@ def replace_text(text: str) -> str:
135143 )
136144 child .write_text (replaced_text )
137145
146+ # Change instructions in the docs to reflect which pip skeleton is in use
147+ replace_in_file (
148+ Path (git_tmp .name ) / "docs/developer/how-to/update-tools.rst" ,
149+ "DiamondLightSource" ,
150+ skeleton_org ,
151+ )
152+
138153 # Commit what we have and push to the original repo
139154 git_tmp ("commit" , "-a" , "-m" , f"Rename python3-pip-skeleton -> { repo } " )
140155 git_tmp ("push" , "origin" , MERGE_BRANCH )
@@ -157,7 +172,7 @@ def validate_package(args) -> str:
157172 return package
158173
159174
160- def verify_not_adopted (root : Path ):
175+ def verify_not_adopted (root : Path , skeleton_git_url : str ):
161176 """Verify that module has not already adopted skeleton"""
162177
163178 # This call does not print anything - the return code is 0 if it is an ancestor
@@ -175,40 +190,148 @@ def verify_not_adopted(root: Path):
175190
176191 assert not_adopted , (
177192 f"Package { root } has already adopted skeleton. You can type:\n "
178- f" git pull --rebase=false { SKELETON } \n "
193+ f" git pull --rebase=false { skeleton_git_url } \n "
179194 "to update. If there were significant upstream changes a re-adopt may be "
180195 "better. use the --force flag to the command you just ran."
181196 )
182197
183198
199+ def obtain_git_author_email (path : Path , force_local = True ):
200+ # If we force local then we require there to be a local .git we can look for
201+ # the username and password on.
202+ # If we don't force local then we will try to look for a local .git, if not found
203+ # git will use the global user.[name, email].
204+ if force_local and not (path / ".git" ).exists ():
205+ raise FileNotFoundError (
206+ ".git could not be found when searching "
207+ f"for a username and password in { path } "
208+ )
209+ author = str (
210+ git ("--git-dir" , path / ".git" , "config" , "--get" , "user.name" ).strip ()
211+ )
212+ author_email = str (
213+ git ("--git-dir" , path / ".git" , "config" , "--get" , "user.email" ).strip ()
214+ )
215+
216+ return author , author_email
217+
218+
184219def new (args ):
185220 path : Path = args .path
186221
222+ package = validate_package (args )
223+
187224 if path .exists ():
188225 assert path .is_dir () and not list (
189226 path .iterdir ()
190227 ), f"Expected { path } to not exist, or be an empty dir"
191228 else :
192229 path .mkdir (parents = True )
193230
194- package = validate_package (args )
231+ if args .full_name and args .email :
232+ author , author_email = args .full_name , args .email
233+ else :
234+ author , author_email = obtain_git_author_email (Path ("." ), force_local = False )
235+
195236 git ("init" , "-b" , "main" , cwd = path )
196237 print (f"Created git repo in { path } " )
197238 merge_skeleton (
198239 path = path ,
199240 org = args .org ,
200- full_name = args . full_name or git ( "config" , "--get" , "user.name" ). strip () ,
201- email = args . email or git ( "config" , "--get" , "user.email" ). strip () ,
241+ full_name = author ,
242+ email = author_email ,
202243 from_branch = args .from_branch or "main" ,
244+ skeleton_org = args .skeleton_org ,
203245 package = package ,
204246 )
205247
206248
207249cfg_issue = """Missing parameter in setup.cfg. Expected format:
208- [metadata]
209- name = example
210- author = Firstname Lastname
211- author_email = [email protected] """ 250+ [metadata]
251+ name = example
252+ author = Firstname Lastname
253+ 254+
255+ ------- pyproject.toml
256+ [[project.authors]]
257+ name = "Firstname Lastname"
258+ 259+ """
260+
261+
262+ def obtain_author_name_email (path : Path ) -> tuple :
263+ author : str = ""
264+ author_email : str = ""
265+ file_path_setup_cfg : Path = path / "setup.cfg"
266+ file_path_pyproject_toml : Path = path / "pyproject.toml"
267+
268+ # Parse for an author name, email. The order of preference used is
269+ # setup.cfg -> pyproject.toml -> .git -> user input.
270+ # Author and Email are recieved together to avoid mismatches from
271+ # obtaining in different places.
272+
273+ if file_path_setup_cfg .exists ():
274+ try :
275+ conf_cfg = ConfigParser ()
276+ conf_cfg .read (file_path_setup_cfg )
277+
278+ if "metadata" in conf_cfg :
279+ if "author" in conf_cfg ["metadata" ]:
280+ author = conf_cfg ["metadata" ]["author" ]
281+ if "author_email" in conf_cfg ["metadata" ]:
282+ author_email = conf_cfg ["metadata" ]["author_email" ]
283+ except Exception as exception :
284+ print (
285+ "\033 [1mUnable to parse setup.cfg because of the following error, "
286+ "will try other sources:\033 [0m"
287+ )
288+ print (exception )
289+ print ()
290+
291+ if (not author or not author_email ) and file_path_pyproject_toml .exists ():
292+ file = open (file_path_pyproject_toml , "rb" )
293+ try :
294+ conf_toml = tomli .load (file )
295+ if "project" in conf_toml and "authors" in conf_toml ["project" ]:
296+ # pyproject.toml will use "author" or "name" so we look for both
297+ for author_variable_name in ["author" , "name" ]:
298+ if author_variable_name in conf_toml ["project" ]["authors" ][0 ]:
299+ author = conf_toml ["project" ]["authors" ][0 ][
300+ author_variable_name
301+ ]
302+ if "email" in conf_toml ["project" ]["authors" ][0 ]:
303+ author_email = conf_toml ["project" ]["authors" ][0 ]["email" ]
304+ except Exception as exception :
305+ # We want to use something else if the pyproject.toml has some errors.
306+ print (
307+ "\033 [1mUnable to parse project.toml because of the following error, "
308+ "will try other sources:\033 [0m"
309+ )
310+ print (exception )
311+ print ()
312+ file .close ()
313+
314+ if not author or not author_email :
315+ try :
316+ author , author_email = obtain_git_author_email (path )
317+ except FileNotFoundError :
318+ print (
319+ "\033 [1mUnable to find a .git in the repo,"
320+ "will try other sources\033 [0m"
321+ )
322+
323+ # If all else fails, just ask the user.
324+ if not author or not author_email :
325+ print (cfg_issue )
326+ print ("Enter author name manually:" )
327+ author = str (input ())
328+ print ("Enter author email manually:" )
329+ author_email = str (input ())
330+
331+ assert author , "Inputted no author"
332+ assert author_email , "Inputted no author_email"
333+
334+ return author , author_email
212335
213336
214337def existing (args ):
@@ -217,22 +340,22 @@ def existing(args):
217340
218341 assert path .is_dir (), f"Expected { path } to be an existing directory"
219342 package = validate_package (args )
220- file_path : Path = path / "setup.cfg"
221- assert file_path .is_file (), "Expected a setup.cfg file in the directory."
343+
222344 if not args .force :
223- verify_not_adopted (args .path )
345+ verify_not_adopted (args .path , skeleton_git_url = SKELETON_URL % args .skeleton_org )
346+
347+ if args .full_name and args .email :
348+ author , author_email = args .full_name , args .email
349+ else :
350+ author , author_email = obtain_author_name_email (path )
224351
225- conf = ConfigParser ()
226- conf .read (path / "setup.cfg" )
227- assert "metadata" in conf , cfg_issue
228- assert "author" in conf ["metadata" ], cfg_issue
229- assert "author_email" in conf ["metadata" ], cfg_issue
230352 merge_skeleton (
231353 path = args .path ,
232354 org = args .org ,
233- full_name = conf [ "metadata" ][ " author" ] ,
234- email = conf [ "metadata" ][ " author_email" ] ,
355+ full_name = author ,
356+ email = author_email ,
235357 from_branch = args .from_branch or "main" ,
358+ skeleton_org = args .skeleton_org ,
236359 package = package ,
237360 )
238361
@@ -256,11 +379,17 @@ def main(args=None):
256379 parser = ArgumentParser ()
257380 subparsers = parser .add_subparsers ()
258381 parser .add_argument ("--version" , action = "version" , version = __version__ )
382+
259383 # Add a command for making a new repo
260384 sub = subparsers .add_parser ("new" , help = "Make a new repo forked from this skeleton" )
261385 sub .set_defaults (func = new )
262386 sub .add_argument ("path" , type = Path , help = "Path to new repo to create" )
263387 sub .add_argument ("--org" , required = True , help = "GitHub organization for the repo" )
388+ sub .add_argument (
389+ "--skeleton-org" ,
390+ default = "DiamondLightSource" ,
391+ help = "The organisation of the python3-pip-skeleton to use" ,
392+ )
264393 sub .add_argument (
265394 "--package" , default = None , help = "Package name, defaults to directory name"
266395 )
@@ -281,9 +410,20 @@ def main(args=None):
281410 sub .add_argument ("path" , type = Path , help = "Path to new repo to existing repo" )
282411 sub .add_argument ("--force" , action = "store_true" , help = "force readoption" )
283412 sub .add_argument ("--org" , required = True , help = "GitHub organization for the repo" )
413+ sub .add_argument (
414+ "--skeleton-org" ,
415+ default = "DiamondLightSource" ,
416+ help = "The organisation of the python3-pip-skeleton to use" ,
417+ )
284418 sub .add_argument (
285419 "--package" , default = None , help = "Package name, defaults to directory name"
286420 )
421+ sub .add_argument (
422+ "--full-name" , default = None , help = "Full name, defaults to git config user.name"
423+ )
424+ sub .add_argument (
425+ "--email" , default = None , help = "Email address, defaults to git config user.email"
426+ )
287427 sub .add_argument (
288428 "--from-branch" ,
289429 default = None ,
0 commit comments