\[ [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': , 'csv': , 'html': } >>> ``` 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 ![](https://i.creativecommons.org/l/by-sa/4.0/88x31.png). This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/)