link experiment
This commit is contained in:
9
Notes/08_Testing_debugging/00_Overview.md
Normal file
9
Notes/08_Testing_debugging/00_Overview.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Overview
|
||||
|
||||
In this section we will cover the basics of:
|
||||
|
||||
* Testing
|
||||
* Logging, error handling and diagnostics
|
||||
* Debugging
|
||||
|
||||
Using Python.
|
||||
264
Notes/08_Testing_debugging/01_Testing.md
Normal file
264
Notes/08_Testing_debugging/01_Testing.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# 8.1 Testing
|
||||
|
||||
## Testing Rocks, Debugging Sucks
|
||||
|
||||
The dynamic nature of Python makes testing critically important to most applications.
|
||||
There is no compiler to find your bugs. The only way to find bugs is to run the code and make sure you try out all of its features.
|
||||
|
||||
## Assertions
|
||||
|
||||
The assertion statement is an internal check for the program.
|
||||
If an expression is not true, it raises a `AssertionError` exception.
|
||||
|
||||
`assert` statement syntax.
|
||||
|
||||
```python
|
||||
assert <expression> [, 'Diagnostic message']
|
||||
```
|
||||
|
||||
For example.
|
||||
|
||||
```python
|
||||
assert isinstance(10, int), 'Expected int'
|
||||
```
|
||||
|
||||
It shouldn't be used to check the user-input.
|
||||
|
||||
### Contract Programming
|
||||
|
||||
Also known as Design By Contract, liberal use of assertions is an approach for designing
|
||||
software. It prescribes that software designers should define precise
|
||||
interface specifications for the components of the software.
|
||||
|
||||
For example, you might put assertions on all inputs and outputs.
|
||||
|
||||
```python
|
||||
def add(x, y):
|
||||
assert isinstance(x, int), 'Expected int'
|
||||
assert isinstance(y, int), 'Expected int'
|
||||
return x + y
|
||||
```
|
||||
|
||||
Checking inputs will immediately catch callers who aren't using appropriate arguments.
|
||||
|
||||
```python
|
||||
>>> add(2, 3)
|
||||
5
|
||||
>>> add('2', '3')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AssertionError: Expected int
|
||||
>>>
|
||||
```
|
||||
|
||||
### Inline Tests
|
||||
|
||||
Assertions can also be used for simple tests.
|
||||
|
||||
```python
|
||||
def add(x, y):
|
||||
return x + y
|
||||
|
||||
assert add(2,2) == 4
|
||||
```
|
||||
|
||||
This way you are including the test in the same module as your code.
|
||||
|
||||
*Benefit: If the code is obviously broken, attempts to import the module will crash.*
|
||||
|
||||
This is not recommended for exhaustive testing.
|
||||
|
||||
### `unittest` Module
|
||||
|
||||
Suppose you have some code.
|
||||
|
||||
```python
|
||||
# simple.py
|
||||
|
||||
def add(x, y):
|
||||
return x + y
|
||||
```
|
||||
|
||||
You can create a separate testing file. For example:
|
||||
|
||||
```python
|
||||
# testsimple.py
|
||||
|
||||
import simple
|
||||
import unittest
|
||||
```
|
||||
|
||||
Then define a testing class.
|
||||
|
||||
```python
|
||||
# testsimple.py
|
||||
|
||||
import simple
|
||||
import unittest
|
||||
|
||||
# Notice that it inherits from unittest.TestCase
|
||||
class TestAdd(unittest.TestCase):
|
||||
...
|
||||
```
|
||||
|
||||
The testing class must inherit from `unittest.TestCase`.
|
||||
|
||||
In the testing class, you define the testing methods.
|
||||
|
||||
```python
|
||||
# testsimple.py
|
||||
|
||||
import simple
|
||||
import unittest
|
||||
|
||||
# Notice that it inherits from unittest.TestCase
|
||||
class TestAdd(unittest.TestCase):
|
||||
def test_simple(self):
|
||||
# Test with simple integer arguments
|
||||
r = simple.add(2, 2)
|
||||
self.assertEqual(r, 5)
|
||||
def test_str(self):
|
||||
# Test with strings
|
||||
r = simple.add('hello', 'world')
|
||||
self.assertEqual(r, 'helloworld')
|
||||
```
|
||||
|
||||
*Important: Each method must start with `test`.
|
||||
|
||||
### Using `unittest`
|
||||
|
||||
There are several built in assertions that come with `unittest`. Each of them asserts a different thing.
|
||||
|
||||
```python
|
||||
# Assert that expr is True
|
||||
self.assertTrue(expr)
|
||||
|
||||
# Assert that x == y
|
||||
self.assertEqual(x,y)
|
||||
|
||||
# Assert that x != y
|
||||
self.assertNotEqual(x,y)
|
||||
|
||||
# Assert that x is near y
|
||||
self.assertAlmostEqual(x,y,places)
|
||||
|
||||
# Assert that callable(arg1,arg2,...) raises exc
|
||||
self.assertRaises(exc, callable, arg1, arg2, ...)
|
||||
```
|
||||
|
||||
This is not an exhaustive list. There are other assertions in the module.
|
||||
|
||||
### Running `unittest`
|
||||
|
||||
To run the tests, turn the code into a script.
|
||||
|
||||
```python
|
||||
# testsimple.py
|
||||
|
||||
...
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
```
|
||||
|
||||
Then run Python on the test file.
|
||||
|
||||
```bash
|
||||
bash % python3 testsimple.py
|
||||
F.
|
||||
========================================================
|
||||
FAIL: test_simple (__main__.TestAdd)
|
||||
--------------------------------------------------------
|
||||
Traceback (most recent call last):
|
||||
File "testsimple.py", line 8, in test_simple
|
||||
self.assertEqual(r, 5)
|
||||
AssertionError: 4 != 5
|
||||
--------------------------------------------------------
|
||||
Ran 2 tests in 0.000s
|
||||
FAILED (failures=1)
|
||||
```
|
||||
|
||||
### Commentary
|
||||
|
||||
Effective unit testing is an art and it can grow to be quite complicated for large applications.
|
||||
|
||||
The `unittest` module has a huge number of options related to test
|
||||
runners, collection of results and other aspects of testing. Consult
|
||||
the documentation for details.
|
||||
|
||||
### Third Party Test Tools
|
||||
|
||||
We won't cover any third party test tools in this course.
|
||||
|
||||
However, there are a few popular alternatives and complements to
|
||||
`unittest`.
|
||||
|
||||
* [pytest](https://pytest.org) - A popular alternative.
|
||||
* [coverage](http://coverage.readthedocs.io) - Code coverage.
|
||||
|
||||
## Exercises
|
||||
|
||||
In this exercise, you will explore the basic mechanics of using
|
||||
Python's `unittest` module.
|
||||
|
||||
In earlier exercises, you wrote a file `stock.py` that contained a `Stock`
|
||||
class. For this exercise, it assumed that you're using the code written
|
||||
for Exercise 7.3. If, for some reason, that's not working,
|
||||
you might want to copy the solution from `Solutions/7_3` to your working
|
||||
directory.
|
||||
|
||||
### (a) Writing Unit Tests
|
||||
|
||||
In a separate file `test_stock.py`, write a set a unit tests
|
||||
for the `Stock` class. To get you started, here is a small
|
||||
fragment of code that tests instance creation:
|
||||
|
||||
|
||||
```python
|
||||
# test_stock.py
|
||||
|
||||
import unittest
|
||||
import stock
|
||||
|
||||
class TestStock(unittest.TestCase):
|
||||
def test_create(self):
|
||||
s = stock.Stock('GOOG', 100, 490.1)
|
||||
self.assertEqual(s.name, 'GOOG')
|
||||
self.assertEqual(s.shares, 100)
|
||||
self.assertEqual(s.price, 490.1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
```
|
||||
|
||||
Run your unit tests. You should get some output that looks like this:
|
||||
|
||||
```
|
||||
.
|
||||
----------------------------------------------------------------------
|
||||
Ran 1 tests in 0.000s
|
||||
|
||||
OK
|
||||
```
|
||||
|
||||
Once you're satisifed that it works, write additional unit tests that
|
||||
check for the following:
|
||||
|
||||
- Make sure the `s.cost` property returns the correct value (49010.0)
|
||||
- Make sure the `s.sell()` method works correctly. It should
|
||||
decrement the value of `s.shares` accordingly.
|
||||
- Make sure that the `s.shares` attribute can't be set to a non-integer value.
|
||||
|
||||
For the last part, you're going to need to check that an exception is raised.
|
||||
An easy way to do that is with code like this:
|
||||
|
||||
```python
|
||||
class TestStock(unittest.TestCase):
|
||||
...
|
||||
def test_bad_shares(self):
|
||||
s = stock.Stock('GOOG', 100, 490.1)
|
||||
with self.assertRaises(TypeError):
|
||||
s.shares = '100'
|
||||
```
|
||||
|
||||
[Next](02_Logging)
|
||||
305
Notes/08_Testing_debugging/02_Logging.md
Normal file
305
Notes/08_Testing_debugging/02_Logging.md
Normal file
@@ -0,0 +1,305 @@
|
||||
# 8.2 Logging
|
||||
|
||||
This section briefly introduces the logging module.
|
||||
|
||||
### `logging` Module
|
||||
|
||||
The `logging` module is a standard library module for recording diagnostic information.
|
||||
It's also a very large module with a lot of sophisticated functionality.
|
||||
We will show a simple example to illustrate its usefulness.
|
||||
|
||||
### Exceptions Revisited
|
||||
|
||||
In the exercises, we wrote a function `parse()` that looked something like this:
|
||||
|
||||
```python
|
||||
# fileparse.py
|
||||
def parse(f, types=None, names=None, delimiter=None):
|
||||
records = []
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line: continue
|
||||
try:
|
||||
records.append(split(line,types,names,delimiter))
|
||||
except ValueError as e:
|
||||
print("Couldn't parse :", line)
|
||||
print("Reason :", e)
|
||||
return records
|
||||
```
|
||||
|
||||
Focus on the `try-except` statement. What should you do in the `except` block?
|
||||
|
||||
Should you print a warning message?
|
||||
|
||||
```python
|
||||
try:
|
||||
records.append(split(line,types,names,delimiter))
|
||||
except ValueError as e:
|
||||
print("Couldn't parse :", line)
|
||||
print("Reason :", e)
|
||||
```
|
||||
|
||||
Or do you silently ignore it?
|
||||
|
||||
```python
|
||||
try:
|
||||
records.append(split(line,types,names,delimiter))
|
||||
except ValueError as e:
|
||||
pass
|
||||
```
|
||||
|
||||
Neither solution is satisfactory because you often want *both* behaviors (user selectable).
|
||||
|
||||
### Using `logging`
|
||||
|
||||
The `logging` module can address this.
|
||||
|
||||
```python
|
||||
# fileparse.py
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def parse(f,types=None,names=None,delimiter=None):
|
||||
...
|
||||
try:
|
||||
records.append(split(line,types,names,delimiter))
|
||||
except ValueError as e:
|
||||
log.warning("Couldn't parse : %s", line)
|
||||
log.debug("Reason : %s", e)
|
||||
```
|
||||
|
||||
The code is modified to issue warning messages or a special `Logger`
|
||||
object. The one created with `logging.getLogger(__name__)`.
|
||||
|
||||
### Logging Basics
|
||||
|
||||
Create a logger object.
|
||||
|
||||
```python
|
||||
log = logging.getLogger(name) # name is a string
|
||||
```
|
||||
|
||||
Issuing log messages.
|
||||
|
||||
```python
|
||||
log.critical(message [, args])
|
||||
log.error(message [, args])
|
||||
log.warning(message [, args])
|
||||
log.info(message [, args])
|
||||
log.debug(message [, args])
|
||||
```
|
||||
|
||||
*Each method represents a different level of severity.*
|
||||
|
||||
All of them create a formatted log message. `args` is used for the `%` operator.
|
||||
|
||||
```python
|
||||
logmsg = message % args # Written to the log
|
||||
```
|
||||
|
||||
### Logging Configuration
|
||||
|
||||
The logging behavior is configured separately.
|
||||
|
||||
```python
|
||||
# main.py
|
||||
|
||||
...
|
||||
|
||||
if __name__ == '__main__':
|
||||
import logging
|
||||
logging.basicConfig(
|
||||
filename = 'app.log', # Log output file
|
||||
level = logging.INFO, # Output level
|
||||
)
|
||||
```
|
||||
|
||||
Typically, this is a one-time configuration at program startup.
|
||||
The configuration is separate from the code that makes the logging calls.
|
||||
|
||||
### Comments
|
||||
|
||||
Logging is highly configurable.
|
||||
You can adjust every aspect of it: output files, levels, message formats, etc.
|
||||
However, the code that uses logging doesn't have to worry about that.
|
||||
|
||||
## Exercises
|
||||
|
||||
### (a) Adding logging to a module
|
||||
|
||||
In Exercise 3.3, you added some error handling to the
|
||||
`fileparse.parse_csv()` function. It looked like this:
|
||||
|
||||
```python
|
||||
# fileparse.py
|
||||
import csv
|
||||
|
||||
def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False):
|
||||
'''
|
||||
Parse a CSV file into a list of records with type conversion.
|
||||
'''
|
||||
if select and not has_headers:
|
||||
raise RuntimeError('select requires column headers')
|
||||
|
||||
rows = csv.reader(lines, delimiter=delimiter)
|
||||
|
||||
# Read the file headers (if any)
|
||||
headers = next(rows) if has_headers else []
|
||||
|
||||
# If specific columns have been selected, make indices for filtering and set output columns
|
||||
if select:
|
||||
indices = [ headers.index(colname) for colname in select ]
|
||||
headers = select
|
||||
|
||||
records = []
|
||||
for rowno, row in enumerate(rows, 1):
|
||||
if not row: # Skip rows with no data
|
||||
continue
|
||||
|
||||
# If specific column indices are selected, pick them out
|
||||
if select:
|
||||
row = [ row[index] for index in indices]
|
||||
|
||||
# Apply type conversion to the row
|
||||
if types:
|
||||
try:
|
||||
row = [func(val) for func, val in zip(types, row)]
|
||||
except ValueError as e:
|
||||
if not silence_errors:
|
||||
print(f"Row {rowno}: Couldn't convert {row}")
|
||||
print(f"Row {rowno}: Reason {e}")
|
||||
continue
|
||||
|
||||
# Make a dictionary or a tuple
|
||||
if headers:
|
||||
record = dict(zip(headers, row))
|
||||
else:
|
||||
record = tuple(row)
|
||||
records.append(record)
|
||||
|
||||
return records
|
||||
```
|
||||
|
||||
Notice the print statements that issue diagnostic messages. Replacing those
|
||||
prints with logging operations is relatively simple. Change the code like this:
|
||||
|
||||
```python
|
||||
# fileparse.py
|
||||
import csv
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False):
|
||||
'''
|
||||
Parse a CSV file into a list of records with type conversion.
|
||||
'''
|
||||
if select and not has_headers:
|
||||
raise RuntimeError('select requires column headers')
|
||||
|
||||
rows = csv.reader(lines, delimiter=delimiter)
|
||||
|
||||
# Read the file headers (if any)
|
||||
headers = next(rows) if has_headers else []
|
||||
|
||||
# If specific columns have been selected, make indices for filtering and set output columns
|
||||
if select:
|
||||
indices = [ headers.index(colname) for colname in select ]
|
||||
headers = select
|
||||
|
||||
records = []
|
||||
for rowno, row in enumerate(rows, 1):
|
||||
if not row: # Skip rows with no data
|
||||
continue
|
||||
|
||||
# If specific column indices are selected, pick them out
|
||||
if select:
|
||||
row = [ row[index] for index in indices]
|
||||
|
||||
# Apply type conversion to the row
|
||||
if types:
|
||||
try:
|
||||
row = [func(val) for func, val in zip(types, row)]
|
||||
except ValueError as e:
|
||||
if not silence_errors:
|
||||
log.warning("Row %d: Couldn't convert %s", rowno, row)
|
||||
log.debug("Row %d: Reason %s", rowno, e)
|
||||
continue
|
||||
|
||||
# Make a dictionary or a tuple
|
||||
if headers:
|
||||
record = dict(zip(headers, row))
|
||||
else:
|
||||
record = tuple(row)
|
||||
records.append(record)
|
||||
|
||||
return records
|
||||
```
|
||||
|
||||
Now that you've made these changes, try using some of your code on
|
||||
bad data.
|
||||
|
||||
```python
|
||||
>>> import report
|
||||
>>> a = report.read_portfolio('Data/missing.csv')
|
||||
Row 4: Bad row: ['MSFT', '', '51.23']
|
||||
Row 7: Bad row: ['IBM', '', '70.44']
|
||||
>>>
|
||||
```
|
||||
|
||||
If you do nothing, you'll only get logging messages for the `WARNING`
|
||||
level and above. The output will look like simple print statements.
|
||||
However, if you configure the logging module, you'll get additional
|
||||
information about the logging levels, module, and more. Type these
|
||||
steps to see that:
|
||||
|
||||
```python
|
||||
>>> import logging
|
||||
>>> logging.basicConfig()
|
||||
>>> a = report.read_portfolio('Data/missing.csv')
|
||||
WARNING:fileparse:Row 4: Bad row: ['MSFT', '', '51.23']
|
||||
WARNING:fileparse:Row 7: Bad row: ['IBM', '', '70.44']
|
||||
>>>
|
||||
```
|
||||
|
||||
You will notice that you don't see the output from the `log.debug()`
|
||||
operation. Type this to change the level.
|
||||
|
||||
```
|
||||
>>> logging.getLogger('fileparse').level = logging.DEBUG
|
||||
>>> a = report.read_portfolio('Data/missing.csv')
|
||||
WARNING:fileparse:Row 4: Bad row: ['MSFT', '', '51.23']
|
||||
DEBUG:fileparse:Row 4: Reason: invalid literal for int() with base 10: ''
|
||||
WARNING:fileparse:Row 7: Bad row: ['IBM', '', '70.44']
|
||||
DEBUG:fileparse:Row 7: Reason: invalid literal for int() with base 10: ''
|
||||
>>>
|
||||
```
|
||||
|
||||
Turn off all, but the most critical logging messages:
|
||||
|
||||
```
|
||||
>>> logging.getLogger('fileparse').level=logging.CRITICAL
|
||||
>>> a = report.read_portfolio('Data/missing.csv')
|
||||
>>>
|
||||
```
|
||||
|
||||
### (b) Adding Logging to a Program
|
||||
|
||||
To add logging to an application, you need to have some mechanism to
|
||||
initialize the logging module in the main module. One way to
|
||||
do this is to include some setup code that looks like this:
|
||||
|
||||
```
|
||||
# This file sets up basic configuration of the logging module.
|
||||
# Change settings here to adjust logging output as needed.
|
||||
import logging
|
||||
logging.basicConfig(
|
||||
filename = 'app.log', # Name of the log file (omit to use stderr)
|
||||
filemode = 'w', # File mode (use 'a' to append)
|
||||
level = logging.WARNING, # Logging level (DEBUG, INFO, WARNING, ERROR, or CRITICAL)
|
||||
)
|
||||
```
|
||||
|
||||
Again, you'd need to put this someplace in the startup steps of your
|
||||
program.
|
||||
|
||||
[Next](03_Debugging)
|
||||
147
Notes/08_Testing_debugging/03_Debugging.md
Normal file
147
Notes/08_Testing_debugging/03_Debugging.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# 8.3 Debugging
|
||||
|
||||
### Debugging Tips
|
||||
|
||||
So, you're program has crashed...
|
||||
|
||||
```bash
|
||||
bash % python3 blah.py
|
||||
Traceback (most recent call last):
|
||||
File "blah.py", line 13, in ?
|
||||
foo()
|
||||
File "blah.py", line 10, in foo
|
||||
bar()
|
||||
File "blah.py", line 7, in bar
|
||||
spam()
|
||||
File "blah.py", 4, in spam
|
||||
line x.append(3)
|
||||
AttributeError: 'int' object has no attribute 'append'
|
||||
```
|
||||
|
||||
Now what?!
|
||||
|
||||
### Reading Tracebacks
|
||||
|
||||
The last line is the specific cause of the crash.
|
||||
|
||||
```bash
|
||||
bash % python3 blah.py
|
||||
Traceback (most recent call last):
|
||||
File "blah.py", line 13, in ?
|
||||
foo()
|
||||
File "blah.py", line 10, in foo
|
||||
bar()
|
||||
File "blah.py", line 7, in bar
|
||||
spam()
|
||||
File "blah.py", 4, in spam
|
||||
line x.append(3)
|
||||
# Cause of the crash
|
||||
AttributeError: 'int' object has no attribute 'append'
|
||||
```
|
||||
|
||||
However, it's not always easy to read or understand.
|
||||
|
||||
*PRO TIP: Paste the whole traceback into Google.*
|
||||
|
||||
### Using the REPL
|
||||
|
||||
Use the option `-i` to keep Python alive when executing a script.
|
||||
|
||||
```bash
|
||||
bash % python3 -i blah.py
|
||||
Traceback (most recent call last):
|
||||
File "blah.py", line 13, in ?
|
||||
foo()
|
||||
File "blah.py", line 10, in foo
|
||||
bar()
|
||||
File "blah.py", line 7, in bar
|
||||
spam()
|
||||
File "blah.py", 4, in spam
|
||||
line x.append(3)
|
||||
AttributeError: 'int' object has no attribute 'append'
|
||||
>>>
|
||||
```
|
||||
|
||||
It preserves the interpreter state. That means that you can go poking around after the crash. Checking variable values and other state.
|
||||
|
||||
### Debugging with Print
|
||||
|
||||
`print()` debugging is quite common.
|
||||
|
||||
*Tip: Make sure you use `repr()`*
|
||||
|
||||
```python
|
||||
def spam(x):
|
||||
print('DEBUG:', repr(x))
|
||||
...
|
||||
```
|
||||
|
||||
`repr()` shows you an accurate representation of a value. Not the *nice* printing output.
|
||||
|
||||
```python
|
||||
>>> from decimal import Decimal
|
||||
>>> x = Decimal('3.4')
|
||||
# NO `repr`
|
||||
>>> print(x)
|
||||
3.4
|
||||
# WITH `repr`
|
||||
>>> print(repr(x))
|
||||
Decimal('3.4')
|
||||
>>>
|
||||
```
|
||||
|
||||
### The Python Debugger
|
||||
|
||||
You can manually launch the debugger inside a program.
|
||||
|
||||
```python
|
||||
def some_function():
|
||||
...
|
||||
breakpoint() # Enter the debugger (Python 3.7+)
|
||||
...
|
||||
```
|
||||
|
||||
This starts the debugger at the `breakpoint()` call.
|
||||
For earlier Python versions:
|
||||
|
||||
```python
|
||||
import pdb
|
||||
...
|
||||
pdb.set_trace() # Instead of `breakpoint()`
|
||||
...
|
||||
```
|
||||
|
||||
### Run under debugger
|
||||
|
||||
You can also run an entire program under debugger.
|
||||
|
||||
```bash
|
||||
bash % python3 -m pdb someprogram.py
|
||||
```
|
||||
|
||||
It will automatically enter the debugger before the first statement. Allowing you to set breakpoints and change the configuration.
|
||||
|
||||
Common debugger commands:
|
||||
|
||||
```code
|
||||
(Pdb) help # Get help
|
||||
(Pdb) w(here) # Print stack trace
|
||||
(Pdb) d(own) # Move down one stack level
|
||||
(Pdb) u(p) # Move up one stack level
|
||||
(Pdb) b(reak) loc # Set a breakpoint
|
||||
(Pdb) s(tep) # Execute one instruction
|
||||
(Pdb) c(ontinue) # Continue execution
|
||||
(Pdb) l(ist) # List source code
|
||||
(Pdb) a(rgs) # Print args of current function
|
||||
(Pdb) !statement # Execute statement
|
||||
```
|
||||
|
||||
For breakpoints location is one of the following.
|
||||
|
||||
```code
|
||||
(Pdb) b 45 # Line 45 in current file
|
||||
(Pdb) b file.py:45 # Line 34 in file.py
|
||||
(Pdb) b foo # Function foo() in current file
|
||||
(Pdb) b module.foo # Function foo() in a module
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user