finished all of over except app, which will need a lot more attention

This commit is contained in:
Martinez 2016-05-13 14:44:12 +02:00
parent 45eb51550a
commit a4665ae277
13 changed files with 529 additions and 1206 deletions

View file

@ -3,16 +3,24 @@
import sys import sys
#from . import app
from . import aux from . import aux
from . import cmd
from . import file
from . import misc
from . import text from . import text
from . import version
print = text.Output("over.__init__", stream=sys.stderr)
try: try:
from . import cython_types as types from . import cython_types as types
except: except:
aux._print('unable to load C implementation, using python instead', text.prefix.warn)
from . import python_types as types from . import python_types as types
core = aux.DeprecationForwarder(sys.modules[__name__], 'over.core', 'over') print("unable to load C implementation, using python instead", print.tl.warn)
textui = aux.DeprecationForwarder(text, 'over.core.textui', 'over.text')
core = aux.DeprecationForwarder(sys.modules[__name__], "over.core", "over")
textui = aux.DeprecationForwarder(text, "over.core.textui", "over.text")
del sys del sys

View file

@ -6,9 +6,9 @@ import traceback
from . import text from . import text
_print = text.Output('over.core', stream=sys.stderr)
class DeprecationForwarder: class DeprecationForwarder:
print = text.Output("over.aux.DeprecationForwarder", stream=sys.stderr)
def __init__(self, target, old_name, new_name): def __init__(self, target, old_name, new_name):
self._target = target self._target = target
self._old_name = old_name self._old_name = old_name
@ -16,5 +16,6 @@ class DeprecationForwarder:
def __getattr__(self, name): def __getattr__(self, name):
caller = traceback.extract_stack()[-2] caller = traceback.extract_stack()[-2]
_print('%s is deprecated, please use %s instead (%s:%d)' %(self._old_name, self._new_name, caller[0], caller[1]), text.prefix.warn) self.print("%s is deprecated, please use %s instead (%s:%d)" %(self._old_name, self._new_name, caller[0], caller[1]), self.print.tl.warn)
return getattr(self._target, name) return getattr(self._target, name)

View file

@ -27,27 +27,27 @@ def char_in_str(chars, string):
return False return False
class Command: class Command:
''' """
A shell command with argument substitution and output capture. A shell command with argument substitution and output capture.
>>> c = Command('process.sh', '-x', 'ARGUMENT') >>> c = Command("process.sh", "-x", "ARGUMENT")
>>> c.ARGUMENT = 'file.txt' >>> c.ARGUMENT = "file.txt"
>>> c.dump() >>> c.dump()
['process.sh', '-x', 'file.txt'] ["process.sh", "-x", "file.txt"]
>>> c.run(stderr=False) # capture stdout >>> c.run(stderr=False) # capture stdout
>>> c.get_output() >>> c.get_output()
b'some output' b"some output"
>>> c.get_output() >>> c.get_output()
b'' # there was no output since the last call b"" # there was no output since the last call
>>> c.get_output() >>> c.get_output()
b'more of it\nand some more' b"more of it\nand some more"
>>> c.get_output() >>> c.get_output()
None # indicates the process ended and there is no more output None # indicates the process ended and there is no more output
''' """
def __init__(self, *sequence): def __init__(self, *sequence):
self.__dict__['sequence_original'] = list(sequence) self.__dict__["sequence_original"] = list(sequence)
self.reset() self.reset()
def __setattr__(self, name, value): def __setattr__(self, name, value):
@ -59,14 +59,14 @@ class Command:
found = True found = True
if not found: if not found:
raise AttributeError('Command has no attribute \'%s\'' %(name)) raise AttributeError('Command has no attribute "%s"' %(name))
def reset(self): def reset(self):
self.__dict__['sequence'] = list(self.sequence_original) self.__dict__["sequence"] = list(self.sequence_original)
self.__dict__['thread'] = None self.__dict__["thread"] = None
self.__dict__['fifo'] = None self.__dict__["fifo"] = None
self.__dict__['terminated'] = False self.__dict__["terminated"] = False
self.__dict__['returncode'] = None self.__dict__["returncode"] = None
def dump(self, sequence=None, pretty=False): def dump(self, sequence=None, pretty=False):
out = [] out = []
@ -88,30 +88,30 @@ class Command:
return out return out
def run(self, stderr=False): def run(self, stderr=False):
''' """
Executes the command in the current environment. Executes the command in the current environment.
stderr capture stderr instead of stdout (can't do both yet) stderr capture stderr instead of stdout (can't do both yet)
''' """
self.__dict__['process'] = subprocess.Popen(self.dump(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=1) self.__dict__["process"] = subprocess.Popen(self.dump(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=1)
self.__dict__['fifo'] = queue.Queue() self.__dict__["fifo"] = queue.Queue()
self.__dict__['thread'] = threading.Thread( self.__dict__["thread"] = threading.Thread(
target=capture_output, target=capture_output,
args=(self.process.stderr if stderr else self.process.stdout, self.fifo) args=(self.process.stderr if stderr else self.process.stdout, self.fifo)
) )
self.__dict__['terminated'] = False self.__dict__["terminated"] = False
self.thread.daemon = True # thread dies with the program self.thread.daemon = True # thread dies with the program
self.thread.start() self.thread.start()
def get_output(self, blocking=False): def get_output(self, blocking=False):
''' """
Returns the output of a currently running process and clears the buffer. Returns the output of a currently running process and clears the buffer.
Returns None if no process is running and no more output is available. Returns None if no process is running and no more output is available.
blocking block until some output is available or the process terminates blocking block until some output is available or the process terminates
''' """
buffer = [] buffer = []
@ -125,8 +125,8 @@ class Command:
buffer.append(self.fifo.get_nowait()) # FIXME nowait needed? buffer.append(self.fifo.get_nowait()) # FIXME nowait needed?
if None in buffer: if None in buffer:
self.__dict__['terminated'] = True self.__dict__["terminated"] = True
self.__dict__['returncode'] = self.process.poll() self.__dict__["returncode"] = self.process.poll()
if len(buffer) == 1: if len(buffer) == 1:
return None return None
@ -134,7 +134,7 @@ class Command:
assert(buffer[-1] is None) assert(buffer[-1] is None)
del buffer[-1] del buffer[-1]
return b''.join(buffer) return b"".join(buffer)
def get_all_output(self): def get_all_output(self):
buffer = [] buffer = []
@ -147,4 +147,4 @@ class Command:
else: else:
buffer.append(chunk) buffer.append(chunk)
return b''.join(buffer) if buffer else None return b"".join(buffer) if buffer else None

View file

