over.core is mostly working

This commit is contained in:
Overwatch 2014-08-13 00:19:54 +02:00
parent 5baa9b75d0
commit 0df57ad386
23 changed files with 1528 additions and 1039 deletions

View file

@ -1,2 +0,0 @@
* sjednotit parsing cmdlajny a configu
* integrovat se zshellem

View file

@ -0,0 +1,14 @@
#! /bin/env python3
# encoding: utf-8
from . import app
from . import aux
from . import file
from . import misc
from . import textui
try:
from . import cython_types as types
except:
aux._print("unable to load C implementation, using python instead", textui.prefix.warn)
from . import python_types as types

View file

@ -1,3 +1,14 @@
#! /bin/env python3
# encoding: utf-8
import sys
# FIXME This very seriously needs to be heavily simplified and de-duplicated
# TODO same parser for cmdline and Config
# TODO zsh and bash integration
# --------------------------------------------------
def _parse(data, dtype):
if dtype == "str":
if data.startswith("\"") and data.endswith("\""):
@ -35,11 +46,15 @@ _over_help_texts = [
("Other", ["When enabled in the program, an OverCore program offers two options: §B--§ghelp§/ and §B--§gover-help§/. The first describes the program and its options, including their types, current values and whether is their current value coming from defaults, config files or the command line. The second option displays the help you're reading right now. You may now guess which rank I hold in the Obvious Corps.", "As the brighter amongst you might have noticed, OverCore likes colors. A lot. Generally, I use blue for §bdata types§/, magenta for §mvalues§/, white and green for §B--§goptions§/ and reserve red and yellow for when §rshit hits§/ §ythe fan§/, where red letters usually tell you §Bwhy§/ and yellow ones tell you §Bwhat§/.", "And it's not just colors! I like money, too. Push donations to §B1sekErApM4zh35RFW7qGWs5Yeo9EYWjyV§/, or catch me somewhere and force me to accept cash."])
]
# --------------------------------------------------
def _output(text, indent=0, newlines=1):
sys.stdout.write(render(paragraph(text, indent=indent), True))
sys.stdout.write(newlines * "\n")
sys.stdout.flush()
# --------------------------------------------------
def _print_help(main, help_texts, chapter=None, list_options=False):
"""
Displays a help text and then quits the program.
@ -131,6 +146,8 @@ _allowed_types = {
"float": [int, float]
}
# --------------------------------------------------
def _verify_type(data, dtype, plural=False):
if plural:
if type(data) == list:
@ -150,6 +167,8 @@ def _verify_type(data, dtype, plural=False):
else:
return False
# --------------------------------------------------
class CfgAccessor:
"""
Provides convenient access to configuration options' values.
@ -171,6 +190,8 @@ class CfgAccessor:
else:
self.main._print("option §r%s§/ doesn't exist" %(opt_name), prefix.fail, exc=GeneralError)
# --------------------------------------------------
class Main:
"""
Application backbone. Provides:
@ -607,6 +628,8 @@ class Main:
def help_over(self, *dummy):
_print_help(self, _over_help_texts)
# --------------------------------------------------
class Option:
def __init__(self, name, dtype, default, description, short_name, plural, use_cfg_file, callback, hidden):
"""

8
core/aux.py Normal file
View file

@ -0,0 +1,8 @@
#! /bin/env python3
# encoding: utf-8
import sys
from . import textui
_print = textui.Output("over.core", stream=sys.stderr)

View file

@ -1,17 +0,0 @@
import os
import re
import sys
import time
import struct
import termios
import fcntl
_version = 2.1
class GeneralError(Exception):
"""General string error. Thrown around a lot these days."""
def __init__(self, description=None):
self.description = description
def __str__(self):
return self.description

View file

