From a4665ae277ae39bc3b8624b41a22db57272bf04b Mon Sep 17 00:00:00 2001 From: Martinez Date: Fri, 13 May 2016 14:44:12 +0200 Subject: [PATCH] finished all of over except app, which will need a lot more attention --- over/__init__.py | 14 +- over/aux.py | 7 +- over/cmd.py | 54 +-- over/cython_types.pyx | 97 +--- over/file.py | 108 +---- over/m/__init__.py | 28 -- over/m/cython_m.pyx | 473 ------------------- over/{m.py => m/float.py} | 11 +- over/m/{python_m.py => matrix_vector.pyx} | 145 +++--- over/misc.py | 100 ++-- over/python_types.py | 97 +--- over/serial/com.py | 55 ++- over/text.py | 546 +++++++++++----------- 13 files changed, 529 insertions(+), 1206 deletions(-) delete mode 100644 over/m/cython_m.pyx rename over/{m.py => m/float.py} (76%) rename over/m/{python_m.py => matrix_vector.pyx} (85%) diff --git a/over/__init__.py b/over/__init__.py index 19889fc..8c55862 100644 --- a/over/__init__.py +++ b/over/__init__.py @@ -3,16 +3,24 @@ import sys +#from . import app from . import aux +from . import cmd +from . import file +from . import misc from . import text +from . import version + +print = text.Output("over.__init__", stream=sys.stderr) try: from . import cython_types as types except: - aux._print('unable to load C implementation, using python instead', text.prefix.warn) from . import python_types as types + + print("unable to load C implementation, using python instead", print.tl.warn) -core = aux.DeprecationForwarder(sys.modules[__name__], 'over.core', 'over') -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 diff --git a/over/aux.py b/over/aux.py index c1c51a1..018fb87 100644 --- a/over/aux.py +++ b/over/aux.py @@ -6,9 +6,9 @@ import traceback from . import text -_print = text.Output('over.core', stream=sys.stderr) - class DeprecationForwarder: + print = text.Output("over.aux.DeprecationForwarder", stream=sys.stderr) + def __init__(self, target, old_name, new_name): self._target = target self._old_name = old_name @@ -16,5 +16,6 @@ class DeprecationForwarder: def __getattr__(self, name): 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) diff --git a/over/cmd.py b/over/cmd.py index 6045017..5895bea 100644 --- a/over/cmd.py +++ b/over/cmd.py @@ -27,27 +27,27 @@ def char_in_str(chars, string): return False class Command: - ''' + """ A shell command with argument substitution and output capture. - >>> c = Command('process.sh', '-x', 'ARGUMENT') - >>> c.ARGUMENT = 'file.txt' + >>> c = Command("process.sh", "-x", "ARGUMENT") + >>> c.ARGUMENT = "file.txt" >>> c.dump() - ['process.sh', '-x', 'file.txt'] + ["process.sh", "-x", "file.txt"] >>> c.run(stderr=False) # capture stdout >>> c.get_output() - b'some output' + b"some 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() - b'more of it\nand some more' + b"more of it\nand some more" >>> c.get_output() None # indicates the process ended and there is no more output - ''' + """ def __init__(self, *sequence): - self.__dict__['sequence_original'] = list(sequence) + self.__dict__["sequence_original"] = list(sequence) self.reset() def __setattr__(self, name, value): @@ -59,14 +59,14 @@ class Command: found = True if not found: - raise AttributeError('Command has no attribute \'%s\'' %(name)) + raise AttributeError('Command has no attribute "%s"' %(name)) def reset(self): - self.__dict__['sequence'] = list(self.sequence_original) - self.__dict__['thread'] = None - self.__dict__['fifo'] = None - self.__dict__['terminated'] = False - self.__dict__['returncode'] = None + self.__dict__["sequence"] = list(self.sequence_original) + self.__dict__["thread"] = None + self.__dict__["fifo"] = None + self.__dict__["terminated"] = False + self.__dict__["returncode"] = None def dump(self, sequence=None, pretty=False): out = [] @@ -88,30 +88,30 @@ class Command: return out def run(self, stderr=False): - ''' + """ Executes the command in the current environment. 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__['fifo'] = queue.Queue() - self.__dict__['thread'] = threading.Thread( + self.__dict__["process"] = subprocess.Popen(self.dump(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=1) + self.__dict__["fifo"] = queue.Queue() + self.__dict__["thread"] = threading.Thread( target=capture_output, 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.start() def get_output(self, blocking=False): - ''' + """ 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. blocking block until some output is available or the process terminates - ''' + """ buffer = [] @@ -125,8 +125,8 @@ class Command: buffer.append(self.fifo.get_nowait()) # FIXME nowait needed? if None in buffer: - self.__dict__['terminated'] = True - self.__dict__['returncode'] = self.process.poll() + self.__dict__["terminated"] = True + self.__dict__["returncode"] = self.process.poll() if len(buffer) == 1: return None @@ -134,7 +134,7 @@ class Command: assert(buffer[-1] is None) del buffer[-1] - return b''.join(buffer) + return b"".join(buffer) def get_all_output(self): buffer = [] @@ -147,4 +147,4 @@ class Command: else: buffer.append(chunk) - return b''.join(buffer) if buffer else None \ No newline at end of file + return b"".join(buffer) if buffer else None diff --git a/over/cython_types.pyx b/over/cython_types.pyx index c3f569a..9b4705a 100644 --- a/over/cython_types.pyx +++ b/over/cython_types.pyx @@ -1,105 +1,32 @@ #! /usr/bin/env python3 # encoding: utf-8 +from collections import OrderedDict + # -------------------------------------------------- -cdef class ndict(dict): - ''' - A dictionary superclass whose keys are exposed as attributes. +cdef class ndict(OrderedDict): + """ + An OrderedDict subclass whose keys are exposed as attributes. >>> d = ndict() >>> d.alpha = 1 - >>> d['alpha'] + >>> d["alpha"] 1 - >>> d['beta'] = 42 + >>> d["beta"] = 42 >>> d.beta 42 >>> d - {'alpha': 1, 'beta': 42} - ''' + {"alpha": 1, "beta": 42} + """ def __getattr__(self, str name): if name in self: return self[name] - elif name.replace('_', '-') in self: - return self[name.replace('_', '-')] + elif name.replace("_", "-") in self: + return self[name.replace("_", "-")] 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): 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)) diff --git a/over/file.py b/over/file.py index f793f91..e078031 100644 --- a/over/file.py +++ b/over/file.py @@ -3,101 +3,33 @@ 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: - ''' - 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): - ''' +def count_lines(path): + """ A reasonably fast and memory-lean line counter. - ''' + """ 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): - lines += buffer.count(b'\n') + lines += buffer.count(b"\n") return lines diff --git a/over/m/__init__.py b/over/m/__init__.py index 569ddfd..e69de29 100644 --- a/over/m/__init__.py +++ b/over/m/__init__.py @@ -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 \ No newline at end of file diff --git a/over/m/cython_m.pyx b/over/m/cython_m.pyx deleted file mode 100644 index 8fa9d7d..0000000 --- a/over/m/cython_m.pyx +++ /dev/null @@ -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 = 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' %(self.stack)) - print('--- self.m = %d (+%d)' %(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 = 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) diff --git a/over/m.py b/over/m/float.py similarity index 76% rename from over/m.py rename to over/m/float.py index 2f36f5f..d25fe5e 100644 --- a/over/m.py +++ b/over/m/float.py @@ -7,6 +7,10 @@ class compare_float: """ def __init__(self, A, B, epsilon=1e-12): + self.A = A + self.B = B + self.epsilon = epsilon + self.lt = False self.le = True self.eq = False @@ -33,11 +37,14 @@ class compare_float: self.greater_or_equal = self.ge self.greater = self.gt self.not_equal = self.ne + + def __repr__(self): + return "compare_float(%.2f, %.2f, epsilon=%.03e)" %(self.A, self.B, self.epsilon) if __name__ == '__main__': 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) assert x.eq and x.le and x.ge and not (x.lt or x.gt) 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) diff --git a/over/m/python_m.py b/over/m/matrix_vector.pyx similarity index 85% rename from over/m/python_m.py rename to over/m/matrix_vector.pyx index d1fb0a5..e5eea13 100644 --- a/over/m/python_m.py +++ b/over/m/matrix_vector.pyx @@ -1,16 +1,16 @@ -#! /usr/bin/env python3 +#! /bin/env python3 # 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 - -class mat4: - ''' +cdef class mat4: + """ A float 4x4 matrix. All arrays are column-major, i.e. OpenGL style: @@ -21,32 +21,30 @@ class mat4: 3 7 11 15 The matrix implements stacking useful for graphics. - ''' + """ def __cinit__(mat4 self): to_alloc = 16 * sizeof(float) self.stack = malloc(to_alloc) 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.size = 1 def _debug(mat4 self): - print('--- self.stack = %d' %(self.stack)) - print('--- self.m = %d (+%d)' %(self.m, self.m - self.stack)) - print('--- self.size = %d' %(self.size)) + print("--- self.stack = %d" %(self.stack)) + print("--- self.m = %d (+%d)" %(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). - ''' - - self.m = + """ length = len(args) @@ -55,7 +53,7 @@ class mat4: length = len(args) 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) @@ -75,7 +73,7 @@ class mat4: matrices = length//16 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 @@ -90,20 +88,22 @@ class mat4: def __getitem__(mat4 self, int i): 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] def __setitem__(self, int i, value): 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 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.stack points to the first matrix @@ -122,12 +122,12 @@ class mat4: if tmp: self.stack = tmp 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 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 cdef uint8_t i cdef float *old_m = self.m - 16 @@ -135,12 +135,12 @@ class mat4: 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') + raise IndexError("pop from an empty stack") self.m -= 16 @@ -153,14 +153,14 @@ class mat4: 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 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() @@ -169,7 +169,7 @@ class mat4: length = len(L) if length > 16: - raise GeneralError('supplied list is longer than 16') + raise ValueError("supplied list is longer than 16") for i in range(16): if i < length: @@ -178,13 +178,17 @@ class mat4: self.m[i] = 0.0 def zero(mat4 self): - '''Fill the matrix with zeroes.''' + """ + Fill the matrix with zeroes. + """ for i in range(16): self.m[i] = 0.0 def identity(mat4 self): - '''Make the matrix an identity.''' + """ + Make the matrix an identity. + """ self.zero() @@ -194,7 +198,9 @@ class mat4: self.m[15] = 1.0 def transpose(mat4 self): - '''Transpose the matrix.''' + """ + Transpose the matrix. + """ cdef float tmp @@ -223,7 +229,9 @@ class mat4: self.m[9] = tmp def invert(mat4 self): - '''Invert the matrix.''' + """ + Invert the matrix. + """ cdef float tmp[16] cdef float det @@ -237,7 +245,7 @@ class mat4: # epsilon pulled straight out of Uranus if det < 0.00005 and det > -0.00005: - print('det=%.1f' %(det)) + 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] @@ -272,11 +280,11 @@ class mat4: 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() @@ -305,11 +313,11 @@ class mat4: 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() @@ -320,11 +328,11 @@ class mat4: 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 @@ -337,15 +345,15 @@ class mat4: 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])) + 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) + return "\n".join(lines) cdef class vec3: - ''' + """ A float 3D vector. >>> v = vec3(1, 1, 0) @@ -361,14 +369,14 @@ cdef class vec3: >>> 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) @@ -377,7 +385,7 @@ cdef class vec3: length = len(args) 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): if i < length: @@ -387,18 +395,18 @@ cdef class vec3: def __getitem__(vec3 self, int i): 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] def __setitem__(vec3 self, int i, float value): 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 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): return (self.v[0], self.v[1], self.v[2]) @@ -410,12 +418,16 @@ cdef class vec3: @property 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) def normalized(vec3 self): - '''Returns this vector, normalized.''' + """ + Returns this vector, normalized. + """ length = self.length @@ -431,29 +443,28 @@ cdef class vec3: 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) diff --git a/over/misc.py b/over/misc.py index 4e2ab10..6a43752 100644 --- a/over/misc.py +++ b/over/misc.py @@ -1,38 +1,36 @@ #! /usr/bin/env python3 # encoding: utf-8 -import copy -import imp -import io -import math -import os import sys -from . import text - # -------------------------------------------------- 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. - ''' + """ + + import imp + import os + + path = os.path.expanduser(path) # remove the .py suffix - mod_dn = os.path.dirname(path) - mod_fn = os.path.basename(path) + mod_dirname = os.path.dirname(path) + mod_filename = os.path.basename(path) - if mod_fn.endswith('.py'): - mod_name = mod_fn[:-3] + if mod_filename.endswith(".py"): + mod_name = mod_filename[:-3] else: # packages for example - mod_name = mod_fn + mod_name = mod_filename fd = None try: - data = imp.find_module(mod_name, [mod_dn]) + data = imp.find_module(mod_name, [mod_dirname]) module = imp.load_module(mod_name, *data) fd = data[0] @@ -45,24 +43,24 @@ def import_module(path): # -------------------------------------------------- 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): 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. + """ - tab_completion enables completion of object names using TAB, however note - that this will make pasting formatted snippets of code difficult - ''' - - import code, readline, rlcompleter + import code + import copy + import readline + import rlcompleter environment = copy.copy(locals) @@ -72,17 +70,17 @@ def console(locals, globals=None, tab_completion=False): environment[key] = value if tab_completion: - readline.parse_and_bind('tab: complete') + readline.parse_and_bind("tab: complete") c = code.InteractiveConsole(environment) - c.interact(banner='') + c.interact(banner="") # -------------------------------------------------- def debugger(): - ''' + """ Drops into the Python Debugger where called. - ''' + """ 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): """ - 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. - 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_ascii is True, each line is suffixed with its ASCII representation. Unprintable characters + 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_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 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 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 - format_str = '%%0%dx ' %(offset_figures) + format_str = "%%0%dx " %(offset_figures) ptr = 0 if show_header: line = [] 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): - line.append('%2x' %(i)) + line.append("%2x" %(i)) 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:]: if indent: - output_io.write(' ' * indent) + output_io.write(" " * indent) if show_offsets: 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)): if i < len(data): c = data[i] - hex_bytes.append('%02x' %(c)) + hex_bytes.append("%02x" %(c)) if 0x20 <= c <= 0x7e: ascii_bytes.append(chr(c)) else: - ascii_bytes.append('.') + ascii_bytes.append(".") 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")) - output_io.write(' '.join(hex_bytes)) + output_io.write(" ".join(hex_bytes)) if use_colors: output_io.write(text.render("§/")) if show_ascii: 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('\n') + output_io.write("\n") ptr += offset diff --git a/over/python_types.py b/over/python_types.py index 3b2c573..5666a75 100644 --- a/over/python_types.py +++ b/over/python_types.py @@ -1,105 +1,32 @@ #! /usr/bin/env python3 # encoding: utf-8 +from collections import OrderedDict + # -------------------------------------------------- -class ndict(dict): - ''' - A dictionary superclass whose keys are exposed as attributes. +class ndict(OrderedDict): + """ + An OrderedDict subclass whose keys are exposed as attributes. >>> d = ndict() >>> d.alpha = 1 - >>> d['alpha'] + >>> d["alpha"] 1 - >>> d['beta'] = 42 + >>> d["beta"] = 42 >>> d.beta 42 >>> d - {'alpha': 1, 'beta': 42} - ''' + {"alpha": 1, "beta": 42} + """ def __getattr__(self, name): if name in self: return self[name] - elif name.replace('_', '-') in self: - return self[name.replace('_', '-')] + elif name.replace("_", "-") in self: + return self[name.replace("_", "-")] else: - raise AttributeError("'ndict' object has no attribute '%s'" %(name)) + raise AttributeError('"ndict" object has no attribute "%s"' %(name)) def __setattr__(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)) diff --git a/over/serial/com.py b/over/serial/com.py index 6b075c4..fb8308e 100644 --- a/over/serial/com.py +++ b/over/serial/com.py @@ -4,55 +4,60 @@ import time import serial -from . import et +from .. import text -prefix = et.prefix +""" +OverTalk protocol spec +====================== -''' -===== OverTalk protocol spec ===== -==== Transport layer ==== +Transport layer +--------------- +TODO: Swap checksum and payload. 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. Frames with destinations unknown to an OverShell are sent to the default interface, or dropped if that's the interface they came from. -==== Command layer ==== +Command layer +------------- +NOTE: This is going to change. + Payload consists of one or more commands: get_command = [ 0x00 | register (1) | target register (1) ] 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 -a set_command that sets the sender's 'target register' to that value. +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 does what is says on the box. -''' +""" class Transport: - ''' + """ 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. 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). - ''' + """ def __init__(self, my_id, interfaces, def_route): - ''' + """ @param my_id OverTalk address of this Transport instance @param interfaces a dict of interfaces keyed by their IDs @param def_route ID of default interface for sending - ''' + """ assert my_id not 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.def_route = def_route @@ -61,7 +66,7 @@ class Transport: self.destination_unknown = 0 self.incoming = None - self._print('on-line') + self.print("on-line", text.Output.tl.done) def update(self): self.incoming = None @@ -95,7 +100,7 @@ class Transport: buffer_length = len(interface.rxbuffer) 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) # a frame has been read, huzzah! interface.reading_frame = False @@ -106,7 +111,7 @@ class Transport: else: interface.malformed_frames += 1 - self._print('broken frame received: %s' %(interface.rxbuffer)) + self.print("broken frame received: §r§%s§.§" %(interface.rxbuffer)) interface.rxbuffer = [] return False @@ -133,15 +138,15 @@ class Transport: self.incoming = (frame[2], payload) else: 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)) else: if source_interface_id == self.def_route: self.destination_unknown += 1 - self._print('unknown destination %d for frame: %s' %(destination, - repr(frame)), prefix.fail) + self.print("unknown destination %d<.> for frame: %s<.>" %(destination, + repr(frame)), text.Output.tl.fail) 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)) def send_data(self, destination, data): @@ -157,7 +162,7 @@ class Transport: s.write(frame) class TTL_Interface: - def __init__(self, interface='/dev/ttyUSB0', baudrate=57600): + def __init__(self, interface="/dev/ttyUSB0", baudrate=57600): try: self.s = serial.Serial(interface, baudrate, timeout=1) except serial.serialutil.SerialException: @@ -184,7 +189,7 @@ class TTL_Interface: return 0 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: self.s.write(bytes(data)) diff --git a/over/text.py b/over/text.py index 96244d0..1bfe467 100644 --- a/over/text.py +++ b/over/text.py @@ -1,13 +1,14 @@ #! /usr/bin/env python3 # encoding: utf-8 +import datetime +import fcntl import math import re -import sys import struct -import fcntl -import termios +import sys 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 where x is the color code from colortags. + <.> resets the color. Use < for a literal . + """ + + 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 = " ?<.>" + start = ">>><.>" + exec = start + warn = " #<.>" + fail = "!!!<.>" + done = " *<.>" + class long: + info = "INFO" + debug = "DEBG<.>" + start = "EXEC<.>" + exec = start + warn = "WARN<.>" + fail = "FAIL<.>" + done = "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: + - tag + - name + - supplied text + """ + + def __init__(self, name, format="[%Y-%m-%d %H:%M:%S] -- , ", 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("", tag) + output = output.replace("", self.name) + output = output.replace("", 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: + 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): self.unit = unit self.top = top @@ -60,7 +331,7 @@ class _ProgressBarChannel: self.set() 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): 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__": pb = ProgressBar( "§%a (§ra/§za) [§=a>§ A] §sa [§rb/§zb] (ETA §TA)",