Skip to content

Commit 31e5794

Browse files
authored
[Tooling] Port of generate_tests.py to Julia (#1111)
* Added helper scripts and modified generate_tests.py for Julia. * Modified JinJa2 macros file for Julia tests. * Added test template for acronym. * Added generated test file for acronym. * Added requirements.txt file for python scripts. * Added JuliaFormatter toml file and a comment in Project toml. * Removed refs to air (the r formatter). * Renamed install check to jlfmt from JuliaFormatter, since it is the CLI that is checked for. * Using subprocess to shell out and call jlfmt instead of using juliacall. * Final touches to formatting settings and template (for now).
1 parent dcdce6e commit 31e5794

9 files changed

Lines changed: 969 additions & 1 deletion

File tree

.JuliaFormatter.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
style = "yas"

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
44
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
55
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
66
Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa"
7+
#JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899"
8+

bin/data.py

Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
from enum import Enum
2+
from dataclasses import dataclass, asdict, fields
3+
import dataclasses
4+
from itertools import chain
5+
import json
6+
from pathlib import Path
7+
from typing import List, Any, Dict, Type
8+
9+
# Tomli was subsumed into Python 3.11.x, but was renamed to to tomllib.
10+
# This avoids ci failures for Python < 3.11.2.
11+
try:
12+
import tomllib
13+
except ModuleNotFoundError:
14+
import tomli as tomllib
15+
16+
17+
18+
def _custom_dataclass_init(self, *args, **kwargs):
19+
# print(self.__class__.__name__, "__init__")
20+
names = [field.name for field in fields(self)]
21+
used_names = set()
22+
23+
# Handle positional arguments
24+
for value in args:
25+
try:
26+
name = names.pop(0)
27+
except IndexError:
28+
raise TypeError(f"__init__() given too many positional arguments")
29+
# print(f'setting {k}={v}')
30+
setattr(self, name, value)
31+
used_names.add(name)
32+
33+
# Handle keyword arguments
34+
for name, value in kwargs.items():
35+
if name in names:
36+
# print(f'setting {k}={v}')
37+
setattr(self, name, value)
38+
used_names.add(name)
39+
elif name in used_names:
40+
raise TypeError(f"__init__() got multiple values for argument '{name}'")
41+
else:
42+
raise TypeError(
43+
f"Unrecognized field '{name}' for dataclass {self.__class__.__name__}."
44+
"\nIf this field is valid, please add it to the dataclass in data.py."
45+
"\nIf adding an object-type field, please create a new dataclass for it."
46+
)
47+
48+
# Check for missing positional arguments
49+
missing = [
50+
f"'{field.name}'" for field in fields(self)
51+
if isinstance(field.default, dataclasses._MISSING_TYPE) and field.name not in used_names
52+
]
53+
if len(missing) == 1:
54+
raise TypeError(f"__init__() missing 1 required positional argument: {missing[0]}")
55+
elif len(missing) == 2:
56+
raise TypeError(f"__init__() missing 2 required positional arguments: {' and '.join(missing)}")
57+
elif len(missing) != 0:
58+
missing[-1] = f"and {missing[-1]}"
59+
raise TypeError(f"__init__() missing {len(missing)} required positional arguments: {', '.join(missing)}")
60+
61+
# Run post init if available
62+
if hasattr(self, "__post_init__"):
63+
self.__post_init__()
64+
65+
66+
@dataclass
67+
class TrackStatus:
68+
__init__ = _custom_dataclass_init
69+
70+
concept_exercises: bool = False
71+
test_runner: bool = False
72+
representer: bool = False
73+
analyzer: bool = False
74+
75+
76+
class IndentStyle(str, Enum):
77+
Space = "space"
78+
Tab = "tab"
79+
80+
81+
@dataclass
82+
class TestRunnerSettings:
83+
average_run_time: float = -1
84+
85+
86+
@dataclass
87+
class EditorSettings:
88+
__init__ = _custom_dataclass_init
89+
90+
indent_style: IndentStyle = IndentStyle.Space
91+
indent_size: int = 4
92+
ace_editor_language: str = "python"
93+
highlightjs_language: str = "python"
94+
95+
def __post_init__(self):
96+
if isinstance(self.indent_style, str):
97+
self.indent_style = IndentStyle(self.indent_style)
98+
99+
100+
class ExerciseStatus(str, Enum):
101+
Active = "active"
102+
WIP = "wip"
103+
Beta = "beta"
104+
Deprecated = "deprecated"
105+
106+
107+
@dataclass
108+
class ExerciseFiles:
109+
__init__ = _custom_dataclass_init
110+
111+
solution: List[str]
112+
test: List[str]
113+
editor: List[str] = None
114+
exemplar: List[str] = None
115+
116+
117+
# practice exercises are different
118+
example: List[str] = None
119+
120+
def __post_init__(self):
121+
if self.exemplar is None:
122+
if self.example is None:
123+
raise ValueError(
124+
"exercise config must have either files.exemplar or files.example"
125+
)
126+
else:
127+
self.exemplar = self.example
128+
delattr(self, "example")
129+
elif self.example is not None:
130+
raise ValueError(
131+
"exercise config must have either files.exemplar or files.example, but not both"
132+
)
133+
134+
135+
@dataclass
136+
class ExerciseConfig:
137+
__init__ = _custom_dataclass_init
138+
139+
files: ExerciseFiles
140+
authors: List[str] = None
141+
forked_from: str = None
142+
contributors: List[str] = None
143+
language_versions: List[str] = None
144+
test_runner: bool = True
145+
source: str = None
146+
source_url: str = None
147+
blurb: str = None
148+
icon: str = None
149+
150+
def __post_init__(self):
151+
if isinstance(self.files, dict):
152+
self.files = ExerciseFiles(**self.files)
153+
for attr in ["authors", "contributors", "language_versions"]:
154+
if getattr(self, attr) is None:
155+
setattr(self, attr, [])
156+
157+
@classmethod
158+
def load(cls, config_file: Path) -> "ExerciseConfig":
159+
with config_file.open() as f:
160+
return cls(**json.load(f))
161+
162+
163+
@dataclass
164+
class ExerciseInfo:
165+
__init__ = _custom_dataclass_init
166+
167+
path: Path
168+
slug: str
169+
name: str
170+
uuid: str
171+
prerequisites: List[str]
172+
type: str = "practice"
173+
status: ExerciseStatus = ExerciseStatus.Active
174+
175+
# concept only
176+
concepts: List[str] = None
177+
178+
# practice only
179+
difficulty: int = 1
180+
topics: List[str] = None
181+
practices: List[str] = None
182+
183+
def __post_init__(self):
184+
if self.concepts is None:
185+
self.concepts = []
186+
if self.topics is None:
187+
self.topics = []
188+
if self.practices is None:
189+
self.practices = []
190+
if isinstance(self.status, str):
191+
self.status = ExerciseStatus(self.status)
192+
193+
@property
194+
def solution_stub(self):
195+
return next(
196+
(
197+
p
198+
for p in self.path.glob("*.py")
199+
if not p.name.endswith("_test.py") and p.name != "example.py"
200+
),
201+
None,
202+
)
203+
204+
@property
205+
def helper_file(self):
206+
return next(self.path.glob("*_data.py"), None)
207+
208+
@property
209+
def test_file(self):
210+
return next(self.path.glob("*_test.py"), None)
211+
212+
@property
213+
def meta_dir(self):
214+
return self.path / ".meta"
215+
216+
@property
217+
def exemplar_file(self):
218+
if self.type == "concept":
219+
return self.meta_dir / "exemplar.py"
220+
return self.meta_dir / "example.py"
221+
222+
@property
223+
def template_path(self):
224+
return self.meta_dir / "template.j2"
225+
226+
@property
227+
def config_file(self):
228+
return self.meta_dir / "config.json"
229+
230+
def load_config(self) -> ExerciseConfig:
231+
return ExerciseConfig.load(self.config_file)
232+
233+
234+
@dataclass
235+
class Exercises:
236+
__init__ = _custom_dataclass_init
237+
238+
concept: List[ExerciseInfo]
239+
practice: List[ExerciseInfo]
240+
foregone: List[str] = None
241+
242+
def __post_init__(self):
243+
if self.foregone is None:
244+
self.foregone = []
245+
for attr_name in ["concept", "practice"]:
246+
base_path = Path("exercises") / attr_name
247+
setattr(
248+
self,
249+
attr_name,
250+
[
251+
(
252+
ExerciseInfo(path=(base_path / e["slug"]), type=attr_name, **e)
253+
if isinstance(e, dict)
254+
else e
255+
)
256+
for e in getattr(self, attr_name)
257+
],
258+
)
259+
260+
def all(self, status_filter={ExerciseStatus.Active, ExerciseStatus.Beta}):
261+
return [
262+
e for e in chain(self.concept, self.practice) if e.status in status_filter
263+
]
264+
265+
266+
@dataclass
267+
class Concept:
268+
__init__ = _custom_dataclass_init
269+
270+
uuid: str
271+
slug: str
272+
name: str
273+
274+
275+
@dataclass
276+
class Feature:
277+
__init__ = _custom_dataclass_init
278+
279+
title: str
280+
content: str
281+
icon: str
282+
283+
284+
@dataclass
285+
class FilePatterns:
286+
__init__ = _custom_dataclass_init
287+
288+
solution: List[str]
289+
test: List[str]
290+
example: List[str]
291+
exemplar: List[str]
292+
editor: List[str] = None
293+
294+
295+
296+
@dataclass
297+
class Config:
298+
__init__ = _custom_dataclass_init
299+
300+
language: str
301+
slug: str
302+
active: bool
303+
status: TrackStatus
304+
blurb: str
305+
version: int
306+
online_editor: EditorSettings
307+
exercises: Exercises
308+
concepts: List[Concept]
309+
key_features: List[Feature] = None
310+
tags: List[Any] = None
311+
test_runner: TestRunnerSettings = None
312+
files: FilePatterns = None
313+
314+
def __post_init__(self):
315+
if isinstance(self.status, dict):
316+
self.status = TrackStatus(**self.status)
317+
if isinstance(self.online_editor, dict):
318+
self.online_editor = EditorSettings(**self.online_editor)
319+
if isinstance(self.test_runner, dict):
320+
self.test_runner = TestRunnerSettings(**self.test_runner)
321+
if isinstance(self.exercises, dict):
322+
self.exercises = Exercises(**self.exercises)
323+
if isinstance(self.files, dict):
324+
self.files = FilePatterns(**self.files)
325+
self.concepts = [
326+
(Concept(**c) if isinstance(c, dict) else c) for c in self.concepts
327+
]
328+
if self.key_features is None:
329+
self.key_features = []
330+
if self.tags is None:
331+
self.tags = []
332+
333+
@classmethod
334+
def load(cls, path="config.json"):
335+
try:
336+
with Path(path).open() as f:
337+
return cls(**json.load(f))
338+
except IOError:
339+
print(f"FAIL: {path} file not found")
340+
raise SystemExit(1)
341+
except TypeError as ex:
342+
print(f"FAIL: {ex}")
343+
raise SystemExit(1)
344+
345+
346+
@dataclass
347+
class TestCaseTOML:
348+
__init__ = _custom_dataclass_init
349+
350+
uuid: str
351+
description: str
352+
include: bool = True
353+
comment: str = ''
354+
355+
356+
@dataclass
357+
class TestsTOML:
358+
__init__ = _custom_dataclass_init
359+
360+
cases: Dict[str, TestCaseTOML]
361+
362+
@classmethod
363+
def load(cls, toml_path: Path):
364+
with toml_path.open("rb") as f:
365+
data = tomllib.load(f)
366+
return cls({uuid: TestCaseTOML(uuid, *opts) for
367+
uuid, opts in
368+
data.items() if
369+
opts.get('include', None) is not False})
370+
371+
372+
if __name__ == "__main__":
373+
374+
class CustomEncoder(json.JSONEncoder):
375+
def default(self, obj):
376+
if isinstance(obj, Path):
377+
return str(obj)
378+
return json.JSONEncoder.default(self, obj)
379+
380+
config = Config.load()
381+
print(json.dumps(asdict(config), cls=CustomEncoder, indent=2))

0 commit comments

Comments
 (0)