-
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Adapt Python generator to Roc * Use | tojson to escape the expected string
- Loading branch information
Showing
10 changed files
with
984 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,3 +2,5 @@ | |
.DS_Store | ||
bin/configlet | ||
bin/configlet.exe | ||
.problem-specifications/ | ||
**/__pycache__/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,391 @@ | ||
# Credit: this code was copied from the Python track and adapted to Roc | ||
|
||
from enum import Enum | ||
from dataclasses import dataclass, asdict, fields | ||
import dataclasses | ||
from itertools import chain | ||
import json | ||
from pathlib import Path | ||
from typing import List, Any, Dict | ||
|
||
# Tomli was subsumed into Python 3.11.x, but was renamed to to tomllib. | ||
# This avoids ci failures for Python < 3.11.2. | ||
try: | ||
import tomllib | ||
except ModuleNotFoundError: | ||
import tomli as tomllib | ||
|
||
|
||
def _custom_dataclass_init(self, *args, **kwargs): | ||
# print(self.__class__.__name__, "__init__") | ||
names = [field.name for field in fields(self)] | ||
used_names = set() | ||
|
||
# Handle positional arguments | ||
for value in args: | ||
try: | ||
name = names.pop(0) | ||
except IndexError: | ||
raise TypeError("__init__() given too many positional arguments") | ||
# print(f'setting {k}={v}') | ||
setattr(self, name, value) | ||
used_names.add(name) | ||
|
||
# Handle keyword arguments | ||
for name, value in kwargs.items(): | ||
if name in names: | ||
# print(f'setting {k}={v}') | ||
setattr(self, name, value) | ||
used_names.add(name) | ||
elif name in used_names: | ||
raise TypeError(f"__init__() got multiple values for argument '{name}'") | ||
else: | ||
raise TypeError( | ||
f"Unrecognized field '{name}' for dataclass {self.__class__.__name__}." | ||
"\nIf this field is valid, please add it to the dataclass in data.py." | ||
"\nIf adding an object-type field, please create a new dataclass for it." | ||
) | ||
|
||
# Check for missing positional arguments | ||
missing = [ | ||
f"'{field.name}'" | ||
for field in fields(self) | ||
if isinstance(field.default, dataclasses._MISSING_TYPE) | ||
and field.name not in used_names | ||
] | ||
if len(missing) == 1: | ||
raise TypeError( | ||
f"__init__() missing 1 required positional argument: {missing[0]}" | ||
) | ||
elif len(missing) == 2: | ||
raise TypeError( | ||
f"__init__() missing 2 required positional arguments: {' and '.join(missing)}" | ||
) | ||
elif len(missing) != 0: | ||
missing[-1] = f"and {missing[-1]}" | ||
raise TypeError( | ||
f"__init__() missing {len(missing)} required positional arguments: {', '.join(missing)}" | ||
) | ||
|
||
# Run post init if available | ||
if hasattr(self, "__post_init__"): | ||
self.__post_init__() | ||
|
||
|
||
@dataclass | ||
class TrackStatus: | ||
__init__ = _custom_dataclass_init | ||
|
||
concept_exercises: bool = False | ||
test_runner: bool = False | ||
representer: bool = False | ||
analyzer: bool = False | ||
|
||
|
||
class IndentStyle(str, Enum): | ||
Space = "space" | ||
Tab = "tab" | ||
|
||
|
||
@dataclass | ||
class TestRunnerSettings: | ||
average_run_time: float = -1 | ||
|
||
|
||
@dataclass | ||
class EditorSettings: | ||
__init__ = _custom_dataclass_init | ||
|
||
indent_style: IndentStyle = IndentStyle.Space | ||
indent_size: int = 4 | ||
ace_editor_language: str = "python" | ||
highlightjs_language: str = "python" | ||
|
||
def __post_init__(self): | ||
if isinstance(self.indent_style, str): | ||
self.indent_style = IndentStyle(self.indent_style) | ||
|
||
|
||
class ExerciseStatus(str, Enum): | ||
Active = "active" | ||
WIP = "wip" | ||
Beta = "beta" | ||
Deprecated = "deprecated" | ||
|
||
|
||
@dataclass | ||
class ExerciseFiles: | ||
__init__ = _custom_dataclass_init | ||
|
||
solution: List[str] | ||
test: List[str] | ||
editor: List[str] = None | ||
exemplar: List[str] = None | ||
|
||
# practice exercises are different | ||
example: List[str] = None | ||
|
||
def __post_init__(self): | ||
if self.exemplar is None: | ||
if self.example is None: | ||
raise ValueError( | ||
"exercise config must have either files.exemplar or files.example" | ||
) | ||
else: | ||
self.exemplar = self.example | ||
delattr(self, "example") | ||
elif self.example is not None: | ||
raise ValueError( | ||
"exercise config must have either files.exemplar or files.example, but not both" | ||
) | ||
|
||
|
||
@dataclass | ||
class ExerciseConfig: | ||
__init__ = _custom_dataclass_init | ||
|
||
files: ExerciseFiles | ||
authors: List[str] = None | ||
forked_from: str = None | ||
contributors: List[str] = None | ||
language_versions: List[str] = None | ||
test_runner: bool = True | ||
source: str = None | ||
source_url: str = None | ||
blurb: str = None | ||
icon: str = None | ||
|
||
def __post_init__(self): | ||
if isinstance(self.files, dict): | ||
self.files = ExerciseFiles(**self.files) | ||
for attr in ["authors", "contributors", "language_versions"]: | ||
if getattr(self, attr) is None: | ||
setattr(self, attr, []) | ||
|
||
@classmethod | ||
def load(cls, config_file: Path) -> "ExerciseConfig": | ||
with config_file.open() as f: | ||
return cls(**json.load(f)) | ||
|
||
|
||
@dataclass | ||
class ExerciseInfo: | ||
__init__ = _custom_dataclass_init | ||
|
||
path: Path | ||
slug: str | ||
name: str | ||
uuid: str | ||
prerequisites: List[str] | ||
type: str = "practice" | ||
status: ExerciseStatus = ExerciseStatus.Active | ||
|
||
# concept only | ||
concepts: List[str] = None | ||
|
||
# practice only | ||
difficulty: int = 1 | ||
topics: List[str] = None | ||
practices: List[str] = None | ||
|
||
def __post_init__(self): | ||
if self.concepts is None: | ||
self.concepts = [] | ||
if self.topics is None: | ||
self.topics = [] | ||
if self.practices is None: | ||
self.practices = [] | ||
if isinstance(self.status, str): | ||
self.status = ExerciseStatus(self.status) | ||
|
||
@property | ||
def solution_stub(self): | ||
return next( | ||
( | ||
p | ||
for p in self.path.glob("*.roc") | ||
if not p.name.endswith("-test.roc") and p.name != "Example.roc" | ||
), | ||
None, | ||
) | ||
|
||
@property | ||
def helper_file(self): | ||
return next(self.path.glob("*Data.roc"), None) | ||
|
||
@property | ||
def test_file(self): | ||
return next(self.path.glob("*-test.roc"), None) | ||
|
||
@property | ||
def meta_dir(self): | ||
return self.path / ".meta" | ||
|
||
@property | ||
def exemplar_file(self): | ||
if self.type == "concept": | ||
return self.meta_dir / "Exemplar.roc" | ||
return self.meta_dir / "Example.roc" | ||
|
||
@property | ||
def template_path(self): | ||
return self.meta_dir / "template.j2" | ||
|
||
@property | ||
def config_file(self): | ||
return self.meta_dir / "config.json" | ||
|
||
def load_config(self) -> ExerciseConfig: | ||
return ExerciseConfig.load(self.config_file) | ||
|
||
|
||
@dataclass | ||
class Exercises: | ||
__init__ = _custom_dataclass_init | ||
|
||
concept: List[ExerciseInfo] | ||
practice: List[ExerciseInfo] | ||
foregone: List[str] = None | ||
|
||
def __post_init__(self): | ||
if self.foregone is None: | ||
self.foregone = [] | ||
for attr_name in ["concept", "practice"]: | ||
base_path = Path("exercises") / attr_name | ||
setattr( | ||
self, | ||
attr_name, | ||
[ | ||
( | ||
ExerciseInfo(path=(base_path / e["slug"]), type=attr_name, **e) | ||
if isinstance(e, dict) | ||
else e | ||
) | ||
for e in getattr(self, attr_name) | ||
], | ||
) | ||
|
||
def all(self, status_filter={ExerciseStatus.Active, ExerciseStatus.Beta}): | ||
return [ | ||
e for e in chain(self.concept, self.practice) if e.status in status_filter | ||
] | ||
|
||
|
||
@dataclass | ||
class Concept: | ||
__init__ = _custom_dataclass_init | ||
|
||
uuid: str | ||
slug: str | ||
name: str | ||
|
||
|
||
@dataclass | ||
class Feature: | ||
__init__ = _custom_dataclass_init | ||
|
||
title: str | ||
content: str | ||
icon: str | ||
|
||
|
||
@dataclass | ||
class FilePatterns: | ||
__init__ = _custom_dataclass_init | ||
|
||
solution: List[str] | ||
test: List[str] | ||
example: List[str] | ||
exemplar: List[str] | ||
editor: List[str] = None | ||
|
||
|
||
@dataclass | ||
class Config: | ||
__init__ = _custom_dataclass_init | ||
|
||
language: str | ||
slug: str | ||
active: bool | ||
status: TrackStatus | ||
blurb: str | ||
version: int | ||
online_editor: EditorSettings | ||
exercises: Exercises | ||
concepts: List[Concept] | ||
key_features: List[Feature] = None | ||
tags: List[Any] = None | ||
test_runner: TestRunnerSettings = None | ||
files: FilePatterns = None | ||
|
||
def __post_init__(self): | ||
if isinstance(self.status, dict): | ||
self.status = TrackStatus(**self.status) | ||
if isinstance(self.online_editor, dict): | ||
self.online_editor = EditorSettings(**self.online_editor) | ||
if isinstance(self.test_runner, dict): | ||
self.test_runner = TestRunnerSettings(**self.test_runner) | ||
if isinstance(self.exercises, dict): | ||
self.exercises = Exercises(**self.exercises) | ||
if isinstance(self.files, dict): | ||
self.files = FilePatterns(**self.files) | ||
self.concepts = [ | ||
(Concept(**c) if isinstance(c, dict) else c) for c in self.concepts | ||
] | ||
if self.key_features is None: | ||
self.key_features = [] | ||
if self.tags is None: | ||
self.tags = [] | ||
|
||
@classmethod | ||
def load(cls, path="config.json"): | ||
try: | ||
with Path(path).open() as f: | ||
return cls(**json.load(f)) | ||
except IOError: | ||
print(f"FAIL: {path} file not found") | ||
raise SystemExit(1) | ||
except TypeError as ex: | ||
print(f"FAIL: {ex}") | ||
raise SystemExit(1) | ||
|
||
|
||
@dataclass | ||
class TestCaseTOML: | ||
__init__ = _custom_dataclass_init | ||
|
||
uuid: str | ||
description: str | ||
include: bool = True | ||
comment: str = "" | ||
|
||
|
||
@dataclass | ||
class TestsTOML: | ||
__init__ = _custom_dataclass_init | ||
|
||
cases: Dict[str, TestCaseTOML] | ||
|
||
@classmethod | ||
def load(cls, toml_path: Path): | ||
with toml_path.open("rb") as f: | ||
data = tomllib.load(f) | ||
return cls( | ||
{ | ||
uuid: TestCaseTOML(uuid, *opts) | ||
for uuid, opts in data.items() | ||
if opts.get("include", None) is not False | ||
} | ||
) | ||
|
||
|
||
if __name__ == "__main__": | ||
|
||
class CustomEncoder(json.JSONEncoder): | ||
def default(self, obj): | ||
if isinstance(obj, Path): | ||
return str(obj) | ||
return json.JSONEncoder.default(self, obj) | ||
|
||
config = Config.load() | ||
print(json.dumps(asdict(config), cls=CustomEncoder, indent=2)) |
Oops, something went wrong.