194 lines
5.3 KiB
Markdown
194 lines
5.3 KiB
Markdown
\[ [Index](index.md) | [Exercise 9.3](ex9_3.md) | []() \]
|
|
|
|
# Exercise 9.4
|
|
|
|
*Objectives:*
|
|
|
|
- Explore circular imports
|
|
- Dynamic module imports
|
|
|
|
In the last exercise, you split the `tableformat.py` file up into submodules.
|
|
The last part of the resulting `tableformat/formatter.py` file has turned into a mess of imports.
|
|
|
|
```python
|
|
# tableformat.py
|
|
...
|
|
|
|
class TableFormatter(ABC):
|
|
@abstractmethod
|
|
def headings(self, headers):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def row(self, rowdata):
|
|
pass
|
|
|
|
from .formats.text import TextTableFormatter
|
|
from .formats.csv import CSVTableFormatter
|
|
from .formats.html import HTMLTableFormatter
|
|
|
|
...
|
|
|
|
def create_formatter(name, column_formats=None, upper_headers=False):
|
|
if name == 'text':
|
|
formatter_cls = TextTableFormatter
|
|
elif name == 'csv':
|
|
formatter_cls = CSVTableFormatter
|
|
elif name == 'html':
|
|
formatter_cls = HTMLTableFormatter
|
|
else:
|
|
raise RuntimeError('Unknown format %s' % name)
|
|
|
|
if column_formats:
|
|
class formatter_cls(ColumnFormatMixin, formatter_cls):
|
|
formats = column_formats
|
|
|
|
if upper_headers:
|
|
class formatter_cls(UpperHeadersMixin, formatter_cls):
|
|
pass
|
|
|
|
return formatter_cls()
|
|
```
|
|
|
|
The imports in the middle of the file are required because the `create_formatter()`
|
|
function needs them to find the appropriate classes. Really, the whole thing is a mess.
|
|
|
|
## (a) Circular Imports
|
|
|
|
Try moving the following import statements to the top of the `formatter.py` file:
|
|
|
|
```python
|
|
# formatter.py
|
|
|
|
from .formats.text import TextTableFormatter
|
|
from .formats.csv import CSVTableFormatter
|
|
from .formats.html import HTMLTableFormatter
|
|
|
|
class TableFormatter(ABC):
|
|
@abstractmethod
|
|
def headings(self, headers):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def row(self, rowdata):
|
|
pass
|
|
|
|
...
|
|
```
|
|
|
|
Observe that nothing works anymore. Try running the `stock.py` program and
|
|
notice the error about `TableFormatter` not being defined. The order
|
|
of import statements matters and you can't just move the imports anywhere
|
|
you want.
|
|
|
|
Move the import statements back where they were. Sigh.
|
|
|
|
## (b) Subclass Registration
|
|
|
|
Try the following experiment and observe:
|
|
|
|
```python
|
|
>>> from structly.tableformat.formats.text import TextTableFormatter
|
|
>>> TextTableFormatter.__module__
|
|
'structly.tableformat.formats.text'
|
|
>>> TextTableFormatter.__module__.split('.')[-1]
|
|
'text'
|
|
>>>
|
|
```
|
|
|
|
Modify the `TableFormatter` base class by adding a dictionary and an
|
|
`__init_subclass__()` method:
|
|
|
|
```python
|
|
class TableFormatter(ABC):
|
|
_formats = { }
|
|
|
|
@classmethod
|
|
def __init_subclass__(cls):
|
|
name = cls.__module__.split('.')[-1]
|
|
TableFormatter._formats[name] = cls
|
|
|
|
@abstractmethod
|
|
def headings(self, headers):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def row(self, rowdata):
|
|
pass
|
|
```
|
|
|
|
This makes the parent class track all of its subclasses. Check it out:
|
|
|
|
```python
|
|
>>> from structly.tableformat.formatter import TableFormatter
|
|
>>> TableFormatter._formats
|
|
{'text': <class 'structly.tableformat.formats.text.TextTableFormatter'>,
|
|
'csv': <class 'structly.tableformat.formats.csv.CSVTableFormatter'>,
|
|
'html': <class 'structly.tableformat.formats.html.HTMLTableFormatter'>}
|
|
>>>
|
|
```
|
|
|
|
Modify the `create_formatter()` function to look up the class in this dictionary
|
|
instead:
|
|
|
|
```python
|
|
def create_formatter(name, column_formats=None, upper_headers=False):
|
|
formatter_cls = TableFormatter._formats.get(name)
|
|
if not formatter_cls:
|
|
raise RuntimeError('Unknown format %s' % name)
|
|
|
|
if column_formats:
|
|
class formatter_cls(ColumnFormatMixin, formatter_cls):
|
|
formats = column_formats
|
|
|
|
if upper_headers:
|
|
class formatter_cls(UpperHeadersMixin, formatter_cls):
|
|
pass
|
|
|
|
return formatter_cls()
|
|
```
|
|
|
|
Run the `stock.py` program. Make sure it still works after you've made these changes.
|
|
Just a note that all of the import statements are still there. You've mainly
|
|
just cleaned up the code a bit and eliminated the hard-wired class names.
|
|
|
|
## (c) Dynamic Imports
|
|
|
|
You're now ready for the final frontier. Delete the following import statements
|
|
altogether:
|
|
|
|
```python
|
|
# formatter.py
|
|
...
|
|
|
|
from .formats.text import TextTableFormatter # DELETE
|
|
from .formats.csv import CSVTableFormatter # DELETE
|
|
from .formats.html import HTMLTableFormatter # DELETE
|
|
...
|
|
```
|
|
|
|
Run your `stock.py` code again--it should fail with an error. It knows nothing about the
|
|
text formatter. Fix it by adding this tiny fragment of code to `create_formatter()`:
|
|
|
|
```python
|
|
def create_formatter(name, column_formats=None, upper_headers=False):
|
|
if name not in TableFormatter._formats:
|
|
__import__(f'{__package__}.formats.{name}')
|
|
...
|
|
```
|
|
|
|
This code attempts a dynamic import of a formatter module if nothing is known about the
|
|
name. The import alone (if it works) will register the class with the `_formats`
|
|
dictionary and everything will just work. Magic!
|
|
|
|
Try running the `stock.py` code and make sure it works afterwards.
|
|
|
|
\[ [Solution](soln9_4.md) | [Index](index.md) | [Exercise 9.3](ex9_3.md) \]
|
|
|
|
----
|
|
`>>>` Advanced Python Mastery
|
|
`...` A course by [dabeaz](https://www.dabeaz.com)
|
|
`...` Copyright 2007-2023
|
|
|
|
. This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/)
|