repr
Stringsrepr
Strings for Debugging Outputprintable representation of an object
my_value = '123' print(str(my_value)) # 123 print('%s' % my_value) # 123 print(f'{my_value}') # 123 print(format(my_value)) # 123 print(my_value.__format__('s')) # 123 print(my_value.__str__()) # 123
print(f'{my_value!r}') # '123'
repr
of built-in Python types can be passed to eval
to get back the original value:
a = '\x07' b = eval(repr(a)) # repr(): get the repr string of the input assert a == b # true
Define the special method __repr__
:
class SomeClass: def __init__(self, x, y): self.x = x self.y = y obj = SomeClass(1, 'foo') print(f'{obj!r}') # <__main__.SomeClass object at 0x10963d6d0> print(obj) # <__main__.SomeClass object at 0x10963d6d0>
class SomeClass: ... def __repr__(self): return f'SomeClass({self.x!r}, {self.y!r})' obj = SomeClass(2, 'bar') print(f'{obj!r}') # SomeClass(2, 'bar') print(obj) # SomeClass(2, 'bar')
<__main__.SomeClass object ...>
is the default repr string.
When __str__
is not defined, it will fallback to repr string.
unittest
ModuleBuilt-in Python unit test module.
TestCase
Subclasses# utils.py def to_str(data): if isinstance(data, str): return data elif isinstance(data, bytes): return data.decode('utf-8') else: raise TypeError('Must supply str or bytes, ' 'found: %r' % data)
# utils.py from unittest import TestCase, main from utils import to_str # we want to test this method class UtilsTestCase(TestCase): def test_to_str_bytes(self): self.assertEqual('hello', to_str(b'hello')) # helper method in TestCase class def test_to_str_str(self): self.assertEqual('hello', to_str('hello')) def test_failing(self): # a test will fail! self.assertEqual('incorrect', to_str('hello')) if __name__ == '__main__': main()
with a helpful error message about what went wrong:
$ python3 utils_test.py
F..
===============================================================
FAIL: test_failing (__main__.UtilsTestCase)
---------------------------------------------------------------
Traceback (most recent call last):
File "utils_test.py", line 15, in test_failing
self.assertEqual('incorrect', to_str('hello'))
AssertionError: 'incorrect' != 'hello'
- incorrect
+ hello
Define own helper method:
class MyTestCase(TestCase): def assertValidTriangle(self, edges): self.assertEqual(len(edges), 3) perimeter = sum(edges) for e in edges: self.assertGreater(perimeter - e, e) def test_triangle_generator(self): my_triangle = TriangleGenerator().generate() self.assertValidTriangle(my_triangle.edges)
subTest
defining multiple test in a single test method:
from utils import to_str class DataDrivenTestCase(TestCase): def test_good(self): good_cases = [ (b'my bytes', 'my bytes'), ('no error', b'no error'), # This one will fail ('other str', 'other str'), ... ] for value, expected in good_cases: with self.subTest(value): # subtests: self.assertEqual(expected, to_str(value))
setUp
, tearDown
, setUpModule
, and tearDownModule
Test harness: have the test environment set up before test methods can be run
Before / After each test method:
class EnvironmentTest(TestCase): def setUp(self): self.test_dir = TemporaryDirectory() self.test_path = Path(self.test_dir.name) def tearDown(self): self.test_dir.cleanup() def test_modify_file(self): with open(self.test_path / 'data.bin', 'w') as f: ...
Before / After all test methods in this module:
# for some integration test: def setUpModule(): createConnection() def tearDownModule(): closeConnection()
Execution order:
def setUpModule(): print('* Module setup') def tearDownModule(): print('* Module clean-up') class MyTest(TestCase): def setUp(self): print('* Test setup') def tearDown(self): print('* Test clean-up') def test_end_to_end1(self): print('* Test 1') def test_end_to_end2(self): print('* Test 2')
“* Module setup
* Test setup
* Test 1
* Test clean-up
.* Test setup
* Test 2
* Test clean-up
.* Module clean-up
Mock:
class DatabaseConnection: ... def get_animals(database, species): ... # query db
from unittest.mock import Mock mock = Mock(spec=get_animals) # minic the get_animals function expected = [ ('Spot', datetime(2019, 6, 5, 11, 15)), ('Fluffy', datetime(2019, 6, 5, 12, 30)), ('Jojo', datetime(2019, 6, 5, 12, 45)), ] mock.return_value = expected
database = object() result = mock(database, 'Jojo') mock.assert_called_once_with(database, 'Jojo') # pass mock.assert_called_once_with(database, 'Giraffe') # AssertionError: expected call not found. from unittest.mock import ANY mock.assert_called_once_with(database, ANY)
Mock exception:
mock.side_effect = MyError('Boom!') result = mock(database, 'Jojo') # raise MyError
Patch module / class attribute:
from unittest.mock import patch with patch('__main__.get_animals'): print(get_animals) # a MagicMock instance
However, it cannot mock build-in C-extension module:
with patch('datetime.datetime.utcnow'): datetime.utcnow.return_value = datetime(2019, 6, 5, 15, 45) # raise TypeError: can't set attributes of built-in/extension type
approach 1: add wrapper
def get_do_rounds_time() return datetime.utcnow() with patch('__main__.get_do_rounds_time'): ...
approach 2: reserved kw argument
def do_rounds(database, species, *, utcnow=datetime.utcnow): now = utcnow() ...
from unittest.mock import DEFAULT with patch.multiple('__main__', # create multiple mocks in 1 call autospec=True, get_food_period=DEFAULT, # create a Mock for this function get_animals=DEFAULT, feed_animal=DEFAULT): now_func = Mock(spec=datetime.utcnow) now_func.return_value = datetime(2019, 6, 5, 15, 45) get_food_period.return_value = timedelta(hours=3) get_animals.return_value = [ ... ]
assertions:
result = do_rounds(database, 'Meerkat', utcnow=now_func) # pass the mocked datetime function assert result == 2 food_func.assert_called_once_with(database, 'Meerkat') animals_func.assert_called_once_with(database, 'Meerkat') feed_func.assert_has_calls( [ call(database, 'Spot', now_func.return_value), call(database, 'Fluffy', now_func.return_value), ], any_order=True)
use a wrapper object to encapsulate the database’s interface
class ZooDatabase: def get_animals(self, species): ... def get_food_period(self, species): ... def feed_animal(self, name, when):
before:
def do_rounds(database, species): # a Database connection feeding_timedelta = get_food_period(database, species) animals = get_animals(database, species) ...
after:
def do_rounds(database, species): # ZooDatabase feeding_timedelta = database.get_food_period(species) animals = database.get_animals(species) ...
DATABASE = None def get_database(): global DATABASE if DATABASE is None: DATABASE = ZooDatabase() return DATABASE def main(argv): database = get_database() species = argv[1] count = do_rounds(database, species) print(f'Fed {count} {species}(s)') return 0
Ref: item 86: Consider Module-Scoped Code to Configure Deployment Environments
mock ZooDatabase
with patch -> now we can test the main
func:
with patch('__main__.DATABASE', spec=ZooDatabase): DATABASE.get_food_period.return_value = timedelta(hours=3) DATABASE.get_animals.return_value = [...] fake_stdout = io.StringIO() with contextlib.redirect_stdout(fake_stdout): main(['program name', 'Meerkat']) found = fake_stdout.getvalue() expected = 'Fed 2 Meerkat(s)\n' assert found == expected
pdb
Interactive Debuggingpdb
pdb
: The Python Debugger
import pdb; pdb.set_trace()
breakpoint()
def some_func: ... breakpoint() # will enter interactive console here ...
pdb
Commandswhere
: print current call stackup
/ down
move up or down call stackstep
run 1 linenext
run until next line of this functionreturn
run until this function returncontinue
run until next breakpointquit
This enables you to debug a program after it’s already raised an exception and crashed.
$ python -m pdb -c continue main.py
...(some error)
Uncaught exception. Entering post mortem debugging
Running 'cont' or 'step' will restart the program
> postmortem_breakpoint.py(16)compute_rmse()
-> rmse = math.sqrt(mean_err)
(pdb)
or by code: import pdb; pdb.pm()
tracemalloc
Understand Memory Usagetracemalloc
to Understand Memory Usage and Leaksgc.get_objects
: list every object currently known by the garbage collector
import gc import waste_memory # a custom memory wasting module found_objects = gc.get_objects() print('Before:', len(found_objects)) hold_reference = waste_memory.run() found_objects = gc.get_objects() print('After: ', len(found_objects)) for obj in found_objects[:3]: print(repr(obj)[:100])
Before: 6207
After: 16801
<waste_memory.MyObject object at 0x10390aeb8>
<waste_memory.MyObject object at 0x10390aef0>
<waste_memory.MyObject object at 0x10390af28>
However, gc
does not tell how objects were allocated
tracemalloc
take snapshot on memory usage:
import tracemalloc tracemalloc.start(10) # Set stack depth time1 = tracemalloc.take_snapshot() # Before snapshot ... time2 = tracemalloc.take_snapshot() # After snapshot stats = time2.compare_to(time1, 'lineno') # Compare snapshots for stat in stats[:3]: print(stat) # print comparison:
waste_memory.py:5: size=2392 KiB (+2392 KiB), count=29994
➥(+29994), average=82 B
waste_memory.py:10: size=547 KiB (+547 KiB), count=10001
➥(+10001), average=56 B
waste_memory.py:11: size=82.8 KiB (+82.8 KiB), count=100
➥(+100), average=848 B