Takeaways

  • Demonstrate basic knowledge of the Python (v3) programming language.
  • Become familiar with pytest and testing with Python.
  • Gain experience writing Python – including adherence to the PEP 0008 style guide.

Relevant Resources

  • This post
  • The README.md file and source documentation in the starter code repository
  • Program documentation
  • argparse, possibly. That code is written for you, but it might be handy to refer to.

Gug

Shortly after you’ve regained your eyesight, completed the Decrypt-inator, and lowered your eyebrows1, you receive a text from your new boss. NICE WORK it reads GITLAB 2 SLOW; PLOT FOILED AGAIN.2

The next text has an audio file attached.

Our story continues

You hold a green and purple envelope in your left hand, a cashier’s check in your right. Who knew evildoers paid so well?3 As you realize it’s not so bad working Dr. Doofenshmirtz, you hear a booming sound. Something heavy and poorly assembled approaches your door.

“HI THERE!” a voice booms. You feel confused, as you haven’t actually opened the door yet. “HI THERE!” it booms again. You open the door and find yourself face-to-face with a smiling mechanical man.

“WE NEED YOU FOR STUFF” he says. As he picks you up, jet engines emerge from his heels. The two of you sail into the distance.

You land back at Doofenshmirtz Evil Inc. Doofenshmirtz is waiting for you. “Oh thank you Norm. I meant that you should take the car to pick them up, but I suppose that was faster.”

Doofenshmirtz leads you away from Norm. “I have to travel out of town this weekend, and, well, I have this problem.” He turns his head cautiously to face a furry creature standing on a nearby lab table.

BUH

“BUH” the creature shouts, elbows in the air. Doofenshmirtz cowers in fear. “Why are you always doing that?” he asks. The creature dashes away.

“So, that was Gug. He’s… it’s?… Norm’s pet from the 8th dimension. I told Norm he should practice with a goldfish first, but nooooo. Of course I have to do all the work, now.”

You see Gug peeking around a corner menacingly. Doofenshmirtz is visibly uncomfortable.

“So anyway. I wrote everything you would need to know to take care of Gug on a little web page. Unfortunately that’s too much for Norm to handle.”

“I need you to write a program to help Norm while I’m gone. If I come back here to find a bag of trash in my bed again, I am going to scream.”

Your Task

Your task is to complete a tool for Norm, so that he can take care of Gug while Dr. Doofenshmirtz is out of town. You are to do the following:

  • Complete the partially implemented program.
  • Complete the partially implemented test functions.

Refer to this assignment’s rubric for further description of point values for completed tasks.

Program Requirements

The requirements for the Gug-inator are described in detail here. More specifically:

All of your code for the Gug-inator will go into a single Python module (guginator.py). Everything in that module must be implemented according to the provided documentation.

All of your test code will go into a separate Python module (test_guginator.py). Make sure you test your logic thoroughly! Use the examples in the documentation to help you out.

Running your Program

Your program will be executed in the following way:

# Assuming we're in the same directory as your guginator.py file
python3.5 guginator.py

As described in the usage documentation, we’ll pass subcommands to your program and check its output.

Automated Tests

You must write unit tests to test the parts (i.e., functions) you define for this assignment. You should have several tests for food(), walk(), and mood().

You will be writing tests using pytest. pytest is a very featureful testing library, but we’re only interested in the basics for now. As you write tests there are only a few things to remember:

  • Your tests need to be in the file named test_guginator.py.
  • Your test functions must start with test_. For example:

    # Good
    def test_addition():
        assert (3 + 5) == 8
    
    # BAD. pytest won't see this!
    def subtraction_test():
        assert (3 - 5) == -2
    
  • Your tests will use the assert keyword to make assertions about things that are true or false about your code. When an assertion fails, the whole test function stops. The test suite will continue, but the test with the failed assertion will end immediately.
  • You should use pytest.xfail when your know that a test should fail, and you want to skip it. In other words, an expected failure (hence xfail). For example, let’s say you’ve got an idea for a test, but you haven’t gotten around to implementing it.

    # We want to run this test eventually, but maybe we're not done writing
    # the function. Using an xfail lets us make a note that we don't actually
    # want to run this test. We *expect* it to fail.
    def test_my_function():
        pytest.xfail("Test isn't implemented yet...")
    
        # This line isn't run, since the xfail at the beginning of the function
        # tells pytest to skip this test.
        assert my_function() == "Bananas"
    

To invoke pytest, simply run the py.test script in your starter code. It will download and the correct version of pytest and use it. You can start py.test like this:

# Assuming we're in the same directory as the py.test executable and our
# test scripts, we can run py.test like this to run all of the tests in
# the current directory
./py.test

Proper Python Style

Your code must live up to the standards of PEP 0008 – Style Guide for Python Code. As mentioned in class, the flake8 tool will be used to check the style of your code. Unfortunately, the version of flake8 that we need is not on the campus machines. There is a flake8 script in your starter code that will download the right version of flake8 and use that instead.

Simply start it like this:

# Assuming we're in the same directory as the flake8 executable and our
# Python code, we can run flake8 like this to check the style of all the
# .py files in the current directory
./flake8 *.py

Make sure that flake8 has no output when you pass it your files. If there are any messages (warnings, errors, etc), you should address them.

DO NOT suppress or exclude any of flake8’s rules.

Further Details