@ -1,175 +0,0 @@
# def textfilter(text, set=None):
# """
# str text text to filter
# dict set {to_replace: replacement}
# Text filter that replaces occurences of to_replace keys with their respective values.
# Defaults to filtering of 'bad' characters if no translational dictionary is provided.
# """
# if not set:
# set = badchars
# for to_replace in set.keys():
# text = text.replace(to_replace, set[to_replace])
# return text
# def cut(text, size, end=0, colors=True):
# """
# str text text to cut
# int size how many chars to return, >=0
# int end return chars from 0 = left, 1 = right
# bool colors skip color tags
# """
# out = ""
# strlen = len(text)
# if end == 0:
# ptr = 0 # go from left
# else:
# ptr = strlen - 1
# while size:
# if end == 0:
# if colors and text[ptr] == "<" and strlen - ptr >= 4 and text[ptr+1] == "C" and text[ptr+3] == ">": # we have a tag
# out += text[ptr:ptr+4]
# ptr += 4
# else:
# out += text[ptr]
# ptr += 1
# size -= 1
# else:
# if colors and text[ptr] == ">" and ptr >= 4 and text[ptr-2] == "C" and text[ptr-3] == "<": # we have a tag
# out = text[ptr-3:ptr+1] + out
# ptr -= 4
# else:
# out = text[ptr] + out
# ptr -= 1
# size -= 1
# # reached end ?
# if (end == 0 and ptr == strlen) or (end == 1 and ptr == -1):
# break
# return out
# def strtrim(text, length, mode=0, dots=True, colors=True):
# """
# str text text to trim
# int length desired length, >1
# int mode -1 = cut chars from the left, 0 = from the middle, 1 = from the right
# bool dots add an ellipsis to the point of cutting
# bool colors dont count color tags into length; also turns the ellipsis blue
# """
# if len(text) <= length:
# return text
# if length <= 3:
# dots = False
# if dots:
# length -= 3
# if mode == -1:
# if dots:
# return "..." + cut(text, length, 1, colors)
# else:
# return cut(text, length, 1, colors)
# elif mode == 0:
# if length%2 == 1:
# part1 = cut(text, length/2+1, 0, colors)
# else:
# part1 = cut(text, length/2, 0, colors)
# part2 = cut(text, length/2, 1, colors)
# if dots:
# part1 += "..."
# return part1 + part2
# else:
# if dots:
# return cut(text, length, 0, colors) + "..."
# else:
# return cut(text, length, 0, colors)
def paragraph(text, width=0, indent=0, prefix=None, stamp=None):
"""
str text text to format
int width required line length; if 0, current terminal width will be used;
if negative, current terminal width minus supplied amount will be used
int indent how many spaces to indent the text with
str prefix prefix each line with it; not counted in width (offsets the lines)
str stamp placed over the first line's indent (stretching the indent if necessary)
Formats text into an indented paragraph that fits the terminal and returns it.
Correctly handles color tags.
"""
#words = [x.strip() for x in text.split() if x.strip()]
words = text.split()
term_width = get_terminal_size()[1]
if not width:
width = term_width
elif width < 0:
width += term_width
lines = [] # individual lines go in this buffer
first = True
while words:
if indent:
if first and stamp:
lines.append([stamp + " "*(indent-1-len(stamp))])
else:
lines.append([" "*(indent-1)]) # first word = indent minus one space (that's gonna get back while joining)
first = False
else:
lines.append([])
while words:
word_added = False
if len(re.sub("§.", "", ' '.join(lines[-1]))) + len(re.sub("§.", "", words[0])) + 1 <= width:
lines[-1].append(words.pop(0))
word_added = True
elif not word_added and len(lines[-1]) == 1 and indent:
# no word added and just the indent's in here = word's too long -> screw indent
# we might try to keep at least a part of the indent - if possible
len_word = len(re.sub("§.", "", words[0]))
if len_word < width:
lines[-1] = [" "*(width - len_word - 1), words.pop(0)]
else:
lines[-1] = [words.pop(0)]
word_added = True
break
elif not word_added and not lines[-1] and not indent:
# no word added, empty line = word's too long -> screw indent
lines[-1] = [words.pop(0)]
word_added = True
break
else:
break
lines_tmp = []
for line in lines:
if prefix:
line.insert(0, prefix)
lines_tmp.append(' '.join(line)) # put words together
return '\n'.join(lines_tmp) # put lines together

