11import importlib .util
22import json
3+ import os
34import sys
5+ import tempfile
46import urllib .parse
7+ from os import execlp
58from pathlib import Path
9+ from types import ModuleType
610from typing import Any , Dict , Optional , Tuple , TypeVar , Union
711
812import click
913import requests
1014
1115from app .exercise_config import ExerciseConfig
1216from app .utils .click import error
17+ from app .utils .general import ensure_str
1318
1419GITMASTERY_CONFIG_NAME = ".gitmastery.json"
1520GITMASTERY_EXERCISE_CONFIG_NAME = ".gitmastery-exercise.json"
@@ -161,29 +166,14 @@ def download_file(url: str, path: str, is_binary: bool) -> None:
161166T = 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-
173169def 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+
219215def 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