Skip to content

Adding Practice Exercise Generator #597

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 36 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ There are several ways to contribute to the Common Lisp track including (but not

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

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

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

### Issues

Feel free to file an issues on the [track repository][track-issues] for problems of any size.
Feel free to file an issues on the [track repository][track-issues] for problems of any size.
Feel free to report typographical errors or poor wording for example.
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.

Expand All @@ -56,15 +56,45 @@ The work needed for a concept exercise can be large, feel free to create an [iss
Practice exercises are intended to allow a student to further practice and extend their knowledge of a concept.
They can be longer and/or more 'clever'.
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.

#### Practice Exercise Generation

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].
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.
There is a generator in the ./bin folder that you can use to generate all of the requisite files from the problem-specifications.
(Note, you _will_ need to have cloned the [problem specifications repository][problem-specs] for the generator to work.)
The generator is written in Python, and you will therefore need to have Python 3.8 or later installed.
You can run the script directly and follow the prompts, or you can run it from the command line.
If you wish to run the generator from the command line, first navigate to your common-lisp repository.
From here, there are two ways to run the generator, the first way being to enter the following:

```sh
python ./bin/lisp_exercise_generator.py
```

and from there, follow the prompts.
The second way is to type in:

```sh
python ./bin/lisp_exercise_generator.py [-f] [path exercise author]
```

where:
- path is the relative or absolute path to your problem-specifications repository
- exercise is the name of the exercise to be generated
- author is your Github handle
- -f is a flag to force overwrite an already existing exercise

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.
**The common-lisp/config.json file will remain unaltered - you will have to manually alter this file.**

A Common Lisp replacement for this generator will be coming "soon".

## Development Setup

This track uses [SBCL][sbcl] for its development.
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.
However any tooling created for this track (such as part of its build system) must work in [SBCL][sbcl].
It is outside the scope of this document to describe how to install a Common Lisp implementation.
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.
However any tooling created for this track (such as part of its build system) must work in [SBCL][sbcl].
It is outside the scope of this document to describe how to install a Common Lisp implementation.
Please refer to the documentation for your chosen implementation for details.

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

239 changes: 239 additions & 0 deletions bin/custom_json_encoder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
#### This code is pretty much just copied from json.encoder with
#### minor differences in _iterencode_list function embedded within
#### _make_iterencode function

import json
from json.encoder import (
encode_basestring,
encode_basestring_ascii,
INFINITY,
c_make_encoder
)

class CustomJSONEncoder(json.JSONEncoder):
## Same as the iterencode method that it is overriding in parent
## json.JSONEncoder class, but all to call the customized
## _make_iterencode function
def iterencode(self, o, _one_shot=False):
if self.check_circular:
markers = {}
else:
markers = None
if self.ensure_ascii:
_encoder = encode_basestring_ascii
else:
_encoder = encode_basestring

def floatstr(o, allow_nan=self.allow_nan,
_repr=float.__repr__, _inf=INFINITY, _neginf=-INFINITY):
# Check for specials. Note that this type of test is processor
# and/or platform-specific, so do tests which don't depend on the
# internals.

if o != o:
text = 'NaN'
elif o == _inf:
text = 'Infinity'
elif o == _neginf:
text = '-Infinity'
else:
return _repr(o)

if not allow_nan:
raise ValueError(
"Out of range float values are not JSON compliant: " +
repr(o))

return text


if (_one_shot and c_make_encoder is not None
and self.indent is None):
_iterencode = c_make_encoder(
markers, self.default, _encoder, self.indent,
self.key_separator, self.item_separator, self.sort_keys,
self.skipkeys, self.allow_nan)
else:
_iterencode = _make_iterencode(
markers, self.default, _encoder, self.indent, floatstr,
self.key_separator, self.item_separator, self.sort_keys,
self.skipkeys, _one_shot)
return _iterencode(o, 0)


## Same function as original, except for _list_iterencode function
def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,
_key_separator, _item_separator, _sort_keys, _skipkeys, _one_shot,
## HACK: hand-optimized bytecode; turn globals into locals
ValueError=ValueError,
dict=dict,
float=float,
id=id,
int=int,
isinstance=isinstance,
list=list,
str=str,
tuple=tuple,
_intstr=int.__repr__,
):