View file

@ -1,153 +0,0 @@
class prefix:
info = (" ", "INFO")
debug = (" §b?§/", "§bDEBG§/")
start = ("§B>>>§/", "§BEXEC§/")
exec = start
warn = (" §y#§/", "§yWARN§/")
fail = ("§r!!!§/", "§rFAIL§/")
done = (" §g*§/", "§gDONE§/")
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"
}
def render(text, colors=True):
"""
Processes text with color tags and either
removes them (with colors=False) or replaces
them with terminal color codes.
"""
text = str(text)
if colors:
tags = re.findall('§[^§]', text)
for tag in tags:
try:
text = text.replace(tag, colortags[tag])
except KeyError:
pass
else:
text = re.sub('§[^§]', '', text)
# unescape actual paragraphs
text = re.sub('§§', '§', text)
return text
def char_length(string):
"""
Returns the length of a string minus all formatting tags.
"""
plain_string = render(string, colors=False)
return len(plain_string)
class Output:
"""
Text UI output renderer.
Prints messages to the stdout with optional eye candy
like colors and timestamps.
Usage:
>>> from over import Output, prefix
>>> say = Output("test", timestamp=True)
>>> say("system initialized")
[2013-02-28 16:41:28] INFO -- test, system initialized
>>> say("system is FUBAR", prefix.fail)
[2013-02-28 16:41:46] FAIL -- test, system is FUBAR
>>> say("I just realized this will not work", prefix.fail, timestamp=False)
!!! I just realized this will not work
TODO initialize with target output streams (which only need to implement .write)
"""
def __init__(self, name, default_suffix="\n", timestamp=False, colors=True, default_prefix=prefix.info, tb=False):
self.name = name
self.timestamp = timestamp
self.colors = colors
self.default_prefix = default_prefix
self.default_suffix = default_suffix
self.tb = tb
def __call__(self, text, prefix=None, suffix=None, indent=0, timestamp=None, colors=None, display_name=True, exc=None, tb=None):
if prefix is None:
prefix = self.default_prefix
if type(prefix) is str:
prefix = (prefix, prefix)
if suffix is None:
suffix = self.default_suffix
if timestamp is None:
timestamp = self.timestamp
if colors is None:
colors = self.colors
if tb is None:
tb = self.tb
output = []
# [2012-11-11 16:52:06] INFO -- ahoj
if timestamp:
output.append(time.strftime('[%Y-%m-%d %H:%M:%S] '))
output.append(prefix[1])
output.append(" -- ")
elif prefix:
output.append(prefix[0])
output.append(" ")
if display_name and self.name:
output.append("%s, " %(self.name))
#output.append(paragraph(str(text), indent=indent))
output.append(str(text))
if suffix:
output.append(suffix)
output = "".join(output)
sys.stdout.write(render(output, colors))
sys.stdout.flush()
if exc:
if exc is True:
if tb:
raise
else:
if sys.exc_info()[0]:
self.__call__("unhandled exception %s raised" %(sys.exc_info()[0].__name__), timestamp=True)
#sys.exit(1)
raise
else:
if tb:
raise exc(render(text, colors=False))
else:
self.__call__("unhandled exception %s raised" %(exc.__name__), timestamp=True)
raise exc
#sys.exit(1)
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)

View file

@ -1,51 +0,0 @@
class File:
"""
A binary r/w file container that transparently abstracts file descriptors away. You just read and write data.
Nonexistent files will be created, including any directories if necessary.
"""
# def __init__(self, path, str encoding="utf-8"):
def __init__(self, path, encoding="utf-8"):
self.encoding = encoding
self._print = Output("over.File", timestamp=True)
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):
self._print("path §r%s§/ exists but is not a file" %(self.path), prefix.fail, exc=GeneralError)
else:
dirname = os.path.dirname(self.path)
if dirname and not os.path.isdir(dirname):
self._print("creating directory §B%s§/" %(dirname), prefix.start)
os.makedirs(dirname)
# create the file
touch(self.path)
@property
def data(self):
"""
Reads the file and returns the contents.
"""
fd = open(self.path, encoding=self.encoding)
data = fd.read()
fd.close()
return data
@data.setter
def data(self, data):
"""
Writes data into the file.
"""
fd = open(self.path, "w", encoding=self.encoding)
fd.write(data)
fd.close()

