List Todos Method

Having our web app import the todos list directly isn’t that smart. What if there’s some work involved, such as a database query? Let’s change to use a class method. We can get PyCharm to help us with the refactoring.

Source for this step

Steps

  1. In models.py, let’s add a method list to the class. After the __repr__ method, start a new method by typing def list(. Hit enter then press Shift-Enter to start a new line.

  2. In the body of this method, type return t and press enter to accept the autocomplete of todos.

  3. list has a squiggly underline. Mouse-over it. PyCharm helpfully tells you Method 'list' may be 'static'.

  4. Can PyCharm do this change for us? Click on list and press Alt-Enter. Choose Make method static.

  5. Press Ctrl-Alt-L to clean up any line formatting complaints. (macOS: Cmd-Alt-L)

  6. Our __repr__ is doing a lot. Perhaps we can refactor it. First, let’s extract the formatting into a property display that can be overridden by subclasses.

  7. Click in the .format of __repr__ (or anywhere on the return line) and choose Refactor -> Extract -> Method.

  8. In the popup’s Method name: box, enter display and click OK.

  9. PyCharm makes a new display method with the logic, and changes __repr__ to call it.

  10. Let’s say you changed your mind. Press Ctrl-Z to undo and all of that work is undone in one unit. (macOS: Cmd-Z)

  11. Or you decided you wanted it. Press Shift-Cmd-Z to redo. (macOS: Shift-Cmd-Z)

  12. Something looks fishy about display. Click somewhere in def display and press Alt-Enter. Sure enough, PyCharm can convert it to a property. Select Convert method to property. PyCharm adds the decorator and changes all usages, such as __repr__.

  13. Next, we’d like the format string to be parameterizable. Click once inside 'Todo {todo_id}' then expand the selection by pressing Ctrl-W three times. PyCharm’s selection should highlight the single quotes. (macOS: Alt-Up)

  14. Open the Refactor popup with Ctrl-Alt-Shift-T. Choose Field. (macOS: Ctrl-T)

  15. In the inline popup, change Initialize in: from current method to constructor, then enter display_fmt into the red box and press enter.

  16. PyCharm has added self.display_fmt to our constructor and changed the display property method to use it.

  17. That’s a lot. Let’s re-run models.py with Shift-F10 to make sure it runs ok. Make sure the models.py run configuration is selected. (macOS: Ctrl-R)

  18. We can now refactor app.py to use this. On the first line inside list_todos, type todos = ToDo and press Alt-Enter to import the ToDo class. Then continue typing .list(), resulting in todos = ToDo.list().

  19. Hmm, we have an unused import, as we don’t get todos from models.py any longer. PyCharm can help. Code -> Optimize Imports not only removes unused imports, but re-organizes your import listing based on a configurable policy.

  20. Does the web app still work? Reload the browser and visit the todo listing to confirm.

  21. At long last, we realize our ToDo typo, and by now, it is used in multiple places in multiple files. PyCharm’s Rename Refactoring to the rescue!

  22. Click in an occurrence of ToDo.

  23. Click on Refactor -> Rename, change to Todo, and click Refactor. Confirm that all cases were fixed in both files, then reload your browser to confirm the web app still works. Optionally, re-run models.py.

  24. Your app.py should match the following:

    app.py in List Todos Method
    from flask import Flask
    
    from models import populate_todos, Todo
    
    app = Flask(__name__)
    
    
    @app.route('/')
    def home_page():
        return 'Hello World! <a href="/todo/">Todos</a>'
    
    
    @app.route('/todo/')
    def list_todos():
        todos = Todo.list()
        div = '<div><a href="/todo/{id}">{title}</a></div>'
        items = [div.format(id=t.id, title=t.title) for t in todos]
        return '\n'.join(items)
    
    
    @app.route('/todo/<todo_id>')
    def show_todo(todo_id):
        return 'Todo {todo_id}'.format(todo_id=todo_id)
    
    
    if __name__ == '__main__':
        populate_todos()
        app.run(debug=True)
    
  25. Your models.py should match the following:

    models.py in List Todos Method
    from random import randint
    
    todos = []
    
    
    class Todo:
        def __init__(self, title):
            self.display_fmt = 'Todo {todo_id}'
            self.title = title
            self.id = randint(1000, 9999)
    
        def __repr__(self):
            return self.display
    
        @property
        def display(self):
            return self.display_fmt.format(todo_id=self.id)
    
        @staticmethod
        def list():
            return todos
    
    
    def populate_todos():
        todos.append(Todo('First'))
    
    
    if __name__ == '__main__':
        populate_todos()
        print(todos)
    

Analysis

Another step with quite a number of small changes:

  • Refactoring static method. The conversion to a static method is not only a useful warning, but doing the work for us lets us complete the task without much interruption.
  • Logic refactoring. The sequence of extracting the logic to a method, converting it for us into a property, then extracting the format string to a constructor field, really showed how PyCharm refactoring can keep us in the development flow.
  • Use, then auto-import. We again see the pattern of typing in a symbol, then letting PyCharm generate the import.
  • Optimize Imports. Cleaning up your imports is tedious, janitorial work. Let PyCharm do it for you. New in PyCharm 2016.2: configure how the imports are laid out, to match your preferences.
  • Project-wide Rename. Sometimes you avoid a rename, just to avoid all the work to find the symbol and fix it. PyCharm makes this refactoring a trivial task.

Extra Credit

  1. We use Alt-Shift-Up to extend the selection. Can this extend beyond the current line, to an entire block?
  2. In Python, what’s the difference between @classmethod and @staticmethod?