Skip to content

Commit 5896f57

Browse files
authored
Move templates to remote repo (#131)
* move template to extra repo * updated docs * adjust list-templates to git remotes * use contextmanager and tmpfile to clean up repositories * python38 * fix incorrect exception handling * python 3.9 fix; doc string * remove misleading line
1 parent dc267f9 commit 5896f57

File tree

20 files changed

+174
-597
lines changed

20 files changed

+174
-597
lines changed

docs.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,8 @@ $ seml project init [OPTIONS] [DIRECTORY]
414414
* `-n, --name TEXT`: The name of the project. (By default inferred from the directory name.)
415415
* `-u, --username TEXT`: The author name to use for the project. (By default inferred from $USER)
416416
* `-m, --usermail TEXT`: The author email to use for the project. (By default empty.)
417+
* `-r, --git-remote TEXT`: The git remote to use for the project. (By default SETTINGS.TEMPLATE_REMOTE.)
418+
* `-c, --git-commit TEXT`: The exact git commit to use. May also be a tag or branch (By default latest)
417419
* `-y, --yes`: Automatically confirm all dialogues with yes.
418420
* `--help`: Show this message and exit.
419421

@@ -429,6 +431,8 @@ $ seml project list-templates [OPTIONS]
429431

430432
**Options**:
431433

434+
* `-r, --git-remote TEXT`: The git remote to use for the project. (By default SETTINGS.TEMPLATE_REMOTE.)
435+
* `-c, --git-commit TEXT`: The exact git commit to use. May also be a tag or branch (By default latest)
432436
* `--help`: Show this message and exit.
433437

434438
## `seml reload-sources`

src/seml/__main__.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1038,21 +1038,64 @@ def init_project_command(
10381038
help='The author email to use for the project. (By default empty.)',
10391039
),
10401040
] = None,
1041+
git_remote: Annotated[
1042+
str,
1043+
typer.Option(
1044+
'-r',
1045+
'--git-remote',
1046+
help='The git remote to use for the project. (By default SETTINGS.TEMPLATE_REMOTE.)',
1047+
),
1048+
] = None,
1049+
git_commit: Annotated[
1050+
str,
1051+
typer.Option(
1052+
'-c',
1053+
'--git-commit',
1054+
help='The exact git commit to use. May also be a tag or branch (By default latest)',
1055+
),
1056+
] = None,
10411057
yes: YesAnnotation = False,
10421058
):
10431059
"""
10441060
Initialize a new project in the given directory.
10451061
"""
1046-
init_project(directory, project_name, user_name, user_mail, template, yes)
1062+
init_project(
1063+
directory,
1064+
project_name,
1065+
user_name,
1066+
user_mail,
1067+
template,
1068+
git_remote,
1069+
git_commit,
1070+
yes,
1071+
)
10471072

10481073

10491074
@app_project.command('list-templates')
10501075
@restrict_collection(False)
1051-
def list_templates_command(ctx: typer.Context):
1076+
def list_templates_command(
1077+
ctx: typer.Context,
1078+
git_remote: Annotated[
1079+
str,
1080+
typer.Option(
1081+
'-r',
1082+
'--git-remote',
1083+
help='The git remote to use for the project. (By default SETTINGS.TEMPLATE_REMOTE.)',
1084+
),
1085+
] = None,
1086+
git_commit: Annotated[
1087+
str,
1088+
typer.Option(
1089+
'-c',
1090+
'--git-commit',
1091+
help='The exact git commit to use. May also be a tag or branch (By default latest)',
1092+
),
1093+
] = None,
1094+
):
10521095
"""
10531096
List available project templates.
10541097
"""
1055-
print_available_templates()
1098+
print_available_templates(git_remote, git_commit)
10561099

10571100

10581101
@dataclass

src/seml/project.py

Lines changed: 123 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import logging
22
import os
3+
from contextlib import contextmanager
34
from pathlib import Path
4-
from typing import Optional, Union
5+
from typing import List, Optional, Union
6+
7+
from seml.settings import SETTINGS
58

69

