You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

418 lines
13 KiB

import json
from base64 import b64encode
from typing import Optional, List, Dict, Any, Union, Callable
from typing_extensions import TypedDict
import wx
from gooey.gui import events
from gooey.gui.lang.i18n import _
from gooey.python_bindings.types import GooeyParams, Item, Group, TopLevelParser, EnrichedItem, \
FieldValue
from gooey.util.functional import associn, assoc, associnMany, compact
from gooey.gui.formatters import formatArgument
from gooey.python_bindings.types import FormField
from gooey.gui.constants import VALUE_PLACEHOLDER
from gooey.gui.formatters import add_placeholder
from gooey.python_bindings.types import CommandPieces, PublicGooeyState
class TimingEvent(TypedDict):
elapsed_time: Optional[str]
estimatedRemaining: Optional[str]
class ProgressEvent(TypedDict):
progress: Optional[int]
class ButtonState(TypedDict):
id: str
style: str
label_id: str
show: bool
enabled: bool
class ProgressState(TypedDict):
show: bool
range: int
value: int
class TimingState(TypedDict):
show: bool
elapsedTime: Optional[str]
estimated_remaining: Optional[str]
class GooeyState(GooeyParams):
fetchingUpdate: bool
screen: str
title: str
subtitle: str
images: Dict[str, str]
image: str
buttons: List[ButtonState]
progress: ProgressState
timing: TimingState
subcommands: List[str]
activeSelection: int
show_error_alert: bool
class FullGooeyState(GooeyState):
forms: Dict[str, List[FormField]]
widgets: Dict[str, Dict[str, Any]]
def extract_items(groups: List[Group]) -> List[Item]:
if not groups:
return []
group = groups[0]
return group['items'] \
+ extract_items(groups[1:]) \
+ extract_items(group['groups'])
def widgets(descriptor: TopLevelParser) -> List[Item]:
return extract_items(descriptor['contents'])
def enrichValue(formState: List[FormField], items: List[Item]) -> List[EnrichedItem]:
formIndex = {k['id']:k for k in formState}
return [EnrichedItem(field=formIndex[item['id']], **item) for item in items] # type: ignore
def positional(items: List[Union[Item, EnrichedItem]]):
return [item for item in items if item['cli_type'] == 'positional']
def optional(items: List[Union[Item, EnrichedItem]]):
return [item for item in items if item['cli_type'] != 'positional']
def cli_pieces(state: FullGooeyState, formatter=formatArgument) -> CommandPieces:
parserName = state['subcommands'][state['activeSelection']]
parserSpec = state['widgets'][parserName]
formState = state['forms'][parserName]
subcommand = parserSpec['command'] if parserSpec['command'] != '::gooey/default' else ''
items = enrichValue(formState, widgets(parserSpec))
positional_args = [formatter(item) for item in positional(items)] # type: ignore
optional_args = [formatter(item) for item in optional(items)] # type: ignore
ignoreFlag = '' if state['suppress_gooey_flag'] else '--ignore-gooey'
return CommandPieces(
target=state['target'],
subcommand=subcommand,
positionals=compact(positional_args),
optionals=compact(optional_args),
ignoreFlag=ignoreFlag
)
def activeFormState(state: FullGooeyState):
subcommand = state['subcommands'][state['activeSelection']]
return state['forms'][subcommand]
def buildInvocationCmd(state: FullGooeyState):
pieces = cli_pieces(state)
return u' '.join(compact([
pieces.target,
pieces.subcommand,
*pieces.optionals,
pieces.ignoreFlag,
'--' if pieces.positionals else '',
*pieces.positionals]))
def buildFormValidationCmd(state: FullGooeyState):
pieces = cli_pieces(state, formatter=cmdOrPlaceholderOrNone)
serializedForm = json.dumps({'active_form': activeFormState(state)})
b64ecoded = b64encode(serializedForm.encode('utf-8'))
return ' '.join(compact([
pieces.target,
pieces.subcommand,
*pieces.optionals,
'--gooey-validate-form',
'--gooey-state ' + b64ecoded.decode('utf-8'),
'--' if pieces.positionals else '',
*pieces.positionals]))
def buildOnCompleteCmd(state: FullGooeyState, was_success: bool):
pieces = cli_pieces(state)
serializedForm = json.dumps({'active_form': activeFormState(state)})
b64ecoded = b64encode(serializedForm.encode('utf-8'))
return u' '.join(compact([
pieces.target,
pieces.subcommand,
*pieces.optionals,
'--gooey-state ' + b64ecoded.decode('utf-8'),
'--gooey-run-is-success' if was_success else '--gooey-run-is-failure',
'--' if pieces.positionals else '',
*pieces.positionals]))
def buildOnSuccessCmd(state: FullGooeyState):
return buildOnCompleteCmd(state, True)
def buildOnErrorCmd(state: FullGooeyState):
return buildOnCompleteCmd(state, False)
def cmdOrPlaceholderOrNone(item: EnrichedItem) -> Optional[str]:
# Argparse has a fail-fast-and-exit behavior for any missing
# values. This poses a problem for dynamic validation, as we
# want to collect _all_ errors to be more useful to the user.
# As such, if there is no value currently available, we pass
# through a stock placeholder values which allows GooeyParser
# to handle it being missing without Argparse exploding due to
# it actually being missing.
if item['cli_type'] == 'positional':
return formatArgument(item) or VALUE_PLACEHOLDER
elif item['cli_type'] != 'positional' and item['required']:
# same rationale applies here. We supply the argument
# along with a fixed placeholder (when relevant i.e. `store`
# actions)
return formatArgument(item) or formatArgument(assoc(item, 'field', add_placeholder(item['field'])))
else:
# Optional values are, well, optional. So, like usual, we send
# them if present or drop them if not.
return formatArgument(item)
def combine(state: GooeyState, params: GooeyParams, formState: List[FormField]) -> FullGooeyState:
"""
I'm leaving the refactor of the form elements to another day.
For now, we'll just merge in the state of the form fields as tracked
in the UI into the main state blob as needed.
"""
subcommand = list(params['widgets'].keys())[state['activeSelection']]
return FullGooeyState(**{
**state,
**params,
'forms': {subcommand: formState}
})
def enable_buttons(state, to_enable: List[str]):
updated = [{**btn, 'enabled': btn['label_id'] in to_enable}
for btn in state['buttons']]
return assoc(state, 'buttons', updated)
def activeCommand(state, params: GooeyParams):
"""
Retrieve the active sub-parser command as determined by the
current selection.
"""
return list(params['widgets'].keys())[state['activeSelection']]
def mergeExternalState(state: FullGooeyState, extern: PublicGooeyState) -> FullGooeyState:
# TODO: insane amounts of helpful validation
subcommand = state['subcommands'][state['activeSelection']]
formItems: List[FormField] = state['forms'][subcommand]
hostForm: List[FormField] = extern['active_form']
return associn(state, ['forms', subcommand], hostForm)
def show_alert(state: FullGooeyState):
return assoc(state, 'show_error_alert', True)
def has_errors(state: FullGooeyState):
"""
Searches through the form elements (including down into
RadioGroup's internal options to find the presence of
any errors.
"""
return any([item['error'] or any(x['error'] for x in item.get('options', []))
for items in state['forms'].values()
for item in items])
def initial_state(params: GooeyParams) -> GooeyState:
buttons = [
('cancel', events.WINDOW_CANCEL, wx.ID_CANCEL),
('start', events.WINDOW_START, wx.ID_OK),
('stop', events.WINDOW_STOP, wx.ID_OK),
('edit', events.WINDOW_EDIT,wx.ID_OK),
('restart', events.WINDOW_RESTART, wx.ID_OK),
('close', events.WINDOW_CLOSE, wx.ID_OK),
]
# helping out the type system
params: Dict[str, Any] = params
return GooeyState(
**params,
fetchingUpdate=False,
screen='FORM',
title=params['program_name'],
subtitle=params['program_description'],
image=params['images']['configIcon'],
buttons=[ButtonState(
id=event_id,
style=style,
label_id=label,
show=label in ('cancel', 'start'),
enabled=True)
for label, event_id, style in buttons],
progress=ProgressState(
show=False,
range=100,
value=0 if params['progress_regex'] else -1
),
timing=TimingState(
show=False,
elapsed_time=None,
estimatedRemaining=None,
),
show_error_alert=False,
subcommands=list(params['widgets'].keys()),
activeSelection=0
)
def header_props(state, params):
return {
'background_color': params['header_bg_color'],
'title': params['program_name'],
'subtitle': params['program_description'],
'height': params['header_height'],
'image_uri': ims['images']['configIcon'],
'image_size': (six.MAXSIZE, params['header_height'] - 10)
}
def form_page(state):
return {
**state,
'buttons': [{**btn, 'show': btn['label_id'] in ('start', 'cancel')}
for btn in state['buttons']]
}
def consoleScreen(_: Callable[[str], str], state: GooeyState):
return {
**state,
'screen': 'CONSOLE',
'title': _("running_title"),
'subtitle': _('running_msg'),
'image': state['images']['runningIcon'],
'buttons': [{**btn,
'show': btn['label_id'] == 'stop',
'enabled': True}
for btn in state['buttons']],
'progress': {
'show': not state['disable_progress_bar_animation'],
'range': 100,
'value': 0 if state['progress_regex'] else -1
},
'timing': {
'show': state['timing_options']['show_time_remaining'],
'elapsed_time': None,
'estimatedRemaining': None
},
'show_error_alert': False
}
def editScreen(_: Callable[[str], str], state: FullGooeyState):
use_buttons = ('cancel', 'start')
return associnMany(
state,
('screen', 'FORM'),
('buttons', [{**btn,
'show': btn['label_id'] in use_buttons,
'enabled': True}
for btn in state['buttons']]),
('image', state['images']['configIcon']),
('title', state['program_name']),
('subtitle', state['program_description']))
def beginUpdate(state: GooeyState):
return {
**enable_buttons(state, ['cancel']),
'fetchingUpdate': True
}
def finishUpdate(state: GooeyState):
return {
**enable_buttons(state, ['cancel', 'start']),
'fetchingUpdate': False
}
def finalScreen(_: Callable[[str], str], state: GooeyState) -> GooeyState:
use_buttons = ('edit', 'restart', 'close')
return associnMany(
state,
('screen', 'CONSOLE'),
('buttons', [{**btn,
'show': btn['label_id'] in use_buttons,
'enabled': True}
for btn in state['buttons']]),
('image', state['images']['successIcon']),
('title', _('finished_title')),
('subtitle', _('finished_msg')),
('progress.show', False),
('timing.show', not state['timing_options']['hide_time_remaining_on_complete']))
def successScreen(_: Callable[[str], str], state: GooeyState) -> GooeyState:
return associnMany(
finalScreen(_, state),
('image', state['images']['successIcon']),
('title', _('finished_title')),
('subtitle', _('finished_msg')))
def errorScreen(_: Callable[[str], str], state: GooeyState) -> GooeyState:
return associnMany(
finalScreen(_, state),
('image', state['images']['errorIcon']),
('title', _('finished_title')),
('subtitle', _('finished_error')))
def interruptedScreen(_: Callable[[str], str], state: GooeyState):
next_state = errorScreen(_, state) if state['force_stop_is_error'] else successScreen(_, state)
return assoc(next_state, 'subtitle', _('finished_forced_quit'))
def updateProgress(state, event: ProgressEvent):
return associn(state, ['progress', 'value'], event['progress'] or 0)
def updateTime(state, event):
return associnMany(
state,
('timing.elapsed_time', event['elapsed_time']),
('timing.estimatedRemaining', event['estimatedRemaining'])
)
def update_time(state, event: TimingEvent):
return {
**state,
'timer': {
**state['timer'],
'elapsed_time': event['elapsed_time'],
'estimatedRemaining': event['estimatedRemaining']
}
}
def present_time(timer):
estimate_time_remaining = timer['estimatedRemaining']
elapsed_time_value = timer['elapsed_time']
if elapsed_time_value is None:
return ''
elif estimate_time_remaining is not None:
return f'{elapsed_time_value}<{estimate_time_remaining}'
else:
return f'{elapsed_time_value}'