Initial code commit

This commit is contained in:
2025-08-05 01:10:28 +01:00
parent 8052597e84
commit 8fa467cb93
7 changed files with 1073 additions and 1 deletions

111
README.md
View File

@@ -1,2 +1,111 @@
# gig
# gig.py
./gig.py [-w] show.yaml
Use the -w flag to run in regular windows rather than full screen.
## YAML
Define your set with:
set:
- item1
- item2
- item3
Define your preshow with:
preshow:
- item4
Define your items:
items:
item1:
title: Title
events:
- Display:
media: File.jpg
at: select
- Lyrics:
text: Text
at: start
## Time markers
Events are scheduled to happen at times. Times can be specified in terms of markers, and modified by bars, beats and hours/minutes/seconds.
There are two predefined markers:
* select - defined as the time the item is selected. This is typically used for an introduction to a piece.
* start - defined as the time the item is started. This is typically when backing tracks are played.
Other markers can be defined for convenience.
Every time specification must include at least one marker. The marker may be modified using addition or subtraction.
The following expressions can be combined:
* `<number> bars` - number of bars at the current tempo. The keywords `bar` and `bars` are both accepted.
* `<number> beats` - number of beats at the current tempo. The keywords `beat` and `beats` are both accepted.
* `<number>` - number of seconds
* `<number>:<number>` - number of minutes and seconds
* `<number>:<number>:<number>` - number of hours, minutes and seconds
Examples:
at: start + 4 bars - 1 beat
at: select + 1:23
## Event types
### Display
Displays image for the audience
(not yet implemented)
* at - time to display the image
* media - filename of the image
### Lyrics
Display lyrics for the audience
* at - time to display the text
* text - text to display
* duration - (not yet implemented)
### Playback
Play audio
* at - time to display the text
* media - filename to play
* device - output device to use (optional)
* channels - output channel map (optional)
* tempo - initial tempo (bpm) (optional)
* beats - initials beats in a bar (optional)
* leadin - time in seconds before the first beat (optional) (not yet implemented)
* loop - (True/False) loop at end of playback (optional) (not yet implemented)
### SetTempo
Set the tempo at a given time
* at - time at which to set the tempo
* tempo - new tempo (bpm) (optional)
* beats - new beats in a bar (optional)
### SetMark
Set time marker
* at - time to set the marker to
* name - name of marker
### ButtonMark
Set time marker based on external keypress
* name - name of marker

31
display.py Executable file
View File

@@ -0,0 +1,31 @@
# display.py
import pyglet
class DisplayContent:
def __init__(self):
pass
def create(self, obj, width, height, x, y):
obj.audiencebatch = pyglet.graphics.Batch()
obj.audiencecontent = []
class DisplayLyrics(DisplayContent):
def __init__(self, text):
self._text = text
def create(self, obj, width, height, x, y):
scale = height / 600
super().create(obj, width, height, x, y)
obj.audiencecontent.append(pyglet.text.Label(self._text,
font_name='Ariel',
font_size=36 * scale,
x=x + width//2, y=y + height//2,
anchor_x='center', anchor_y='center',
width=width, align='center',
multiline=True,
batch=obj.audiencebatch))

155
events.py Normal file
View File