View file

@ -1,340 +0,0 @@
# class Autoloader:
# """
# Python module autoloader. To activate, call et.autoloader.register() from python shell.
# Try not to rely on it as it'll betray you when there's more than one new module in a single command.
# """
# def __init__(self):
# self.original_handler = sys.excepthook
# def register(self, deact=False):
# if not deact:
# sys.excepthook = self
# else:
# sys.excepthook = self.original_handler
# def __call__(self, exctype, value, trace):
# if exctype == NameError:
# module_name = textfilter(value.args[0], {"name '": "", "' is not defined": ""})
# #retval = _exec("import %s" % module_name, trace)
# command_locals = trace.tb_frame.f_locals
# command_globals = trace.tb_frame.f_globals
# try: # retval relates to import success only
# exec "import %s" % module_name in command_locals, command_globals
# retval = True
# output("Autoloaded module §g%s§/" %(module_name), 0, timestamp=True)
# exec trace.tb_frame.f_code in command_locals, command_globals
# retval = True
# except ImportError:
# retval = False
# except:
# traceback.print_exc()
# if exctype != NameError or not retval:
# traceback.print_exception(exctype, value, trace)
# def wait(seconds_target, text="", minutes=True, progress_bar=True, countdown=False):
# """
# int seconds how long to wait
# str text display this text before the timer
# bool minutes use mm:ss ?
# bool progress_bar display a progress bar ? TODO
# bool countdown count down instead of up
# Wait clock. Don't use it on larger scales, i.e. hours, as it accumulates little error every tick
# and you may end up waiting extra minutes or even dead.
# Returns True if zero is reached, False if interrupted.
# """
# seconds = 0
# try:
# while seconds <= seconds_target:
# if countdown:
# seconds_use = seconds_target - seconds
# else:
# seconds_use = seconds
# if minutes:
# mins = str(seconds_use/60).zfill(2)
# secs = str(seconds_use%60).zfill(2)
# mins_target = str(seconds_target/60).zfill(2)
# secs_target = str(seconds_target%60).zfill(2)
# time_tag = "%s:%s / %s:%s" %(mins, secs, mins_target, secs_target)
# else:
# time_tag = "%s / %s" %(seconds_use, seconds_target)
# prog_bar = " "
# if progress_bar:
# # determine how much space we have to crap up... I mean use
# # 13 = 8 spaces at the end, 5 for ' [>] '
# width_max = get_terminal_size()[1] - len(re.sub("§.", "", text)) - len(time_tag) - 20
# left_side = int((seconds/float(seconds_target)) * width_max)
# right_side = width_max - left_side
# prog_bar = " [%s>%s] " %('='*left_side, ' '*right_side)
# output(text+prog_bar+time_tag+' \r', 0, newline=False)
# if not seconds == seconds_target:
# time.sleep(1)
# seconds += 1
# except (EOFError, KeyboardInterrupt):
# output(text+prog_bar+time_tag+' ', 3)
# return False
# else:
# output(text+prog_bar+time_tag+' ', 2)
# return True
# def progress(part, width=None, before="", after="", show_percent=True, newline=False):
# """
# Display a progress bar.
# float part 0.0 to 1.0
# int width width in cols, defaults to terminal width
# str before text displayed before the progress bar
# str after text displayed after the progress bar
# bool show_percent display percent after the bar (but before the after part)
# bool newline append a \n
# """
# if not width:
# width = get_terminal_size()[1]
# width_bar = width - 2 - 2 - 1
# # a b c
# # a = space on each side of the terminal
# # b = [ and ]
# # c = >
# if before:
# width_bar -= 1 + len(before)
# before = before + " "
# if after:
# width_bar -= 1 + len(after)
# after = " " + after
# if show_percent:
# width_bar -= 5
# percent = "%s%% " %(str(int(round(part*100))).rjust(3))
# if not newline:
# r_part = "\r"
# else:
# r_part = ""
# output(" %s%s[%s>%s]%s %s" %(before, percent, "="*int(part*width_bar), " "*(int((1-part)*width_bar)), after, r_part), newline=newline)
# class Unit:
# _prefixes = ( ("Y", 24), ("Z", 21), ("E", 18), ("P", 15), ("T", 12), ("G", 9), ("M", 6), ("k", 3), ("", 0),
# ("m", -3), ("μ", -6), ("n", -9), ("p", -12), ("f", -15), ("a", -18), ("z", -21), ("y", -24))
# #("c", -2),
# def __init__(self, initializer, force_unit=None, dimension=1, space=True):
# self.space = space
# self.dimension = dimension
# if type(initializer) == str: # init from text
# integer, decimal, unit = re.findall("(\d+)[.,]*(\d*)\s*(\D+)", initializer)[0]
# self.value = float("%s.%s" %(integer, decimal))
# if len(unit) >= 2:
# prefix = self._prefix_to_multiplier(unit[0])
# if prefix != None:
# self.value *= 10**(prefix*self.dimension)
# self.unit = unit[1:]
# else:
# self.unit = unit
# else:
# self.unit = unit
# if force_unit:
# self.unit = force_unit
# else: # init from float, str unit and int dimension
# self.value = float(initializer)
# self.unit = force_unit
# def _prefix_to_multiplier(self, prefix):
# for p, mul in self._prefixes:
# if p == prefix:
# return mul
# return None
# def __repr__(self):
# value = self.value
# if value < 0:
# sign = -1
# value *= -1
# else:
# sign = 1
# if value == 0.0:
# e = 0
# else:
# e = round(math.log(value, 10), 6)
# for prefix, mul in self._prefixes:
# if mul*self.dimension <= e:
# break
# if self.unit:
# unit = self.unit
# else:
# unit = ""
# if self.space:
# space = " "
# else:
# space = ""
# return "%.2f%s%s%s" %(sign*value/10**(mul*self.dimension), space, prefix, unit)
# by ephemient@stackoverflow.com
def touch(fname, times=None):
with open(fname, 'a'):
os.utime(fname, times)
def console(environment):
"""
Opens up a Python console.
Typical usage: over.console(locals())
"""
import code, readline, rlcompleter
readline.parse_and_bind("tab: complete")
_print = Output("over.core", default_suffix=".\n", timestamp=True, tb=False)
_print("opening Python console", prefix.start)
c = code.InteractiveConsole(environment)
c.interact(banner="")
class enum:
"""
Emulates a C++-like enum type.
Based on a py2 enum function by Alec Thomas and acjohnson55.
"""
def __init__(self, name, words, start=0):
self.typename = name
self.reverse_enums = dict(enumerate(words, start))
self.enums = dict((value, key) for key, value in self.reverse_enums.items())
def name(self, value):
if value in self.reverse_enums:
return self.reverse_enums[value]
else:
raise AttributeError("No attribute of %s has a value of %s." %(self, value))
def __getattr__(self, aname):
if aname in self.enums:
return self.enums[aname]
else:
raise AttributeError("%s not in %s." %(aname, self))
def __repr__(self):
return "<enum %s>" %(self.typename)
class map:
def __init__(self, source=None):
"""
source is a zipped list: [(key, value), (key, value), ...]
"""
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))
def batch_gen(data, batch_size):
"""
by rpr (stackoverflow)
"""
for i in range(0, len(data), batch_size):
yield data[i:i+batch_size]
class ndict(dict):
def __init__(self, *args, **kwargs):
dict.__init__(self, *args, **kwargs)
def __getattr__(self, name):
if name in self:
return self[name]
else:
raise AttributeError("'ndict' object has no attribute '%s'" %(name))
def __setattr__(self, name, value):
self[name] = value

