CMSI 386
Homework #2
Due: 2018-10-16

For this homework assignment, you'll be warming up to Python, and hopefully getting pretty good at it. Again, I will supply the unit tests.

Readings

Please read:

Instructions

You will be adding to the private GitHub repository you started in the previous assignment. After adding and modifying files, your repository will look like this:

  .
  ├── README.md
  ├── .gitignore
  ├── homework1/
  │   ├── package-lock.json
  │   ├── package.json
  │   ├── src/
  │   │   └── warmup.js
  │   └── test/
  │       └── warmup-test.js
  └── homework2
      ├── tests/
      │   ├── __init__.py
      │   └── warmup_test.py
      └── warmup.py

I encourage you to work in pairs. Please submit only one solution set for your team. It does not matter under whose GitHub account your work is stored. What matters, if you wish to not receive a zero, is that I can clone your repo and run my tests. Since the repo is to be private, please allow me as a contributor to your repo (so I can run and comment on your work). My github name is rtoal.

Make sure you are using a linter. Non-linted code will be severely marked down to the point where your grade is not a passing one.

Your homework submission will be the state of your repository on the branch master, at the due datetime, which is 2018-10-16T23:59:59-07:00. That’s late Tuesday evening in the America/Los Angeles time zone.

Setting up your Project

Here are instructions for creating your project. I assume you are already signed up with GitHub, already know how to create private repositories and clone them, and know the basics of git.

  1. Create the homework2 folder and navigate to it so it is the current directory.
  2. Ensure you have at least Python 3.6 on your machine.
  3. In the homework2 folder, run the following commands (assuming you have a Unix-like shell; if you don’t, you are on your own...)
    $ python3 -m venv env
    $ source env/bin/activate
    (env) $ pip install requests
    (env) $ pip install cryptography
    (env) $ pip install pytest
    

    The first instruction installs a virtual enviroment in a new folder called env (look for it and make sure it is there). The second instruction enters the virtual environment. Once inside the virtual environment (you can see that you are in it because your command line prompt has the (env) prefix now), the command python will run Python 3. Yay! The next three instructions fetch packages from the a global Python package repository and install them inside your virtual enviroment. This means they will not conflict with any other Python installations on your machine.

    You can check that everything got installed by running pip freeze. You should see something similar to:

    (env) $ pip freeze
    asn1crypto==0.24.0
    atomicwrites==1.2.1
    attrs==18.2.0
    certifi==2018.8.24
    cffi==1.11.5
    chardet==3.0.4
    cryptography==2.3.1
    idna==2.7
    more-itertools==4.3.0
    pluggy==0.7.1
    py==1.6.0
    pycparser==2.19
    pytest==3.8.1
    requests==2.19.1
    six==1.11.0
    urllib3==1.23
    
  4. Add the following lines to your .gitignore file (in the top-level folder of your repo):
    env
    .cache
    __pycache__
    *.pyc
    
  5. Create your tests folder (within homework2). In that folder, create an empty file called __init__.py and the file warmup_test.py. Populate the latter file with this content:
    import re
    import math
    import pytest
    from warmup import (change, strip_quotes, scramble, say, triples, powers,
                        interleave, Cylinder, make_crypto_functions, random_name)
    
    
    def test_change():
        assert change(0) == (0, 0, 0, 0)
        assert change(97) == (3, 2, 0, 2)
        assert change(8) == (0, 0, 1, 3)
        assert change(250) == (10, 0, 0, 0)
        assert change(144) == (5, 1, 1, 4)
        assert change(97) == (3, 2, 0, 2)
        assert change(100000000000) == (4000000000, 0, 0, 0)
        with pytest.raises(ValueError) as excinfo:
            change(-50)
        assert str(excinfo.value) == 'amount cannot be negative'
    
    def test_strip_quotes():
        assert strip_quotes('') == ''
        assert strip_quotes('Hello, world') == 'Hello, world'
        assert strip_quotes('"\'') == ''
        assert strip_quotes('a"""\'\'"z') == 'az'
    
    def test_scramble():
        for s in ['a', 'rat', 'JavaScript testing', '', 'zzz', '^*))^*>^▱ÄÈËɡɳɷ']:
            assert sorted(s) == sorted(scramble(s))
        possibilities = set(['ABC', 'ACB', 'BAC', 'BCA', 'CAB', 'CBA'])
        for _ in range(200):
            possibilities.discard(scramble('ABC'))
        assert not possibilities
    
    def test_say():
        assert say() == ''
        assert say('hi')() == 'hi'
        assert say('hi')('there')() == 'hi there'
        assert say('hello')('my')('name')('is')('Colette')() == 'hello my name is Colette'
    
    def test_triples():
        assert triples(0) == []
        assert triples(5) == [(3, 4, 5)]
        assert set(triples(40)) == set([(3, 4, 5), (5, 12, 13), (6, 8, 10), (7, 24, 25), (8, 15, 17),
                                        (9, 12, 15), (10, 24, 26), (12, 16, 20), (12, 35, 37),
                                        (15, 20, 25), (15, 36, 39), (16, 30, 34), (18, 24, 30),
                                        (20, 21, 29), (21, 28, 35), (24, 32, 40)])
    
    def test_powers():
        p = powers(2, 10)
        assert next(p) == 1
        assert next(p) == 2
        assert next(p) == 4
        assert next(p) == 8
        with pytest.raises(StopIteration):
            next(p)
        assert list(powers(2, -5)) == []
        assert list(powers(7, 0)) == []
        assert list(powers(3, 1)) == [1]
        assert list(powers(2, 63)) == [1, 2, 4, 8, 16, 32]
        assert list(powers(2, 64)) == [1, 2, 4, 8, 16, 32, 64]
    
    def test_interleave():
        assert interleave([]) == []
        assert interleave([1, 4, 6]) == [1, 4, 6]
        assert interleave([], 2, 3) == [2, 3]
        assert interleave([1], 9) == [1, 9]
        assert interleave([8, 8, 3, 9], 1) == [8, 1, 8, 3, 9]
        assert interleave([2], 7, '8', {}) == [2, 7, '8', {}]
        a = [1, 2, 3, 4]
        assert interleave(a, 10, 20, 30) == [1, 10, 2, 20, 3, 30, 4]
        # Test input list not destroyed
        assert a == [1, 2, 3, 4]
    
    def test_cylinder():
        c = Cylinder(radius=10, height=5)
        assert c.height == 5
        assert c.radius == 10
        c = Cylinder(height=5)
        assert c.height == 5
        assert c.radius == 1
        c = Cylinder(radius=5)
        assert c.height == 1
        assert c.radius == 5
        c = Cylinder()
        assert c.height == 1
        assert c.radius == 1
        c = Cylinder(radius=2, height=10)
        assert pytest.approx(c.volume, 0.000001) == 40 * math.pi
        assert pytest.approx(c.surface_area, 0.000001) == 48 * math.pi
        c.widen(3)
        assert c.radius == 6
        c.stretch(2)
        assert c.height == 20
        assert pytest.approx(c.surface_area, 0.000001) == 312 * math.pi
        assert pytest.approx(c.volume, 0.000001) == 720 * math.pi
    
    def test_crypto():
        assert isinstance(make_crypto_functions(b"A2qK5XG3qX1MfLrGacD9AGVG2sbZYkvFFki94qbkVhE="), tuple)
        e, d = make_crypto_functions(b"A2qK5XG3qX1MfLrGacD9AGVG2sbZYkvFFki94qbkVhE=")
        for s in [b'', b'\xfe9iP\x05\x22\x490opXZ@1##', b'Helllllllllooooooo world']:
            assert d(e(s)) == s
    
    def test_random_name():
        p = random_name(gender='female', region='canada')
        assert isinstance(p, str)
        assert len(p) > 3
        assert ', ' in p
        with pytest.raises(ValueError) as excinfo:
            random_name(gender='fjweiuw', region='canada')
        assert re.match(r'{"error":\s*"Invalid gender"}', str(excinfo.value))
    
  6. Do the problems below in the file homework2/warmup.py. Write a little, test a little, back and forth. You know how it’s done. To run the test suite, you enter:
    (env) $ pytest -v
    
    in the homework2 folder. (Make sure you are in the virtual environment, of course.)

