Skip to content
This repository was archived by the owner on Mar 13, 2024. It is now read-only.

Commit 965063a

Browse files
committed
Merge branch 'main' into incorrect_organization_replacement_in_general_README
2 parents 14a16cb + 67dfa74 commit 965063a

File tree

7 files changed

+291
-31
lines changed

7 files changed

+291
-31
lines changed

docs/user/how-to/existing.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ How to adopt the skeleton in an existing repo
44
If you have an existing repo and would like to adopt the skeleton structure
55
then you can use the commandline tool to merge the skeleton into your repo::
66

7-
python3-pip-skeleton existing /path/to/existing/repo --org my_github_user_or_org
7+
python3-pip-skeleton existing /path/to/existing/repo --org my_github_user_or_org --skeleton-org some_institution
88

99
This will:
1010

1111
- Take the repo name from the last element of the path
1212
- Take the package name from the repo name unless overridden by ``--package``
1313
- Clone the existing repo in /tmp
1414
- Create a new orphan merge branch from the skeleton repo
15+
- Use the version of the skeleton in ``some_institution``'s organization (default ``DiamondLightSource``)
1516
- Create a single commit that modifies the skeleton with the repo and package name
1617
- Push that merge branch back to the existing repo
1718
- Merge with the currently checked out branch, leaving you to fix the conflicts
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Creating a new skeleton fork
2+
============================
3+
4+
There may be some features in the DiamondLightSource skeleton which aren't required,
5+
or some you want to add which Diamond doesn't require. The python3-pip-skeleton can be used
6+
with any ``https://github.com/SomeInstitution/python3-pip-skeleton`` forked from
7+
``https://github.com/DiamondLightSource/python3-pip-skeleton`` by using ``--skeleton-org SomeInstitution``
8+
when implementing the skeleton on a new or existing repo.
9+
10+
The forked skeleton repo has to have the name ``python3-pip-skeleton``. It can be changed however the
11+
institution requires, then DiamondLightSource's skeleton can be periodically pulled from upstream
12+
to acquire the latest changes.

docs/user/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ side-bar.
2626
:caption: How-to Guides
2727
:maxdepth: 1
2828

29+
how-to/maintain-your-own-skeleton
2930
how-to/run-container
3031
how-to/existing
3132
how-to/update

docs/user/tutorials/new.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
Creating a new repo from the skeleton
22
=====================================
33

4-
Once you have followed the `installation` tutorial, you can use the
4+
Once you have followed the ``installation`` tutorial, you can use the
55
commandline tool to make a new repo that inherits the skeleton::
66

7-
python3-pip-skeleton new /path/to/be/created --org my_github_user_or_org
7+
python3-pip-skeleton new /path/to/be/created --org my_github_user_or_org --skeleton-org some_institution
88

99
This will:
1010

1111
- Take the repo name from the last element of the path
1212
- Take the package name from the repo name unless overridden by ``--package``
1313
- Create a new repo at the requested path, forked from the skeleton repo
1414
- Create a single commit that modifies the skeleton with the repo and package name
15+
- Use the version of the skeleton in ``some_institution``'s organization (default ``DiamondLightSource``)
1516

1617

1718
Getting started with your new repo

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ classifiers = [
1313
"Programming Language :: Python :: 3.11",
1414
]
1515
description = "One line description of your module"
16-
dependencies = [] # Add project dependencies here, e.g. ["click", "numpy"]
16+
dependencies = ["tomli"] # Add project dependencies here, e.g. ["click", "numpy"]
1717
dynamic = ["version"]
1818
license.file = "LICENSE"
1919
readme = "README.rst"

src/python3_pip_skeleton/__main__.py

Lines changed: 162 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@
77
from tempfile import TemporaryDirectory
88
from typing import Dict, List, Optional, Tuple
99

10+
import tomli
11+
1012
from . 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
1720
MERGE_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+
184219
def 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

207249
cfg_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+
author_email = [email protected]
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

214337
def 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

Comments
 (0)