@ -1,105 +1,32 @@
#! /usr/bin/env python3 #! /usr/bin/env python3
# encoding: utf-8 # encoding: utf-8
from collections import OrderedDict
# -------------------------------------------------- # --------------------------------------------------
cdef class ndict(dict): cdef class ndict(OrderedDict):
''' """
A dictionary superclass whose keys are exposed as attributes. An OrderedDict subclass whose keys are exposed as attributes.
>>> d = ndict() >>> d = ndict()
>>> d.alpha = 1 >>> d.alpha = 1
>>> d['alpha'] >>> d["alpha"]
1 1
>>> d['beta'] = 42 >>> d["beta"] = 42
>>> d.beta >>> d.beta
42 42
>>> d >>> d
{'alpha': 1, 'beta': 42} {"alpha": 1, "beta": 42}
''' """
def __getattr__(self, str name): def __getattr__(self, str name):
if name in self: if name in self:
return self[name] return self[name]
elif name.replace('_', '-') in self: elif name.replace("_", "-") in self:
return self[name.replace('_', '-')] return self[name.replace("_", "-")]
else: else:
raise AttributeError("'ndict' object has no attribute '%s'" %(name)) raise AttributeError('"ndict" object has no attribute "%s"' %(name))
def __setattr__(self, str name, value): def __setattr__(self, str name, value):
self[name] = value self[name] = value
# --------------------------------------------------
class map:
'''
An ordered dict.
'''
def __init__(self, source=None):
'''
Initialize the map.
The source is a sequence of (key, value) tuples.
'''
if source:
self.keys, self.vals = zip(*source)
else:
self.keys = []
self.vals = []
def __get_index(self, key):
if key in self.keys:
return self.keys.index(key)
else:
return None
def __getitem__(self, key):
i = self.__get_index(key)
if i is None:
raise KeyError(key)
else:
return self.vals[i]
def __setitem__(self, key, val):
i = self.__get_index(key)
if i is None:
self.keys.append(key)
self.vals.append(val)
else:
self.vals[i] = val
def __contains__(self, item):
return item in self.keys
def index(self, item):
return self.keys.index(item)
def sort(self, key=None):
tmp_keys = self.keys
tmp_vals = self.vals
self.keys = []
self.vals = []
for K, V in sorted(zip(tmp_keys, tmp_vals), key=key):
self.keys.append(K)
self.vals.append(V)
@property
def items(self):
return zip(self.keys, self.vals)
def __len__(self):
return len(self.vals)
def __repr__(self):
pairs = []
for i in range(len(self.keys)):
pairs.append('%s: %s' %(repr(self.keys[i]), repr(self.vals[i])))
return '<{%s}>' %(', '.join(pairs))

View file

@ -3,101 +3,33 @@
import os import os
from .aux import _print # --------------------------------------------------
from .text import prefix
def touch(path, times=None):
"""
Sets a `path`'s atime and mtime.
`times` is a tuple of (atime, mtime) and defaults to "now".
"""
path = os.path.expanduser(path)
with open(path, "a"):
os.utime(path, times)
# -------------------------------------------------- # --------------------------------------------------
class File: def count_lines(path):
''' """
A binary r/w file container that abstracts file descriptors away. You just read and write data.
Nonexistent files will be created, including any directories if necessary.
'''
def __init__(self, path, encoding=None):
'''
encoding which encoding to use when opening files; None means binary (raw)
'''
self.encoding = encoding
if path[0] == '~':
self.path = os.path.join(os.getenv('HOME'), path[2:])
else:
self.path = path
if not os.path.isfile(self.path):
if os.path.exists(self.path):
_print('path §y%s§/ exists but §ris not a file§/' %(self.path), prefix.fail)
raise RuntimeError
else:
dirname = os.path.dirname(self.path)
if dirname and not os.path.isdir(dirname):
_print('creating directory §B%s§/' %(dirname))
os.makedirs(dirname)
# create the file
touch(self.path)
@property
def data(self):
'''
Reads the file and returns the contents.
'''
if self.encoding:
fd = open(self.path, encoding=self.encoding)
else:
fd = open(self.path, 'rb')
data = fd.read()
fd.close()
return data
@data.setter
def data(self, data):
'''
Writes data into the file.
'''
if self.encoding:
fd = open(self.path, 'w', encoding=self.encoding)
else:
fd = open(self.path, 'wb')
fd.write(data)
fd.close()
def __repr__(self):
return 'over.core.File(%s %s)' %(self.path, self.encoding if self.encoding else 'raw')
# --------------------------------------------------
def touch(fname, times=None):
'''
Sets a filename's atime and mtime.
times is a tuple of (atime, mtime) and defaults to 'now'.
'''
with open(fname, 'a'):
os.utime(fname, times)
# --------------------------------------------------
def count_lines(filename):
'''
A reasonably fast and memory-lean line counter. A reasonably fast and memory-lean line counter.
''' """
lines = 0 lines = 0
buffer = bytearray(2048) buffer = bytearray(int(2**20))
path = os.path.expanduser(path)
with open(filename, 'rb') as f: with open(path, "rb") as f:
while f.readinto(buffer): while f.readinto(buffer):
lines += buffer.count(b'\n') lines += buffer.count(b"\n")
return lines return lines

View file

@ -1,28 +0,0 @@
#! /usr/bin/env python3
# encoding: utf-8
#
# Part of Project Overwatch
import sys
try:
from .cython_m import mat4, vec3
except ImportError:
print('!!! [%s] unable to load native implementation, using python instead' %(__name__), file=sys.stderr)
from .python_m import mat4, vec3
del sys
class MathError(Exception):
def __init__(self, description):
self.description = description
def __str__(self):
return self.description
class GeneralError(Exception):
def __init__(self, description):
self.description = description
def __str__(self):
return self.description

View file

@ -1,473 +0,0 @@
#! /bin/env python3
# encoding: utf-8
'''
Vector And Matrix Math
TODO some nice description
'''
from libc.stdlib cimport malloc, realloc, free
from libc.stdint cimport uint8_t, uint16_t, uint64_t
from libc.math cimport sin, cos, sqrt
class MathError(Exception):
def __init__(self, description):
self.description = description
def __str__(self):
return self.description
class GeneralError(Exception):
def __init__(self, description):
self.description = description
def __str__(self):
return self.description
cdef class mat4:
'''
A float 4x4 matrix.
All arrays are column-major, i.e. OpenGL style:
0 4 8 12
1 5 9 13
2 6 10 14
3 7 11 15
The matrix implements stacking useful for graphics.
'''
def __cinit__(mat4 self):
to_alloc = 16 * sizeof(float)
self.stack = <float *>malloc(to_alloc)
if not self.stack:
raise MemoryError('Unable to malloc %d B for mat4.' %(to_alloc))
self.m = self.stack
self.size = 1
def _debug(mat4 self):
print('--- self.stack = %d' %(<uint64_t>self.stack))
print('--- self.m = %d (+%d)' %(<uint64_t>self.m, self.m - self.stack))
print('--- self.size = %d' %(self.size))
def __init__(mat4 self, *args):
'''
Create a ma4t.
Accepts any number of parameters between 0 and 16 to fill the
matrix from the upper left corner going down (column-wise).
'''
length = len(args)
if length == 1 and isinstance(args[0], (list, tuple)):
args = args[0]
length = len(args)
if length > 16:
raise MathError('Attempt to initialize a mat4 with %d arguments.' %(length))
self.load_from(args)
def __dealloc__(mat4 self):
free(self.stack)
def __getstate__(mat4 self):
state = []
for i in range(self.m - self.stack + 16):
state.append(self.stack[i])
return state
def __setstate__(mat4 self, state):
length = len(state)
matrices = length//16
if not matrices*16 == length:
raise GeneralError('mat4 __setstate__ got %d floats as a state' %(length))
self.m = self.stack
slot_full = False
for start in range(0, length, 16):
if slot_full:
self.push()
slot_full = False
self.load_from(state[start:start+16])
slot_full = True
def __getitem__(mat4 self, int i):
if i > 16 or i < 0:
raise IndexError('element index out of range(16)')
return self.m[i]
def __setitem__(self, int i, value):
if i > 16 or i < 0:
raise IndexError('element index out of range(16)')
self.m[i] = value
def push(mat4 self):
'''
Push the current matrix into the stack and load up an empty one (a zero matrix)
'''
# self.m points to the current matrix
# self.stack points to the first matrix
# self.size how many matrices are allocated
# ensure there's room for one more
cdef unsigned int used = 1 + (self.m - self.stack) / 16
cdef unsigned int empty = self.size - used
cdef float *tmp
if not empty:
self.size += 1
to_alloc = self.size * 16 * sizeof(float)
tmp = <float *>realloc(self.stack, to_alloc)
if tmp:
self.stack = tmp
else:
raise MemoryError('Unable to malloc %d B for mat4.' %(to_alloc))
# advance the pointer to the new one
self.m = self.stack + 16 * used
# at this point there's at least enough space for one matrix
# copy the old matrix into the new one
cdef uint8_t i
cdef float *old_m = self.m - 16
for i in range(16):
self.m[i] = old_m[i]
def pop(mat4 self):
'''
Pop a matrix from the stack.
'''
if self.m == self.stack:
raise IndexError('pop from an empty stack')
self.m -= 16
def get_list(mat4 self):
L = []
for i in range(16):
L.append(self.m[i])
return L
def load_from(mat4 self, L):
'''
Fill the current matrix from a either a list of values, column-major,
or another matrix. This method doesn't modify the stack, only the
current matrix is read and modified.
If the number of values isn't 16, it will be padded to 16 by zeros.
If it's larger, GeneralError will be raised.
'''
if isinstance(L, mat4):
L = L.get_list()
length = 16
else:
length = len(L)
if length > 16:
raise GeneralError('supplied list is longer than 16')
for i in range(16):
if i < length:
self.m[i] = L[i]
else:
self.m[i] = 0.0
def zero(mat4 self):
'''Fill the matrix with zeroes.'''
for i in range(16):
self.m[i] = 0.0
def identity(mat4 self):
'''Make the matrix an identity.'''
self.zero()
self.m[0] = 1.0
self.m[5] = 1.0
self.m[10] = 1.0
self.m[15] = 1.0
def transpose(mat4 self):
'''Transpose the matrix.'''
cdef float tmp
tmp = self.m[1]
self.m[1] = self.m[4]
self.m[4] = tmp
tmp = self.m[2]
self.m[2] = self.m[8]
self.m[8] = tmp
tmp = self.m[3]
self.m[3] = self.m[12]
self.m[12] = tmp
tmp = self.m[7]
self.m[7] = self.m[13]
self.m[13] = tmp
tmp = self.m[11]
self.m[11] = self.m[14]
self.m[14] = tmp
tmp = self.m[6]
self.m[6] = self.m[9]
self.m[9] = tmp
def invert(mat4 self):
'''Invert the matrix.'''
cdef float tmp[16]
cdef float det
tmp[0] = self.m[5]*self.m[10]*self.m[15] - self.m[5]*self.m[11]*self.m[14] - self.m[9]*self.m[6]*self.m[15] + self.m[9]*self.m[7]*self.m[14] + self.m[13]*self.m[6]*self.m[11] - self.m[13]*self.m[7]*self.m[10]
tmp[4] = -self.m[4]*self.m[10]*self.m[15] + self.m[4]*self.m[11]*self.m[14] + self.m[8]*self.m[6]*self.m[15] - self.m[8]*self.m[7]*self.m[14] - self.m[12]*self.m[6]*self.m[11] + self.m[12]*self.m[7]*self.m[10]
tmp[8] = self.m[4]*self.m[9]*self.m[15] - self.m[4]*self.m[11]*self.m[13] - self.m[8]*self.m[5]*self.m[15] + self.m[8]*self.m[7]*self.m[13] + self.m[12]*self.m[5]*self.m[11] - self.m[12]*self.m[7]*self.m[9]
tmp[12] = -self.m[4]*self.m[9]*self.m[14] + self.m[4]*self.m[10]*self.m[13] + self.m[8]*self.m[5]*self.m[14] - self.m[8]*self.m[6]*self.m[13] - self.m[12]*self.m[5]*self.m[10] + self.m[12]*self.m[6]*self.m[9]
det = self.m[0]*tmp[0] + self.m[1]*tmp[4] + self.m[2]*tmp[8] + self.m[3]*tmp[12]
# epsilon pulled straight out of Uranus
if det < 0.00005 and det > -0.00005:
print('det=%.1f' %(det))
return
tmp[1] = -self.m[1]*self.m[10]*self.m[15] + self.m[1]*self.m[11]*self.m[14] + self.m[9]*self.m[2]*self.m[15] - self.m[9]*self.m[3]*self.m[14] - self.m[13]*self.m[2]*self.m[11] + self.m[13]*self.m[3]*self.m[10]
tmp[5] = self.m[0]*self.m[10]*self.m[15] - self.m[0]*self.m[11]*self.m[14] - self.m[8]*self.m[2]*self.m[15] + self.m[8]*self.m[3]*self.m[14] + self.m[12]*self.m[2]*self.m[11] - self.m[12]*self.m[3]*self.m[10]
tmp[9] = -self.m[0]*self.m[9]*self.m[15] + self.m[0]*self.m[11]*self.m[13] + self.m[8]*self.m[1]*self.m[15] - self.m[8]*self.m[3]*self.m[13] - self.m[12]*self.m[1]*self.m[11] + self.m[12]*self.m[3]*self.m[9]
tmp[13] = self.m[0]*self.m[9]*self.m[14] - self.m[0]*self.m[10]*self.m[13] - self.m[8]*self.m[1]*self.m[14] + self.m[8]*self.m[2]*self.m[13] + self.m[12]*self.m[1]*self.m[10] - self.m[12]*self.m[2]*self.m[9]
tmp[2] = self.m[1]*self.m[6]*self.m[15] - self.m[1]*self.m[7]*self.m[14] - self.m[5]*self.m[2]*self.m[15] + self.m[5]*self.m[3]*self.m[14] + self.m[13]*self.m[2]*self.m[7] - self.m[13]*self.m[3]*self.m[6]
tmp[6] = -self.m[0]*self.m[6]*self.m[15] + self.m[0]*self.m[7]*self.m[14] + self.m[4]*self.m[2]*self.m[15] - self.m[4]*self.m[3]*self.m[14] - self.m[12]*self.m[2]*self.m[7] + self.m[12]*self.m[3]*self.m[6]
tmp[10] = self.m[0]*self.m[5]*self.m[15] - self.m[0]*self.m[7]*self.m[13] - self.m[4]*self.m[1]*self.m[15] + self.m[4]*self.m[3]*self.m[13] + self.m[12]*self.m[1]*self.m[7] - self.m[12]*self.m[3]*self.m[5]
tmp[14] = -self.m[0]*self.m[5]*self.m[14] + self.m[0]*self.m[6]*self.m[13] + self.m[4]*self.m[1]*self.m[14] - self.m[4]*self.m[2]*self.m[13] - self.m[12]*self.m[1]*self.m[6] + self.m[12]*self.m[2]*self.m[5]
tmp[3] = -self.m[1]*self.m[6]*self.m[11] + self.m[1]*self.m[7]*self.m[10] + self.m[5]*self.m[2]*self.m[11] - self.m[5]*self.m[3]*self.m[10] - self.m[9]*self.m[2]*self.m[7] + self.m[9]*self.m[3]*self.m[6]
tmp[7] = self.m[0]*self.m[6]*self.m[11] - self.m[0]*self.m[7]*self.m[10] - self.m[4]*self.m[2]*self.m[11] + self.m[4]*self.m[3]*self.m[10] + self.m[8]*self.m[2]*self.m[7] - self.m[8]*self.m[3]*self.m[6]
tmp[11] = -self.m[0]*self.m[5]*self.m[11] + self.m[0]*self.m[7]*self.m[9] + self.m[4]*self.m[1]*self.m[11] - self.m[4]*self.m[3]*self.m[9] - self.m[8]*self.m[1]*self.m[7] + self.m[8]*self.m[3]*self.m[5]
tmp[15] = self.m[0]*self.m[5]*self.m[10] - self.m[0]*self.m[6]*self.m[9] - self.m[4]*self.m[1]*self.m[10] + self.m[4]*self.m[2]*self.m[9] + self.m[8]*self.m[1]*self.m[6] - self.m[8]*self.m[2]*self.m[5]
det = 1.0 / det
self.m[0] = tmp[0] * det
self.m[1] = tmp[1] * det
self.m[2] = tmp[2] * det
self.m[3] = tmp[3] * det
self.m[4] = tmp[4] * det
self.m[5] = tmp[5] * det
self.m[6] = tmp[6] * det
self.m[7] = tmp[7] * det
self.m[8] = tmp[8] * det
self.m[9] = tmp[9] * det
self.m[10] = tmp[10] * det
self.m[11] = tmp[11] * det
self.m[12] = tmp[12] * det
self.m[13] = tmp[13] * det
self.m[14] = tmp[14] * det
self.m[15] = tmp[15] * det
def mulm(mat4 self, mat4 B, bint inplace=False):
'''
Return a matrix that is the result of multiplying this matrix by another.
M = self * mat4 B
'''
cdef uint8_t i
cdef mat4 tmp = mat4()
tmp.m[0] = self.m[0] * B.m[0] + self.m[4] * B.m[1] + self.m[8] * B.m[2] + self.m[12] * B.m[3]
tmp.m[1] = self.m[1] * B.m[0] + self.m[5] * B.m[1] + self.m[9] * B.m[2] + self.m[13] * B.m[3]
tmp.m[2] = self.m[2] * B.m[0] + self.m[6] * B.m[1] + self.m[10] * B.m[2] + self.m[14] * B.m[3]
tmp.m[3] = self.m[3] * B.m[0] + self.m[7] * B.m[1] + self.m[11] * B.m[2] + self.m[15] * B.m[3]
tmp.m[4] = self.m[0] * B.m[4] + self.m[4] * B.m[5] + self.m[8] * B.m[6] + self.m[12] * B.m[7]
tmp.m[5] = self.m[1] * B.m[4] + self.m[5] * B.m[5] + self.m[9] * B.m[6] + self.m[13] * B.m[7]
tmp.m[6] = self.m[2] * B.m[4] + self.m[6] * B.m[5] + self.m[10] * B.m[6] + self.m[14] * B.m[7]
tmp.m[7] = self.m[3] * B.m[4] + self.m[7] * B.m[5] + self.m[11] * B.m[6] + self.m[15] * B.m[7]
tmp.m[8] = self.m[0] * B.m[8] + self.m[4] * B.m[9] + self.m[8] * B.m[10] + self.m[12] * B.m[11]
tmp.m[9] = self.m[1] * B.m[8] + self.m[5] * B.m[9] + self.m[9] * B.m[10] + self.m[13] * B.m[11]
tmp.m[10] = self.m[2] * B.m[8] + self.m[6] * B.m[9] + self.m[10] * B.m[10] + self.m[14] * B.m[11]
tmp.m[11] = self.m[3] * B.m[8] + self.m[7] * B.m[9] + self.m[11] * B.m[10] + self.m[15] * B.m[11]
tmp.m[12] = self.m[0] * B.m[12] + self.m[4] * B.m[13] + self.m[8] * B.m[14] + self.m[12] * B.m[15]
tmp.m[13] = self.m[1] * B.m[12] + self.m[5] * B.m[13] + self.m[9] * B.m[14] + self.m[13] * B.m[15]
tmp.m[14] = self.m[2] * B.m[12] + self.m[6] * B.m[13] + self.m[10] * B.m[14] + self.m[14] * B.m[15]
tmp.m[15] = self.m[3] * B.m[12] + self.m[7] * B.m[13] + self.m[11] * B.m[14] + self.m[15] * B.m[15]
if inplace:
for i in range(16):
self.m[i] = tmp.m[i]
else:
return tmp
def mulv(mat4 self, vec3 v):
'''
Return a vec3 that is the result of multiplying this matrix by a vec3.
u = self * v
'''
cdef mat4 tmp = vec3()
tmp.v[0] = v.v[0]*self.m[0] + v.v[1]*self.m[4] + v.v[2]*self.m[8] + self.m[12]
tmp.v[1] = v.v[0]*self.m[1] + v.v[1]*self.m[5] + v.v[2]*self.m[9] + self.m[13]
tmp.v[2] = v.v[0]*self.m[2] + v.v[1]*self.m[6] + v.v[2]*self.m[10] + self.m[14]
return tmp
def mulf(mat4 self, f):
'''
Return a matrix that is the result of multiplying this matrix by a scalar.
M = self * f
'''
cdef mat4 tmp = mat4()
cdef int i
for i in range(16):
tmp.m[i] = self.m[i] * f
return tmp
def __repr__(mat4 self):
lines = []
lines.append('mat4(%.1f %.1f %.1f %.1f' %(self.m[0], self.m[4], self.m[8], self.m[12]))
lines.append(' %.1f %.1f %.1f %.1f' %(self.m[1], self.m[5], self.m[9], self.m[13]))
lines.append(' %.1f %.1f %.1f %.1f' %(self.m[2], self.m[6], self.m[10], self.m[14]))
lines.append(' %.1f %.1f %.1f %.1f)' %(self.m[3], self.m[7], self.m[11], self.m[15]))
return '\n'.join(lines)
cdef class vec3:
'''
A float 3D vector.
>>> v = vec3(1, 1, 0)
>>> w = vec3(0, 1, 1)
>>> v.length
1.4142135623730951
>>> v.dot(w)
1.0
>>> v.cross(w)
vec4(1.00, 1.00, 1.00)
>>> v + w
vec4(1.00, 2.00, 1.00)
>>> w - v
vec4(-1.00, 0.00, 1.00)
'''
def __init__(vec3 self, *args):
'''
Create a vec3.
Accepts any number of parameters between 0 and 3 to fill the vector from the left.
'''
length = len(args)
if length == 1 and isinstance(args[0], (list, tuple)):
args = args[0]
length = len(args)
if length > 3:
raise MathError('Attempt to initialize a vec3 with %d arguments.' %(length))
for i in range(3):
if i < length:
self.v[i] = args[i]
else:
self.v[i] = 0.0
def __getitem__(vec3 self, int i):
if i >= 3 or i < 0:
raise IndexError('element index out of range(3)')
return self.v[i]
def __setitem__(vec3 self, int i, float value):
if i >= 3 or i < 0:
raise IndexError('element index out of range(3)')
self.v[i] = value
def __repr__(vec3 self):
return 'vec3(%.2f, %.2f, %.2f)' %(self.v[0], self.v[1], self.v[2])
def __getstate__(vec3 self):
return (self.v[0], self.v[1], self.v[2])
def __setstate__(vec3 self, state):
self.v[0] = state[0]
self.v[1] = state[1]
self.v[2] = state[2]
@property
def length(vec3 self):
'''Contains the geometric length of the vector.'''
return sqrt(self.v[0]**2 + self.v[1]**2 + self.v[2]**2)
def normalized(vec3 self):
'''Returns this vector, normalized.'''
length = self.length
return vec3(self.v[0] / length, self.v[1] / length, self.v[2] / length)
def __add__(vec3 L, vec3 R):
return vec3(L.v[0] + R.v[0], L.v[1] + R.v[1], L.v[2] + R.v[2])
def __sub__(vec3 L, vec3 R):
return vec3(L.v[0] - R.v[0], L.v[1] - R.v[1], L.v[2] - R.v[2])
def __neg__(vec3 self):
return vec3(-self.v[0], -self.v[1], -self.v[2])
def dot(vec3 L, vec3 R):
'''
Returns the dot product of the two vectors.
E.g. u.dot(v) -> u . v
'''
return L.v[0] * R.v[0] + L.v[1] * R.v[1] + L.v[2] * R.v[2]
def cross(vec3 L, vec3 R):
'''
Returns the cross product of the two vectors.
E.g. u.cross(v) -> u x v
'''
return vec3(L.v[1]*R.v[2] - L.v[2]*R.v[1], L.v[0]*R.v[2] - L.v[2]*R.v[0], L.v[0]*R.v[1] - L.v[1]*R.v[0])
def __mul__(vec3 L, R):
'''
Multiplication of a vec3 by a float.
The float has to be on the right.
'''
return vec3(L.v[0] * R, L.v[1] * R, L.v[2] * R)

View file

@ -7,6 +7,10 @@ class compare_float:
""" """
def __init__(self, A, B, epsilon=1e-12): def __init__(self, A, B, epsilon=1e-12):
self.A = A
self.B = B
self.epsilon = epsilon
self.lt = False self.lt = False
self.le = True self.le = True
self.eq = False self.eq = False
@ -34,10 +38,13 @@ class compare_float:
self.greater = self.gt self.greater = self.gt
self.not_equal = self.ne self.not_equal = self.ne
def __repr__(self):
return "compare_float(%.2f, %.2f, epsilon=%.03e)" %(self.A, self.B, self.epsilon)
if __name__ == '__main__': if __name__ == '__main__':
x = compare_float(1, 2, 0.5) x = compare_float(1, 2, 0.5)
assert x.lt and x.less and not (x.le or x.ge) assert x.lt and x.less and not (x.gt or x.ge)
x = compare_float(2, 2, 0.5) x = compare_float(2, 2, 0.5)
assert x.eq and x.le and x.ge and not (x.lt or x.gt) assert x.eq and x.le and x.ge and not (x.lt or x.gt)
x = compare_float(3, 2, 0.5) x = compare_float(3, 2, 0.5)
assert x.gt and not (x.ge or x.eq or x.lt) assert x.gt and x.ge and not (x.le or x.eq or x.lt)

View file

@ -1,16 +1,16 @@
#! /usr/bin/env python3 #! /bin/env python3
# encoding: utf-8 # encoding: utf-8
''' """
Vector And Matrix Math Vector and Matrix Math
"""
Pure Python implementation. from libc.stdlib cimport malloc, realloc, free
''' from libc.stdint cimport uint8_t, uint16_t, uint64_t
from libc.math cimport sin, cos, sqrt
from math import sin, cos, sqrt cdef class mat4:
"""
class mat4:
'''
A float 4x4 matrix. A float 4x4 matrix.
All arrays are column-major, i.e. OpenGL style: All arrays are column-major, i.e. OpenGL style:
@ -21,32 +21,30 @@ class mat4:
3 7 11 15 3 7 11 15
The matrix implements stacking useful for graphics. The matrix implements stacking useful for graphics.
''' """
def __cinit__(mat4 self): def __cinit__(mat4 self):
to_alloc = 16 * sizeof(float) to_alloc = 16 * sizeof(float)
self.stack = <float *>malloc(to_alloc) self.stack = <float *>malloc(to_alloc)
if not self.stack: if not self.stack:
raise MemoryError('Unable to malloc %d B for mat4.' %(to_alloc)) raise MemoryError("Unable to malloc %d B for mat4." %(to_alloc))
self.m = self.stack self.m = self.stack
self.size = 1 self.size = 1
def _debug(mat4 self): def _debug(mat4 self):
print('--- self.stack = %d' %(<uint64_t>self.stack)) print("--- self.stack = %d" %(<uint64_t>self.stack))
print('--- self.m = %d (+%d)' %(<uint64_t>self.m, self.m - self.stack)) print("--- self.m = %d (+%d)" %(<uint64_t>self.m, self.m - self.stack))
print('--- self.size = %d' %(self.size)) print("--- self.size = %d" %(self.size))
def __init__(mat4 self, *args): def __init__(mat4 self, *args):
''' """
Create a ma4t. Create a ma4t.
Accepts any number of parameters between 0 and 16 to fill the Accepts any number of parameters between 0 and 16 to fill the
matrix from the upper left corner going down (column-wise). matrix from the upper left corner going down (column-wise).
''' """
self.m =
length = len(args) length = len(args)
@ -55,7 +53,7 @@ class mat4:
length = len(args) length = len(args)
if length > 16: if length > 16:
raise MathError('Attempt to initialize a mat4 with %d arguments.' %(length)) raise ValueError("Attempt to initialize a mat4 with %d arguments." %(length))
self.load_from(args) self.load_from(args)
@ -75,7 +73,7 @@ class mat4:
matrices = length//16 matrices = length//16
if not matrices*16 == length: if not matrices*16 == length:
raise GeneralError('mat4 __setstate__ got %d floats as a state' %(length)) raise ValueError("mat4 __setstate__ got %d floats as a state" %(length))
self.m = self.stack self.m = self.stack
@ -90,20 +88,22 @@ class mat4:
def __getitem__(mat4 self, int i): def __getitem__(mat4 self, int i):
if i > 16 or i < 0: if i > 16 or i < 0:
raise IndexError('element index out of range(16)') raise IndexError("element index out of range(16)")
return self.m[i] return self.m[i]
def __setitem__(self, int i, value): def __setitem__(self, int i, value):
if i > 16 or i < 0: if i > 16 or i < 0:
raise IndexError('element index out of range(16)') raise IndexError("element index out of range(16)")
self.m[i] = value self.m[i] = value
def push(mat4 self): def push(mat4 self):
''' """
Push the current matrix into the stack and load up an empty one (a zero matrix) Push the current matrix into the stack and load up an empty one (a zero matrix).
'''
TODO consider copying the matrix instead?
"""
# self.m points to the current matrix # self.m points to the current matrix
# self.stack points to the first matrix # self.stack points to the first matrix
@ -122,12 +122,12 @@ class mat4:
if tmp: if tmp:
self.stack = tmp self.stack = tmp
else: else:
raise MemoryError('Unable to malloc %d B for mat4.' %(to_alloc)) raise MemoryError("Unable to malloc %d B for mat4." %(to_alloc))
# advance the pointer to the new one # advance the pointer to the new one
self.m = self.stack + 16 * used self.m = self.stack + 16 * used
# at this point there's at least enough space for one matrix # at this point there"s at least enough space for one matrix
# copy the old matrix into the new one # copy the old matrix into the new one
cdef uint8_t i cdef uint8_t i
cdef float *old_m = self.m - 16 cdef float *old_m = self.m - 16
@ -135,12 +135,12 @@ class mat4:
self.m[i] = old_m[i] self.m[i] = old_m[i]
def pop(mat4 self): def pop(mat4 self):
''' """
Pop a matrix from the stack. Pop a matrix from the stack.
''' """
if self.m == self.stack: if self.m == self.stack:
raise IndexError('pop from an empty stack') raise IndexError("pop from an empty stack")
self.m -= 16 self.m -= 16
@ -153,14 +153,14 @@ class mat4:
return L return L
def load_from(mat4 self, L): def load_from(mat4 self, L):
''' """
Fill the current matrix from a either a list of values, column-major, Fill the current matrix from a either a list of values, column-major,
or another matrix. This method doesn't modify the stack, only the or another matrix. This method doesn't modify the stack, only the
current matrix is read and modified. current matrix is read and modified.
If the number of values isn't 16, it will be padded to 16 by zeros. If the number of values isn"t 16, it will be padded to 16 by zeros.
If it's larger, GeneralError will be raised. If it's larger, GeneralError will be raised.
''' """
if isinstance(L, mat4): if isinstance(L, mat4):
L = L.get_list() L = L.get_list()
@ -169,7 +169,7 @@ class mat4:
length = len(L) length = len(L)
if length > 16: if length > 16:
raise GeneralError('supplied list is longer than 16') raise ValueError("supplied list is longer than 16")
for i in range(16): for i in range(16):
if i < length: if i < length:
@ -178,13 +178,17 @@ class mat4:
self.m[i] = 0.0 self.m[i] = 0.0
def zero(mat4 self): def zero(mat4 self):
'''Fill the matrix with zeroes.''' """
Fill the matrix with zeroes.
"""
for i in range(16): for i in range(16):
self.m[i] = 0.0 self.m[i] = 0.0
def identity(mat4 self): def identity(mat4 self):
'''Make the matrix an identity.''' """
Make the matrix an identity.
"""
self.zero() self.zero()
@ -194,7 +198,9 @@ class mat4:
self.m[15] = 1.0 self.m[15] = 1.0
def transpose(mat4 self): def transpose(mat4 self):
'''Transpose the matrix.''' """
Transpose the matrix.
"""
cdef float tmp cdef float tmp
@ -223,7 +229,9 @@ class mat4:
self.m[9] = tmp self.m[9] = tmp
def invert(mat4 self): def invert(mat4 self):
'''Invert the matrix.''' """
Invert the matrix.
"""
cdef float tmp[16] cdef float tmp[16]
cdef float det cdef float det
@ -237,7 +245,7 @@ class mat4:
# epsilon pulled straight out of Uranus # epsilon pulled straight out of Uranus
if det < 0.00005 and det > -0.00005: if det < 0.00005 and det > -0.00005:
print('det=%.1f' %(det)) print("det=%.1f" %(det))
return return
tmp[1] = -self.m[1]*self.m[10]*self.m[15] + self.m[1]*self.m[11]*self.m[14] + self.m[9]*self.m[2]*self.m[15] - self.m[9]*self.m[3]*self.m[14] - self.m[13]*self.m[2]*self.m[11] + self.m[13]*self.m[3]*self.m[10] tmp[1] = -self.m[1]*self.m[10]*self.m[15] + self.m[1]*self.m[11]*self.m[14] + self.m[9]*self.m[2]*self.m[15] - self.m[9]*self.m[3]*self.m[14] - self.m[13]*self.m[2]*self.m[11] + self.m[13]*self.m[3]*self.m[10]
@ -272,11 +280,11 @@ class mat4:
self.m[15] = tmp[15] * det self.m[15] = tmp[15] * det
def mulm(mat4 self, mat4 B, bint inplace=False): def mulm(mat4 self, mat4 B, bint inplace=False):
''' """
Return a matrix that is the result of multiplying this matrix by another. Return a matrix that is the result of multiplying this matrix by another.
M = self * mat4 B M = self * mat4 B
''' """
cdef uint8_t i cdef uint8_t i
cdef mat4 tmp = mat4() cdef mat4 tmp = mat4()
@ -305,11 +313,11 @@ class mat4:
return tmp return tmp
def mulv(mat4 self, vec3 v): def mulv(mat4 self, vec3 v):
''' """
Return a vec3 that is the result of multiplying this matrix by a vec3. Return a vec3 that is the result of multiplying this matrix by a vec3.
u = self * v u = self * v
''' """
cdef mat4 tmp = vec3() cdef mat4 tmp = vec3()
@ -320,11 +328,11 @@ class mat4:
return tmp return tmp
def mulf(mat4 self, f): def mulf(mat4 self, f):
''' """
Return a matrix that is the result of multiplying this matrix by a scalar. Return a matrix that is the result of multiplying this matrix by a scalar.
M = self * f M = self * f
''' """
cdef mat4 tmp = mat4() cdef mat4 tmp = mat4()
cdef int i cdef int i
@ -337,15 +345,15 @@ class mat4:
def __repr__(mat4 self): def __repr__(mat4 self):
lines = [] lines = []
lines.append('mat4(%.1f %.1f %.1f %.1f' %(self.m[0], self.m[4], self.m[8], self.m[12])) lines.append("mat4(%.1f %.1f %.1f %.1f" %(self.m[0], self.m[4], self.m[8], self.m[12]))
lines.append(' %.1f %.1f %.1f %.1f' %(self.m[1], self.m[5], self.m[9], self.m[13])) lines.append(" %.1f %.1f %.1f %.1f" %(self.m[1], self.m[5], self.m[9], self.m[13]))
lines.append(' %.1f %.1f %.1f %.1f' %(self.m[2], self.m[6], self.m[10], self.m[14])) lines.append(" %.1f %.1f %.1f %.1f" %(self.m[2], self.m[6], self.m[10], self.m[14]))
lines.append(' %.1f %.1f %.1f %.1f)' %(self.m[3], self.m[7], self.m[11], self.m[15])) lines.append(" %.1f %.1f %.1f %.1f)" %(self.m[3], self.m[7], self.m[11], self.m[15]))
return '\n'.join(lines) return "\n".join(lines)
cdef class vec3: cdef class vec3:
''' """
A float 3D vector. A float 3D vector.
>>> v = vec3(1, 1, 0) >>> v = vec3(1, 1, 0)
@ -361,14 +369,14 @@ cdef class vec3:
>>> w - v >>> w - v
vec4(-1.00, 0.00, 1.00) vec4(-1.00, 0.00, 1.00)
''' """
def __init__(vec3 self, *args): def __init__(vec3 self, *args):
''' """
Create a vec3. Create a vec3.
Accepts any number of parameters between 0 and 3 to fill the vector from the left. Accepts any number of parameters between 0 and 3 to fill the vector from the left.
''' """
length = len(args) length = len(args)
@ -377,7 +385,7 @@ cdef class vec3:
length = len(args) length = len(args)
if length > 3: if length > 3:
raise MathError('Attempt to initialize a vec3 with %d arguments.' %(length)) raise ValueError("Attempt to initialize a vec3 with %d arguments." %(length))
for i in range(3): for i in range(3):
if i < length: if i < length:
@ -387,18 +395,18 @@ cdef class vec3:
def __getitem__(vec3 self, int i): def __getitem__(vec3 self, int i):
if i >= 3 or i < 0: if i >= 3 or i < 0:
raise IndexError('element index out of range(3)') raise IndexError("element index out of range(3)")
return self.v[i] return self.v[i]
def __setitem__(vec3 self, int i, float value): def __setitem__(vec3 self, int i, float value):
if i >= 3 or i < 0: if i >= 3 or i < 0:
raise IndexError('element index out of range(3)') raise IndexError("element index out of range(3)")
self.v[i] = value self.v[i] = value
def __repr__(vec3 self): def __repr__(vec3 self):
return 'vec3(%.2f, %.2f, %.2f)' %(self.v[0], self.v[1], self.v[2]) return "vec3(%.2f, %.2f, %.2f)" %(self.v[0], self.v[1], self.v[2])
def __getstate__(vec3 self): def __getstate__(vec3 self):
return (self.v[0], self.v[1], self.v[2]) return (self.v[0], self.v[1], self.v[2])
@ -410,12 +418,16 @@ cdef class vec3:
@property @property
def length(vec3 self): def length(vec3 self):
'''Contains the geometric length of the vector.''' """
Contains the geometric length of the vector.
"""
return sqrt(self.v[0]**2 + self.v[1]**2 + self.v[2]**2) return sqrt(self.v[0]**2 + self.v[1]**2 + self.v[2]**2)
def normalized(vec3 self): def normalized(vec3 self):
'''Returns this vector, normalized.''' """
Returns this vector, normalized.
"""
length = self.length length = self.length
@ -431,29 +443,28 @@ cdef class vec3:
return vec3(-self.v[0], -self.v[1], -self.v[2]) return vec3(-self.v[0], -self.v[1], -self.v[2])
def dot(vec3 L, vec3 R): def dot(vec3 L, vec3 R):
''' """
Returns the dot product of the two vectors. Returns the dot product of the two vectors.
E.g. u.dot(v) -> u . v E.g. u.dot(v) -> u . v
''' """
return L.v[0] * R.v[0] + L.v[1] * R.v[1] + L.v[2] * R.v[2] return L.v[0] * R.v[0] + L.v[1] * R.v[1] + L.v[2] * R.v[2]
def cross(vec3 L, vec3 R): def cross(vec3 L, vec3 R):
''' """
Returns the cross product of the two vectors. Returns the cross product of the two vectors.
E.g. u.cross(v) -> u x v E.g. u.cross(v) -> u x v
"""
'''
return vec3(L.v[1]*R.v[2] - L.v[2]*R.v[1], L.v[0]*R.v[2] - L.v[2]*R.v[0], L.v[0]*R.v[1] - L.v[1]*R.v[0]) return vec3(L.v[1]*R.v[2] - L.v[2]*R.v[1], L.v[0]*R.v[2] - L.v[2]*R.v[0], L.v[0]*R.v[1] - L.v[1]*R.v[0])
def __mul__(vec3 L, R): def __mul__(vec3 L, R):
''' """
Multiplication of a vec3 by a float. Multiplication of a vec3 by a float.
The float has to be on the right. The float has to be on the right.
''' """
return vec3(L.v[0] * R, L.v[1] * R, L.v[2] * R) return vec3(L.v[0] * R, L.v[1] * R, L.v[2] * R)

View file

@ -1,38 +1,36 @@
#! /usr/bin/env python3 #! /usr/bin/env python3
# encoding: utf-8 # encoding: utf-8
import copy
import imp
import io
import math
import os
import sys import sys
from . import text
# -------------------------------------------------- # --------------------------------------------------
def import_module(path): def import_module(path):
''' """
Imports a python file as a module. The path can be relative or absolute. Imports a python file as a module. The `path` can be relative or absolute.
Based on the work of Yuval Greenfield released into the public domain. Based on the work of Yuval Greenfield released into the public domain.
''' """
import imp
import os
path = os.path.expanduser(path)
# remove the .py suffix # remove the .py suffix
mod_dn = os.path.dirname(path) mod_dirname = os.path.dirname(path)
mod_fn = os.path.basename(path) mod_filename = os.path.basename(path)
if mod_fn.endswith('.py'): if mod_filename.endswith(".py"):
mod_name = mod_fn[:-3] mod_name = mod_filename[:-3]
else: else:
# packages for example # packages for example
mod_name = mod_fn mod_name = mod_filename
fd = None fd = None
try: try:
data = imp.find_module(mod_name, [mod_dn]) data = imp.find_module(mod_name, [mod_dirname])
module = imp.load_module(mod_name, *data) module = imp.load_module(mod_name, *data)
fd = data[0] fd = data[0]
@ -45,24 +43,24 @@ def import_module(path):
# -------------------------------------------------- # --------------------------------------------------
def batch_gen(data, batch_size): def batch_gen(data, batch_size):
''' """
Split data (a sequence) into sequences batch_size elements long. Split `data` (a sequence) into sequences `batch_size` elements long.
''' """
for i in range(0, len(data), batch_size): for i in range(0, len(data), batch_size):
yield data[i:i+batch_size] yield data[i:i+batch_size]
# -------------------------------------------------- # --------------------------------------------------
def console(locals, globals=None, tab_completion=False): def console(locals, globals=None, tab_completion=True):
''' """
Opens up a Python console. Opens up a Python console.
"""
tab_completion enables completion of object names using TAB, however note import code
that this will make pasting formatted snippets of code difficult import copy
''' import readline
import rlcompleter
import code, readline, rlcompleter
environment = copy.copy(locals) environment = copy.copy(locals)
@ -72,17 +70,17 @@ def console(locals, globals=None, tab_completion=False):
environment[key] = value environment[key] = value
if tab_completion: if tab_completion:
readline.parse_and_bind('tab: complete') readline.parse_and_bind("tab: complete")
c = code.InteractiveConsole(environment) c = code.InteractiveConsole(environment)
c.interact(banner='') c.interact(banner="")
# -------------------------------------------------- # --------------------------------------------------
def debugger(): def debugger():
''' """
Drops into the Python Debugger where called. Drops into the Python Debugger where called.
''' """
import pdb import pdb
@ -92,42 +90,46 @@ def debugger():
def hexdump(data, indent=0, offset=16, show_header=True, show_offsets=True, show_ascii=True, use_colors=True, output=sys.stdout): def hexdump(data, indent=0, offset=16, show_header=True, show_offsets=True, show_ascii=True, use_colors=True, output=sys.stdout):
""" """
Writes a hex dump of 'data' to 'output'. Writes a hex dump of `data` to `output`.
The output text is indented with spaces and contains 'offset' bytes per line. The output text is indented with spaces and contains `offset` bytes per line.
If show_header is True, a single line with byte numbers preceeds all output. If `show_header` is True, a single line with byte numbers preceeds all output.
If show_offsets is True, each line is prefixed with the address of the first byte of that line. If `show_offsets` is True, each line is prefixed with the address of the first byte of that line.
If show_ascii is True, each line is suffixed with its ASCII representation. Unprintable characters If `show_ascii` is True, each line is suffixed with its ASCII representation. Unprintable characters
are replaced with a dot. are replaced with a dot.
The 'output' must implement a .write(str) method. If 'output' is None, the string is returned instead. The `output` must implement a .write(str x) method. If `output` is None, the string is returned instead.
""" """
import io
import math
from . import text
output_io = io.StringIO() if not output else output output_io = io.StringIO() if not output else output
if type(data) is not bytes: if type(data) is not bytes:
raise ValueError('data must be bytes') raise ValueError("data must be bytes")
offset_figures = math.ceil(math.log2(len(data)) / 8) * 2 if data else 2 offset_figures = math.ceil(math.log2(len(data)) / 8) * 2 if data else 2
format_str = '%%0%dx ' %(offset_figures) format_str = "%%0%dx " %(offset_figures)
ptr = 0 ptr = 0
if show_header: if show_header:
line = [] line = []
if show_offsets: if show_offsets:
line.append('offset'[:offset_figures].ljust(offset_figures + 2)) line.append("offset"[:offset_figures].ljust(offset_figures + 2))
for i in range(offset): for i in range(offset):
line.append('%2x' %(i)) line.append("%2x" %(i))
if show_ascii: if show_ascii:
line.append(' *{0}*'.format("ASCII".center(offset, "-"))) line.append(" *{0}*".format("ASCII".center(offset, "-")))
output_io.write(' '.join(line) + '\n') output_io.write(" ".join(line) + "\n")
while data[ptr:]: while data[ptr:]:
if indent: if indent:
output_io.write(' ' * indent) output_io.write(" " * indent)
if show_offsets: if show_offsets:
output_io.write(format_str %(ptr)) output_io.write(format_str %(ptr))
@ -138,25 +140,25 @@ def hexdump(data, indent=0, offset=16, show_header=True, show_offsets=True, show
for local_i, i in enumerate(range(ptr, ptr+offset)): for local_i, i in enumerate(range(ptr, ptr+offset)):
if i < len(data): if i < len(data):
c = data[i] c = data[i]
hex_bytes.append('%02x' %(c)) hex_bytes.append("%02x" %(c))
if 0x20 <= c <= 0x7e: if 0x20 <= c <= 0x7e:
ascii_bytes.append(chr(c)) ascii_bytes.append(chr(c))
else: else:
ascii_bytes.append('.') ascii_bytes.append(".")
elif i == len(data): elif i == len(data):
hex_bytes.extend([' '] * (offset - local_i)) hex_bytes.extend([" "] * (offset - local_i))
if use_colors: output_io.write(text.render("§B")) if use_colors: output_io.write(text.render("§B"))
output_io.write(' '.join(hex_bytes)) output_io.write(" ".join(hex_bytes))
if use_colors: output_io.write(text.render("§/")) if use_colors: output_io.write(text.render("§/"))
if show_ascii: if show_ascii:
output_io.write(text.render(" |§B") if use_colors else " |") output_io.write(text.render(" |§B") if use_colors else " |")
output_io.write(''.join(ascii_bytes)) output_io.write("".join(ascii_bytes))
output_io.write(text.render("§/|") if use_colors else "|") output_io.write(text.render("§/|") if use_colors else "|")
output_io.write('\n') output_io.write("\n")
ptr += offset ptr += offset

View file

@ -1,105 +1,32 @@
#! /usr/bin/env python3 #! /usr/bin/env python3
# encoding: utf-8 # encoding: utf-8
from collections import OrderedDict
# -------------------------------------------------- # --------------------------------------------------
class ndict(dict): class ndict(OrderedDict):
''' """
A dictionary superclass whose keys are exposed as attributes. An OrderedDict subclass whose keys are exposed as attributes.
>>> d = ndict() >>> d = ndict()
>>> d.alpha = 1 >>> d.alpha = 1
>>> d['alpha'] >>> d["alpha"]
1 1
>>> d['beta'] = 42 >>> d["beta"] = 42
>>> d.beta >>> d.beta
42 42
>>> d >>> d
{'alpha': 1, 'beta': 42} {"alpha": 1, "beta": 42}
''' """
def __getattr__(self, name): def __getattr__(self, name):
if name in self: if name in self:
return self[name] return self[name]
elif name.replace('_', '-') in self: elif name.replace("_", "-") in self:
return self[name.replace('_', '-')] return self[name.replace("_", "-")]
else: else:
raise AttributeError("'ndict' object has no attribute '%s'" %(name)) raise AttributeError('"ndict" object has no attribute "%s"' %(name))
def __setattr__(self, name, value): def __setattr__(self, name, value):
self[name] = value self[name] = value
# --------------------------------------------------
class map:
'''
An ordered dict.
'''
def __init__(self, source=None):
'''
Initialize the map.
The source is a sequence of (key, value) tuples.
'''
if source:
self.keys, self.vals = zip(*source)
else:
self.keys = []
self.vals = []
def __get_index(self, key):
if key in self.keys:
return self.keys.index(key)
else:
return None
def __getitem__(self, key):
i = self.__get_index(key)
if i is None:
raise KeyError(key)
else:
return self.vals[i]
def __setitem__(self, key, val):
i = self.__get_index(key)
if i is None:
self.keys.append(key)
self.vals.append(val)
else:
self.vals[i] = val
def __contains__(self, item):
return item in self.keys
def index(self, item):
return self.keys.index(item)
def sort(self, key=None):
tmp_keys = self.keys
tmp_vals = self.vals
self.keys = []
self.vals = []
for K, V in sorted(zip(tmp_keys, tmp_vals), key=key):
self.keys.append(K)
self.vals.append(V)
@property
def items(self):
return zip(self.keys, self.vals)
def __len__(self):
return len(self.vals)
def __repr__(self):
pairs = []
for i in range(len(self.keys)):
pairs.append('%s: %s' %(repr(self.keys[i]), repr(self.vals[i])))
return '<{%s}>' %(', '.join(pairs))

View file

@ -4,55 +4,60 @@
import time import time
import serial import serial
from . import et from .. import text
prefix = et.prefix """
OverTalk protocol spec
======================
''' Transport layer
===== OverTalk protocol spec ===== ---------------
==== Transport layer ==== TODO: Swap checksum and payload.
frame = [ 0x2a | destination (1) | source (1) | payload length (1) | payload (n) | checksum (1) ] frame = [ 0x2a | destination (1) | source (1) | payload length (1) | payload (n) | checksum (1) ]
Any time an 0x2a is encountered the parser should flush its state and begin reading a new frame. Any time a 0x2a is encountered the parser should flush its state and begin reading a new frame.
Bytes 0x2a, 0x2b, 0x11 and 0x13 are prefixed with 0x2b and inverted (XORed with 0xff) to avoid collisions. Bytes 0x2a, 0x2b, 0x11 and 0x13 are prefixed with 0x2b and inverted (XORed with 0xff) to avoid collisions.
Frames with destinations unknown to an OverShell are sent to the default interface, or dropped if Frames with destinations unknown to an OverShell are sent to the default interface, or dropped if
that's the interface they came from. that's the interface they came from.
==== Command layer ==== Command layer
-------------
NOTE: This is going to change.
Payload consists of one or more commands: Payload consists of one or more commands:
get_command = [ 0x00 | register (1) | target register (1) ] get_command = [ 0x00 | register (1) | target register (1) ]
set_command = [ 0x01 | register (1) | length (1) | value (n) ] set_command = [ 0x01 | register (1) | length (1) | value (n) ]
With a get_command the sender requests the receiver to read its own 'register' and issue With a get_command the sender requests the receiver to read its own `register` and issue
a set_command that sets the sender's 'target register' to that value. a set_command that sets the sender's `target register` to that value.
A set_command does what is says on the box. A set_command does what is says on the box.
''' """
class Transport: class Transport:
''' """
OverTalk Transport layer implementation. OverTalk Transport layer implementation.
Reads data from multiple interfaces and either router frames or receives them. Reads data from multiple interfaces and either routes frames or receives them.
A received frame is then made available via a public attribute. A received frame is then made available via a public attribute.
Interfaces are objects that implement methods read() (returns one byte of data as int), write(buffer, len), Interfaces are objects that implement methods read() (returns one byte of data as int), write(buffer, len),
and waiting property (contains number of bytes waiting). and waiting property (contains number of bytes waiting).
''' """
def __init__(self, my_id, interfaces, def_route): def __init__(self, my_id, interfaces, def_route):
''' """
@param my_id OverTalk address of this Transport instance @param my_id OverTalk address of this Transport instance
@param interfaces a dict of interfaces keyed by their IDs @param interfaces a dict of interfaces keyed by their IDs
@param def_route ID of default interface for sending @param def_route ID of default interface for sending
''' """
assert my_id not in interfaces assert my_id not in interfaces
assert def_route in interfaces assert def_route in interfaces
self._print = et.Output('over.com.Transport', default_suffix='\n', timestamp=True) self.print = text.Output("over.serial.Transport")
self.my_id = my_id self.my_id = my_id
self.def_route = def_route self.def_route = def_route
@ -61,7 +66,7 @@ class Transport:
self.destination_unknown = 0 self.destination_unknown = 0
self.incoming = None self.incoming = None
self._print('<Cg>on-line<C/>') self.print("on-line", text.Output.tl.done)
def update(self): def update(self):
self.incoming = None self.incoming = None
@ -95,7 +100,7 @@ class Transport:
buffer_length = len(interface.rxbuffer) buffer_length = len(interface.rxbuffer)
if buffer_length >= 4: if buffer_length >= 4:
# at this point we can read frame's declared size # at this point we can read frame"s declared size
if buffer_length >= interface.rxbuffer[3] + 5: # header (4) + payload + checksum (1) if buffer_length >= interface.rxbuffer[3] + 5: # header (4) + payload + checksum (1)
# a frame has been read, huzzah! # a frame has been read, huzzah!
interface.reading_frame = False interface.reading_frame = False
@ -106,7 +111,7 @@ class Transport:
else: else:
interface.malformed_frames += 1 interface.malformed_frames += 1
self._print('broken frame received: <Cr>%s<C/>' %(interface.rxbuffer)) self.print("broken frame received: §r§%s§.§" %(interface.rxbuffer))
interface.rxbuffer = [] interface.rxbuffer = []
return False return False
@ -133,15 +138,15 @@ class Transport:
self.incoming = (frame[2], payload) self.incoming = (frame[2], payload)
else: else:
if destination in self.interfaces: if destination in self.interfaces:
self._print('routing frame to [%d]' %(destination)) self.print("routing frame to [%d]" %(destination))
self.interfaces[destination].write(self.escape_frame(frame)) self.interfaces[destination].write(self.escape_frame(frame))
else: else:
if source_interface_id == self.def_route: if source_interface_id == self.def_route:
self.destination_unknown += 1 self.destination_unknown += 1
self._print('unknown destination <Cr>%d<C/> for frame: <Cy>%s<C/>' %(destination, self.print("unknown destination <r>%d<.> for frame: <y>%s<.>" %(destination,
repr(frame)), prefix.fail) repr(frame)), text.Output.tl.fail)
else: else:
self._print('routing frame to default route [%d]' %(self.def_route)) self.print("routing frame to default route [%d]" %(self.def_route))
self.interfaces[self.def_route].write(self.escape_frame(frame)) self.interfaces[self.def_route].write(self.escape_frame(frame))
def send_data(self, destination, data): def send_data(self, destination, data):
@ -157,7 +162,7 @@ class Transport:
s.write(frame) s.write(frame)
class TTL_Interface: class TTL_Interface:
def __init__(self, interface='/dev/ttyUSB0', baudrate=57600): def __init__(self, interface="/dev/ttyUSB0", baudrate=57600):
try: try:
self.s = serial.Serial(interface, baudrate, timeout=1) self.s = serial.Serial(interface, baudrate, timeout=1)
except serial.serialutil.SerialException: except serial.serialutil.SerialException:
@ -184,7 +189,7 @@ class TTL_Interface:
return 0 return 0
def write(self, data): def write(self, data):
print('Sending:', ''.join([hex(x)[2:].zfill(2) for x in data])) print("Sending:", "".join([hex(x)[2:].zfill(2) for x in data]))
if self.s: if self.s:
self.s.write(bytes(data)) self.s.write(bytes(data))

View file

@ -1,13 +1,14 @@
#! /usr/bin/env python3 #! /usr/bin/env python3
# encoding: utf-8 # encoding: utf-8
import datetime
import fcntl
import math import math
import re import re
import sys
import struct import struct
import fcntl import sys
import termios
import time import time
import termios
# -------------------------------------------------- # --------------------------------------------------
@ -41,7 +42,277 @@ def lexical_join(words, oxford=False):
# -------------------------------------------------- # --------------------------------------------------
class Unit:
"""
A object that represents numbers and units in human-readable form.
TODO use significant digits instead of rigid order boundaries
TODO float superclass?
TODO base_2 prefixes (Ki, Mi, ...)
"""
_prefixes = (
("Y", 24), ("Z", 21), ("E", 18), ("P", 15), ("T", 12), ("G", 9), ("M", 6), ("k", 3),
("h", 2), ("D", 1), ("", 0), ("d", -1), ("c", -2), ("m", -3), ("μ", -6), ("n", -9),
("p", -12), ("f", -15), ("a", -18), ("z", -21), ("y", -24)
)
def __init__(self,
value, unit=None, dimension=1,
use_prefixes="YZEPTGMkmμnpfazy", format="%.2f pU",
logarithmic=False, log_base=10
):
"""
value the numerical value of the variable (int or float)
unit the symbol to use, if any (str or None)
dimension the dimensionality of the value (1, 2, 3, ...)
use_prefixes which multiplier prefixes to use
- the default "YZEPTGMkmμnpfazy" omits "c" for centi- and "D" for deca-
format use printf notation for value (e.g. %010.5f), p for prefix and U for unit
logarithmic when True, log_10 prefixes are used
log_base logarithm base value
note that deca- is correctly rendered as "da", the "D" is used in use_prefixes only
"""
self.value = float(value)
self.unit = unit if unit else ""
self.dimension = dimension
self.use_prefixes = use_prefixes
self.format = format
self.logarithmic = logarithmic
self.log_base = log_base
if self.logarithmic and (self.value < 0 or self.log_base < 0):
raise ValueError("math domain error (negative values can't be represented in dB)")
def __str__(self):
if self.value == 0.0:
e = 0
else:
e = round(math.log(abs(self.value), 10), 6)
if self.logarithmic:
prefix = "dB"
value = math.log(self.value, self.log_base) * 10
else:
for prefix, mul in self._prefixes:
if prefix in self.use_prefixes and mul*self.dimension <= e:
break
value = self.value / 10**(mul*self.dimension)
output = self.format %(value)
output = output.replace("p", prefix if prefix != "D" else "da") # deca- handler
output = output.replace("U", self.unit)
return output
def __repr__(self):
return "over.text.Unit(%s)" %(self)
# --------------------------------------------------
colortags = {
"r": "\x1b[31;01m",
"g": "\x1b[32;01m",
"y": "\x1b[33;01m",
"b": "\x1b[34;01m",
"m": "\x1b[35;01m",
"c": "\x1b[36;01m",
"B": "\x1b[01m",
".": "\x1b[39;49;00m" # reset
}
def render(text, colors=True):
"""
Processes text with color tags and either
removes them (with colors=False) or replaces
them with terminal color codes.
Color tags are <x> where x is the color code from colortags.
<.> resets the color. Use <<x> for a literal <x>.
"""
text = str(text)
output = []
window = []
for c in text:
window.append(c)
if len(window) < 3:
continue
elif len(window) == 4:
output.append(window.pop(0))
# the window now contains three chars
if window[0] == "<" and window[2] == ">":
code = window[1]
if code in colortags.keys():
if not output or output[-1] != "<":
if colors:
output.append(colortags[code])
window.clear()
elif output and output[-1] == "<":
window.pop(0) # remove the opening "<" so the rest of the escaped sequence slides into output
return "".join(output + window)
# --------------------------------------------------
def rfind(text, C, start, length):
"""
Returns the highest index of C in text[start:] that is <= start+length.
Color tags aren't counted into length.
"""
# find the index of the last C in colorless text
colorless = render(text[start:], False)
indices_colorless = [i for i, c in enumerate(colorless) if c == C and i < length]
# get indices of all Cs in the original
indices = [i for i, c in enumerate(text[start:]) if c == C]
# return the original index at the same position as the last in colorless
return start + indices[len(indices_colorless) - 1]
def paragraph(text, width=0, indent=0, stamp=None):
"""
str text text to format
int width required line length; if 0, current terminal width will be used
int indent how many spaces to indent the text with
str stamp placed into the first line's indent (whether it fits is your problem)
Formats text into an indented paragraph that fits the terminal and returns it.
Correctly handles colors.
"""
fit_into_width = (width or get_terminal_size()[1]) - indent
lines = []
last = 0
# break into lines
while True:
idx = rfind(text, " ", last, fit_into_width)
adj_last = last + 1 if last else 0
if len(text[adj_last:]) < fit_into_width or idx == -1:
lines.append(text[adj_last:])
break
elif idx == last:
return (stamp or "") + text
else:
line = text[adj_last:idx]
last = idx
lines.append(line)
# indent
indent_str = " " * indent
for i, line in enumerate(lines):
if stamp and i == 0:
lines[i] = stamp.ljust(indent) + line
else:
lines[i] = indent_str + line
# join
return "\n".join(lines)
# --------------------------------------------------
def strlen(string):
"""
Returns the length of a string minus all color tags.
"""
plain_string = render(string, colors=False)
return len(plain_string)
# --------------------------------------------------
class Output:
class tag:
class short:
info = " "
debug = " <b>?<.>"
start = "<B>>>><.>"
exec = start
warn = " <y>#<.>"
fail = "<r>!!!<.>"
done = " <g>*<.>"
class long:
info = "INFO"
debug = "<b>DEBG<.>"
start = "<B>EXEC<.>"
exec = start
warn = "<y>WARN<.>"
fail = "<r>FAIL<.>"
done = "<g>DONE<.>"
ts = tag.short
tl = tag.long
"""
Text UI output renderer.
Prints messages to the stdout with optional eye candy
like colors and timestamps.
Formatting
==========
You can use all formatting characters supported by strftime, as well as:
<T> - tag
<n> - name
<t> - supplied text
"""
def __init__(self, name, format="[%Y-%m-%d %H:%M:%S] <T> -- <n>, <t>", colors=True, end=".\n", stream=sys.stderr):
self.name = name
self.format = format
self.colors = colors
self.end = end
self.stream = stream
def __call__(self, text, tag=None, format=None, colors=None, end=None):
tag = tag or self.__class__.tag.long.info
format = format or self.format
colors = colors or self.colors
end = end or self.end
output = datetime.datetime.now().strftime(format)
output = output.replace("<T>", tag)
output = output.replace("<n>", self.name)
output = output.replace("<t>", text)
output += end
self.stream.write(render(output, colors))
self.stream.flush()
# --------------------------------------------------
def get_terminal_size():
"""
Returns current terminal's (rows, cols).
"""
terminal = sys.stdout.fileno()
try:
return struct.unpack("HHHH", fcntl.ioctl(terminal, termios.TIOCGWINSZ, struct.pack("HHHH", 0, 0, 0, 0)))
except IOError:
return (40, 80)
# --------------------------------------------------
class _ProgressBarChannel: class _ProgressBarChannel:
print = Output("over.text._ProgressBarChannel")
def __init__(self, unit, top, precision, use_prefixes=True, prefix_base2=False, min_width_raw=0, min_width_rate=0, min_width_time=0, min_width_percent=7): def __init__(self, unit, top, precision, use_prefixes=True, prefix_base2=False, min_width_raw=0, min_width_rate=0, min_width_time=0, min_width_percent=7):
self.unit = unit self.unit = unit
self.top = top self.top = top
@ -60,7 +331,7 @@ class _ProgressBarChannel:
self.set() self.set()
if prefix_base2: if prefix_base2:
_print("Unit does not yet support base2 prefixes (e.g. Gi, Mi), using decadic (G, M) instead", prefix.warn) self.print("Unit does not yet support base2 prefixes (e.g. Gi, Mi), using decadic (G, M) instead", self.print.tl.warn)
def set(self, value=None): def set(self, value=None):
if value is not None: if value is not None:
@ -303,273 +574,6 @@ class ProgressBar:
# -------------------------------------------------- # --------------------------------------------------
class Unit:
"""
A object that represents numbers and units in human-readable form.
TODO use significant digits instead of rigid order boundaries
TODO float superclass?
TODO base_2 prefixes (Ki, Mi, ...)
"""
_prefixes = (
("Y", 24), ("Z", 21), ("E", 18), ("P", 15), ("T", 12), ("G", 9), ("M", 6), ("k", 3),
("h", 2), ("D", 1), ("", 0), ("d", -1), ("c", -2), ("m", -3), ("μ", -6), ("n", -9),
("p", -12), ("f", -15), ("a", -18), ("z", -21), ("y", -24)
)
def __init__(self,
value, unit=None, dimension=1,
use_prefixes="YZEPTGMkmμnpfazy", format="%.2f pU",
logarithmic=False, log_base=10
):
"""
value the numerical value of the variable (int or float)
unit the symbol to use, if any (str or None)
dimension the dimensionality of the value (1, 2, 3, ...)
use_prefixes which multiplier prefixes to use
- the default "YZEPTGMkmμnpfazy" omits "c" for centi- and "D" for deca-
format use printf notation for value (e.g. %010.5f), p for prefix and U for unit
logarithmic when True, log_10 prefixes are used
log_base logarithm base value
note that deca- is correctly rendered as "da", the "D" is used in use_prefixes only
"""
self.value = float(value)
self.unit = unit if unit else ""
self.dimension = dimension
self.use_prefixes = use_prefixes
self.format = format
self.logarithmic = logarithmic
self.log_base = log_base
if self.logarithmic and (self.value < 0 or self.log_base < 0):
raise ValueError("math domain error (negative values can't be represented in dB)")
def __str__(self):
if self.value == 0.0:
e = 0
else:
e = round(math.log(abs(self.value), 10), 6)
if self.logarithmic:
prefix = "dB"
value = math.log(self.value, self.log_base) * 10
else:
for prefix, mul in self._prefixes:
if prefix in self.use_prefixes and mul*self.dimension <= e:
break
value = self.value / 10**(mul*self.dimension)
output = self.format %(value)
output = output.replace("p", prefix if prefix != "D" else "da") # deca- handler
output = output.replace("U", self.unit)
return output
def __repr__(self):
return "over.text.Unit(%s)" %(self)
# --------------------------------------------------
colortag_char = "§"
colortags = {
"r": "\x1b[31;01m",
"g": "\x1b[32;01m",
"y": "\x1b[33;01m",
"b": "\x1b[34;01m",
"m": "\x1b[35;01m",
"c": "\x1b[36;01m",
"B": "\x1b[01m",
".": "\x1b[39;49;00m" # reset
}
def render(text, colors=True):
"""
Processes text with color tags and either
removes them (with colors=False) or replaces
them with terminal color codes.
Color tags are §x§ where x is the color code from colortags.
§.§ resets the color. Use §§x§ for a literal §x§.
"""
text = str(text)
output = []
window = []
for c in text:
window.append(c)
if len(window) < 3:
continue
elif len(window) == 4:
output.append(window.pop(0))
# the window now contains three chars
if window[0] == window[2] == colortag_char:
code = window[1]
if code in colortags.keys() and (not output or output[-1] != colortag_char):
if colors:
output.append(colortags[code])
window.clear()
return "".join(output + window)
# --------------------------------------------------
def rfind(text, C, start, length):
"""
Returns the highest index of C in text[start:] that is <= start+length.
Color tags aren't counted into length.
"""
# find the index of the last C in colorless text
colorless = render(text[start:], False)
indices_colorless = [i for i, c in enumerate(colorless) if c == C and i < length]
# get indices of all Cs in the original
indices = [i for i, c in enumerate(text[start:]) if c == C]
# return the original index at the same position as the last in colorless
return start + indices[len(indices_colorless) - 1]
def paragraph(text, width=0, indent=0, stamp=None):
"""
str text text to format
int width required line length; if 0, current terminal width will be used
int indent how many spaces to indent the text with
str stamp placed into the first line's indent (whether it fits is your problem)
Formats text into an indented paragraph that fits the terminal and returns it.
Correctly handles colors.
"""
fit_into_width = (width or get_terminal_size()[1]) - indent
lines = []
last = 0
# break into lines
while True:
idx = rfind(text, " ", last, fit_into_width)
adj_last = last + 1 if last else 0
if len(text[adj_last:]) < fit_into_width or idx == -1:
lines.append(text[adj_last:])
break
elif idx == last:
return (stamp or "") + text
else:
line = text[adj_last:idx]
last = idx
lines.append(line)
# indent
indent_str = " " * indent
for i, line in enumerate(lines):
if stamp and i == 0:
lines[i] = stamp.ljust(indent) + line
else:
lines[i] = indent_str + line
# join
return "\n".join(lines)
# --------------------------------------------------
def strlen(string):
"""
Returns the length of a string minus all color tags.
"""
plain_string = render(string, colors=False)
return len(plain_string)
# --------------------------------------------------
class Output:
class tag:
class short:
info = " "
debug = " §b§?§.§"
start = "§B§>>>§.§"
exec = start
warn = " §y§#§.§"
fail = "§r§!!!§.§"
done = " §g§*§.§"
class long:
info = "INFO"
debug = "§b§DEBG§.§"
start = "§B§EXEC§.§"
exec = start
warn = "§y§WARN§.§"
fail = "§r§FAIL§.§"
done = "§g§DONE§.§"
ts = tag.short
tl = tag.long
"""
Text UI output renderer.
Prints messages to the stdout with optional eye candy
like colors and timestamps.
Formatting
==========
You can use all formatting characters supported by strftime, as well as:
§T§ - tag
§n§ - name
§t§ - supplied text
"""
def __init__(self, name, format="[%Y-%m-%d %H:%M:%S] §T§ -- §n§, §t§", colors=True, end=".\n", stream=sys.stderr):
self.name = name
self.format = format
self.colors = colors
self.end = end
self.stream = stream
def __call__(self, text, tag=None, format=None, colors=None, end=None):
tag = tag or self.__class__.tag.long.info
format = format or self.format
colors = colors or self.colors
end = end or self.end
output = datetime.datetime.now().strftime(format)
output = output.replace("§T§", tag)
output = output.replace("§n§", self.name)
output = output.replace("§t§", text)
output += end
self.stream.write(render(output, colors))
self.stream.flush()
# --------------------------------------------------
def get_terminal_size():
"""
Returns current terminal's (rows, cols).
"""
terminal = sys.stdout.fileno()
try:
return struct.unpack("HHHH", fcntl.ioctl(terminal, termios.TIOCGWINSZ, struct.pack("HHHH", 0, 0, 0, 0)))
except IOError:
return (40, 80)
# --------------------------------------------------
if __name__ == "__main__": if __name__ == "__main__":
pb = ProgressBar( pb = ProgressBar(
"§%a (§ra/§za) [§=a>§ A] §sa [§rb/§zb] (ETA §TA)", "§%a (§ra/§za) [§=a>§ A] §sa [§rb/§zb] (ETA §TA)",