The Module You Are To Write

The file warmup.py should be a Python module exporting the following functions. For details of how they are “supposed to work,” see the unit tests above!

  1. A function that accepts a number of U.S. cents and returns a tuple containing, respectively, the smallest number of U.S. quarters, dimes, nickels, and pennies that equal the given amount.
  2. A function that accepts a string and returns a new string equivalent to the argument but with all apostrophes and double quotes removed.
  3. A function that randomly permutes a string. What does random mean? It means that each time you call the function for a given argument, all possible permutations are equally likely. Random is not the same as arbitrary.
  4. A generator function that yields successive powers of a base starting at 1 and going up to some limit.
  5. A function returning a list of all (positive) integer Pythagorean triples for all hypotenuse values up to, and including, some value.
  6. A “chainable” function that accepts one string per call, but when called without arguments, returns the words previously passed, in order, separated by a single space.
  7. A function that interleaves an array with a bunch of values. If the array length is not the same as the number of values to interleave, the “extra” elements should end up at the end of the result.
  8. A traditional Python Cylinder class. The initializer should accept a radius and height as keyword arguments; both should default to 1 if not passed in. Include volume and surface area methods exposed as properties, as well as a widen method that mutates the radius by a given factor and a stretch method to grow the height.
  9. A function that accepts a Fernet key and returns a tuple of two functions. The first function encrypts a bytes object with the key. The second decrypts. Both functions accept a bytes object and return a bytes object. Use the cryptography package. You are responsible for reading the documentation for the package; we are not going over this in class.
  10. A function that returns a random name from the uinames API. This function should use the requests module and you are to fetch the data synchronously (SORRY!) Pass the URL query parameters as kwargs. The only parameters your function are to accept are gender and region. Do not just pass all paramters from your caller through to uinames. Do pass the value 1 to uinames for the API parameter amount. You are to transform the response from uinames to the form “surname, name”.

Grading

Ten points per function.