View file

@ -1,31 +0,0 @@
import imp
def import_module(path):
"""
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.
"""
# remove the .py suffix
mod_dn = os.path.dirname(path)
mod_fn = os.path.basename(path)
if mod_fn.endswith(".py"):
mod_name = mod_fn[:-3]
else:
# packages for example
mod_name = mod_fn
fd = None
try:
data = imp.find_module(mod_name, [mod_dn])
module = imp.load_module(mod_name, *data)
fd = data[0]
finally:
if fd is not None:
fd.close()
return module

87
core/file.py Normal file
View file

@ -0,0 +1,87 @@
#! /bin/env python3
# encoding: utf-8
import os
from .aux import _print
from .textui import prefix
# --------------------------------------------------
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)

65
core/misc.py Normal file
View file

@ -0,0 +1,65 @@
#! /bin/env python3
# encoding: utf-8
import imp
import os
# --------------------------------------------------
def import_module(path):
"""
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.
"""
# remove the .py suffix
mod_dn = os.path.dirname(path)
mod_fn = os.path.basename(path)
if mod_fn.endswith(".py"):
mod_name = mod_fn[:-3]
else:
# packages for example
mod_name = mod_fn
fd = None
try:
data = imp.find_module(mod_name, [mod_dn])
module = imp.load_module(mod_name, *data)
fd = data[0]
finally:
if fd is not None:
fd.close()
return module
# --------------------------------------------------
def batch_gen(data, batch_size):
"""
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(environment, tab_completion=False):
"""
Opens up a Python console.
environment is a dictionary typically returned by locals() or similar
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
readline.parse_and_bind("tab: complete")
c = code.InteractiveConsole(environment)
c.interact(banner="")