Important Notes

  • Do not show off. It doesn’t matter if you’ve been writing Python for years. Follow the directions and earn your points. We are looking for you to practice using specific tools and concepts.
  • You will need to access member variables of argparse.Namespace objects. Refer to the module documentation to get an idea of which members are relevant.
  • You will not need to change anything in main(). That’s all given to you for a reason.
  • Make sure your functions are documented well. You may copy the docs from the module documentation for the ones in guginator.py, but you’ll need to write your own for those in test_guginator.py.
  • Your output must be formatted the same as the example output. diff will be used to compare your output to model output.

Tips & Hints

  • Try using str.split with tuple unpacking when you’re working with coordinates in walk().
  • Make sure you’re catching specific exceptions. You’ll lose points for catching Exception or similar.
  • You can access relevant members of the argparse.Namespace object like this:
    • args.day - A str
    • args.path - A list of str
    • args.left - A str
    • args.right - A str
  • You’ll have to define a custom exception class. Use the documentation!
  • Try using str.lower to normalize string case prior to comparison or key lookup.
  • Look at the grouping of the advice in the rubric. It is structured that way to hint at what we’re expecting!

Point Value

This assignment is worth 100 points. It will be graded according to the following rubric:

Feature Points Possible Mostly or completely incorrect (0% of points possible) Needs improvement (50% of points possible) Adequate, but still some deficiencies (75% of points possible) Mostly or completely correct (100% of points possible)
Implements and raises custom exception class appropriately 5 -5 -3 -2 0
Thoroughly tests functionality of food 5 -5 -3 -2 0
Demonstrates understanding of dictionaries 5 -5 -3 -2 0
Demonstrates understanding of exception handling 5 -5 -3 -2 0
Achieves correct output from food 5 -5 -3 -2 0
Thoroughly tests functionality of walk 5 -5 -3 -2 0
Demonstrates understanding of for-loops 5 -5 -3 -2 0
Demonstrates understanding of int constructor 5 -5 -3 -2 0
Demonstrates understanding of tuples and unpacking 5 -5 -3 -2 0
Achieves correct output from walk 5 -5 -3 -2 0
Thoroughly tests functionality of mood 5 -5 -3 -2 0
Demonstrates understanding of “in” operator 5 -5 -3 -2 0
Demonstrates understanding of list slicing 5 -5 -3 -2 0
Demonstrates understanding of negative indexing 5 -5 -3 -2 0
Achieves correct output from mood 5 -5 -3 -2 0
flake8 outputs no suggestions for improvement (no warnings, no errors, etc.) 10 -10 -3 0
Program runs without runtime (or syntax) errors 5 -5 0
Code Review (sufficient comments, implementation looks Pythonic, etc.) 10 -10 -5 -3 0
Points possible 100        

Each cell indicates how many points out of the available points will be awarded for that feature (row) and assessment level (column).

Note that “demonstrating understanding” of a concept implies that you use that concept as part of your solution. Superfluous demonstration won’t fly.

Cells marked -- cannot be achieved.

Submission

Deadline

11:59:59 PM CDT – Tuesday, September 12, 2017

Submission Procedure

Refer to the assignment submission page on the course website for details on submitting your code to GitLab.

Grading Procedure

When we grade your assignment, we will do the following (roughly).

# Run flake8 and check for style issues
./flake8 *.py

# Run unit tests
./py.test

# Run your guginator and all of its subcommands using various arguments
python3.5 guginator.py food Monday

FAQ New!

When I run the starter code, all I see is a backtrace. How should I start the assignment?

First of all, running the starter code is a smart thing to try. Good thinking.

For this assignment, we’re actually missing several vital functions that are outlined in the module documentation. Without food(), the food subcommand won’t work. Same story with walk and mood.

If you’re just getting started, try writing simple versions of those functions just to get things running. For example, your food() might look like this:

def food(args):
    print(args)

Once you have simple implementations for food(), walk(), and mood(), you can try them out.

$ python3.5 guginator.py food Monday
Namespace(day='Monday', func=<function food at 0x7fb079f27f28>)
None

From here, follow the usage docs and module docs to get things up-to-spec.


How do I know if the functions are implemented the right way?

As mentioned in class, read the rubric carefully. Each section (food, walk, and mood) explains what we expect to see in your solution. It requires reading between the lines a bit, but make sure you follow it.

For example, the food section of the rubric says you “Demonstrates understanding of dictionaries”. Instead of writing a big ol’ if/elif/else for food(), you should use a dict to look up the food for a given day.

We grade your assignment using the rubric. Make sure you follow it.


How can we ignore capitalization of user input?

Try normalizing your strings. For example, you could use the .lower() method for str to return the all-lowercase version of a string.

Definitely don’t use regular expressions.


What should we do if Norm doesn’t travel the required 6 blocks East/West or 8 blocks North/West?

Don’t worry about that part.

The walk subcommand should just calculate the distances traveled. If Norm doesn’t walk far enough, that’ll be his problem to deal with. Your code should not behave any differently if he doesn’t travel far enough.


I have some jokes to share. Can I change the output?

No.

We have our own suite of unit tests that we’ll be running against your code. It’s important that your output matches the documentation.


What happens if Norm travels diagonally (e.g., from 0,0 to 2,2)?

Norm won’t enter coordinates that way. You may assume that he will always enter coordinates such that his travel is strictly North, South, East, or West. He won’t travel diagonally.

Although Norm can fly, Gug is afraid of heights and must walk along city blocks.


Why am I seeing None in my output?

Remember that Python functions always return something. If you don’t return a value from a function, the function implicitly returns None.

Pay careful attention to the module docs. They explicitly state what each function should return.

  1. Both of them. 

  2. Someone really ought to upgrade that machine. 

  3. Something something corporate America, blah blah blah.