if _indent is not None and not isinstance(_indent, str):
_indent = ' ' * _indent

## Customized function now creates inline arrays/lists instead of
## newlining + indenting all elements
def _iterencode_list(lst, _current_indent_level):
if not lst:
yield '[]'
return
if markers is not None:
markerid = id(lst)
if markerid in markers:
raise ValueError("Circular reference detected")
markers[markerid] = lst
buf = '['
first = True
for value in lst:
if first:
first = False
else:
buf = _item_separator
if isinstance(value, str):
yield buf + _encoder(value)
elif value is None:
yield buf + 'null'
elif value is True:
yield buf + 'true'
elif value is False:
yield buf + 'false'
elif isinstance(value, int):
# Subclasses of int/float may override __repr__, but we still
# want to encode them as integers/floats in JSON. One example
# within the standard library is IntEnum.
yield buf + _intstr(value)
elif isinstance(value, float):
# see comment above for int
yield buf + _floatstr(value)
else:
yield buf
if isinstance(value, (list, tuple)):
chunks = _iterencode_list(value, _current_indent_level)
elif isinstance(value, dict):
chunks = _iterencode_dict(value, _current_indent_level)
else:
chunks = _iterencode(value, _current_indent_level)
yield from chunks
yield ']'
if markers is not None:
del markers[markerid]

def _iterencode_dict(dct, _current_indent_level):
if not dct:
yield '{}'
return
if markers is not None:
markerid = id(dct)
if markerid in markers:
raise ValueError("Circular reference detected")
markers[markerid] = dct
yield '{'
if _indent is not None:
_current_indent_level += 1
newline_indent = '\n' + _indent * _current_indent_level
item_separator = _item_separator + newline_indent
yield newline_indent
else:
newline_indent = None
item_separator = _item_separator
first = True
if _sort_keys:
items = sorted(dct.items())
else:
items = dct.items()
for key, value in items:
if isinstance(key, str):
pass
# JavaScript is weakly typed for these, so it makes sense to
# also allow them. Many encoders seem to do something like this.
elif isinstance(key, float):
# see comment for int/float in _make_iterencode
key = _floatstr(key)
elif key is True:
key = 'true'
elif key is False:
key = 'false'
elif key is None:
key = 'null'
elif isinstance(key, int):
# see comment for int/float in _make_iterencode
key = _intstr(key)
elif _skipkeys:
continue
else:
raise TypeError(f'keys must be str, int, float, bool or None, '
f'not {key.__class__.__name__}')
if first:
first = False
else:
yield item_separator
yield _encoder(key)
yield _key_separator
if isinstance(value, str):
yield _encoder(value)
elif value is None:
yield 'null'
elif value is True:
yield 'true'
elif value is False:
yield 'false'
elif isinstance(value, int):
# see comment for int/float in _make_iterencode
yield _intstr(value)
elif isinstance(value, float):
# see comment for int/float in _make_iterencode
yield _floatstr(value)
else:
if isinstance(value, (list, tuple)):
chunks = _iterencode_list(value, _current_indent_level)
elif isinstance(value, dict):
chunks = _iterencode_dict(value, _current_indent_level)
else:
chunks = _iterencode(value, _current_indent_level)
yield from chunks
if newline_indent is not None:
_current_indent_level -= 1
yield '\n' + _indent * _current_indent_level
yield '}'
if markers is not None:
del markers[markerid]

def _iterencode(o, _current_indent_level):
if isinstance(o, str):
yield _encoder(o)
elif o is None:
yield 'null'
elif o is True:
yield 'true'
elif o is False:
yield 'false'
elif isinstance(o, int):
# see comment for int/float in _make_iterencode
yield _intstr(o)
elif isinstance(o, float):
# see comment for int/float in _make_iterencode
yield _floatstr(o)
elif isinstance(o, (list, tuple)):
yield from _iterencode_list(o, _current_indent_level)
elif isinstance(o, dict):
yield from _iterencode_dict(o, _current_indent_level)
else:
if markers is not None:
markerid = id(o)
if markerid in markers:
raise ValueError("Circular reference detected")
markers[markerid] = o
o = _default(o)
yield from _iterencode(o, _current_indent_level)
if markers is not None:
del markers[markerid]
return _iterencode
Loading