Skip to content

Commit 23240b3

Browse files
authored
Adding Practice Exercise Generator (#597)
* Adding Practice Exercise Generator This adds an exercise generator (as a Python script). It also updates the common-lisp/README.md file with instructions on how to use it. * Create lisp_exercise_generator.exe Created executable. Removed references to Python in the README when mentioning the exercise generator - the existence of the executable removes the need for it. * Added CLI functionality Added CLI functionality as an additional option. Updated README to reflect this. Created new executable from updated code. * Fixed possible bug In list creation, went from using `'(...` to using `(list...` to remove possible problems when creating nested lists. * Removed generator executable Removed the executable. Updated README to reflect use of Python to run the generator. Changed generated test equality: equalp -> equal * Fixed bools lispified as ints
1 parent e4b4f6b commit 23240b3

File tree

3 files changed

+686
-7
lines changed

3 files changed

+686
-7
lines changed

README.md

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ There are several ways to contribute to the Common Lisp track including (but not
2020

2121
There are two guides to the structure of the track and tooling which would be good to be familiar with.
2222

23-
* The [language track guide][language-track-guide].
23+
* The [language track guide][language-track-guide].
2424
This describes how all the language tracks are put together, as well
2525
as details about the common metadata.
2626

@@ -29,7 +29,7 @@ This describes the interface to the various tooling (test runner, representer an
2929

3030
### Issues
3131

32-
Feel free to file an issues on the [track repository][track-issues] for problems of any size.
32+
Feel free to file an issues on the [track repository][track-issues] for problems of any size.
3333
Feel free to report typographical errors or poor wording for example.
3434
You can greatly help improve the quality of the exercises by filing reports of invalid solutions that pass tests or of valid solutions that fail tests.
3535

@@ -56,15 +56,45 @@ The work needed for a concept exercise can be large, feel free to create an [iss
5656
Practice exercises are intended to allow a student to further practice and extend their knowledge of a concept.
5757
They can be longer and/or more 'clever'.
5858
Refer to the document on the [anatomy of a practice exercise][practice-exercise] for details of the parts that are needed for a concept exercise.
59+
60+
#### Practice Exercise Generation
61+
5962
Many practice exercises are part of a canonical set of exercises shared across tracks (information on this can be found in the [problem specifications repository][problem-specs].
60-
Before creating a new practice exercise please see if there is already a canonical problem defined there, if there is, this track will want to adhere to it rather than implementing something different.
63+
There is a generator in the ./bin folder that you can use to generate all of the requisite files from the problem-specifications.
64+
(Note, you _will_ need to have cloned the [problem specifications repository][problem-specs] for the generator to work.)
65+
The generator is written in Python, and you will therefore need to have Python 3.8 or later installed.
66+
You can run the script directly and follow the prompts, or you can run it from the command line.
67+
If you wish to run the generator from the command line, first navigate to your common-lisp repository.
68+
From here, there are two ways to run the generator, the first way being to enter the following:
69+
70+
```sh
71+
python ./bin/lisp_exercise_generator.py
72+
```
73+
74+
and from there, follow the prompts.
75+
The second way is to type in:
76+
77+
```sh
78+
python ./bin/lisp_exercise_generator.py [-f] [path exercise author]
79+
```
80+
81+
where:
82+
- path is the relative or absolute path to your problem-specifications repository
83+
- exercise is the name of the exercise to be generated
84+
- author is your Github handle
85+
- -f is a flag to force overwrite an already existing exercise
86+
87+
Any one of these methods will generate and fill in all the necessary files, with the exception of the .meta/example.lisp file, which you will need to complete yourself.
88+
**The common-lisp/config.json file will remain unaltered - you will have to manually alter this file.**
89+
90+
A Common Lisp replacement for this generator will be coming "soon".
6191

6292
## Development Setup
6393

6494
This track uses [SBCL][sbcl] for its development.
65-
Since Common Lisp is a standardized language and (at present) exercises only use features and behavior specified by the standard any other conforming implementation could be used for development of features for the track.
66-
However any tooling created for this track (such as part of its build system) must work in [SBCL][sbcl].
67-
It is outside the scope of this document to describe how to install a Common Lisp implementation.
95+
Since Common Lisp is a standardized language and (at present) exercises only use features and behavior specified by the standard any other conforming implementation could be used for development of features for the track.
96+
However any tooling created for this track (such as part of its build system) must work in [SBCL][sbcl].
97+
It is outside the scope of this document to describe how to install a Common Lisp implementation.
6898
Please refer to the documentation for your chosen implementation for details.
6999

70100
The track also uses [QuickLisp][quicklisp] for system management.
@@ -120,4 +150,3 @@ To run the build "manually" execute the following from the root directory of the
120150
[workflow-config-checker]: https://github.com/exercism/common-lisp/blob/main/.github/workflows/config-checker.yml
121151
[workflow-configlet]: https://github.com/exercism/common-lisp/blob/main/.github/workflows/configlet.yml
122152
[workflow-text-exercises]: https://github.com/exercism/common-lisp/blob/main/.github/workflows/test-exercises.yml
123-

bin/custom_json_encoder.py

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
#### This code is pretty much just copied from json.encoder with
2+
#### minor differences in _iterencode_list function embedded within
3+
#### _make_iterencode function
4+
5+
import json
6+
from json.encoder import (
7+
encode_basestring,
8+
encode_basestring_ascii,
9+
INFINITY,
10+
c_make_encoder
11+
)
12+
13+
class CustomJSONEncoder(json.JSONEncoder):
14+
## Same as the iterencode method that it is overriding in parent
15+
## json.JSONEncoder class, but all to call the customized
16+
## _make_iterencode function
17+
def iterencode(self, o, _one_shot=False):
18+
if self.check_circular:
19+
markers = {}
20+
else:
21+
markers = None
22+
if self.ensure_ascii:
23+
_encoder = encode_basestring_ascii
24+
else:
25+
_encoder = encode_basestring
26+
27+
def floatstr(o, allow_nan=self.allow_nan,
28+
_repr=float.__repr__, _inf=INFINITY, _neginf=-INFINITY):
29+
# Check for specials. Note that this type of test is processor
30+
# and/or platform-specific, so do tests which don't depend on the
31+
# internals.
32+
33+
if o != o:
34+
text = 'NaN'
35+
elif o == _inf:
36+
text = 'Infinity'
37+
elif o == _neginf:
38+
text = '-Infinity'
39+
else:
40+
return _repr(o)
41+
42+
if not allow_nan:
43+
raise ValueError(
44+
"Out of range float values are not JSON compliant: " +
45+
repr(o))
46+
47+
return text
48+
49+
50+
if (_one_shot and c_make_encoder is not None
51+
and self.indent is None):
52+
_iterencode = c_make_encoder(
53+
markers, self.default, _encoder, self.indent,
54+
self.key_separator, self.item_separator, self.sort_keys,
55+
self.skipkeys, self.allow_nan)
56+
else:
57+
_iterencode = _make_iterencode(
58+
markers, self.default, _encoder, self.indent, floatstr,
59+
self.key_separator, self.item_separator, self.sort_keys,
60+
self.skipkeys, _one_shot)
61+
return _iterencode(o, 0)
62+
63+
64+
## Same function as original, except for _list_iterencode function
65+
def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,
66+
_key_separator, _item_separator, _sort_keys, _skipkeys, _one_shot,
67+
## HACK: hand-optimized bytecode; turn globals into locals
68+
ValueError=ValueError,
69+
dict=dict,
70+
float=float,
71+
id=id,
72+
int=int,
73+
isinstance=isinstance,
74+
list=list,
75+
str=str,
76+
tuple=tuple,
77+
_intstr=int.__repr__,
78+
):
79+
80+
if _indent is not None and not isinstance(_indent, str):
81+
_indent = ' ' * _indent
82+
83+
## Customized function now creates inline arrays/lists instead of
84+
## newlining + indenting all elements
85+
def _iterencode_list(lst, _current_indent_level):
86+
if not lst:
87+
yield '[]'
88+
return
89+
if markers is not None:
90+
markerid = id(lst)
91+
if markerid in markers:
92+
raise ValueError("Circular reference detected")
93+
markers[markerid] = lst
94+
buf = '['
95+
first = True
96+
for value in lst:
97+
if first:
98+
first = False
99+
else:
100+
buf = _item_separator
101+
if isinstance(value, str):
102+
yield buf + _encoder(value)
103+
elif value is None:
104+
yield buf + 'null'
105+
elif value is True:
106+
yield buf + 'true'
107+
elif value is False:
108+
yield buf + 'false'
109+
elif isinstance(value, int):
110+
# Subclasses of int/float may override __repr__, but we still
111+
# want to encode them as integers/floats in JSON. One example
112+
# within the standard library is IntEnum.
113+
yield buf + _intstr(value)
114+
elif isinstance(value, float):
115+
# see comment above for int
116+
yield buf + _floatstr(value)
117+
else:
118+
yield buf
119+
if isinstance(value, (list, tuple)):
120+
chunks = _iterencode_list(value, _current_indent_level)
121+
elif isinstance(value, dict):
122+
chunks = _iterencode_dict(value, _current_indent_level)
123+
else:
124+
chunks = _iterencode(value, _current_indent_level)
125+
yield from chunks
126+
yield ']'
127+
if markers is not None:
128+
del markers[markerid]
129+
130+
def _iterencode_dict(dct, _current_indent_level):
131+
if not dct:
132+
yield '{}'
133+
return
134+
if markers is not None:
135+
markerid = id(dct)
136+
if markerid in markers:
137+
raise ValueError("Circular reference detected")
138+
markers[markerid] = dct
139+
yield '{'
140+
if _indent is not None:
141+
_current_indent_level += 1
142+
newline_indent = '\n' + _indent * _current_indent_level
143+
item_separator = _item_separator + newline_indent
144+
yield newline_indent
145+
else:
146+
newline_indent = None
147+
item_separator = _item_separator
148+
first = True
149+
if _sort_keys:
150+
items = sorted(dct.items())
151+
else:
152+
items = dct.items()
153+
for key, value in items:
154+
if isinstance(key, str):
155+
pass
156+
# JavaScript is weakly typed for these, so it makes sense to
157+
# also allow them. Many encoders seem to do something like this.
158+
elif isinstance(key, float):
159+
# see comment for int/float in _make_iterencode
160+
key = _floatstr(key)
161+
elif key is True:
162+
key = 'true'
163+
elif key is False:
164+
key = 'false'
165+
elif key is None:
166+
key = 'null'
167+
elif isinstance(key, int):
168+
# see comment for int/float in _make_iterencode
169+
key = _intstr(key)
170+
elif _skipkeys:
171+
continue
172+
else:
173+
raise TypeError(f'keys must be str, int, float, bool or None, '
174+
f'not {key.__class__.__name__}')
175+
if first:
176+
first = False
177+
else:
178+
yield item_separator
179+
yield _encoder(key)
180+
yield _key_separator
181+
if isinstance(value, str):
182+
yield _encoder(value)
183+
elif value is None:
184+
yield 'null'
185+
elif value is True:
186+
yield 'true'
187+
elif value is False:
188+
yield 'false'
189+
elif isinstance(value, int):
190+
# see comment for int/float in _make_iterencode
191+
yield _intstr(value)
192+
elif isinstance(value, float):
193+
# see comment for int/float in _make_iterencode
194+
yield _floatstr(value)
195+
else:
196+
if isinstance(value, (list, tuple)):
197+
chunks = _iterencode_list(value, _current_indent_level)
198+
elif isinstance(value, dict):
199+
chunks = _iterencode_dict(value, _current_indent_level)
200+
else:
201+
chunks = _iterencode(value, _current_indent_level)
202+
yield from chunks
203+
if newline_indent is not None:
204+
_current_indent_level -= 1
205+
yield '\n' + _indent * _current_indent_level
206+
yield '}'
207+
if markers is not None:
208+
del markers[markerid]
209+
210+
def _iterencode(o, _current_indent_level):
211+
if isinstance(o, str):
212+
yield _encoder(o)
213+
elif o is None:
214+
yield 'null'
215+
elif o is True:
216+
yield 'true'
217+
elif o is False:
218+
yield 'false'
219+
elif isinstance(o, int):
220+
# see comment for int/float in _make_iterencode
221+
yield _intstr(o)
222+
elif isinstance(o, float):
223+
# see comment for int/float in _make_iterencode
224+
yield _floatstr(o)
225+
elif isinstance(o, (list, tuple)):
226+
yield from _iterencode_list(o, _current_indent_level)
227+
elif isinstance(o, dict):
228+
yield from _iterencode_dict(o, _current_indent_level)
229+
else:
230+
if markers is not None:
231+
markerid = id(o)
232+
if markerid in markers:
233+
raise ValueError("Circular reference detected")
234+
markers[markerid] = o
235+
o = _default(o)
236+
yield from _iterencode(o, _current_indent_level)
237+
if markers is not None:
238+
del markers[markerid]
239+
return _iterencode

0 commit comments

Comments
 (0)