1414"""
1515import argparse
1616import filecmp
17+ import importlib .util
1718import json
1819import logging
1920import os
2930
3031from jinja2 import Environment , FileSystemLoader , TemplateNotFound , UndefinedError
3132
32- VERSION = '0.1.1'
33+ VERSION = "0.2.0"
3334
34- DEFAULT_SPEC_LOCATION = os .path .join ('..' , ' problem-specifications' )
35- RGX_WORDS = re .compile (r' [-_\s]|(?=[A-Z])' )
35+ DEFAULT_SPEC_LOCATION = os .path .join (".." , " problem-specifications" )
36+ RGX_WORDS = re .compile (r" [-_\s]|(?=[A-Z])" )
3637
3738logging .basicConfig ()
38- logger = logging .getLogger (' generator' )
39+ logger = logging .getLogger (" generator" )
3940logger .setLevel (logging .WARN )
4041
4142
@@ -44,8 +45,9 @@ def replace_all(string, chars, rep):
4445 Replace any char in chars with rep, reduce runs and strip terminal ends.
4546 """
4647 trans = str .maketrans (dict (zip (chars , repeat (rep ))))
47- return re .sub ("{0}+" .format (re .escape (rep )), rep ,
48- string .translate (trans )).strip (rep )
48+ return re .sub ("{0}+" .format (re .escape (rep )), rep , string .translate (trans )).strip (
49+ rep
50+ )
4951
5052
5153def to_snake (string ):
@@ -61,7 +63,7 @@ def camel_case(string):
6163 """
6264 Convert pretty much anything to CamelCase.
6365 """
64- return '' .join (w .title () for w in to_snake (string ).split ('_' ))
66+ return "" .join (w .title () for w in to_snake (string ).split ("_" ))
6567
6668
6769def get_tested_properties (spec ):
@@ -79,9 +81,9 @@ def get_tested_properties(spec):
7981
8082def error_case (case ):
8183 return (
82- "expected" in case and
83- isinstance (case ["expected" ], dict ) and
84- "error" in case ["expected" ]
84+ "expected" in case
85+ and isinstance (case ["expected" ], dict )
86+ and "error" in case ["expected" ]
8587 )
8688
8789
@@ -103,9 +105,7 @@ def load_canonical(exercise, spec_path):
103105 """
104106 Loads the canonical data for an exercise as a nested dictionary
105107 """
106- full_path = os .path .join (
107- spec_path , 'exercises' , exercise , 'canonical-data.json'
108- )
108+ full_path = os .path .join (spec_path , "exercises" , exercise , "canonical-data.json" )
109109 with open (full_path ) as f :
110110 spec = json .load (f )
111111 spec ["properties" ] = get_tested_properties (spec )
@@ -116,7 +116,7 @@ def load_additional_tests(exercise):
116116 """
117117 Loads additional tests from .meta/additional_tests.json
118118 """
119- full_path = os .path .join (' exercises' , exercise , ' .meta' , ' additional_tests.json' )
119+ full_path = os .path .join (" exercises" , exercise , " .meta" , " additional_tests.json" )
120120 try :
121121 with open (full_path ) as f :
122122 data = json .load (f )
@@ -129,7 +129,7 @@ def format_file(path):
129129 """
130130 Runs black auto-formatter on file at path
131131 """
132- check_call ([' black' , '-q' , path ])
132+ check_call ([" black" , "-q" , path ])
133133
134134
135135def compare_existing (rendered , tests_path ):
@@ -150,54 +150,69 @@ def generate_exercise(env, spec_path, exercise, check=False):
150150 False: saves rendered to tests file
151151 """
152152 slug = os .path .basename (exercise )
153+ meta_dir = os .path .join (exercise , ".meta" )
154+ plugins_module = None
155+ plugins_name = "plugins"
156+ plugins_source = os .path .join (meta_dir , f"{ plugins_name } .py" )
153157 try :
158+ if os .path .isfile (plugins_source ):
159+ plugins_spec = importlib .util .spec_from_file_location (
160+ plugins_name , plugins_source
161+ )
162+ plugins_module = importlib .util .module_from_spec (plugins_spec )
163+ sys .modules [plugins_name ] = plugins_module
164+ plugins_spec .loader .exec_module (plugins_module )
154165 spec = load_canonical (slug , spec_path )
155166 additional_tests = load_additional_tests (slug )
156167 spec ["additional_cases" ] = additional_tests
157- template_path = posixpath .join (slug , ' .meta' , ' template.j2' )
168+ template_path = posixpath .join (slug , " .meta" , " template.j2" )
158169 template = env .get_template (template_path )
159- tests_path = os .path .join (
160- exercise , f'{ to_snake (slug )} _test.py'
161- )
170+ tests_path = os .path .join (exercise , f"{ to_snake (slug )} _test.py" )
162171 spec ["has_error_case" ] = has_error_case (spec ["cases" ])
163- logger .info (f'{ slug } : attempting render' )
172+ if plugins_module is not None :
173+ spec [plugins_name ] = plugins_module
174+ logger .info (f"{ slug } : attempting render" )
164175 rendered = template .render (** spec )
165- with NamedTemporaryFile ('w' , delete = False ) as tmp :
176+ with NamedTemporaryFile ("w" , delete = False ) as tmp :
166177 tmp .write (rendered )
167178 try :
168- logger .debug (f' { slug } : formatting tmp file' )
179+ logger .debug (f" { slug } : formatting tmp file" )
169180 format_file (tmp .name )
170181 except FileNotFoundError as e :
171- logger .error (f' { slug } : the black utility must be installed' )
182+ logger .error (f" { slug } : the black utility must be installed" )
172183 return False
173184
174185 if check :
175186 try :
176187 if not filecmp .cmp (tmp .name , tests_path ):
177- logger .error (f'{ slug } : check failed; tests must be regenerated with bin/generate_tests.py' )
188+ logger .error (
189+ f"{ slug } : check failed; tests must be regenerated with bin/generate_tests.py"
190+ )
178191 return False
179192 finally :
180193 os .remove (tmp .name )
181194 else :
182195 shutil .move (tmp .name , tests_path )
183- print (f' { slug } generated at { tests_path } ' )
184- except (TypeError , UndefinedError ) as e :
196+ print (f" { slug } generated at { tests_path } " )
197+ except (TypeError , UndefinedError , SyntaxError ) as e :
185198 logger .debug (str (e ))
186- logger .error (f' { slug } : generation failed' )
199+ logger .error (f" { slug } : generation failed" )
187200 return False
188201 except TemplateNotFound as e :
189202 logger .debug (str (e ))
190- logger .info (f' { slug } : no template found; skipping' )
203+ logger .info (f" { slug } : no template found; skipping" )
191204 except FileNotFoundError as e :
192205 logger .debug (str (e ))
193- logger .info (f' { slug } : no canonical data found; skipping' )
206+ logger .info (f" { slug } : no canonical data found; skipping" )
194207 return True
195208
196209
197210def generate (
198- exercise_glob , spec_path = DEFAULT_SPEC_LOCATION ,
199- stop_on_failure = False , check = False ,
200- ** kwargs
211+ exercise_glob ,
212+ spec_path = DEFAULT_SPEC_LOCATION ,
213+ stop_on_failure = False ,
214+ check = False ,
215+ ** kwargs ,
201216):
202217 """
203218 Primary entry point. Generates test files for all exercises matching exercise_glob
@@ -206,14 +221,14 @@ def generate(
206221 if not shutil .which ("black" ):
207222 logger .error ("the black utility must be installed" )
208223 sys .exit (1 )
209- loader = FileSystemLoader ([' config' , ' exercises' ])
224+ loader = FileSystemLoader ([" config" , " exercises" ])
210225 env = Environment (loader = loader , keep_trailing_newline = True )
211- env .filters [' to_snake' ] = to_snake
212- env .filters [' camel_case' ] = camel_case
213- env .filters [' regex_replace' ] = regex_replace
214- env .tests [' error_case' ] = error_case
226+ env .filters [" to_snake" ] = to_snake
227+ env .filters [" camel_case" ] = camel_case
228+ env .filters [" regex_replace" ] = regex_replace
229+ env .tests [" error_case" ] = error_case
215230 result = True
216- for exercise in glob (os .path .join (' exercises' , exercise_glob )):
231+ for exercise in glob (os .path .join (" exercises" , exercise_glob )):
217232 if not generate_exercise (env , spec_path , exercise , check ):
218233 result = False
219234 if stop_on_failure :
@@ -222,34 +237,28 @@ def generate(
222237 sys .exit (1 )
223238
224239
225- if __name__ == ' __main__' :
240+ if __name__ == " __main__" :
226241 parser = argparse .ArgumentParser ()
242+ parser .add_argument ("exercise_glob" , nargs = "?" , default = "*" , metavar = "EXERCISE" )
227243 parser .add_argument (
228- 'exercise_glob' , nargs = '?' , default = '*' , metavar = 'EXERCISE'
229- )
230- parser .add_argument (
231- '--version' , action = 'version' ,
232- version = '%(prog)s {} for Python {}' .format (
233- VERSION , sys .version .split ("\n " )[0 ],
234- )
244+ "--version" ,
245+ action = "version" ,
246+ version = "%(prog)s {} for Python {}" .format (VERSION , sys .version .split ("\n " )[0 ]),
235247 )
236- parser .add_argument ('-v' , ' --verbose' , action = ' store_true' )
248+ parser .add_argument ("-v" , " --verbose" , action = " store_true" )
237249 parser .add_argument (
238- '-p' , '--spec-path' ,
250+ "-p" ,
251+ "--spec-path" ,
239252 default = DEFAULT_SPEC_LOCATION ,
240253 help = (
241- 'path to clone of exercism/problem-specifications '
242- '(default: %(default)s)'
243- )
244- )
245- parser .add_argument (
246- '--stop-on-failure' ,
247- action = 'store_true'
254+ "path to clone of exercism/problem-specifications " "(default: %(default)s)"
255+ ),
248256 )
257+ parser .add_argument ("--stop-on-failure" , action = "store_true" )
249258 parser .add_argument (
250- ' --check' ,
251- action = ' store_true' ,
252- help = ' check if tests are up-to-date, but do not modify test files'
259+ " --check" ,
260+ action = " store_true" ,
261+ help = " check if tests are up-to-date, but do not modify test files" ,
253262 )
254263 opts = parser .parse_args ()
255264 if opts .verbose :
0 commit comments