outlines MVC

This commit is contained in:
waldek 2021-12-06 23:32:49 +01:00
parent 196ad4abeb
commit 41cb2d3980
1 changed files with 570 additions and 0 deletions

View File

@ -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