Skip to content

Init exercise script #389

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 14 commits into from
Nov 11, 2017
Merged

Conversation

coriolinus
Copy link
Member

@coriolinus coriolinus commented Nov 7, 2017

Add a script which generates an exercise stub. Closes #280.

  • Generates all required files and formatting
  • Generates README.md
  • Generates test cases if the exercise has a properly configured canonical-data.json
  • Interactively configures config.json

This should make it easier for first-time contributors to jump in, hopefully.

The goal is to ease contribution by setting up a project template
and configuring tests from the canonical data, if any. Would-be
contributors should be able to simply run this script and configure
a few values in order to add a new exercise.
Copy link
Member

@petertseng petertseng left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are too many unrelated changes in the README. I would not include the unrelated README changes, only the (I believe) one actual addition (instructions for adding the script). Otherwise this PR is doing more than what it claims to do.


This module requires Python3.5 or newer
"""
from __future__ import print_function
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that this will have no effect on Python 3, but will allow this script to run and print its error messages on Python 2, when it instead would not be able to.

If this guess was correct, then there is no need to respond to it.

sys.exit(1)

# check version info
if sys.version_info[0] != 3 or sys.version_info[1] < 5:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would voice some concern here about "But what if Python 4 comes out someday and this script works on it without modification!" but I guess this line can be dealt with at that point in the far future, if it still exists when Python 4 comes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that through when writing the line. My reasoning: Python3 included breaking changes from Python2. If there is ever a Python4 in the future, we can expect it to include some changes which break Python3 scripts anyway. This line simply ensures that, for this script at least, someone takes a look at exactly what the breaking changes are and whether things are affected before someone can run this and have things explode in their face.

return sp.stdout.strip()


REPO_ROOT = output_of('git rev-parse --show-toplevel')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential worry if the script is run when CWD is outside of the rust repo (including possibly in any other repo)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This script will create a Rust exercise in the exercises directory of whatever repo the CWD is currently in when the script is run. If the CWD is not in a git repo, the script breaks. If the CWD is in a repo without an exercises directory, the script breaks. No attempts are made to do anything smart in those cases; it just prints the traceback and exits.

In practice, I can't see anyone ever actually running this from within a non-Rust repo; I think the worry is more theoretical than practical. The bits which insert the entry into config.json are interactive anway, meaning that if someone runs this as a script they'll use the --dont-update-config flag, which means they can just cp or mv or rm the generated directory appropriately.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One might consider a solution that is not sensitive to where the CWD is: the script determines its own location, and acts on the config.json and exercises directories at the proper relative location to itself.

then we run into problems if the person running the script has moved it before running it.

I will say that I am far more likely to run this with CWD being outside of the repo than I am to move this script.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm not too worried about people moving the script.

However, something like os.chdir(os.path.dirname(__file__)) before this line would probably fix both issues. I'll look into it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just pushed a version which does this; local testing confirms that the script now works correctly when CWD is outside the repo.

exercise_dir = os.path.join(EXERCISES, name)
# blank out the default lib.rs
with inside(exercise_dir):
with open(os.path.join('src', 'lib.rs'), 'w') as lib_rs:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

truncate would also be acceptable.


cd = get_problem_specification(name)
if cd is None:
print("No problem specification for {} found".format(name))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

someone running this might think "oh no! does that mean it failed and did nothing of use?!" so I would suggest to make it clear that it did in fact do something after not finding the specification.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

someone running this might think "oh no! does that mean it failed and did nothing of use?!" so I would suggest to make it clear that it did in fact do something after not finding the specification.

I revoke my objection. It is handled, but in the respective functions (make_new_exercise prints "Creating new exercise from scratch")

else:
make_exercise_with_specifications(name, exercise_dir, cd)
with inside(exercise_dir):
generate_readme(name, True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to look for definition of generate_readme to understand what the boolean argument is. I would suggest giving it a name.

same

print("Don't forget that `README.md` is automatically generated; update this within `.meta\description.md`.", file=description)
with open('metadata.yml', 'w') as metadata:
print("---", file=metadata)
print("blurb: \"{} (created {})\"".format(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Putting the "created" portion here may encourage contributors to leave it there.

I think we do not want to encourage that, because it doesn't seem to be relevant to http://exercism.io/languages/rust/exercises when the exercise was created. I would remove it.

print("source: \"\"", file=metadata)
print("source_url: \"\"", file=metadata)
with inside('tests'):
with open('{}.rs'.format(name), 'a') as tests_rs:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

guess: a in case the runner of this script had already created some tests, and we do not want this script to overwrite them.

If this guess was correct, then there is no need to respond to it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No; if the exercise directory already exists, make_exercise() aborts.

The 'a' is because make_exercise() pre-populates the file with use {name}::*;.


def write_cases(cases):
for item in cases:
if all(key in item for key in ('description', 'input', 'expected')):
Copy link
Member

@petertseng petertseng Nov 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the input isn't necessarily in input, but it would be difficult to come up with something that works for all exercises. Some tracks have created ideas and you may find some of them in exercism/discussions#155 (comment) . I wouldn't mind enforcing in the problem-specifications schema that all inputs must appear under input (possibly as an object that contains multiple keys, since some problems have multiple inputs) but obviously that is far out of scope for this PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm aware that some specifications include their inputs under other keys, but I think the best way forward for scripts like this is to enforce that all input is in input (possibly as an object) and all expected results are in expected. As you say, that's an issue out of scope for this PR.

The closest I've seen to a schema for the canonical-data.json format is in the problem-specifications README, and that uses input and expected, so that's all I feel the need to support for now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that more often than not, input isn't actually used, which poses a problem. I may introduce code to do something like what vimscript does, now that you've pointed out the issue.

import argparse
parser = argparse.ArgumentParser(description='Create a Rust Track exercise for Exercism')
parser.add_argument('name', help='name of the exercise to create')
parser.add_argument('--dont-create-exercise', action='store_true',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am used to seeing --create-exercise and --no-create-exercise, but I don't care and it might not be the case that this is an accepted convention.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't seen any other tools of this type; this was all ab nihilo.

I feel that a verb in the argument is more expressive than simple negation.

- Call generate_readme(.. get_problem_specification) by keyword
- Remove created time from blurb
- For all cases without `input` specified, collect all non-specified
  fields into an `input` tuple
- Use f-strings instead of .format() when possible
- Add exercise-local .gitignore for Cargo.lock
- Add option to use the maplit crate for exercises which use many maps
  in their tests. This can make the tests substantially more compact
  and readable.
- Set Cargo.toml version according to specification version
- Generate substantial inline documentation in the tests based on
  various descriptions and comments
- Generate a distinct property test function for each property
- Add hints about input tuples to property test functions
- Add hints about likely implementations to property test functions
- Ensure that the output compiles
@coriolinus
Copy link
Member Author

Made some improvements. Turns out the pov exercise makes for a really good test case: it's not already in Rust, it has several sections with their own comments, and the cases involve deep nesting and internal comments and descriptions.

Ironically, it's one of the exercises which would probably require the most work to actually bring into the Rust track, even with this script doing much of the gruntwork, just because Rust makes you work harder to build recursive structures.

I should be more careful with using auto-prettying editors on files
for which auto-prettying will generate large changesets.
Turns out that `-` is not a legal character in a module name, so
we need to adjust the crate definition and the import in the test file
print("Cargo.lock # We're building a library, not a binary", file=gitignore)
print(" # http://doc.crates.io/faq.html#why-do-binaries-have-cargolock-in-version-control-but-not-libraries", file=gitignore)
with open(os.path.join('src', 'lib.rs'), 'w') as lib_rs:
lib_rs.truncate()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

excuse me that this is the only comment I give right now instead of a full review, as my allotted time is up:

I meant https://docs.python.org/3/library/os.html#os.truncate such that the file no longer needs to be opened

But I'm not sure that actually makes a difference, so I actually don't care.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, ok. It still works, so I suppose it's just a case where there are a few valid paths to accomplish a goal in the standard library.

coriolinus added a commit to coriolinus/exercism_rust that referenced this pull request Nov 9, 2017
This was a fun challenge to implement in Rust, but it also serves
as a useful demo of the code generation implemented in exercism#389:
past implementing the example, I had to edit five lines of code
in tests/book-store.rs and three lines to add the stub in src/lib.rs.
Everything else was generated automatically.

Comments are invited pertaining both to this exercise and to the output
of the exercise generator.
Turns out .gitignore doesn't know about line-trailing comments,
so just move the comments to prefix the actual ignore line.
This script will always prettify config.json when it's run, as an
inescapable part of its read-edit-write cycle. Therefore, I'm adding
its edits here, so that users of the script aren't stuck with
config.json edits which are irrelevant to their PR.
Also:
- clarify .gitignore comment on Cargo.lock rules
- fix bug in which Cargo.lock was printed to stdout
@coriolinus
Copy link
Member Author

Went through the code one last time, and added a few bugfixes and a version bump. I think that once Travis signs off on this, it'll be ready to merge.

coriolinus added a commit that referenced this pull request Nov 11, 2017
* Add exercise book-store

This was a fun challenge to implement in Rust, but it also serves
as a useful demo of the code generation implemented in #389:
past implementing the example, I had to edit five lines of code
in tests/book-store.rs and three lines to add the stub in src/lib.rs.
Everything else was generated automatically.

Comments are invited pertaining both to this exercise and to the output
of the exercise generator.

* Revert irrelevant config.json edits

* Remove debug prints and generated comments not intended for the student
@coriolinus coriolinus merged commit 65b9c8d into exercism:master Nov 11, 2017
@coriolinus coriolinus deleted the init-exercise-script branch November 11, 2017 13:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants