Browse Source

cleaned up old code, added helper utils

pull/240/head
chriskiehl 7 years ago
parent
commit
cd4afcc30c
12 changed files with 320 additions and 128 deletions
  1. 26
      gooey/gui/application.py
  2. 18
      gooey/gui/cli.py
  3. 30
      gooey/gui/events.py
  4. 158
      gooey/gui/processor.py
  5. 7
      gooey/gui/pubsub.py
  6. 22
      gooey/gui/seeder.py
  7. 42
      gooey/gui/util/freeze.py
  8. 10
      gooey/gui/util/functional.py
  9. 26
      gooey/gui/util/wx_util.py
  10. 15
      gooey/gui/validators.py
  11. 0
      gooey/util/__init__.py
  12. 94
      gooey/util/functional.py

26
gooey/gui/application.py

@ -3,21 +3,35 @@ Main runner entry point for Gooey.
'''
import wx
import wx.lib.inspection
from gooey.gui.lang import i18n
from gooey.gui.controller import Controller
from gooey.gui import image_repository
from gooey.gui.containers.application import GooeyApplication
from gooey.util.functional import merge
def run(build_spec):
app = wx.App(False)
i18n.load(build_spec['language_dir'], build_spec['language'])
image_repository.patch_images(build_spec['image_dir'])
controller = Controller(build_spec)
controller.run()
imagesPaths = image_repository.loadImages(build_spec['image_dir'])
gapp = GooeyApplication(merge(build_spec, imagesPaths))
# wx.lib.inspection.InspectionTool().Show()
gapp.Show()
app.MainLoop()
def build_app(build_spec):
app = wx.App(False)
i18n.load(build_spec['language_dir'], build_spec['language'])
imagesPaths = image_repository.loadImages(build_spec['image_dir'])
gapp = GooeyApplication(merge(build_spec, imagesPaths))
# wx.lib.inspection.InspectionTool().Show()
gapp.Show()
return app

18
gooey/gui/cli.py

@ -0,0 +1,18 @@
from itertools import chain
from copy import deepcopy
from gooey.util.functional import compact
def buildCliString(target, cmd, positional, optional):
positionals = deepcopy(positional)
if positionals:
positionals.insert(0, "--")
cmd_string = ' '.join(compact(chain(optional, positional)))
if cmd != '::gooey/default':
cmd_string = u'{} {}'.format(cmd, cmd_string)
return u'{} --ignore-gooey {}'.format(target, cmd_string)

30
gooey/gui/events.py

@ -1,20 +1,26 @@
"""
App wide event registry
Everything in the application is communitcated via pubsub. These are the events that
tie everythign together.
Everything in the application is communicated via pubsub. These are the events
that tie everything together.
"""
import wx
new_id = lambda: str(wx.NewId())
WINDOW_STOP = wx.NewId()
WINDOW_CANCEL = wx.NewId()
WINDOW_CLOSE = wx.NewId()
WINDOW_START = wx.NewId()
WINDOW_RESTART = wx.NewId()
WINDOW_EDIT = wx.NewId()
WINDOW_CHANGE = wx.NewId()
PANEL_CHANGE = wx.NewId()
LIST_BOX = wx.NewId()
CONSOLE_UPDATE = wx.NewId()
EXECUTION_COMPLETE = wx.NewId()
PROGRESS_UPDATE = wx.NewId()
WINDOW_STOP = new_id()
WINDOW_CANCEL = new_id()
WINDOW_CLOSE = new_id()
WINDOW_START = new_id()
WINDOW_RESTART = new_id()
WINDOW_EDIT = new_id()
USER_INPUT = wx.NewId()
WINDOW_CHANGE = new_id()
PANEL_CHANGE = new_id()
LIST_BOX = new_id()

158
gooey/gui/processor.py

@ -1,98 +1,102 @@
import os
import re
import subprocess
import sys
from functools import partial
from multiprocessing.dummy import Pool
import sys
from gooey.gui import events
from gooey.gui.pubsub import pub
from gooey.gui.util.casting import safe_float
from gooey.gui.util.functional import unit, bind
from gooey.gui.util.taskkill import taskkill
from gooey.util.functional import unit, bind
class ProcessController(object):
def __init__(self, progress_regex, progress_expr):
self._process = None
self.progress_regex = progress_regex
self.progress_expr = progress_expr
def was_success(self):
self._process.communicate()
return self._process.returncode == 0
def __init__(self, progress_regex, progress_expr, encoding):
self._process = None
self.progress_regex = progress_regex
self.progress_expr = progress_expr
self.encoding = encoding
self.wasForcefullyStopped = False
def poll(self):
if not self._process:
raise Exception('Not started!')
self._process.poll()
def was_success(self):
self._process.communicate()
return self._process.returncode == 0
def stop(self):
if self.running():
taskkill(self._process.pid)
def poll(self):
if not self._process:
raise Exception('Not started!')
self._process.poll()
def running(self):
return self._process and self.poll() is None
def stop(self):
if self.running():
self.wasForcefullyStopped = True
taskkill(self._process.pid)
def run(self, command):
env = os.environ.copy()
env["GOOEY"] = "1"
try:
self._process = subprocess.Popen(
command.encode(sys.getfilesystemencoding()),
bufsize=1, stdout=subprocess.PIPE, stdin=subprocess.PIPE,
stderr=subprocess.STDOUT, shell=True, env=env)
except:
self._process = subprocess.Popen(
command,
bufsize=1, stdout=subprocess.PIPE, stdin=subprocess.PIPE,
stderr=subprocess.STDOUT, shell=True, env=env)
Pool(1).apply_async(self._forward_stdout, (self._process,))
def running(self):
return self._process and self.poll() is None
def _forward_stdout(self, process):
'''
Reads the stdout of `process` and forwards lines and progress
to any interested subscribers
'''
while True:
line = process.stdout.readline()
if not line:
break
pub.send_message('console_update', msg=line)
pub.send_message('progress_update', progress=self._extract_progress(line))
pub.send_message('execution_complete')
def run(self, command):
self.wasForcefullyStopped = False
env = os.environ.copy()
env["GOOEY"] = "1"
env["PYTHONIOENCODING"] = self.encoding
try:
self._process = subprocess.Popen(
command.encode(sys.getfilesystemencoding()),
bufsize=1, stdout=subprocess.PIPE, stdin=subprocess.PIPE,
stderr=subprocess.STDOUT, shell=True, env=env)
except:
self._process = subprocess.Popen(
command,
bufsize=1, stdout=subprocess.PIPE, stdin=subprocess.PIPE,
stderr=subprocess.STDOUT, shell=True, env=env)
Pool(1).apply_async(self._forward_stdout, (self._process,))
def _extract_progress(self, text):
'''
Finds progress information in the text using the
user-supplied regex and calculation instructions
'''
# monad-ish dispatch to avoid the if/else soup
find = partial(re.search, string=text.strip())
regex = unit(self.progress_regex)
match = bind(regex, find)
result = bind(match, self._calculate_progress)
return result
def _forward_stdout(self, process):
'''
Reads the stdout of `process` and forwards lines and progress
to any interested subscribers
'''
while True:
line = process.stdout.readline()
if not line:
break
pub.send_message(events.CONSOLE_UPDATE, msg=line.decode(self.encoding))
pub.send_message(events.PROGRESS_UPDATE,
progress=self._extract_progress(line))
pub.send_message(events.EXECUTION_COMPLETE)
def _calculate_progress(self, match):
'''
Calculates the final progress value found by the regex
'''
if not self.progress_expr:
return safe_float(match.group(1))
else:
return self._eval_progress(match)
def _extract_progress(self, text):
'''
Finds progress information in the text using the
user-supplied regex and calculation instructions
'''
# monad-ish dispatch to avoid the if/else soup
find = partial(re.search, string=text.strip().decode(self.encoding))
regex = unit(self.progress_regex)
match = bind(regex, find)
result = bind(match, self._calculate_progress)
return result
def _eval_progress(self, match):
'''
Runs the user-supplied progress calculation rule
'''
_locals = {k: safe_float(v) for k, v in match.groupdict().items()}
if "x" not in _locals:
_locals["x"] = [safe_float(x) for x in match.groups()]
try:
return int(eval(self.progress_expr, {}, _locals))
except:
return None
def _calculate_progress(self, match):
'''
Calculates the final progress value found by the regex
'''
if not self.progress_expr:
return safe_float(match.group(1))
else:
return self._eval_progress(match)
def _eval_progress(self, match):
'''
Runs the user-supplied progress calculation rule
'''
_locals = {k: safe_float(v) for k, v in match.groupdict().items()}
if "x" not in _locals:
_locals["x"] = [safe_float(x) for x in match.groups()]
try:
return int(eval(self.progress_expr, {}, _locals))
except:
return None

7
gooey/gui/pubsub.py

@ -1,3 +1,4 @@
import wx
from collections import defaultdict
__ALL__ = ['pub']
@ -5,21 +6,19 @@ __ALL__ = ['pub']
class PubSub(object):
'''
A super simplified clone of Wx.lib.pubsub since it doesn't exist on linux
*grumble grumble* Stupid abandoned wx project... >:( *grumble*
'''
def __init__(self):
self.registry = defaultdict(list)
def subscribe(self, handler, event):
def subscribe(self, event, handler):
self.registry[event].append(handler)
def send_message(self, event, **kwargs):
for event_handler in self.registry.get(event, []):
event_handler(**kwargs)
wx.CallAfter(event_handler, **kwargs)
pub = PubSub()

22
gooey/gui/seeder.py

@ -0,0 +1,22 @@
"""
Util for talking to the client program in order to retrieve
dynamic defaults for the UI
"""
import json
import subprocess
def fetchDynamicProperties(target, encoding):
"""
Sends a gooey-seed-ui request to the client program it retrieve
dynamically generated defaults with which to seed the UI
"""
cmd = '{} {}'.format(target, 'gooey-seed-ui --ignore-gooey')
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if proc.returncode != 0:
out, _ = proc.communicate()
return json.loads(out.decode(encoding))
else:
# TODO: useful feedback
return {}

42
gooey/gui/util/freeze.py

@ -1,26 +1,28 @@
import sys
import os
import sys
def is_frozen():
return getattr(sys, 'frozen', False)
return getattr(sys, 'frozen', False)
def get_resource_path(*args):
if is_frozen():
# MEIPASS explanation:
# https://pythonhosted.org/PyInstaller/#run-time-operation
basedir = getattr(sys, '_MEIPASS', None)
if not basedir:
basedir = os.path.dirname(sys.executable)
resource_dir = os.path.join(basedir, 'gooey')
if not os.path.isdir(resource_dir):
raise IOError(
("Cannot locate Gooey resources. It seems that the program was frozen, "
"but resource files were not copied into directory of the executable "
"file. Please copy `languages` and `images` folders from gooey module "
"directory into `{}{}` directory. Using PyInstaller, a.datas in .spec "
"file must be specified.".format(resource_dir, os.sep)))
else:
resource_dir = os.path.normpath(os.path.join(os.path.dirname(__file__), '..', '..'))
return os.path.join(resource_dir, *args)
def getResourcePath(*args):
if is_frozen():
# MEIPASS explanation:
# https://pythonhosted.org/PyInstaller/#run-time-operation
basedir = getattr(sys, '_MEIPASS', None)
if not basedir:
basedir = os.path.dirname(sys.executable)
resource_dir = os.path.join(basedir, 'gooey')
if not os.path.isdir(resource_dir):
raise IOError(
(
"Cannot locate Gooey resources. It seems that the program was frozen, "
"but resource files were not copied into directory of the executable "
"file. Please copy `languages` and `images` folders from gooey module "
"directory into `{}{}` directory. Using PyInstaller, a.datas in .spec "
"file must be specified.".format(resource_dir, os.sep)))
else:
resource_dir = os.path.normpath(
os.path.join(os.path.dirname(__file__), '..', '..'))
return os.path.join(resource_dir, *args)

10
gooey/gui/util/functional.py

@ -1,10 +1,6 @@
'''
Simple monad-ish bindings
'''
def unit(val):
return val
def bind(val, f):
return f(val) if val else None

26
gooey/gui/util/wx_util.py

@ -3,10 +3,26 @@ Collection of Utility methods for creating often used, pre-styled wx Widgets
"""
import wx
from contextlib import contextmanager
from gooey.gui.three_to_four import Constants
@contextmanager
def transactUI(obj):
"""
Coarse grain UI locking to avoid glitchy UI updates
"""
obj.Freeze()
try:
yield
finally:
obj.Layout()
obj.Thaw()
styles = {
'h0': (wx.FONTFAMILY_DEFAULT, Constants.WX_FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False),
'h1': (wx.FONTFAMILY_DEFAULT, Constants.WX_FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False),
@ -15,6 +31,8 @@ styles = {
}
def make_bold(statictext):
pointsize = statictext.GetFont().GetPointSize()
font = wx.Font(pointsize, *styles['bold'])
@ -22,8 +40,12 @@ def make_bold(statictext):
def dark_grey(statictext):
darkgray = (54, 54, 54)
statictext.SetForegroundColour(darkgray)
return withColor(statictext, (54, 54, 54))
def withColor(statictext, hex):
statictext.SetForegroundColour(hex)
return statictext
def h0(parent, label):

15
gooey/gui/validators.py

@ -0,0 +1,15 @@
def runValidator(f, value):
"""
Attempt to run the user supplied validation function
Fall back to False in the even of any errors
"""
try:
return f(value)
except:
return False

0
gooey/util/__init__.py

94
gooey/util/functional.py

@ -0,0 +1,94 @@
"""
A collection of functional utilities/helpers
"""
from functools import reduce
from copy import deepcopy
from itertools import chain, dropwhile
def getin(m, path, default=None):
"""returns the value in a nested dict"""
keynotfound = ':com.gooey-project/not-found'
result = reduce(lambda acc, val: acc.get(val, {keynotfound: None}), path, m)
# falsey values like 0 would incorrectly trigger the default to be returned
# so the keynotfound val is used to signify a miss vs just a falesy val
if isinstance(result, dict) and keynotfound in result:
return default
return result
def assoc(m, key, val):
"""Copy-on-write associates a value in a dict"""
cpy = deepcopy(m)
cpy[key] = val
return cpy
def associn(m, path, value):
""" Copy-on-write associates a value in a nested dict """
def assoc_recursively(m, path, value):
if not path:
return value
p = path[0]
return assoc(m, p, assoc_recursively(m.get(p,{}), path[1:], value))
return assoc_recursively(m, path, value)
def merge(*maps):
"""Merge all maps left to right"""
copies = map(deepcopy, maps)
return reduce(lambda acc, val: acc.update(val) or acc, copies)
def flatmap(f, coll):
"""Applies concat to the result of applying f to colls"""
return list(chain(*map(f, coll)))
def indexunique(f, coll):
"""Build a map from the collection keyed off of f
e.g.
[{id:1,..}, {id:2, ...}] => {1: {id:1,...}, 2: {id:2,...}}
Note: duplicates, if present, are overwritten
"""
return zipmap(map(f, coll), coll)
def findfirst(f, coll):
"""Return first occurrence matching f, otherwise None"""
result = list(dropwhile(f, coll))
return result[0] if result else None
def zipmap(keys, vals):
"""Return a map from keys to values"""
return dict(zip(keys, vals))
def compact(coll):
"""Returns a new list with all falsy values removed"""
return list(filter(None, coll))
def ifPresent(f):
"""Execute f only if value is present and not None"""
def inner(value):
if value:
return f(value)
else:
return True
return inner
def identity(x):
"""Identity function always returns the supplied argument"""
return x
def unit(val):
return val
def bind(val, f):
return f(val) if val else None
Loading…
Cancel
Save