Skip to content

Commit 6786aa9

Browse files
committed
Support exercise utils configuration
1 parent 70bab72 commit 6786aa9

File tree

3 files changed

+69
-39
lines changed

3 files changed

+69
-39
lines changed

app/commands/download.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
get_gitmastery_file_path,
2727
get_variable_from_url,
2828
hands_on_exists,
29-
load_file_namespace,
29+
load_namespace_with_exercise_utils,
3030
read_gitmastery_exercise_config,
3131
require_gitmastery_root,
3232
)
@@ -223,7 +223,9 @@ def download_hands_on(
223223
os.makedirs(hands_on)
224224
os.chdir(hands_on)
225225

226-
hands_on_namespace = load_file_namespace(f"hands_on/{hands_on_without_prefix}.py")
226+
hands_on_namespace = load_namespace_with_exercise_utils(
227+
f"hands_on/{hands_on_without_prefix}.py"
228+
)
227229
requires_git = hands_on_namespace.get("__requires_git__", False)
228230
requires_github = hands_on_namespace.get("__requires_github__", False)
229231

app/utils/general.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
def ensure_str(val) -> str:
2+
if isinstance(val, bytes):
3+
return val.decode("utf-8", errors="replace").strip()
4+
return str(val).strip()

app/utils/gitmastery.py

Lines changed: 61 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
import importlib.util
22
import json
3+
import os
34
import sys
5+
import tempfile
46
import urllib.parse
7+
from os import execlp
58
from pathlib import Path
9+
from types import ModuleType
610
from typing import Any, Dict, Optional, Tuple, TypeVar, Union
711

812
import click
913
import requests
1014

1115
from app.exercise_config import ExerciseConfig
1216
from app.utils.click import error
17+
from app.utils.general import ensure_str
1318

1419
GITMASTERY_CONFIG_NAME = ".gitmastery.json"
1520
GITMASTERY_EXERCISE_CONFIG_NAME = ".gitmastery-exercise.json"
@@ -161,29 +166,14 @@ def download_file(url: str, path: str, is_binary: bool) -> None:
161166
T = TypeVar("T")
162167

163168

164-
def load_file_namespace(file_path: str) -> dict[str, Any]:
165-
sys.dont_write_bytecode = True
166-
py_file = fetch_file_contents(get_gitmastery_file_path(file_path), False)
167-
namespace: Dict[str, Any] = {}
168-
exec(py_file, namespace)
169-
sys.dont_write_bytecode = False
170-
return namespace
171-
172-
173169
def get_variable_from_url(
174170
exercise: str,
175171
file_path: str,
176172
variable_name: str,
177173
default_value: Optional[T] = None,
178174
) -> Optional[T]:
179-
sys.dont_write_bytecode = True
180-
py_file = fetch_file_contents(
181-
get_gitmastery_file_path(f"{exercise}/{file_path}"), False
182-
)
183-
namespace: Dict[str, Any] = {}
184-
exec(py_file, namespace)
175+
namespace = load_namespace_with_exercise_utils(f"{exercise}/{file_path}")
185176
variable = namespace.get(variable_name, default_value)
186-
sys.dont_write_bytecode = False
187177
return variable
188178

189179

@@ -216,6 +206,12 @@ def hands_on_exists(hands_on: str, timeout: int = 5) -> bool:
216206
return False
217207

218208

209+
# We hardcode this list because to fetch it dynamically requires a Github API call
210+
# which we only have 60/hour so it's unwise to do it
211+
# TODO(woojiahao): Find a better way around this
212+
EXERCISE_UTILS_FILES = ["__init__", "cli", "git", "file", "gitmastery"]
213+
214+
219215
def execute_py_file_function_from_url(
220216
exercise: str, file_path: str, function_name: str, params: Dict[str, Any]
221217
) -> Optional[Any]:
@@ -224,27 +220,55 @@ def execute_py_file_function_from_url(
224220
get_gitmastery_file_path(f"{exercise}/{file_path}"), False
225221
)
226222
namespace: Dict[str, Any] = {}
227-
exec(py_file, namespace)
228-
if function_name not in namespace:
229-
return None
230-
result = namespace[function_name](**params)
231-
sys.dont_write_bytecode = False
232-
return result
233-
234223

235-
def execute_py_file_function_from_file(
236-
file_path: str, function_name: str, **params: Dict[Any, Any]
237-
) -> Optional[Any]:
238-
path = Path(file_path)
224+
with tempfile.TemporaryDirectory() as tmpdir:
225+
package_root = os.path.join(tmpdir, "exercise_utils")
226+
os.makedirs(package_root, exist_ok=True)
227+
228+
for filename in EXERCISE_UTILS_FILES:
229+
exercise_utils_path = get_gitmastery_file_path(
230+
f"exercise_utils/{filename}.py"
231+
)
232+
exercise_utils_src = fetch_file_contents(exercise_utils_path, False)
233+
with open(f"{package_root}/{filename}.py", "w", encoding="utf-8") as f:
234+
f.write(ensure_str(exercise_utils_src))
235+
236+
sys.path.insert(0, tmpdir)
237+
try:
238+
exec(py_file, namespace)
239+
if function_name not in namespace:
240+
sys.dont_write_bytecode = False
241+
return None
242+
243+
result = namespace[function_name](**params)
244+
sys.dont_write_bytecode = False
245+
return result
246+
finally:
247+
sys.path.remove(tmpdir)
248+
249+
250+
def load_namespace_with_exercise_utils(file_path: str) -> Dict[str, Any]:
239251
sys.dont_write_bytecode = True
240-
spec = importlib.util.spec_from_file_location(function_name, path)
241-
assert spec is not None and spec.loader is not None
242-
module = importlib.util.module_from_spec(spec)
243-
spec.loader.exec_module(module)
244-
func = getattr(module, function_name, None)
245-
if callable(func):
246-
result = func(**params)
247-
sys.dont_write_bytecode = False
248-
return result
252+
py_file = fetch_file_contents(get_gitmastery_file_path(file_path), False)
253+
namespace: Dict[str, Any] = {}
254+
255+
with tempfile.TemporaryDirectory() as tmpdir:
256+
package_root = os.path.join(tmpdir, "exercise_utils")
257+
os.makedirs(package_root, exist_ok=True)
258+
259+
for filename in EXERCISE_UTILS_FILES:
260+
exercise_utils_path = get_gitmastery_file_path(
261+
f"exercise_utils/{filename}.py"
262+
)
263+
exercise_utils_src = fetch_file_contents(exercise_utils_path, False)
264+
with open(f"{package_root}/{filename}.py", "w", encoding="utf-8") as f:
265+
f.write(ensure_str(exercise_utils_src))
266+
267+
sys.path.insert(0, tmpdir)
268+
try:
269+
exec(py_file, namespace)
270+
finally:
271+
sys.path.remove(tmpdir)
272+
249273
sys.dont_write_bytecode = False
250-
return None
274+
return namespace

0 commit comments

Comments
 (0)