-
Notifications
You must be signed in to change notification settings - Fork 1
/
project_setup.py
231 lines (184 loc) · 7.19 KB
/
project_setup.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
"""Helps setup a new project based on this template."""
import argparse
import functools
import os
from pathlib import Path
from typing import List, Tuple
# this is GitPython
import git
TEMPLATE_PACKAGE_NAME = "package_name"
TEMPLATE_REPO_URL = "https://github.com/ssec-jhu/base-template"
TEMPLATE_RTD = "ssec-jhu-base-template"
TEMPLATE_CODECOV = "ssec-jhu/base-template"
UPDATED_PYPROJECT_TOML = "pyproject.toml [project.urls] ✅"
UPDATED_README = "Updated README.md ✅, but you need to add a token to the codecov badge!"
UPDATED_DIR = "Renamed {old_dir} to {new_dir} ✅"
UPDATED_FILE = "Updated package_name to {package_name} in {file_path} ✅"
SETUP_COMPLETE = "{package_name} setup complete! 🎉🎉🎉\n"
def make_name_safe(name: str) -> str:
"""Makes a string safe to use as a package name."""
return name.lower().replace(" ", "_").replace("-", "_")
def valid_file_predicate(file_path: Path) -> bool:
"""Checks if a file is valid for modification."""
is_valid_extension = file_path.suffix not in [
# images
".png",
".jpg",
".jpeg",
]
not_ds_store = file_path.name != ".DS_Store"
not_this = file_path.name != Path(__file__).name
return is_valid_extension and not_ds_store and not_this
def get_package_name(
repo_url: str,
) -> str:
"""Guesses the package name from the repo URL."""
return make_name_safe(Path(repo_url).stem)
def replace_file_contents(
pairs_to_replace: List[Tuple[str, str]],
file_path: Path,
) -> bool:
"""Replaces the contents of a file.
pairs_to_replace (List[Tuple[str, str]]): A list of tuples where the first
element is the string to replace and the second element is the new string
file_path (Path): The path to the file to modify
Returns:
bool: True if the file was modified, False otherwise
"""
with open(file_path, "r") as f:
content = f.read()
new_content = functools.reduce(lambda x, y: x.replace(*y), pairs_to_replace, content)
if new_content != content:
with open(file_path, "w") as f:
f.write(new_content)
return True
return False
def self_destruct(parent_dir: Path = Path(".")) -> None:
"""Deletes this file and its tests."""
os.remove(parent_dir / Path(__file__).name)
os.remove(parent_dir / Path("tests/test_project_setup.py"))
# remove the dependency on GitPython
for env in ["dev", "test"]:
with open(parent_dir / Path(f"requirements/{env}.txt"), "r") as f:
requirements = list(map(lambda line: line.strip(), f.readlines()))
requirements = list(filter(lambda x: "GitPython" not in x, requirements))
with open(parent_dir / Path(f"requirements/{env}.txt"), "w") as f:
f.write("\n".join(requirements))
def rtd_project_guess(
repo_url: str,
) -> str:
"""Guesses the RTD project name from the repo URL."""
return Path(repo_url).stem
def codecov_project_guess(
repo_url: str,
) -> str:
"""Guesses the codecov project name from the repo URL."""
return "/".join(Path(repo_url).parts[-2:]).replace(".git", "")
def run_setup(
repo_url: str,
package_name: str,
repo: git.Repo = git.Repo(),
commit_as_you_go: bool = True,
) -> None:
"""Helper to setup a new project based on this template.
It does the following things:
1. Updates the pyproject.toml and README.md files
2. Renames the package_name directory to a python safe version of the
repo name
3. Updates the package_name and <package_name> in all files to a python safe
version of the repo name
Args:
repo_url (str): The URL of the repo
package_name (str): The name of the package
repo (git.Repo): The git repo to use
commit_as_you_go (bool): Whether to commit after each step
Returns:
None
"""
def commit_func(message: str) -> None:
repo.git.commit(
*list(
filter(
lambda arg: len(arg) > 0,
[
"--dry-run" if not commit_as_you_go else "",
"-qam",
message,
],
)
)
)
# Step 1: Update the pyproject.toml and README.md ==========================
rtd_guess = rtd_project_guess(repo_url)
codecov_guess = codecov_project_guess(repo_url)
pyproject_toml_replacement_pairs = [
(TEMPLATE_REPO_URL, repo_url),
]
if replace_file_contents(
pyproject_toml_replacement_pairs,
Path(repo.working_dir) / Path("pyproject.toml"),
):
print(UPDATED_PYPROJECT_TOML)
readme_replacement_pairs = [
(TEMPLATE_REPO_URL, repo_url),
(TEMPLATE_RTD, rtd_guess),
(TEMPLATE_CODECOV, codecov_guess),
]
if replace_file_contents(
readme_replacement_pairs,
Path(repo.working_dir) / Path("README.md"),
):
print(UPDATED_README)
commit_func("updated pyproject.toml and README.md")
# Step 2: Update the package_name in all files =============================
# filter out files that we don't want to modify
files_to_check = list(
filter(
valid_file_predicate,
map(lambda f: Path(repo.working_dir) / Path(f), repo.git.ls_tree("-r", "--name-only", "HEAD").split("\n")),
)
)
# order matters, replace the bracketed version first
package_name_replacement_pairs = [
("<" + TEMPLATE_PACKAGE_NAME + ">", package_name),
(TEMPLATE_PACKAGE_NAME, package_name),
]
for file_path in files_to_check:
# only print positive results
if replace_file_contents(package_name_replacement_pairs, file_path):
print(UPDATED_FILE.format(package_name=package_name, file_path=file_path))
# Step 3: Rename the package_name directory ================================
repo.git.mv(TEMPLATE_PACKAGE_NAME, package_name)
repo.git.commit("-m", f"renamed dir {TEMPLATE_PACKAGE_NAME} to {package_name}")
print(UPDATED_DIR.format(old_dir=TEMPLATE_PACKAGE_NAME, new_dir=package_name))
commit_func("renamed dir {TEMPLATE_PACKAGE_NAME} to {package_name}")
self_destruct(Path(repo.working_dir))
commit_func("removed base-template setup files")
print(SETUP_COMPLETE.format(package_name=package_name))
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--repo_url",
type=str,
default="",
help="The URL of the repo, we get this from git, so you probably don't need to set this.",
)
parser.add_argument(
"--package_name",
type=str,
default="",
help="The name of the package, we guess this from the repo URL. If you want to override it, set it here.",
)
parser.add_argument(
"--commit_as_you_go",
action=argparse.BooleanOptionalAction,
help="Whether to commit after each step. Default is False.",
)
arguments = parser.parse_args()
repo_url = arguments.repo_url or git.Repo().remotes.origin.url
package_name = arguments.package_name or get_package_name(repo_url)
run_setup(
repo_url=repo_url,
package_name=package_name,
commit_as_you_go=bool(arguments.commit_as_you_go),
)