mirror of https://github.com/chriskiehl/Gooey.git
12 changed files with 320 additions and 128 deletions
Split View
Diff Options
-
26gooey/gui/application.py
-
18gooey/gui/cli.py
-
30gooey/gui/events.py
-
158gooey/gui/processor.py
-
7gooey/gui/pubsub.py
-
22gooey/gui/seeder.py
-
42gooey/gui/util/freeze.py
-
10gooey/gui/util/functional.py
-
26gooey/gui/util/wx_util.py
-
15gooey/gui/validators.py
-
0gooey/util/__init__.py
-
94gooey/util/functional.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) |
@ -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() |
@ -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 |
@ -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 {} |
|||
|
@ -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) |
@ -1,10 +1,6 @@ |
|||
''' |
|||
Simple monad-ish bindings |
|||
''' |
|||
|
|||
|
|||
def unit(val): |
|||
return val |
|||
|
|||
def bind(val, f): |
|||
return f(val) if val else None |
|||
|
|||
|
|||
|
@ -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,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 |
Write
Preview
Loading…
Cancel
Save