@@ -0,0 +1,155 @@
# events.py
import soundfile
import sounddevice
import timings
import display
import numpy as np
class Event:
def __init__(self, at='start'):
self._at = timings.Timing(at)
def set_timings(self, timings):
self._at.set_timings(timings)
def set_audiencecontent(self, audiencecontent):
self.audiencecontent = audiencecontent
@property
def scheduled(self):
return self._at.scheduled
@scheduled.setter
def scheduled(self, value):
self._at.scheduled = value
@property
def at(self):
return self._at.at
def prepare(self):
pass
def start(self):
pass
def stop(self):
pass
class Display(Event):
def __init__(self, media, *args, **kwargs):
super().__init__(*args, **kwargs)
self._media = media
class Playback(Event):
def __init__(self, media, device=None, channels=[1,2], tempo=None, beats=None, leadin=0, loop=False, *args, **kwargs):
super().__init__(*args, **kwargs)
self._media = media
self._device = device
self._mapping = np.atleast_1d(np.array(channels, copy=True))
self._channels = self._mapping.max()
self._mapping -= 1
self._tempo = tempo
self._beats = beats
self._leadin = leadin
self._loop = loop
def prepare(self):
self._at._timings.register_tempo(self._tempo, self._beats, self._at)
# def start(self):
# print("Start playback")
# data, fs = soundfile.read(self._media)
# sounddevice.play(data, fs, device=self._device, mapping=self._mapping)
def start(self):
print("Start playback")
self._data, fs = soundfile.read(self._media)
self._current_frame = 0
self._stream = sounddevice.OutputStream(
samplerate=fs, device=self._device, channels=self._channels,
callback=self.callback, finished_callback=self.finished_callback)
self._stream.start()
def callback(self, outdata, frames, time, status):
if status:
print(status)
chunksize = min(len(self._data) - self._current_frame, frames)
outdata[:chunksize, self._mapping] = self._data[self._current_frame:self._current_frame + chunksize]
if chunksize < frames:
outdata[chunksize:, self._mapping] = 0
raise sounddevice.CallbackStop()
self._current_frame += chunksize
def finished_callback(self):
self._data = None
print("Playback finished")
def stop(self):
print("Stop playback")
self._stream.stop(True)
self._stream.close(True)
self._stream = None
class SetTempo(Event):
def __init__(self, tempo=None, beats=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self._tempo = tempo
self._beats = beats
def prepare(self):
self._at._timings.register_tempo(self._tempo, self._beats, self._at)
def start(self):
pass
def stop(self):
pass
class SetMark(Event):
def __init__(self, name, *args, **kwargs):
super().__init__(*args, **kwargs)
self._name = name
def prepare(self):
self._at._timings.register_mark(self._name, self._at)
def start(self):
pass
def stop(self):
pass
class ButtonMark(Event):
def __init__(self, name, *args, **kwargs):
super().__init__(*args, **kwargs)
self._name = name
def prepare(self):
self._at._timings.register_mark(self._name, None)
def start(self):
pass
def stop(self):
pass
class Lyrics(Event):
def __init__(self, text, duration=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self._text = text
self._duration = duration
def start(self):
self.audiencecontent.new_content(display.DisplayLyrics(self._text))
print("Started displaying lyrics")
print(self._text)
def stop(self):
self.audiencecontent.new_content(display.DisplayContent())
print("Stopped displaying lyrics")

47
factory.py Normal file
View File

@@ -0,0 +1,47 @@
# factory.py
import events
from item import Item
class MakeItems:
def __new__(cls, config):
items = {}
for item in config:
kwargs = {}
for key in config[item].keys():
if key == 'events':
kwargs[key] = MakeEvents(config[item][key])
else:
kwargs[key] = config[item][key]
items[item] = Item(**kwargs)
if hasattr(items[item], 'prepare'):
items[item].prepare()
return items
class MakeEvents:
def __new__(cls, config):
objects = []
for x in config:
for key in x:
args = []
kwargs = {}
if isinstance(x[key], list):
args = x[key]
elif isinstance(x[key], dict):
kwargs = x[key]
elif x[key] is None:
pass
else:
args[0] = x[key]
obj = getattr(events, key)(*args, **kwargs)
objects.append(obj)
return objects
# This just returns an array for now, we'll see if we need to make it into a class later.
class MakeSet:
def __new__(cls, config, items):
for item in config:
if item not in items:
raise KeyError('{} not in items'.format(item))
return config

398
gig.py Executable file
View File

@@ -0,0 +1,398 @@
#!/usr/bin/python3
import argparse
import sounddevice
import soundfile
import pyglet
from pyglet.window import key
import factory
import display
import yaml
import time
import dbus
class EventRunner:
def __init__(self, gigconfig):
self._gigconfig = gigconfig
self._running = False
self._selected = None
self._playing = None
self._active = []
self._currentitem = None
def set_audiencecontent(self, audiencecontent):
for item in self._gigconfig['items']:
self._gigconfig['items'][item].set_audiencecontent(audiencecontent)
def select(self, pos):
self._selected = pos
self._playing = None
self.activate('select')
def play(self):
self._playing = self._selected
self._selected = None
self.activate('start')
def advance(self):
if self._playing is not None:
return self.activate()
elif self._selected is not None:
self.play()
return True
return False
def activate(self, marker=None):
newitem = self.get_item()
if self._currentitem != newitem:
selected = self._selected
self.stop()
newitem.reset()
self._currentitem = newitem
if marker is None:
marker = 'select'
elif marker != 'select':
return False
self._selected = selected
now = time.time()
if not self._currentitem.activate(marker, now):
return False
for event in self._currentitem._events:
if event.at is not None and not event.scheduled:
pyglet.clock.schedule_once(self.update, event.at - now, event)
print("Scheduled event {} at {} ({} from now)".format(event, event.at, event.at - now))
event.scheduled = True
self._next_button = self._currentitem.get_next_button()
return True
def stop(self):
pyglet.clock.unschedule(self.update)
self._playing = None
self._selected = None
for event in self._active:
event.stop()
self._active = []
if self._currentitem is not None:
self._currentitem.reset()
def update(self, dt, event):
event.start()
self._active.append(event)
def pause(self):
pass
def get_item(self, pos=None):
if pos is None:
pos = self._playing
if pos is None:
pos = self._selected
if pos is None:
return None
item = self._gigconfig['setlist'][pos]
return self._gigconfig['items'][item]
def status(self):
if self._playing is not None:
status = 'Playing {}'.format(self.get_item().get_title())
if self._next_button is not None:
button = "Marker {}".format(self._next_button)
else:
button = "Select next"
elif self._selected is not None:
status = 'Selected {}'.format(self.get_item().get_title())
button = "Play"
else:
status = 'Waiting'
button = "Select"
return {
'selected' : self._selected,
'playing' : self._playing,
'running' : self._running,
'status' : status,
'button' : button,
}
class MainWindow(pyglet.window.Window):
def __init__(self, gigconfig, eventrunner, audiencecontent, *args, **kwargs):
super().__init__(*args, **kwargs)
self._gigconfig = gigconfig
self._eventrunner = eventrunner
self._audiencecontent = audiencecontent
self._audiencecontent.register(self)
self._cursorpos = 0
self._playpos = None
self._selectpos = None
self.on_resize(self.width, self.height)
self.update_cursor(0)
def on_resize(self, width, height):
self._scale = self.height / 600
self.batch = pyglet.graphics.Batch()
self.status = pyglet.text.Label('Status bar',
font_name='Ariel',
font_size=18 * self._scale,
x=self.width//2, y=0,
anchor_x='left', anchor_y='bottom',
batch=self.batch)
self.next_action = pyglet.text.Label('Next:',
font_name='Ariel',
font_size=18 * self._scale,
x=self.width//2, y=40 * self._scale,
anchor_x='left', anchor_y='bottom',
batch=self.batch)
self._spacing = 40 * self._scale
self._cursorspread = 300 * self._scale
self._cursorwidth = 15 * self._scale
self._cursorheight = 20 * self._scale
y_increment = self._spacing
y_counter = self.height - y_increment
# They're really buttons, but...
self.buttons = []
for item in self._gigconfig['setlist']:
self.buttons.append(pyglet.text.Label(self._gigconfig['items'][item].get_title(),
font_name='Ariel',
font_size=14 * self._scale,
width=self._cursorspread,
x=self.width//4, y=y_counter,
anchor_x='center', anchor_y='center',
batch=self.batch))
y_counter -= y_increment
self._cursor1x = self.width//4 - self._cursorspread/2
self._cursor2x = self.width//4 + self._cursorspread/2
self._cursory = self.height//2
self.cursor1 = pyglet.shapes.Triangle(self._cursor1x, self._cursory,
self._cursor1x-self._cursorwidth, self._cursory-self._cursorheight/2,
self._cursor1x-self._cursorwidth, self._cursory+self._cursorheight/2,
(255, 255, 255, 255),
batch=self.batch)
self.cursor2 = pyglet.shapes.Triangle(self._cursor2x, self._cursory,
self._cursor2x+self._cursorwidth, self._cursory-self._cursorheight/2,
self._cursor2x+self._cursorwidth, self._cursory+self._cursorheight/2,
(255, 255, 255, 255),
batch=self.batch)
self.update_objects()
self.audience_update()
def audience_update(self):
self.switch_to()
self._audiencecontent.create(self, self.width // 2, self.height // 2, x=self.width//2, y=self.height//4)
def update_objects(self):
status = self._eventrunner.status()
self._selectpos = status['selected']
self._playpos = status['playing']
self.set_status(status['status'])
self.set_next_action(status['button'])
self.update_cursor()
def update_cursor(self, pos=None):
if pos is not None:
self._cursorpos = pos
y_increment = self._spacing
y_counter = self.height // 2 + y_increment * self._cursorpos
for i, button in enumerate(self.buttons):
button.y = y_counter
if i == self._playpos:
button.color = (0, 255, 0, 255)
elif i == self._selectpos:
button.color = (255, 255, 0, 255)
else:
button.color = (255, 255, 255, 255)
y_counter -= y_increment
def select(self):
if self._playpos == self._cursorpos:
if not self._eventrunner.advance():
if self._cursorpos < len(self.buttons) - 1:
self.update_cursor(self._cursorpos + 1)
self._eventrunner.select(self._cursorpos)
elif self._selectpos == self._cursorpos:
self._eventrunner.play()
else:
self._eventrunner.select(self._cursorpos)
self.update_objects()
def set_status(self, text):
self.status.text = text
def set_next_action(self, text):
self.next_action.text = "Next: {}".format(text)
def action_up(self):
if self._cursorpos > 0:
self.update_cursor(self._cursorpos - 1)
def action_down(self):
if self._cursorpos < len(self.buttons) - 1:
self.update_cursor(self._cursorpos + 1)
def action_select(self):
self.select()
def action_stop(self):
if self._playpos is not None:
self._eventrunner.stop()
elif self._selectpos is not None:
self._eventrunner.stop()
else:
self.action_up()
self.update_objects()
def on_key_press(self, symbol, modifiers):
if symbol == key.UP:
self.action_up()
elif symbol == key.DOWN:
self.action_down()
elif symbol == key.ENTER:
self.action_select()
elif symbol == key.SPACE:
self.action_stop()
else:
super().on_key_press(symbol, modifiers)
def on_draw(self):
self.clear()
self.batch.draw()
self.audiencebatch.draw()
class AudienceContent:
def __init__(self, gigconfig, eventrunner):
self._gigconfig = gigconfig
self._eventrunner = eventrunner
self._objects = []
self._eventrunner.set_audiencecontent(self)
self._display = display.DisplayContent()
def register(self, obj):
self._objects.append(obj)
def update(self):
for obj in self._objects:
obj.audience_update()
def create(self, obj, width, height, x=0, y=0):
self._display.create(obj, width, height, x, y)
def new_content(self, obj):
self._display = obj
self.update()
class AudienceWindow(pyglet.window.Window):
def __init__(self, audiencecontent, *args, **kwargs):
super().__init__(*args, **kwargs)
self._audiencecontent = audiencecontent
self._audiencecontent.register(self)
self._audiencecontent.create(self, self.width, self.height)
def audience_update(self):
self.switch_to()
self._audiencecontent.create(self, self.width, self.height)
def on_resize(self, width, height):
print("New size is: {} x {}".format(width, height))
self._audiencecontent.create(self, width, height)
def on_draw(self):
self.clear()
self.audiencebatch.draw()
def process_config(config):
items = factory.MakeItems(config['items'])
setlist = factory.MakeSet(config['set'], items)
preshow = factory.MakeSet(config['preshow'], items)
if 'defaults' in config:
defaults = config['defaults']
else:
defaults = {}
return {'items' : items, 'setlist' : setlist, 'preshow' : preshow}
def load_config(filename):
with open(filename, encoding='utf8') as file:
y = yaml.safe_load(file)
config = process_config(y)
return config
def playback(filename):
data, fs = soundfile.read(filename)
#sounddevice.play(data, fs, device='alsa,hw:CARD=QU16')
sounddevice.play(data, fs, device='QU-16 alsa', mapping=[21,22])
sounddevice.wait()
def main():
parser = argparse.ArgumentParser(prog='gig.py', description='Gig showrunning')
parser.add_argument('filename')
parser.add_argument('-w', '--window', action='store_true', help='Run in a window')
args = parser.parse_args()
config = load_config(args.filename)
display = pyglet.display.get_display()
screens = display.get_screens()
# print(screens)
mainscreen = screens[0]
if len(screens) > 1:
audiencescreen = screens[1]
else:
audiencescreen = None
print(mainscreen)
print(audiencescreen)
eventrunner = EventRunner(gigconfig=config)
audiencecontent = AudienceContent(gigconfig=config, eventrunner=eventrunner)
if audiencescreen is not None:
window2 = AudienceWindow(audiencecontent=audiencecontent, fullscreen=False, screen=audiencescreen, resizable=True)
if not args.window:
window2.set_location(audiencescreen.x, audiencescreen.y)
window2.set_size(audiencescreen.width, audiencescreen.height)
window2.set_mouse_position(audiencescreen.x, audiencescreen.y)
window2.set_fullscreen(True)
#window2.set_location(audiencescreen.x, audiencescreen.y)
#event_logger = pyglet.window.event.WindowEventLogger()
#window2.push_handlers(event_logger)
window = MainWindow(gigconfig=config, eventrunner=eventrunner, audiencecontent=audiencecontent, fullscreen=False, screen=mainscreen, resizable=True)
if not args.window:
window.set_location(mainscreen.x, mainscreen.y)
window.set_size(mainscreen.width, mainscreen.height)
window.set_mouse_position(mainscreen.x, mainscreen.y)
window.set_fullscreen(True)
#window.set_location(mainscreen.x, mainscreen.y)
try:
# Let's set up some stuff to inhibit the screensaver
bus = dbus.SessionBus()
saver = bus.get_object('org.freedesktop.ScreenSaver', '/ScreenSaver')
saver_interface = dbus.Interface(saver, dbus_interface='org.freedesktop.ScreenSaver')
# now we can inhibit the screensaver
cookie=saver_interface.Inhibit("gig.py", "Don't blank the screen during the gig")
except Exception:
pass
pyglet.app.run()
if __name__ == "__main__":
main()

53
item.py Normal file
View File

@@ -0,0 +1,53 @@
# item.py
import timings
class Item:
def __init__(self, title, events):
self._title = title
self._events = events
self._timings = timings.Timings()
for event in self._events:
event.set_timings(self._timings)
event.prepare()
self.resolve()
def reset(self):
self._timings.reset()
self.resolve()
def resolve(self):
self._timings.resolve()
self._timings.resolve(final=True)
def eventkey(x):
x = x.at
if x is None:
return (1, x)
else:
return (0, x)
self._events.sort(key=eventkey)
# print("Item {}".format(self._title))
# for event in self._events:
# print("Event at time {}".format(event.at))
def activate(self, marker, time):
if marker is None:
marker = self._timings.get_button()
if marker is None:
return False
self._timings.set_marker(marker, time)
self.resolve()
return True
def get_next_button(self):
return self._timings.get_button()
def get_title(self):
return self._title
def get_timings(self):
return self._timings
def set_audiencecontent(self, audiencecontent):
for event in self._events:
event.set_audiencecontent(audiencecontent)

279
timings.py Normal file
View File

@@ -0,0 +1,279 @@
# timings.py
import re
class TimingError(Exception):
def __init__(self, message, at=None):
self.message = message
self.at = at
super().__init__(self.message)
def __str__(self):
return f'{self.message} in string "{self.at}"'
class Timings:
def __init__(self):
self._timings = []
self._markers = {}
self._marker_objects = {}
self._marker_buttons = []
self._tempo_objects = []
self._tempo = []
self._resolved = False
def reset(self):
self._markers = {}
self._resolved = False
for timing in self._timings:
timing.reset()
def set_marker(self, marker, time):
self._markers[marker] = time
def get_button(self):
for button in self._marker_buttons:
if button not in self._markers:
return button
return None
def add_timing(self, timing):
self._timings.append(timing)
self._resolved = False
def lookup(self, marker):
return self._markers[marker]
def evaluate(self, value, modifier, context):
if modifier in {'bar', 'bars'}:
return self.bar_at(context) * value
elif modifier in {'beat', 'beats'}:
return self.beat_at(context) * value
else:
raise ValueError('Invalid modifier: {}'.format(modifier))
def tempo_at(self, at):
# Some nice defaults. Maybe these should live elsewhere?
tempo = 120
beats = 4
if at is not None:
for (t, b, tat) in self._tempo:
if tat is not None and tat <= at:
tempo = t
beats = b
#print("tempo_at({}) = ({}, {})".format(at, tempo, beats))
return (tempo, beats)
def bar_at(self, at):
(tempo, beats) = self.tempo_at(at)
#print("bar_at({}) = {}".format(at, 60 * beats / tempo))
return 60 * beats / tempo
def beat_at(self, at):
(tempo, beats) = self.tempo_at(at)
#print("beat_at({}) = {}".format(at, 60 / tempo))
return 60 / tempo
def register_mark(self, name, at):
self._marker_objects[name] = at
if at is None:
self._marker_buttons.append(name)
self._resolved = False
def register_tempo(self, tempo, beats, at):
self._tempo_objects.append((tempo, beats, at))
self._resolved = False
def resolve(self, final=False):
try:
def namespaced_key(x):
if x is None:
return (1, x)
else:
return (0, x)
self._tempo_objects.sort(key=lambda x: namespaced_key(x[2].evaluate()))
self._tempo = [(t, b, a.evaluate()) for (t, b, a) in self._tempo_objects]
for name in self._marker_objects.keys():
if self._marker_objects[name] is not None:
self._markers[name] = self._marker_objects[name].evaluate()
self._timings.sort(key=lambda x: namespaced_key(x.evaluate()))
except Exception:
if final:
raise
self._resolved = True
class BinOp:
def __init__(self, op, a=None, b=None):
self.op = op
self.a = a
self.b = b
def combine(self, b):
if self.b is not None:
raise TimingError('Binary operation already has a rhs')
self.b = b
def __str__(self):
return 'BinOp({} {} {})'.format(str(self.a), self.op, str(self.b))
class ImmediateNumber:
def __init__(self, n):
self._n = [float(n)]
self._modifier = None
def add_part(self, n):
if len(self._n) >= 2:
raise TimingError('Too many parts in HH:MM:SS')
if self._modifier is not None:
raise TimingError('Can\'t modify HH:MM:SS with bar/beats')
self._n.append(float(n))
def add_modifier(self, modifier):
if len(self._n) > 1:
raise TimingError('bar/beat modifier can not be applied to HH:MM:SS')
if self._modifier is not None:
raise TimingError('bar/beat modifier can not be applied twice to the same number')
self._modifier = modifier
def type(self):
if self._modifier is not None:
return self._modifier
else:
return 'time'
def value(self):
if self._modifier is not None:
return (self._n[0], self._modifier)
else:
value = 0
for n in self._n:
value *= 60
value += n
return value
def __str__(self):
if self._modifier is not None:
return 'ImmediateNumber({} {})'.format(':'.join(self._n), self._modifier)
else:
return 'ImmediateNumber({})'.format(':'.join(self._n))
class Timing:
def __init__(self, at):
self._at = str(at)
self._at_cached = None
self._scheduled = False
self.parse()
def reset(self):
self._at_cached = None
self._scheduled = False
@property
def scheduled(self):
return self._scheduled
@scheduled.setter
def scheduled(self, value):
self._scheduled = value
def set_timings(self, timings):
self._timings = timings
self._timings.add_timing(self)
def parse(self):
def collapse(stack):
if len(stack) == 0:
return stack
if len(stack) > 2:
raise TimingError('Invalid expression')
if isinstance(stack[0], BinOp):
if stack[1] == ':':
raise TimingError('Invalid expression')
stack[0].combine(stack[1])
stack.pop(1)
if stack[0] == ':':
raise TimingError('Invalid expression')
if len(stack) > 1:
raise TimingError('Invalid expression')
return stack
modifiers = {'bar', 'bars', 'beat', 'beats'}
operators = {'+', '-'}
tokens = self.tokenise()
stack = []
try:
for token in tokens:
if token in operators:
a = collapse(stack)
if len(a) != 1:
raise TimingError('Binary operation must have valid lhs')
op = BinOp(token, a=a[0])
stack = [op]
elif token in modifiers:
if len(stack) < 1:
raise TimingError('Modifier without value', self._at)
if isinstance(stack[-1], ImmediateNumber):
stack[-1].add_modifier(token)
else:
raise TimingError('Modifier can only follow number', self._at)
elif re.match(r'\d+\.\d+|\d+', token):
if (len(stack) >= 2) and (stack[-1] == ':'):
if isinstance(stack[-2], ImmediateNumber):
stack[-2].add_part(token)
stack.pop()
else:
raise TimingError('HH:MM:SS must start with a number', self._at)
else:
stack.append(ImmediateNumber(token))
else:
stack.append(token)
stack = collapse(stack)
except TimingError as e:
e.at = self._at
raise
if len(stack) != 1:
raise TimingError('Invalid expression', self._at)
self._expr = stack[0]
def tokenise(self):
pattern = r'\d+\.\d+|\d+|\w+|[+\-:]'
#print(self._at)
tokens = re.findall(pattern, self._at)
tokens = [x for x in tokens if x != '']
#print(tokens)
return tokens
@property
def at(self):
if self._at_cached is None:
self._at_cached = self.evaluate()
return self._at_cached
def evaluate(self, tree=None, context=None):
if tree is None:
tree = self._expr
if isinstance(tree, BinOp):
a = self.evaluate(tree.a, context)
b = self.evaluate(tree.b, a)
if a is None:
return None
if b is None:
return None
if tree.op == '+':
return a + b
elif tree.op == '-':
return a - b
else:
raise ValueError('op must be + or -')
elif isinstance(tree, ImmediateNumber):
if tree.type() == 'time':
return tree.value()
else:
(value, modifier) = tree.value()
return self._timings.evaluate(value, modifier, context)
else:
try:
return self._timings.lookup(tree)
except KeyError:
return None