Try   HackMD

Collaborative Research Software Engineering in Python - solutions to tasks

! TASK 1 ! Write a new test case that tests the daily_max() function, adding it to test/test_models.py. Also regenerate your requirements.txt file, commit your changes, and merge the test-suite branch with the develop branch. (5-10 min)

  • You could choose to write your functions very similarly to daily_mean(), defining input and expected output variables followed by the equality assertion.
  • Use test cases that are suitably different.
  • Once added, run all the tests again with python -m pytest tests/test_models.py, and have a look at your new tests pass.

Solution (examples)

def test_daily_max_integers(): """Test that max function works for an array of positive integers.""" from inflammation.models import daily_max test_input = np.array([[4, 2, 5], [1, 6, 2], [4, 1, 9]]) test_result = np.array([4, 6, 9]) npt.assert_array_equal(daily_max(test_input), test_result)

One can also check cases where seeing an error is the correct behaviour (we need to import pytest to be able to execute pytest.raises()):

import pytest ... def test_daily_max_string(): """Test for TypeError when passing strings""" from inflammation.models import daily_max with pytest.raises(TypeError): error_expected = daily_max([['Hi', 'there'], ['Alfe', 'folks']])

We regenerate our requirements.txt file, commit our changes, and merge the test-suite branch with the develop branch.

$ pip3 freeze > requirements.txt
$ git add requirements.txt tests/test_models.py   # alternative: git add ./
$ git commit -m "Add initial test cases for daily_max() and daily_min()"
$ git checkout develop
$ git merge test-suite

! TASK 2 ! Rewrite your test functions for daily_max() using test parameterisation. (5-10 min)

  • Make sure you're back in the test-suite branch.
  • Once added, run all the tests again with python -m pytest tests/test_models.py, and have a look at your new tests pass.
  • Commit your changes, and merge the test-suite branch with the develop branch.

Solution (examples)

@pytest.mark.parametrize( "test, expected", [ ([ [0, 0], [0, 0], [0, 0] ], [0, 0]), ([ [1, 2], [3, 4], [5, 6] ], [5, 6]), ]) def test_daily_max(test, expected): """Test mean function works for array of zeroes and positive integers.""" from inflammation.models import daily_max npt.assert_array_equal(daily_max(np.array(test)), np.array(expected))

We can also incorporate the case where seeing an error is the correct behaviour by including a third argument name indicating the expected error raises (e.g., expect_raises), as well as the expected error raises (None for where we don't expect to see any):

@pytest.mark.parametrize( "test, expected, expect_raises", [ ([ [0, 0], [0, 0], [0, 0] ], [0, 0], None), ([ [1, 2], [3, 4], [5, 6] ], [5, 6], None), ([['Hi', 'there'], ['ALife', 'folks']], ['Whats', 'up'] , TypeError) ]) def test_daily_max(test, expected, expect_raises): """Test mean function works for array of zeroes and positive integers.""" from inflammation.models import daily_max if expect_raises is not None: with pytest.raises(TypeError): daily_max([['Hi', 'there'], ['ALife', 'folks']]) else: npt.assert_array_equal(daily_max(np.array(test)), np.array(expected))

! QUESTION 1 ! What outputs do you expect here?

Alice = Patient('Alice') print(Alice) obs = Alice.add_observation(3) print(obs) Bob = Person('Bob') print(Bob) obs = Bob.add_observation(4) print(obs)

Output:

Alice
3
Bob
AttributeError: 'Person' object has no attribute 'add_observation'

An error is thrown because we cannot add an observation to bob, who is a Person but not a Patient.

! TASK 3 ! Write a Doctor class to hold the data representing a single doctor:

  • It should have a name attribute, as well as a list of patients that this doctor is responsible for.
  • If you have the time, write corresponding tests in test_patient.py.
class Doctor(Person): """A doctor in an inflammation study.""" def __init__(self, name): super().__init__(name) self.patients = [] def add_patient(self, new_patient): # A crude check by name if this patient is already looked after # by this doctor before adding them for patient in self.patients: if patient.name == new_patient.name: return self.patients.append(new_patient) def __str__(self): return {'name': self.name, 'patients': self.patients}
  • Doctor class inherits from Person class, and instantiates self.patients as another attribute (an empty list at the start).
  • We add patients to doctors in the same way as observations had been added to patients.

Possible tests:

def test_add_patient_to_doctor(): """Check patients are being added correctly by a doctor. """ from inflammation.models import Doctor, Patient doc = Doctor("Sheila Wheels") new_patient = Patient("Bob") doc.add_patient(new_patient) assert doc.patients is not None assert len(doc.patients) == 1
  • We instantiate both Doctor and Patient objects, add the patient to the doctor, and check whether doc.patients is neither None, nor empty.
def test_doctor_is_person(): """Check if a doctor is a person.""" from inflammation.models import Doctor, Person doc = Doctor("Sheila Wheels") assert isinstance(doc, Person)
  • We instantiate a Doctor object, and check whether it is a Person object using isinstance().

! QUESTION 2 ! Which of these functions are pure?

def add_one(x): return x + 1 def say_hello(name): print('Hello', name) def append_item_1(a_list, item): a_list += [item] return a_list def append_item_2(a_list, item): result = a_list + [item] return result
  • add_one is pure - it has no effects other than to return a value and this value will always be the same when given the same inputs.
  • say_hello is not pure - printing text is a side effect.
  • append_item_1 is not pure - the argument a_list gets modified as a side effect.
  • append_item_2 is pure - result is a new variable, so this time a_list does not get modified.

! TASK 4 ! Write a decorator that measures the time time taken to execute a particular function using the time.process_time_ns() function.

  • You need to import time.
  • To get a time stamp, you can simply write start = time.process_time_ns(), and get another time stamp once the calculation in question is done using end = time.process_time_ns().
  • Use this function to measure its execution time:
def measure_me(n): total = 0 for i in range(n): total += i * i return total

Solution (example):

import time def profile(func): def inner(*args, **kwargs): start = time.process_time_ns() result = func(*args, **kwargs) stop = time.process_time_ns() print("Took {0} seconds".format((stop - start) / 1e9)) return result return inner @profile def measure_me(n): total = 0 for i in range(n): total += i * i return total print(measure_me(1000000))

Output

Took 0.124199753 seconds 333332833333500000