710
def init_project(
@@ -10,6 +13,8 @@ def init_project(
1013
user_name: Optional[str] = None,
1114
user_mail: Optional[str] = None,
1215
template: str = 'default',
16+
git_remote: Optional[str] = None,
17+
git_commit: Optional[str] = None,
1318
yes: bool = False,
1419
):
1520
"""
@@ -27,16 +32,22 @@ def init_project(
2732
The email of the user. If not given, ''.
2833
template : str
2934
The template to use for the project.
35+
git_repo : Optional[str]
36+
The URL of the git repository to use.
37+
git_commit : Optional[str]
38+
The commit to use.
39+
git_branch : Optional[str]
40+
The branch to use.
3041
yes : bool
3142
If True, no confirmation is asked before initializing the project.
3243
"""
33-
import importlib.resources
3444
from click import prompt
3545
from gitignore_parser import parse_gitignore
3646

3747
if directory is None:
3848
directory = Path()
3949
directory = Path(directory).absolute()
50+
4051
# Ensure that the directory exists
4152
if not directory.exists():
4253
directory.mkdir(parents=True)
@@ -48,84 +59,132 @@ def init_project(
4859
type=bool,
4960
):
5061
exit(1)
51-
logging.info(f'Initializing project in "{directory}" using template "{template}"')
52-
53-
template_path = (
54-
Path(str(importlib.resources.files('seml')))
55-
/ 'templates'
56-
/ 'project'
57-
/ template
58-
)
59-
if not template_path.exists():
60-
logging.error(f'Template "{template}" does not exist')
61-
exit(1)
62-
63-
if project_name is None:
64-
project_name = directory.name
65-
if user_name is None:
66-
user_name = os.getenv('USER', os.getenv('USERNAME', 'user'))
67-
if user_mail is None:
68-
user_mail = 'my@mail.com'
69-
format_map = dict(
70-
project_name=project_name, user_name=user_name, user_mail=user_mail
71-
)
72-
73-
gitignore_path = template_path / '.gitignore'
74-
if gitignore_path.exists():
75-
ignore_file = parse_gitignore(gitignore_path)
76-
else:
77-
78-
def ignore_file(file_path: str):
79-
return False
80-
81-
# Copy files one-by-one
82-
for src in template_path.glob('**/*'):
83-
# skip files ignored by .gitignore
84-
if ignore_file(str(src)):
85-
continue
86-
# construct destination
87-
file_name = src.relative_to(template_path)
88-
target_file_name = Path(str(file_name).format_map(format_map))
89-
dst = directory / target_file_name
90-
# Create directories
91-
if src.is_dir():
92-
if not dst.exists():
93-
dst.mkdir()
94-
elif not dst.exists():
95-
# For templates fill in variables
96-
if src.suffix.endswith('.template'):
97-
dst = dst.with_suffix(src.suffix.removesuffix('.template'))
98-
dst.write_text(src.read_text().format_map(format_map))
99-
else:
100-
# Other files copy directly
101-
dst.write_bytes(src.read_bytes())
62+
63+
tmp_dir = checkout_template_repo(git_remote, git_commit)
64+
with checkout_template_repo(git_remote, git_commit) as tmp_dir:
65+
template_path = tmp_dir / 'templates' / template
66+
if not template_path.exists():
67+
logging.error(f'Template "{template}" does not exist')
68+
exit(1)
69+
70+
logging.info(
71+
f'Initializing project in "{directory}" using template "{template}".'
72+
)
73+
74+
if project_name is None:
75+
project_name = directory.name
76+
if user_name is None:
77+
user_name = os.getenv('USER', os.getenv('USERNAME', 'user'))
78+
if user_mail is None:
79+
user_mail = 'my@mail.com'
80+
format_map = dict(
81+
project_name=project_name, user_name=user_name, user_mail=user_mail
82+
)
83+
84+
gitignore_path = template_path / '.gitignore'
85+
if gitignore_path.exists():
86+
ignore_file = parse_gitignore(gitignore_path)
87+
else:
88+
89+
def ignore_file(file_path: str):
90+
return False
91+
92+
# Copy files one-by-one
93+
for src in template_path.glob('**/*'):
94+
# skip files ignored by .gitignore
95+
if ignore_file(str(src)):
96+
continue
97+
# construct destination
98+
file_name = src.relative_to(template_path)
99+
target_file_name = Path(str(file_name).format_map(format_map))
100+
dst = directory / target_file_name
101+
# Create directories
102+
if src.is_dir():
103+
if not dst.exists():
104+
dst.mkdir()
105+
elif not dst.exists():
106+
# For templates fill in variables
107+
if src.suffix.endswith('.template'):
108+
dst = dst.with_suffix(src.suffix.replace('.template', ''))
109+
dst.write_text(src.read_text().format_map(format_map))
110+
else:
111+
# Other files copy directly
112+
dst.write_bytes(src.read_bytes())
102113
logging.info('Project initialized successfully')
103114

104115

105-
def get_available_templates():
116+
@contextmanager
117+
def checkout_template_repo(
118+
git_remote: Optional[str] = None, git_commit: Optional[str] = None
119+
):
120+
"""
121+
Context manager to clone the template repository. The cloned repository
122+
is deleted after the context is left.
123+
124+
Parameters
125+
----------
126+
git_remote : Optional[str]
127+
The git remote to use.
128+
git_commit : Optional[str]
129+
The git commit to use.
130+
"""
131+
import tempfile
132+
from git import Repo
133+
134+
if git_remote is None:
135+
git_remote = SETTINGS.TEMPLATE_REMOTE
136+
137+
with tempfile.TemporaryDirectory(dir=SETTINGS.TMP_DIRECTORY) as tmp_dir:
138+
try:
139+
repo = Repo.clone_from(git_remote, tmp_dir)
140+
if git_commit is not None:
141+
repo.head.reference = repo.commit(git_commit)
142+
repo.head.reset(index=True, working_tree=True)
143+
except Exception as e:
144+
logging.error(
145+
f'Failed to clone git repository "{git_remote}" to "{tmp_dir}"'
146+
)
147+
logging.error(e)
148+
exit(1)
149+
yield Path(repo.working_dir)
150+
151+
152+
def get_available_templates(
153+
git_remote: Optional[str] = None, git_commit: Optional[str] = None
154+
) -> List[str]:
106155
"""
107156
Return a list of available templates.
108157
158+
Parameters
159+
----------
160+
git_remote : Optional[str]
161+
The git remote to use.
162+
git_commit : Optional[str]
163+
The git commit to use.
164+
109165
Returns
110166
-------
111167
List[str]
112168
A list of available templates.
113169
"""
114-
import importlib.resources
170+
with checkout_template_repo(git_remote, git_commit) as repo:
171+
return [template.name for template in (repo / 'templates').iterdir()]
115172

116-
return [
117-
template.name
118-
for template in (
119-
Path(str(importlib.resources.files('seml'))) / 'templates' / 'project'
120-
).iterdir()
121-
]
122173

123-
124-
def print_available_templates():
174+
def print_available_templates(
175+
git_remote: Optional[str] = None, git_commit: Optional[str] = None
176+
):
125177
"""
126178
Print the available templates.
179+
180+
Parameters
181+
----------
182+
git_remote : Optional[str]
183+
The git remote to use.
184+
git_commit : Optional[str]
185+
The git commit to use.
127186
"""
128187
result = 'Available templates:'
129-
for template in get_available_templates():
188+
for template in get_available_templates(git_remote, git_commit):
130189
result += f'\n - {template}'
131190
logging.info(result)

src/seml/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
# Directory which is used on the compute nodes to dump scripts and Python code.
2020
# Only change this if you know what you're doing.
2121
'TMP_DIRECTORY': '/tmp',
22+
'TEMPLATE_REMOTE': 'https://github.com/TUM-DAML/seml-templates.git',
2223
'DATABASE': {
2324
# location of the MongoDB config. Default: $HOME/.config/seml/monogdb.config
2425
'MONGODB_CONFIG_PATH': APP_DIR / 'mongodb.config'

0 commit comments

Comments
 (0)