outlines MVC
This commit is contained in:
parent
196ad4abeb
commit
41cb2d3980
|
@ -2744,6 +2744,576 @@ if __name__ == "__main__":
|
|||
|
||||
## MVC design pattern
|
||||
|
||||
A simple console only MVC.
|
||||
We'll add the GUI view in a bit.
|
||||
|
||||
```python
|
||||
import wx
|
||||
|
||||
|
||||
class ConsoleView(object):
|
||||
"""A view for console."""
|
||||
|
||||
def select_task(self):
|
||||
"""Asks which index to look up."""
|
||||
idx = input("which task do you want to see? ")
|
||||
return idx
|
||||
|
||||
def show(self, task):
|
||||
"""Displays the task to the console. This method is called from the
|
||||
controller."""
|
||||
print("your task: {}".format(task))
|
||||
|
||||
def error(self, msg):
|
||||
"""Prints error messages coming from the controller."""
|
||||
print("error: {}".format(msg))
|
||||
|
||||
|
||||
class WxView(wx.Dialog):
|
||||
pass
|
||||
|
||||
|
||||
class Model(object):
|
||||
"""The model houses add data and should implement all methods related to
|
||||
adding, modifying and deleting tasks."""
|
||||
|
||||
db = ["go to the shops", "dryhop beer", "drop of motorbike"]
|
||||
|
||||
def get_task(self, idx):
|
||||
"""Performs a task lookun into the database and returns it when found.
|
||||
If no task is found, it returns an error message that will be displayed
|
||||
in the view (via the controller)."""
|
||||
try:
|
||||
task = Model.db[idx]
|
||||
except IndexError:
|
||||
task = "task with {} not found!".format(idx)
|
||||
return task
|
||||
|
||||
|
||||
class Controller(object):
|
||||
"""Binds the model and the view together."""
|
||||
|
||||
def __init__(self, view):
|
||||
self.model = Model()
|
||||
self.view = view
|
||||
|
||||
def run(self):
|
||||
"""The controller's main function. Depending on what type of view is
|
||||
selected, a different event loop is setup. Do note that the ConsoleView
|
||||
is not a real event loop, just a basic flow of action."""
|
||||
if self.view is ConsoleView:
|
||||
self.view = self.view()
|
||||
self._run_console_view()
|
||||
elif self.view is WxView:
|
||||
app = wx.App()
|
||||
self.view = self.view()
|
||||
self.view._set_controller(self)
|
||||
app.MainLoop()
|
||||
|
||||
def get_task_from_model(self, idx):
|
||||
"""Needed for the WxView to communicate with the controller."""
|
||||
task = self.model.get_task(idx)
|
||||
self.view.show_task(task)
|
||||
|
||||
def _run_console_view(self):
|
||||
"""Super simple event loop."""
|
||||
while True:
|
||||
try:
|
||||
idx = self.view.select_task()
|
||||
idx = int(idx)
|
||||
except Exception as e:
|
||||
self.view.error(e)
|
||||
continue
|
||||
task = self.model.get_task(idx)
|
||||
self.view.show(task)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
view = ConsoleView
|
||||
# view = WxView
|
||||
app = Controller(view)
|
||||
app.run()
|
||||
```
|
||||
|
||||
And now with the implemented `WxView` class.
|
||||
|
||||
```python
|
||||
import wx
|
||||
|
||||
|
||||
class ConsoleView(object):
|
||||
"""A view for console."""
|
||||
|
||||
def select_task(self):
|
||||
"""Asks which index to look up."""
|
||||
idx = input("which task do you want to see? ")
|
||||
return idx
|
||||
|
||||
def show(self, task):
|
||||
"""Displays the task to the console. This method is called from the
|
||||
controller."""
|
||||
print("your task: {}".format(task))
|
||||
|
||||
def error(self, msg):
|
||||
"""Prints error messages coming from the controller."""
|
||||
print("error: {}".format(msg))
|
||||
|
||||
|
||||
class WxView(wx.Dialog):
|
||||
"""A view using a wx.Dialog window"""
|
||||
|
||||
def __init__(self):
|
||||
wx.Dialog.__init__(self, None, title="Task Manager")
|
||||
self.panel = wx.Panel(self)
|
||||
self.box = wx.BoxSizer(wx.VERTICAL)
|
||||
self.task = wx.StaticText(self.panel, label="your task")
|
||||
self.idx = wx.SpinCtrl(self.panel)
|
||||
self.button = wx.Button(self.panel, label="submit")
|
||||
self.button.Bind(wx.EVT_BUTTON, self.select_task)
|
||||
self.box.Add(self.task, 0, wx.EXPAND, 1)
|
||||
self.box.Add(self.idx, 0, wx.EXPAND, 1)
|
||||
self.box.Add(self.button, 0, wx.EXPAND, 1)
|
||||
self.panel.SetSizer(self.box)
|
||||
self.Show()
|
||||
|
||||
def _set_controller(self, controller):
|
||||
"""Set the controller so the view can communicate it's requests to it
|
||||
and update it's values too."""
|
||||
self.controller = controller
|
||||
|
||||
def select_task(self, event):
|
||||
"""Gets the index to look up in the model and submits the request to
|
||||
the controller."""
|
||||
idx = self.idx.GetValue()
|
||||
self.controller.get_task_from_model(idx)
|
||||
|
||||
def show_task(self, task):
|
||||
"""Updates the visual label in the view with the task. This method is
|
||||
called from the controller."""
|
||||
self.task.SetLabel(task)
|
||||
|
||||
|
||||
class Model(object):
|
||||
"""The model houses add data and should implement all methods related to
|
||||
adding, modifying and deleting tasks."""
|
||||
|
||||
db = ["go to the shops", "dryhop beer", "drop of motorbike"]
|
||||
|
||||
def get_task(self, idx):
|
||||
"""Performs a task lookun into the database and returns it when found.
|
||||
If no task is found, it returns an error message that will be displayed
|
||||
in the view (via the controller)."""
|
||||
try:
|
||||
task = Model.db[idx]
|
||||
except IndexError:
|
||||
task = "task with {} not found!".format(idx)
|
||||
return task
|
||||
|
||||
|
||||
class Controller(object):
|
||||
"""Binds the model and the view together."""
|
||||
|
||||
def __init__(self, view):
|
||||
self.model = Model()
|
||||
self.view = view
|
||||
|
||||
def run(self):
|
||||
"""The controller's main function. Depending on what type of view is
|
||||
selected, a different event loop is setup. Do note that the ConsoleView
|
||||
is not a real event loop, just a basic flow of action."""
|
||||
if self.view is ConsoleView:
|
||||
self.view = self.view()
|
||||
self._run_console_view()
|
||||
elif self.view is WxView:
|
||||
app = wx.App()
|
||||
self.view = self.view()
|
||||
self.view._set_controller(self)
|
||||
app.MainLoop()
|
||||
|
||||
def get_task_from_model(self, idx):
|
||||
"""Needed for the WxView to communicate with the controller."""
|
||||
task = self.model.get_task(idx)
|
||||
self.view.show_task(task)
|
||||
|
||||
def _run_console_view(self):
|
||||
"""Super simple event loop."""
|
||||
while True:
|
||||
try:
|
||||
idx = self.view.select_task()
|
||||
idx = int(idx)
|
||||
except Exception as e:
|
||||
self.view.error(e)
|
||||
continue
|
||||
task = self.model.get_task(idx)
|
||||
self.view.show(task)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
view = WxView
|
||||
app = Controller(view)
|
||||
app.run()
|
||||
```
|
||||
|
||||
For a GUI only login generator an implementation without MVC could look a bit like this.
|
||||
Note that the actual calculation is done inside the window itself.
|
||||
This is not a good idea because we should separate responsibilities into classes!
|
||||
|
||||
```python
|
||||
import wx
|
||||
import login_generator
|
||||
|
||||
|
||||
class MainWindow(wx.Dialog):
|
||||
def __init__(self):
|
||||
wx.Dialog.__init__(self, None, title="Login Generator")
|
||||
self.full_window = wx.Panel(self)
|
||||
self.full_window_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
self.full_window_sizer.Add(self.create_top_panel(), 0, wx.EXPAND | wx.ALL, 20)
|
||||
self.full_window_sizer.Add(self.create_bottom_panel(), 0, wx.EXPAND, 0)
|
||||
self.full_window.SetSizer(self.full_window_sizer)
|
||||
self.Show()
|
||||
|
||||
def create_bottom_panel(self):
|
||||
bottom_panel = wx.Panel(self.full_window)
|
||||
bottom_panel_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
self.login_list = wx.ListCtrl(bottom_panel, style=wx.LC_REPORT)
|
||||
self.login_list.Bind(wx.EVT_RIGHT_UP, self.show_popup)
|
||||
self.login_list.InsertColumn(0, 'username', width=200)
|
||||
self.login_list.InsertColumn(1, 'password', width=200)
|
||||
bottom_panel_sizer.Add(self.login_list, 0, wx.EXPAND | wx.ALL, 200)
|
||||
return bottom_panel
|
||||
|
||||
def create_top_panel(self):
|
||||
top_panel = wx.Panel(self.full_window)
|
||||
top_panel_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
self.login_amount = wx.TextCtrl(top_panel)
|
||||
self.login_complex = wx.CheckBox(top_panel, label="complex")
|
||||
self.login_create = wx.Button(top_panel, label="Create")
|
||||
self.login_create.Bind(wx.EVT_BUTTON, self.add_login)
|
||||
top_panel_sizer.Add(self.login_amount, 1, wx.EXPAND|wx.ALL,0)
|
||||
top_panel_sizer.Add(self.login_complex, 1, wx.EXPAND|wx.ALL,0)
|
||||
top_panel_sizer.Add(self.login_create, 1, wx.EXPAND|wx.ALL,0)
|
||||
top_panel.SetSizer(top_panel_sizer)
|
||||
return top_panel
|
||||
|
||||
def show_popup(self, event):
|
||||
menu = wx.Menu()
|
||||
menu.Append(1, "Copy selected items")
|
||||
menu.Bind(wx.EVT_MENU, self.copy_items, id=1)
|
||||
self.PopupMenu(menu)
|
||||
|
||||
def copy_items(self, event):
|
||||
selected_items = []
|
||||
for i in range(self.login_list.GetItemCount()):
|
||||
if self.login_list.IsSelected(i):
|
||||
username = selected_items.append(
|
||||
self.login_list.GetItem(i, 0).GetText()
|
||||
)
|
||||
password = selected_items.append(
|
||||
self.login_list.GetItem(i, 1).GetText()
|
||||
)
|
||||
clipdata = wx.TextDataObject()
|
||||
clipdata.SetText("\n".join(selected_items))
|
||||
wx.TheClipboard.Open()
|
||||
wx.TheClipboard.SetData(clipdata)
|
||||
wx.TheClipboard.Close()
|
||||
|
||||
def add_login(self, event):
|
||||
amount = self.login_amount.GetValue()
|
||||
complex = self.login_complex.GetValue()
|
||||
try:
|
||||
amount = int(amount)
|
||||
except:
|
||||
amount = 1
|
||||
for i in range(0, amount):
|
||||
username = login_generator.generate_username()
|
||||
password = login_generator.generate_password(12, complex)
|
||||
index = self.login_list.InsertItem(0, username)
|
||||
self.login_list.SetItem(index, 1, password)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = wx.App()
|
||||
win = MainWindow()
|
||||
app.MainLoop()
|
||||
```
|
||||
|
||||
Now let's assume the generate username and password function take some calculation time.
|
||||
We'll add in a fake `time.sleep` to simulate.
|
||||
|
||||
```python
|
||||
import wx
|
||||
import login_generator
|
||||
import time
|
||||
|
||||
|
||||
class MainWindow(wx.Dialog):
|
||||
def __init__(self):
|
||||
wx.Dialog.__init__(self, None, title="Login Generator")
|
||||
self.full_window = wx.Panel(self)
|
||||
self.full_window_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
self.full_window_sizer.Add(self.create_top_panel(), 0, wx.EXPAND | wx.ALL, 20)
|
||||
self.full_window_sizer.Add(self.create_bottom_panel(), 0, wx.EXPAND, 0)
|
||||
self.full_window.SetSizer(self.full_window_sizer)
|
||||
self.Show()
|
||||
|
||||
def create_bottom_panel(self):
|
||||
bottom_panel = wx.Panel(self.full_window)
|
||||
bottom_panel_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
self.login_list = wx.ListCtrl(bottom_panel, style=wx.LC_REPORT)
|
||||
self.login_list.Bind(wx.EVT_RIGHT_UP, self.show_popup)
|
||||
self.login_list.InsertColumn(0, 'username', width=200)
|
||||
self.login_list.InsertColumn(1, 'password', width=200)
|
||||
bottom_panel_sizer.Add(self.login_list, 0, wx.EXPAND | wx.ALL, 200)
|
||||
return bottom_panel
|
||||
|
||||
def create_top_panel(self):
|
||||
top_panel = wx.Panel(self.full_window)
|
||||
top_panel_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
self.login_amount = wx.TextCtrl(top_panel)
|
||||
self.login_complex = wx.CheckBox(top_panel, label="complex")
|
||||
self.login_create = wx.Button(top_panel, label="Create")
|
||||
self.login_create.Bind(wx.EVT_BUTTON, self.add_login)
|
||||
top_panel_sizer.Add(self.login_amount, 1, wx.EXPAND|wx.ALL,0)
|
||||
top_panel_sizer.Add(self.login_complex, 1, wx.EXPAND|wx.ALL,0)
|
||||
top_panel_sizer.Add(self.login_create, 1, wx.EXPAND|wx.ALL,0)
|
||||
top_panel.SetSizer(top_panel_sizer)
|
||||
return top_panel
|
||||
|
||||
def show_popup(self, event):
|
||||
menu = wx.Menu()
|
||||
menu.Append(1, "Copy selected items")
|
||||
menu.Bind(wx.EVT_MENU, self.copy_items, id=1)
|
||||
self.PopupMenu(menu)
|
||||
|
||||
def copy_items(self, event):
|
||||
selected_items = []
|
||||
for i in range(self.login_list.GetItemCount()):
|
||||
if self.login_list.IsSelected(i):
|
||||
username = selected_items.append(
|
||||
self.login_list.GetItem(i, 0).GetText()
|
||||
)
|
||||
password = selected_items.append(
|
||||
self.login_list.GetItem(i, 1).GetText()
|
||||
)
|
||||
clipdata = wx.TextDataObject()
|
||||
clipdata.SetText("\n".join(selected_items))
|
||||
wx.TheClipboard.Open()
|
||||
wx.TheClipboard.SetData(clipdata)
|
||||
wx.TheClipboard.Close()
|
||||
|
||||
def add_login(self, event):
|
||||
amount = self.login_amount.GetValue()
|
||||
complex_bool = self.login_complex.GetValue()
|
||||
try:
|
||||
amount = int(amount)
|
||||
except:
|
||||
amount = 1
|
||||
for i in range(0, amount):
|
||||
username = login_generator.generate_username()
|
||||
password = login_generator.generate_password(12, complex_bool)
|
||||
time.sleep(1)
|
||||
index = self.login_list.InsertItem(0, username)
|
||||
self.login_list.SetItem(index, 1, password)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = wx.App()
|
||||
win = MainWindow()
|
||||
app.MainLoop()
|
||||
```
|
||||
|
||||
A clear separation of responsabilities can be acchieved via an MVC pattern and a login *library*.
|
||||
The library code is quite straightforward and goes as follows.
|
||||
It's basically the same code we did before but with added `try except` blocks.
|
||||
Can you tell me why I added those?
|
||||
|
||||
```python
|
||||
import random
|
||||
import string
|
||||
|
||||
|
||||
def load_file(filename):
|
||||
"""
|
||||
We load a file and make a list out of it. Note that the same function is
|
||||
used for both files (both adjectives and subjects). Functions should be
|
||||
made as generic as possible.
|
||||
|
||||
There IS a problem you can fix, some logins will have spaces in them. Try
|
||||
to remove them in this function!
|
||||
"""
|
||||
words = []
|
||||
with open(filename, "r") as fp:
|
||||
lines = fp.readlines()
|
||||
for line in lines:
|
||||
words.append(line.strip()) # what does strip() do, what does append() do? remember CTRL+Q!
|
||||
return words
|
||||
|
||||
|
||||
def generate_username():
|
||||
"""
|
||||
We'll generate a random pair of adjectives and subjects from two wordlists.
|
||||
You NEED to have both files in you python project for this to work! Note
|
||||
the capitalize method call to make it all prettier...
|
||||
"""
|
||||
try:
|
||||
adjectives = load_file("./adjectives.txt")
|
||||
except:
|
||||
adjectives = ["big", "funny", "normal", "red"]
|
||||
try:
|
||||
subjects = load_file("./subjects.txt")
|
||||
except:
|
||||
subjects = ["giraffe", "elephant", "cougar", "tiger"]
|
||||
adjective = random.choice(adjectives)
|
||||
subject = random.choice(subjects)
|
||||
username = adjective.capitalize() + subject.capitalize()
|
||||
return username
|
||||
|
||||
|
||||
def generate_password(length=10, complictated=True):
|
||||
"""
|
||||
We generate a password with default settings. You can overide these by
|
||||
changing the arguments in the function call.
|
||||
"""
|
||||
password = ""
|
||||
if complictated:
|
||||
chars = string.ascii_letters + string.digits + string.punctuation
|
||||
else:
|
||||
chars = string.ascii_letters
|
||||
for i in range(0, length):
|
||||
password += random.choice(chars)
|
||||
return password
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# let's do some testing!
|
||||
username_test = generate_username()
|
||||
print(username_test)
|
||||
password_test = generate_password()
|
||||
print(password_test)
|
||||
```
|
||||
|
||||
And now the GUI code nicely split up in a **model**, **controller** and a **view**.
|
||||
The overhead is quite large but it makes the code a lot more scalable.
|
||||
|
||||
```python
|
||||
import wx
|
||||
import login_generator
|
||||
import time
|
||||
import threading
|
||||
import queue
|
||||
|
||||
|
||||
class View(wx.Dialog):
|
||||
def __init__(self, controller):
|
||||
wx.Dialog.__init__(self, None, title="Login Generator")
|
||||
self.controller = controller
|
||||
self.full_window = wx.Panel(self)
|
||||
self.full_window_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
self.full_window_sizer.Add(self.create_top_panel(), 0, wx.EXPAND | wx.ALL, 20)
|
||||
self.full_window_sizer.Add(self.create_bottom_panel(), 0, wx.EXPAND, 0)
|
||||
self.full_window.SetSizer(self.full_window_sizer)
|
||||
self.Show()
|
||||
|
||||
def create_bottom_panel(self):
|
||||
bottom_panel = wx.Panel(self.full_window)
|
||||
bottom_panel_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
self.login_list = wx.ListCtrl(bottom_panel, style=wx.LC_REPORT)
|
||||
self.login_list.Bind(wx.EVT_RIGHT_UP, self.show_popup)
|
||||
self.login_list.InsertColumn(0, 'username', width=200)
|
||||
self.login_list.InsertColumn(1, 'password', width=200)
|
||||
bottom_panel_sizer.Add(self.login_list, 0, wx.EXPAND | wx.ALL, 200)
|
||||
return bottom_panel
|
||||
|
||||
def create_top_panel(self):
|
||||
top_panel = wx.Panel(self.full_window)
|
||||
top_panel_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
self.login_amount = wx.TextCtrl(top_panel)
|
||||
self.login_complex = wx.CheckBox(top_panel, label="complex")
|
||||
self.login_create = wx.Button(top_panel, label="Create")
|
||||
self.login_create.Bind(wx.EVT_BUTTON, self.submit_request)
|
||||
top_panel_sizer.Add(self.login_amount, 1, wx.EXPAND|wx.ALL,0)
|
||||
top_panel_sizer.Add(self.login_complex, 1, wx.EXPAND|wx.ALL,0)
|
||||
top_panel_sizer.Add(self.login_create, 1, wx.EXPAND|wx.ALL,0)
|
||||
top_panel.SetSizer(top_panel_sizer)
|
||||
return top_panel
|
||||
|
||||
def show_popup(self, event):
|
||||
menu = wx.Menu()
|
||||
menu.Append(1, "Copy selected items")
|
||||
menu.Bind(wx.EVT_MENU, self.copy_items, id=1)
|
||||
self.PopupMenu(menu)
|
||||
|
||||
def copy_items(self, event):
|
||||
selected_items = []
|
||||
for i in range(self.login_list.GetItemCount()):
|
||||
if self.login_list.IsSelected(i):
|
||||
username = selected_items.append(
|
||||
self.login_list.GetItem(i, 0).GetText()
|
||||
)
|
||||
password = selected_items.append(
|
||||
self.login_list.GetItem(i, 1).GetText()
|
||||
)
|
||||
clipdata = wx.TextDataObject()
|
||||
clipdata.SetText("\n".join(selected_items))
|
||||
wx.TheClipboard.Open()
|
||||
wx.TheClipboard.SetData(clipdata)
|
||||
wx.TheClipboard.Close()
|
||||
|
||||
def submit_request(self, event):
|
||||
amount = self.login_amount.GetValue()
|
||||
complex_bool = self.login_complex.GetValue()
|
||||
try:
|
||||
amount = int(amount)
|
||||
except:
|
||||
amount = 1
|
||||
self.controller.get_new_logins(amount, complex_bool)
|
||||
|
||||
def update_logins(self, login):
|
||||
username, password = login
|
||||
index = self.login_list.InsertItem(0, username)
|
||||
self.login_list.SetItem(index, 1, password)
|
||||
|
||||
|
||||
class Controller(object):
|
||||
def __init__(self):
|
||||
self.app = wx.App()
|
||||
self.view = View(self)
|
||||
self.model = Model(self)
|
||||
|
||||
def get_new_logins(self, amount, complex_bool):
|
||||
self.model.generate_login(amount, complex_bool)
|
||||
|
||||
def set_new_logins(self, logins):
|
||||
self.view.update_logins(logins)
|
||||
|
||||
|
||||
class Model(threading.Thread):
|
||||
def __init__(self, controller):
|
||||
threading.Thread.__init__(self, target=self.main)
|
||||
self.controller = controller
|
||||
self.queue = queue.Queue()
|
||||
self.start()
|
||||
|
||||
def main(self):
|
||||
while True:
|
||||
amount, complex_bool = self.queue.get()
|
||||
username = login_generator.generate_username()
|
||||
password = login_generator.generate_password(12, complex_bool)
|
||||
logins = (username, password)
|
||||
self.controller.set_new_logins(logins)
|
||||
time.sleep(1)
|
||||
|
||||
def generate_login(self, amount, complex_bool):
|
||||
for i in range(0, amount):
|
||||
self.queue.put((amount, complex_bool))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mvc = Controller()
|
||||
mvc.app.MainLoop()
|
||||
```
|
||||
|
||||
# Coding challenge - Login generator with GUI
|
||||
|
||||
# Coding challenge - Trivial pursuit with GUI
|
||||
|
|
Loading…
Reference in New Issue