151
core/python_types.py Normal file
View file

@ -0,0 +1,151 @@
#! /bin/env python3
# encoding: utf-8
# --------------------------------------------------
class ndict(dict):
"""
A dictionary superclass whose keys are exposed as attributes.
>>> d = ndict()
>>> d.alpha = 1
>>> d["alpha"]
1
>>> d["beta"] = 42
>>> d.beta
42
>>> d
{'alpha': 1, 'beta': 42}
"""
def __init__(self, *args, **kwargs):
dict.__init__(self, *args, **kwargs)
def __getattr__(self, name):
if name in self:
return self[name]
else:
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.
TODO fixme bug
>>> map(((1, 2, 3, 4), ("a", "b", "c", "d")))
"""
print(repr(source))
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))
# --------------------------------------------------
class enum:
"""
Emulates a C++-like enum type.
Based on a py2 enum function by Alec Thomas and acjohnson55.
"""
def __init__(self, name, words, start=0):
"""
Initializes the enum.
name is used for __repr__ only
words is a sequence of enum keys
start is the numerical value of the first key
"""
self.typename = name
self.reverse_enums = dict(enumerate(words, start))
self.enums = dict((value, key) for key, value in self.reverse_enums.items())
def name(self, value):
if value in self.reverse_enums:
return self.reverse_enums[value]
else:
raise AttributeError("no %s has a value of %s" %(self, value))
def __getattr__(self, aname):
if aname in self.enums:
return self.enums[aname]
else:
raise AttributeError("%s not in %s" %(aname, self))
def __repr__(self):
return "<enum %s>" %(self.typename)
# --------------------------------------------------

475
core/textui.py Normal file
View file

@ -0,0 +1,475 @@
#! /bin/env python3
# encoding: utf-8
import math
import re
import sys
import time
# --------------------------------------------------
class ProgressBar:
"""
An animated progress bar.
TODO derive Wait() from this
"""
def __init__(self, width, top, unit, reverse=False):
"""
width width of the "widget" including all text
top the 100% value
unit name of the unit to display
reverse True to expand the progress bar from right to left
"""
self.width = width
self.value = 0
self.top = top
self.unit = unit
self.reverse = reverse
self.old_len = 0
self.t_start = None
def draw(self):
if not self.t_start:
self.t_start = time.time()
if self.old_len:
sys.stderr.write("\b" * self.old_len)
transferred = Unit(self.value, self.unit)
dt = time.time() - self.t_start
if dt > 0:
speed = self.value / dt
else:
speed = 0.0
speed = Unit(speed, "%s/s" %(self.unit))
ratio = self.value / self.top
pb_done = "=" * int(self.width * ratio)
pb_rem = " " * int(self.width * (1 - ratio))
symbol = ">"
if self.reverse:
symbol = "<"
pb_done, pb_rem = pb_rem, pb_done
text = "%s [%s%s%s] %s" %(transferred, pb_done, symbol, pb_rem, speed)
sys.stderr.write(text)
current_len = len(text)
tail = self.old_len - current_len
self.old_len = current_len
if tail > 0:
sys.stderr.write(" " * tail)
sys.stderr.write("\b" * tail)
sys.stderr.flush()
def update(self, value):
self.value = value
self.draw()
# --------------------------------------------------
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.core.textui.Unit(%s)" %(self)
# --------------------------------------------------
def paragraph(text, width=0, indent=0, prefix=None, stamp=None):
"""
str text text to format
int width required line length; if 0, current terminal width will be used;
- if negative, current terminal width minus supplied amount will be used
int indent how many spaces to indent the text with
str prefix prefix each line with it; not counted in width (offsets the lines)
str stamp placed over the first line's indent (stretching the indent if necessary)
Formats text into an indented paragraph that fits the terminal and returns it.
Correctly handles color tags.
"""
#words = [x.strip() for x in text.split() if x.strip()]
words = text.split()
term_width = get_terminal_size()[1]
if not width:
width = term_width
elif width < 0:
width += term_width
lines = [] # individual lines go in this buffer
first = True
while words:
if indent:
if first and stamp:
lines.append([stamp + " "*(indent-1-len(stamp))])
else:
lines.append([" "*(indent-1)]) # first word = indent minus one space (that's gonna get back while joining)
first = False
else:
lines.append([])
while words:
word_added = False
if len(re.sub("§.", "", ' '.join(lines[-1]))) + len(re.sub("§.", "", words[0])) + 1 <= width:
lines[-1].append(words.pop(0))
word_added = True
elif not word_added and len(lines[-1]) == 1 and indent:
# no word added and just the indent's in here = word's too long -> screw indent
# we might try to keep at least a part of the indent - if possible
len_word = len(re.sub("§.", "", words[0]))
if len_word < width:
lines[-1] = [" "*(width - len_word - 1), words.pop(0)]
else:
lines[-1] = [words.pop(0)]
word_added = True
break
elif not word_added and not lines[-1] and not indent:
# no word added, empty line = word's too long -> screw indent
lines[-1] = [words.pop(0)]
word_added = True
break
else:
break
lines_tmp = []
for line in lines:
if prefix:
line.insert(0, prefix)
lines_tmp.append(' '.join(line)) # put words together
return '\n'.join(lines_tmp) # put lines together
# --------------------------------------------------
# def strtrim(text, length, mode=0, dots=True, colors=True):
# """
# str text text to trim
# int length desired length, >1
# int mode -1 = cut chars from the left, 0 = from the middle, 1 = from the right
# bool dots add an ellipsis to the point of cutting
# bool colors dont count color tags into length; also turns the ellipsis blue
# """
# if len(text) <= length:
# return text
# if length <= 3:
# dots = False
# if dots:
# length -= 3
# if mode == -1:
# if dots:
# return "..." + cut(text, length, 1, colors)
# else:
# return cut(text, length, 1, colors)
# elif mode == 0:
# if length%2 == 1:
# part1 = cut(text, length/2+1, 0, colors)
# else:
# part1 = cut(text, length/2, 0, colors)
# part2 = cut(text, length/2, 1, colors)
# if dots:
# part1 += "..."
# return part1 + part2
# else:
# if dots:
# return cut(text, length, 0, colors) + "..."
# else:
# return cut(text, length, 0, colors)
# --------------------------------------------------
# def textfilter(text, set=None):
# """
# str text text to filter
# dict set {to_replace: replacement}
# Text filter that replaces occurences of to_replace keys with their respective values.
# Defaults to filtering of 'bad' characters if no translational dictionary is provided.
# """
# if not set:
# set = badchars
# for to_replace in set.keys():
# text = text.replace(to_replace, set[to_replace])
# return text
# --------------------------------------------------
# def cut(text, size, end=0, colors=True):
# """
# str text text to cut
# int size how many chars to return, >=0
# int end return chars from 0 = left, 1 = right
# bool colors skip color tags
# """
# out = ""
# strlen = len(text)
# if end == 0:
# ptr = 0 # go from left
# else:
# ptr = strlen - 1
# while size:
# if end == 0:
# if colors and text[ptr] == "<" and strlen - ptr >= 4 and text[ptr+1] == "C" and text[ptr+3] == ">": # we have a tag
# out += text[ptr:ptr+4]
# ptr += 4
# else:
# out += text[ptr]
# ptr += 1
# size -= 1
# else:
# if colors and text[ptr] == ">" and ptr >= 4 and text[ptr-2] == "C" and text[ptr-3] == "<": # we have a tag
# out = text[ptr-3:ptr+1] + out
# ptr -= 4
# else:
# out = text[ptr] + out
# ptr -= 1
# size -= 1
# # reached end ?
# if (end == 0 and ptr == strlen) or (end == 1 and ptr == -1):
# break
# return out
# --------------------------------------------------
class prefix:
info = (" ", "INFO")
debug = (" §b?§/", "§bDEBG§/")
start = ("§B>>>§/", "§BEXEC§/")
exec = start
warn = (" §y#§/", "§yWARN§/")
fail = ("§r!!!§/", "§rFAIL§/")
done = (" §g*§/", "§gDONE§/")
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"
}
def render(text, colors=True):
"""
Processes text with color tags and either
removes them (with colors=False) or replaces
them with terminal color codes.
"""
text = str(text)
if colors:
tags = re.findall('§[^§]', text)
for tag in tags:
try:
text = text.replace(tag, colortags[tag])
except KeyError:
pass
else:
text = re.sub('§[^§]', '', text)
# unescape actual paragraphs
text = re.sub('§§', '§', text)
return text
# --------------------------------------------------
def char_length(string):
"""
Returns the length of a string minus all formatting tags.
"""
plain_string = render(string, colors=False)
return len(plain_string)
# --------------------------------------------------
class Output:
"""
Text UI output renderer.
Prints messages to the stdout with optional eye candy
like colors and timestamps.
Usage:
>>> from over import Output, prefix
>>> say = Output("test", timestamp=True)
>>> say("system initialized")
[2013-02-28 16:41:28] INFO -- test, system initialized
>>> say("system is FUBAR", prefix.fail)
[2013-02-28 16:41:46] FAIL -- test, system is FUBAR
>>> say("I just realized this will not work", prefix.fail, timestamp=False)
!!! I just realized this will not work
TODO use a generic format string
"""
def __init__(self, name, timestamp=True, colors=True, default_prefix=prefix.info, default_suffix=".\n", stream=sys.stdout):
self.name = name
self.timestamp = timestamp
self.colors = colors
self.default_prefix = default_prefix
self.default_suffix = default_suffix
self.stream = stream
def __call__(self, text, prefix=None, suffix=None, indent=0, timestamp=None, colors=None, display_name=True):
if prefix is None:
prefix = self.default_prefix
if type(prefix) is str:
prefix = (prefix, prefix)
if suffix is None:
suffix = self.default_suffix
if timestamp is None:
timestamp = self.timestamp
if colors is None:
colors = self.colors
output = []
# [2012-11-11 16:52:06] INFO -- ahoj
if timestamp:
output.append(time.strftime('[%Y-%m-%d %H:%M:%S] '))
output.append(prefix[1])
output.append(" -- ")
elif prefix:
output.append(prefix[0])
output.append(" ")
if display_name and self.name:
output.append("%s, " %(self.name))
#output.append(paragraph(str(text), indent=indent))
output.append(str(text))
if suffix:
output.append(suffix)
output = "".join(output)
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)