Skip to content

Commit dd698d1

Browse files
cmccandlessyawpitch
andcommitted
Add track Contributing Guide and Generator documentation (exercism#1900)
* Add Track Contributing Guide * Add test generator documentation * fix heading format * fix table HTML * incorporate review suggestions Suggestions: exercism#1900 (review) Co-authored-by: Michael Morehouse <640167+yawpitch@users.noreply.github.com>
1 parent ede3016 commit dd698d1

4 files changed

Lines changed: 309 additions & 4 deletions

File tree

CONTRIBUTING.md

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# Contributing Guide
2+
3+
This document supplements the [Exercism contributing guide]; all contributors should read that document before proceeding.
4+
5+
## Table of Contents
6+
7+
- [Contributing Guide](#contributing-guide)
8+
* [Architecture](#architecture)
9+
* [Implementing an exercise](#implementing-an-exercise)
10+
+ [Exercise structure](#exercise-structure)
11+
+ [Generating Exercise READMEs](#generating-exercise-readmes)
12+
- [Requirements](#requirements)
13+
- [Generating all READMEs](#generating-all-readmes)
14+
- [Generating a single README](#generating-a-single-readme)
15+
+ [Implementing tests](#implementing-tests)
16+
+ [Example solutions](#example-solutions)
17+
+ [config.json](#configjson)
18+
* [Implementing Track-specific Exercises](#implementing-track-specific-exercises)
19+
* [Pull Request Tips](#pull-request-tips)
20+
21+
22+
## Architecture
23+
24+
Exercism tracks inherit exercise definitions from the [problem-specifications] repository in the form of description files
25+
(from which exercise READMEs are [generated](#GeneratingExerciseREADMEs))
26+
27+
28+
## Implementing an exercise
29+
30+
### Exercise structure
31+
32+
```Bash
33+
exercises/[EXERCISE]/
34+
├── [EXERCISE].py
35+
├── [EXERCISE]_test.py
36+
├── example.py
37+
├── .meta
38+
│ ├── template.j2
39+
│ ├── additional_tests.json
40+
│ └── hints.md
41+
└── README.md
42+
```
43+
44+
Files:
45+
46+
| File | Description | Source |
47+
|:--- |:--- |:--- |
48+
| [[EXERCISE].py](exercises/two-fer/two_fer.py) | Solution stub | Manually created by the implementer |
49+
| [[EXERCISE]_test.py](exercises/two-fer/two_fer_test.py) | Exercise test suite | Automatically generated if `.meta/template.j2` is present, otherwise manually created by the implementer |
50+
| [example.py](exercises/two-fer/example.py) | Example solution used to automatically verify the `[EXERCISE]_test.py` suite | Manually created by the implementer |
51+
| [.meta/template.j2](exercises/two-fer/.meta/template.j2) | Test generation template; if present used to automatically generate `[EXERCISE]_test.py` (See [generator documentation](docs/GENERATOR.md)) | Manually created by implementer |
52+
| [.meta/additional_tests.json](exercises/word-count/.meta/additional_tests.json) | Defines additional track-specific test cases; if `.meta/template.j2` is also present these test will be incorporated into the automatically generated `[EXERCISE]_test.py` | Manually created by the implementer |
53+
| [.meta/hints.md](exercises/high-scores/.meta/hints.md) | Contains track-specific hints that are automatically included in the generated `README.md` file | Manually created by the implementer |
54+
| [README.md](exercises/two-fer/README.md) | Exercise README | [Generated by `configlet` tool](#GeneratingExerciseREADMEs) |
55+
56+
### Generating Exercise READMEs
57+
58+
#### Requirements
59+
- A local clone of the [problem-specifications] repository.
60+
- [configlet]: may be obtained either by
61+
- (**Recommended**) Following installation instructions at the above link
62+
- Running `bin/fetch-configlet` (`configlet` binary will be downloaded to the repository `bin/`)
63+
64+
#### Generating all READMEs
65+
66+
```
67+
configlet generate <path/to/track> --spec-path path/to/problem/specifications
68+
```
69+
70+
#### Generating a single README
71+
72+
```
73+
configlet generate <path/to/track> --spec-path path/to/problem/specifications --only example-exercise
74+
```
75+
76+
### Implementing tests
77+
78+
If an unimplemented exercise has a `canonical-data.json` file in the [problem-specifications] repository, a generation template must be created. See the [test generator documentation](docs/GENERATOR.md) for more information.
79+
80+
If an unimplemented exercise does not have a `canonical-data.json` file, the test file must be written manually (use existing test files for examples).
81+
82+
### Example solutions
83+
84+
Example solution files serve two purposes:
85+
86+
1. Verification of the tests
87+
2. Example implementation for mentor/student reference
88+
89+
### config.json
90+
91+
[`config.json`](config.json) is used by the website to determine which exercises to load an in what order. It also contains some exercise metadata, such as difficulty, labels, and if the exercise is a core exercise. New entries should be places just before the first exercise that is marked `"deprecated": true`:
92+
93+
```JSON
94+
{
95+
"slug": "current-exercise",
96+
"uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
97+
"core": false,
98+
"unlocked_by": null,
99+
"difficulty": 1,
100+
"topics": [
101+
"strings"
102+
]
103+
},
104+
<<< HERE
105+
{
106+
"slug": "old-exercise",
107+
"uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
108+
"core": false,
109+
"unlocked_by": null,
110+
"difficulty": 2,
111+
"topics": null,
112+
"deprecated": true
113+
},
114+
```
115+
116+
Fields
117+
<table>
118+
<tr>
119+
<td>slug</td>
120+
<td>Hyphenated lowercase exercise name</td>
121+
</tr>
122+
<tr>
123+
<td>uuid</td>
124+
<td>Generate using <code>configlet uuid</code></td>
125+
</tr>
126+
<tr>
127+
<td>core</td>
128+
<td>Set to <code>false</code>; core exercises are decided by track maintainers</td>
129+
</tr>
130+
<tr>
131+
<td>unlocked_by</td>
132+
<td>Slug for the core exercise that unlocks the new one</td>
133+
</tr>
134+
<tr>
135+
<td>difficulty</td>
136+
<td><code>1</code> through <code>10</code>. Discuss with reviewer if uncertain.</td>
137+
</tr>
138+
<tr>
139+
<td>topics</td>
140+
<td>Array of relevant topics from the <a href="https://github.com/exercism/problem-specifications/blob/master/TOPICS.txt">topics list</a> </td>
141+
</tr>
142+
</table>
143+
144+
145+
## Implementing Track-specific Exercises
146+
147+
Similar to implementing a canonical exercise that has no `canonical-data.json`, but the exercise README will also need to be written manually. Carefully follow the structure of generated exercise READMEs.
148+
149+
150+
## Pull Request Tips
151+
152+
Before committing:
153+
- Run `configlet fmt` and `configlet lint` before committing if [`config.json`](config.json) has been modified
154+
- Run [flake8] to ensure all Python code conforms to style standards
155+
- Run `test/check-exercises.py [EXERCISE]` to check if your test changes function correctly
156+
- If you modified or created a `hints.md` file, [regenerate the README](#GeneratingExerciseREADMEs)
157+
- If your changes affect multiple exercises, try to break them up into a separate PR for each exercise.
158+
159+
160+
[configlet]: https://github.com/exercism/configlet
161+
[Exercism contributing guide]: https://github.com/exercism/docs/blob/master/contributing-to-language-tracks/README.md
162+
[problem-specifications]: https://github.com/exercism/problem-specifications
163+
[topics list]: https://github.com/exercism/problem-specifications/blob/master/TOPICS.txt
164+
[flake8]: http://flake8.pycqa.org/

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ Exercism exercises in Python
88

99
## Contributing Guide
1010

11-
Please see the [contributing guide](https://github.com/exercism/docs/blob/master/contributing-to-language-tracks/README.md)
11+
Please see the [Exercism contributing guide](https://github.com/exercism/docs/blob/master/contributing-to-language-tracks/README.md)
12+
and the [Python track contributing guide](CONTRIBUTING.md)
1213

1314

1415
## Working on the Exercises

bin/generate_tests.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,23 @@ def generate(
218218
)
219219
)
220220
parser.add_argument('-v', '--verbose', action='store_true')
221-
parser.add_argument('-p', '--spec-path', default=DEFAULT_SPEC_LOCATION)
222-
parser.add_argument('--stop-on-failure', action='store_true')
223-
parser.add_argument('--check', action='store_true')
221+
parser.add_argument(
222+
'-p', '--spec-path',
223+
default=DEFAULT_SPEC_LOCATION,
224+
help=(
225+
'path to clone of exercism/problem-specifications '
226+
'(default: %(default)s)'
227+
)
228+
)
229+
parser.add_argument(
230+
'--stop-on-failure',
231+
action='store_true'
232+
)
233+
parser.add_argument(
234+
'--check',
235+
action='store_true',
236+
help='check if tests are up-to-date, but do not modify test files'
237+
)
224238
opts = parser.parse_args()
225239
if opts.verbose:
226240
logger.setLevel(logging.DEBUG)

docs/GENERATOR.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Exercism Python Track Test Generator
2+
3+
The Python track uses a generator script and [Jinja2] templates for
4+
creating test files from the canonical data.
5+
6+
## Table of Contents
7+
8+
- [Exercism Python Track Test Generator](#exercism-python-track-test-generator)
9+
* [Script Usage](#script-usage)
10+
* [Test Templates](#test-templates)
11+
+ [Conventions](#conventions)
12+
+ [Layout](#layout)
13+
+ [Overriding Imports](#overriding-imports)
14+
+ [Ignoring Properties](#ignoring-properties)
15+
* [Learning Jinja](#learning-jinja)
16+
* [Creating a templates](#creating-a-templates)
17+
18+
## Script Usage
19+
20+
Test generation requires a local copy of the [problem-specifications] repository.
21+
22+
Run `bin/generate_tests.py --help` for usage information.
23+
24+
## Test Templates
25+
26+
Test templates support [Jinja2] syntax, and have the following context
27+
variables available from the canonical data:
28+
- `exercise`: The hyphenated name of the exercise (ex: `two-fer`)
29+
- `version`: The canonical data version (ex. `1.2.0`)
30+
- `cases`: A list of case objects or a list of `cases` lists. For exact
31+
structure for the exercise you're working on, please consult
32+
`canonical-data.json`
33+
- `has_error_case`: Indicates if any test case expects an
34+
error to be thrown (ex: `False`)
35+
- `additional_cases`: similar structure to `cases`, but is populated from the exercise's `.meta/additional_tests.json` file if one exists (for an example, see `exercises/word-count/.meta/additional_tests.json`)
36+
37+
### Conventions
38+
39+
- General-use macros for highly repeated template structures are defined in `config/generator_macros.j2`.
40+
- These may be imported with the following:
41+
`{%- import "generator_macros.j2" as macros with context -%}`
42+
- All test templates should end with `{{ macros.footer() }}`.
43+
- All Python class names should be in CamelCase (ex: `TwoFer`).
44+
- Convert using `{{ "two-fer" | camel_case }}`
45+
- All Python module and function names should be in snake_case
46+
(ex: `high_scores`, `personal_best`).
47+
- Convert using `{{ "personalBest" | to_snake }}`
48+
- Track-specific tests are defined in the option file `.meta/additional_tests.json`. The JSON object defined in this file is to
49+
have a single key, `cases`, which has the same structure as `cases` in
50+
`canonical-data.json`.
51+
- Track-specific tests should be placed after canonical tests in test
52+
files.
53+
- Track-specific tests should be marked in the test file with the following comment:
54+
```
55+
# Additional tests for this track
56+
```
57+
58+
### Layout
59+
60+
Most templates will look something like this:
61+
62+
```Jinja2
63+
{%- import "generator_macros.j2" as macros with context -%}
64+
{{ macros.header() }}
65+
66+
class {{ exercise | camel_case }}Test(unittest.TestCase):
67+
{% for case in cases -%}
68+
def test_{{ case["description"] | to_snake }}(self):
69+
value = {{ case["input"]["value"] }}
70+
expected = {{ case["expected"] }}
71+
self.assertEqual({{ case["property"] }}(value), expected)
72+
73+
{% endfor %}
74+
75+
{{ macros.footer() }}
76+
```
77+
78+
### Overriding Imports
79+
80+
The names imported in `macros.header()` may be overridden by adding
81+
a list of alternate names to import (ex:`clock`):
82+
83+
```Jinja2
84+
{{ macros.header(["Clock"])}}
85+
```
86+
87+
### Ignoring Properties
88+
89+
On rare occasion, it may be necessary to filter out properties that
90+
are not tested in this track. The `header` macro also accepts an
91+
`ignore` argument (ex: `high-scores`):
92+
93+
```Jinja2
94+
{{ macros.header(ignore=["scores"]) }}
95+
```
96+
97+
## Learning Jinja
98+
99+
Starting with the [Jinja Documentation] is highly recommended, but a complete reading is not strictly necessary.
100+
101+
Additional Resources:
102+
- [Primer on Jinja Templating]
103+
- [Python Jinja tutorial]
104+
105+
106+
## Creating a templates
107+
108+
1. Create `.meta/template.j2` for the exercise you are implementing,
109+
and open it in your editor.
110+
2. Copy and paste the [example layout](#Layout) in the template file
111+
and save.
112+
3. Make the appropriate changes to the template file until it produces
113+
valid test code, referencing the exercise's `canonical-data.json` for
114+
input names and case structure.
115+
- Use the [available macros](config/generator_macros.j2) to avoid re-writing standardized sections.
116+
- If you are implementing a template for an existing exercise,
117+
matching the exact structure of the existing test file is not a
118+
requirement, but minimizing differences will make PR review a much smoother process for everyone involved.
119+
120+
121+
122+
[Jinja2]: https://jinja.pocoo.org/
123+
[Jinja Documentation]: https://jinja.palletsprojects.com/en/2.10.x/
124+
[Primer on Jinja Templating]: https://realpython.com/primer-on-jinja-templating/
125+
[Python Jinja tutorial]: http://zetcode.com/python/jinja/
126+
[problem-specifications]: https://github.com/exercism/problem-specifications

0 commit comments

Comments
 (0)