commit 6f28ac03828cdaf97d445365d3c60e6b5fba1aa8 Author: Overwatch Date: Mon Aug 4 16:51:07 2014 +0200 Initial commit (Cython implementation) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..07d2832 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.so +*.pyc +__pycache__ diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..d35e4e0 --- /dev/null +++ b/__init__.py @@ -0,0 +1,141 @@ +#! /bin/env python3 +# encoding: utf-8 + +import time +import sys + +#from . import ag +#from . import gl +#from . import et +#from . import scn +#from . import ui +#from . import cmd +#from . import com +#from . import loop +from . import core + +class ProgressBar: + def __init__(self, width, total=1.0, unit="", prefix=None, suffix=None, show_percent=True, show_absolute=True, show_rate=True, show_etc=True, draw_on_update=True): + self.width = width + self.total = total + self.unit = unit + self.prefix = prefix + self.suffix = suffix + self.show_percent = show_percent + self.show_absolute = show_absolute + self.show_rate = show_rate + self.show_etc = show_etc + self.draw_on_update = draw_on_update + + self.percentage_printf = "%5.1f %%" + self.absolute_printf_total = "%d/%d %s," + self.absolute_printf = "%d %s," + self.rate_printf_total = "%d %s/s," + self.rate_printf = "%d %s/s" + self.etc_printf = "ETA %d s" + + self._current = 0.0 + self._ratio = 0.0 + self._rate = 0.0 + self._runner = 0.0 + + self._start = None + + self._longest_line = 0 + + @property + def current(self): + return self._current + + @current.setter + def current(self, value): + self._current = value + + self.update() + + if self.draw_on_update: + self.draw() + + def update(self): + if self._start is None: + self._start = time.time() + + dt = (time.time() - self._start) + + if dt > 0: + self._rate = self._current / dt + else: + self._rate = 0 + + if self.total is None: + self._ratio = None + self._etc = None + self._runner = (self._runner + 0.01) % 1.0 + + else: + self._ratio = self._current / self.total + self._etc = dt * (1 / self._ratio - 1) + + def draw(self, overdraw=0): + output = [] + + if self.prefix: + output.append(self.prefix) + + if self.show_percent and self.total is not None: + output.append(self.percentage_printf %(self._ratio * 100)) + + if self.total is None: + count_full = int(self._runner * (self.width - 5)) + count_empty = self.width - count_full - 5 + + output.append("[%s<=>%s]" %(" " * count_full, " " * count_empty)) + else: + count_full = int(self._ratio * (self.width - 3)) + count_empty = self.width - count_full - 3 + + output.append("[%s>%s]" %("=" * count_full, " " * count_empty)) + + if self.show_absolute: + if self.total is None: + output.append(self.absolute_printf %(self._current, self.unit)) # TODO et.Unit + else: + output.append(self.absolute_printf_total %(self._current, self.total, self.unit)) + + if self.show_rate: + if self.total is None: + output.append(self.rate_printf %(self._rate, self.unit)) + else: + output.append(self.rate_printf_total %(self._rate, self.unit)) + + if self.show_etc and self.total is not None: + output.append(self.etc_printf %(self._etc)) + + if self.suffix: + output.append(self.suffix) + + line = " ".join(output) + line_len = len(line) + + if line_len > self._longest_line: + self._longest_line = line_len + + sys.stdout.write("\r%s\r" %(" " * (self._longest_line + overdraw))) + sys.stdout.write(line) + sys.stdout.flush() + + def finish(self): + if self.total is not None: + self.current = self.total + + if not self.draw_on_update: + self.draw(overdraw=2) + + sys.stdout.write("\n") + sys.stdout.flush() + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + pass diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..61568a3 --- /dev/null +++ b/build.sh @@ -0,0 +1,11 @@ +#! /bin/zsh +# encoding: utf-8 + +for dir in src-[0-9][0-9]_* +do + echo "Building in ${dir}" + cd "$dir" + ./build.sh + cd .. + ln -s $dir/*.so . +done diff --git a/src-00_core/TODO b/src-00_core/TODO new file mode 100644 index 0000000..f5ae857 --- /dev/null +++ b/src-00_core/TODO @@ -0,0 +1,2 @@ + * sjednotit parsing cmdlajny a configu + * integrovat se zshellem diff --git a/src-00_core/build.sh b/src-00_core/build.sh new file mode 100755 index 0000000..a1cc611 --- /dev/null +++ b/src-00_core/build.sh @@ -0,0 +1,63 @@ +#! /bin/zsh +# encoding: utf-8 + +setopt extendedglob + +function die() { + echo "\n\n>>> Failed during ${1}, aborting." + exit 1 +} + +function translate() { + echo -n "Translating from Python to C: " + + first=1 + + for item in ${@} + do + if [ $first -eq 1 ]; then + echo -n "${item}" + first=0 + else + echo -n ", ${item}" + fi + + cython -3 ${item}.pyx -o ${item}.c || die "translating" + done + + echo "." +} + +function combobulate() { + name="$1" + + echo -n > "$name.pyx" + + for part in src/$name.*.pyx + do + # echo "Combobulating $part..." + echo "###############################################################################" >> "$name.pyx" + echo "# Combobulated from file $part" >> "$name.pyx" + echo "###############################################################################\n" >> "$name.pyx" + cat "$part" >> "$name.pyx" + echo >> "$name.pyx" + done +} + +CFLAGS=(-Wall -pedantic -std=c99 -fPIC) +LFLAGS=(-shared) + +combobulate core +ln -s src/interface/* . + +# call Cython +translate core + +echo -n "Compiling and linking: core" +gcc $CFLAGS -I/usr/include/python3.3 -pthread -c core.c || die "compilation" +gcc $LFLAGS -L/usr/lib -lpython3.3 core.o -o core.so || die "linking" +rm -f core.{c,o} +echo "." + +# remove temporary sources +rm -f *.pyx *.pxd diff --git a/src-00_core/src/core.00-header.pyx b/src-00_core/src/core.00-header.pyx new file mode 100644 index 0000000..f04a32a --- /dev/null +++ b/src-00_core/src/core.00-header.pyx @@ -0,0 +1,17 @@ +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 diff --git a/src-00_core/src/core.10-text.pyx b/src-00_core/src/core.10-text.pyx new file mode 100644 index 0000000..472cd32 --- /dev/null +++ b/src-00_core/src/core.10-text.pyx @@ -0,0 +1,175 @@ +# 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 diff --git a/src-00_core/src/core.20-Output.pyx b/src-00_core/src/core.20-Output.pyx new file mode 100644 index 0000000..a5580b1 --- /dev/null +++ b/src-00_core/src/core.20-Output.pyx @@ -0,0 +1,153 @@ +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) diff --git a/src-00_core/src/core.30-File.pyx b/src-00_core/src/core.30-File.pyx new file mode 100644 index 0000000..5edc436 --- /dev/null +++ b/src-00_core/src/core.30-File.pyx @@ -0,0 +1,51 @@ +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() diff --git a/src-00_core/src/core.40-Main.pyx b/src-00_core/src/core.40-Main.pyx new file mode 100644 index 0000000..1d9e532 --- /dev/null +++ b/src-00_core/src/core.40-Main.pyx @@ -0,0 +1,627 @@ +def _parse(data, dtype): + if dtype == "str": + if data.startswith("\"") and data.endswith("\""): + value = data[1:-1].replace("\\\"", "\"") + else: + raise GeneralError + + elif dtype == "bool": + if data == "True": + value = True + elif data == "False": + value = False + else: + raise GeneralError + + elif dtype == "int": + try: + value = int(data) + except ValueError: + raise GeneralError + + elif dtype == "float": + try: + value = float(data) + except ValueError: + raise GeneralError + + return value + +_over_help_texts = [ + ("OverCore: what, how and huh?", ["OverCore is a Python 3 module that provides basic functionality for programs. Functionality such as configuration, commandline parsing, text handling and output, file handling, a non-interactive help system and (not all that much) more."]), + ("Data Types", ["OverCore currently supports 4 data types.", "A §bbool§/ is either exactly §mTrue§/ or exactly §mFalse§/. Bool options that are §mTrue§/ look like this: §B--§goption§/. The same name only §mFalse§/ would be §B--no-§goption§/. Makes sense, doesn't it? Their short versions are either §B+§go§/ for §mTrue§/ or §B-§go§/ for §mFalse§/.", "A §bstr§/ing is just any raw text. Remember to enclose it in quotes if it has spaces or other nasty characters in it.", "An §bint§/eger is a whole number. Negative, zero, positive.", "Finally, a §bfloat§/ is any real number."]), + ("The Commandline Parser", ["This, in conjunction with the configuration system, is the strongest part of OverCore and the very reason for its continued existence.", "Each configurable option can be assigned to on the command line. Take an option named §Bres-file§/ as an example. To assign to it, you can use §B--§gres-file§/ §msomething§/ (it's case sensitive). That's it, now its value is §msomething§/! Pretty easy, right?", "Now, you're probably thinking: \"I'm not typing that all over again, Martin!\" Well, you don't have to! Options can have their short names. It's a single letter (again, case sensitive) with a plus or minus sign in front of it. So §B--§gres-file§/ §msomething§/ becomes §B+§gf§/ §msomething§/. That's much better, ain't it? And there's more. Short names can be grouped together. If you have a bunch of bool switches, like §B--§garmed§/ (short: §B+§gA§/), §B--no-§gsound§/ (short: §B-§gS§/), §B--no-§gstore§/ (short: §B-§gs§/) and §B--§gforce§/ (short: §B+§gF§/), you can group their shorts into groups with the same boolean value: §B-§gSs§/ §B+§gAF§/. You can even have a non-bool option in a group, it just has to be on the very right (because it needs to be followed by data): §B-§gSs§/ §B+§gAFf§/ §msomething§/. It doesn't matter if that group begins with §B+§/ or §B-§/.", "If you use an option more than once, its last (rightmost) occurence applies. For example, after §B+§gf§/ §msomething§/ §B+§gf§/ §mor_other§/ is parsed, option §Bres-file§/ holds the value §mor_other§/. It goes the same for bools: after §B+§gA§/ §B--no-§garmed§/ §B+§gA§/, §Barmed§/ is §mTrue§/. However, if an option is §cplural§/, all occurences are used. Sequence §B+§gA§/ §B-§gA§/ §B+§gA§/ would be [§mTrue§/, §mFalse§/, §mTrue§/], and §B--§gnum§/ §m1§/ §B--§gnum§/ §m2§/ would end up looking like [§m1§/, §m2§/]. You don't need to type §B--§gnum§/ for every field either: §B--§gnum§/ §m1§/ §m2§/ would work exactly the same. That's because the parser keeps reading everything after a plural option (that takes data, i.e. not bools) right until it encounters two dashes, like those of a following option. You can use just the two dashes to stop the parsing manually, usually when you don't want to follow with a long option. Example: §B--§gnum§/ §m1§/ §m5§/ §m9§/ §B--§/. I repeat: a plural (non-bool) option needs to be terminated by two dashes. This wouldn't work: §B--§gnum§/ §m1§/ §m5§/ §m9§/ §B+§gf§/ §mthat_one§/, everything after §B--§gnum§/ would be consumed as its data, §Bincluding +§gf§/ §mthat_one§/."]), + ("The Config File", ["If enabled in the program code, a config file will be generated when you first run it. By default, everything in the config file will be filled with default values and commented out. The general syntax as well as individual options are explained inside. If you run a newer version of the program that offers more configurable options, the config file will be automatically updated. If you'd like to modify an option in the config file, first uncomment it and then change its value. Resolving order for option values is 1) default (hardcoded), 2) config file and 3) commandline arguments."]), + ("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. + + str chapter display only this chapter + bool list_options follow the static printout by a list of options and their current values + """ + + # dirty as fuck :-) + if not chapter and not main.version is None: + if help_texts == _over_help_texts: + _output(">>> §yOverCore§/ version §y%s§/, licensed under §yAO-JSL§/" %(_version), newlines=2) + else: + _output("§y%s§/ version §y%s§/, licensed under §y%s§/" %(main.name, main.version, main.license), newlines=2) + + # general help texts + for help_text in help_texts: + _output("[§B%s§/]" %(help_text[0])) + for p in help_text[1]: + _output(p, indent=4, newlines=2) + + if list_options: + _output("[§BAvailable Options§/]") + + for option in [opt for opt in main.options if not opt.hidden]: + if option.dtype == "bool" and not option.callback: + long_part = "§B--§g{0}§/ or §B--no-§g{0}§/".format(option.name) + else: + long_part = "§B--§g%s§/" %(option.name) + + if option.short_name: + if option.dtype == "bool" and not option.callback: + short_part = " (§B+§g{0}§/ or §B-§g{0}§/)".format(option.short_name) + else: + short_part = " (§B+§g%s§/)" %(option.short_name) + else: + short_part = "" + + if option.plural: + plural_part = "§cplural§/ " + else: + plural_part = "" + + # if option.dtype == "boolaaa": + # _output("%s%s; §bbool§/" %(long_part, short_part), indent=4) + # else: + _output("%s%s; %s§b%s§/" %(long_part, short_part, plural_part, option.dtype), indent=4) + + if option.source == "cmdline": + src = "§mcmdline§/" + elif option.source == "config": + src = "§yconfig§/" + else: + src = "default" + + if option.plural: + if option.dtype == "str": + value = "§c[§/%s§c]§/" %(", ".join("\"§m%s§/\"" %(x) for x in option.value)) + else: + value = "§c[§m%s§c]§/" %("§/, §m".join(str(x) for x in option.value)) + + else: + if option.dtype == "str": + value = "\"§m%s§/\"" %(option.value) + else: + value = "§m%s§/" %(option.value) + + if not option.callback: + _output("Current value (%s): %s" %(src, value), indent=6) + + if not option.use_cfg_file: + _output("§c(ignores config file)§/", indent=6) + + _output(option.description, indent=8, newlines=2) + + _output("[§BTargets§/]") + + if main.targets: + _output("§m%s§/" %("§/, §m".join(main.targets)), newlines=2, indent=4) + else: + _output("n/a", newlines=2, indent=4) + + main.exit(1, True) + +_allowed_types = { + "bool": [bool], + "str": [str], + "int": [int], + "float": [int, float] +} + +def _verify_type(data, dtype, plural=False): + if plural: + if type(data) == list: + for d in data: + v = _verify_type(d, dtype) + + if not v: + return False + + return True + else: + return False + + else: + if type(data) in _allowed_types[dtype]: + return True + else: + return False + +class CfgAccessor: + """ + Provides convenient access to configuration options' values. + + e.g. something = main.cfg.option_name + """ + + def __init__(self, main): + self.main = main + + def __getattr__(self, opt_name): + matches = [opt for opt in self.main.options if opt.name.replace("-", "_") == opt_name] + + if matches: + if len(matches) == 1: + return matches[0].value + else: + self.main._print("game over, lights out! OverCore internal buggaroo", prefix.fail, exc=GeneralError) + else: + self.main._print("option §r%s§/ doesn't exist" %(opt_name), prefix.fail, exc=GeneralError) + +class Main: + """ + Application backbone. Provides: + * A configuration system consisting of + * a given set of (hardcoded) default values + * a configuration file that overrides the defaults + * a command line parser that overrides all of the above + * Help system that generates help text based on program's configurable options. + """ + + def __init__(self, name, version=None, license=None, cfg_file=None, cmdline=sys.argv[1:], allow_exit=True): + """ + str name program name + str version program version + str license program license + str cfg_file path to config file, optional + list cmdline command line to parse; defaults to system cmdline (sys.argv) + + Initializes over.Main and opens/creates the config file (if desired). + """ + + self.cfg = CfgAccessor(self) + self.name = name + self.version = version + self.license = license + self.cmdline = cmdline + self.help_texts = [] # (chapter, [paragraphs]) + self.allow_exit = allow_exit + self._print = Output("over.Main", default_suffix=".\n", timestamp=True) + + if cfg_file: + self.cfg_file = File(cfg_file) + + if not self.cfg_file.data: + self.cfg_file.data = """# Et configuration file for %s +# Licensed under %s +# +# Syntax +# There are 4 data types: bool, int, float and str (string). +# * Bools are exactly True or False. +# * Ints are negative integers, positive integers and zero. +# * Floats are all real numbers. They don't need to have a decimal point. +# * Strings need to be enclosed in double quotes, e.g. "like this". +# +# An option is either singular or plural. Plurals are arrays, so they ecpect to be +# in comma-separated lists: ["this", "is", "a", "str", "example"]. +# +# Only lines beginning with a '#' are treated as comments. + +""" %(self.name, self.license) + + self._print("creating configuration file §B%s§/" %(self.cfg_file.path), prefix.start) + else: + self.cfg_file = None + + # cmdline parser and config + self.options = [] # Option objects + self.targets = [] # target words directly from the command line + self.unknowns = [] # parser couldn't figure out how to chew these + + self.help_texts = [] # (chapter, [paragraphs]) tuples + + def __repr__(self): + return "over.Main(name=%s)" %(self.name) + + def exit(self, retval=0, silent=False): + """ + int retval return value + bool silent don't mention it + + Shutdown the program and exit. + + Can be disabled by setting self.allow_exit to False. + """ + + if self.allow_exit: + if not silent: + self._print("exiting with §B%d§/" %(retval), prefix.info) + + sys.exit(retval) + + def dump(self, ignore=[]): + """ + return current configuration in Et Commandline format + """ + + out = [] + for option in [option for option in self.options if option.name not in ignore]: + if option.type == "bool": + if option.plural: + for value in option.value: + if value: + out.append("--%s" %(option.name)) + else: + out.append("--no-%s" %(option.name)) + elif option.value is not None: + if option.value: + out.append("--%s" %(option.name)) + else: + out.append("--no-%s" %(option.name)) + else: + if option.plural: + out.append("--%s" %(option.name)) + for value in option.value: + if option.type == "str": + value = '"%s"' %(value.replace('"', r'\"')) + out.append(str(value)) + out.append("--") + elif option.value is not None: + out.append("--%s" %(option.name)) + if option.type == "str": + option.value = '"%s"' %(option.value.replace('"', r'\"')) + out.append(str(option.value)) + + return out + + def reset_to_default(self): + """ + Resets internal state to builtin defaults (i.e. as it is before self.parse() is called). + """ + + self.targets = [] + self.unknowns = [] + + for option in self.options: + option.source = "default" + option.value = option.default + + def add_option(self, name, dtype, default, description, short_name=None, plural=False, callback=None, use_cfg_file=True, hidden=False): + """ + str name Name of the option. See #0 + str dtype Option's data type. See #1 + str default Option's default value. See #2 + str description Option's description (displayed in --help). + str short_name Option's short name. See #3 + bool plural True to make the option plural. + bool use_cfg_file True to use the config file for this option. + fn callback Register a callback for this option. See #4 + bool hidden hide the option from --help + + Add an option to the configuration system. + + #0 Option naming + Each option has a name. This name is directly used on the command line prefixed by two dashes. + For example option "warp-speed" would be called like so: --warp-speed. + Bools take two appearances: --warp-speed is True, while --no-warp-speed is False. + Their values can be accessed from the program as main.cfg.option_name. Options with dashes + in their names (an operator in python) can be accessed as well, just substitute dashes + for underscores. Option --warp-speed would be available under main.cfg.warp_speed. + + #1 Data types. + There are four data types. + * "bool" - either exactly True or False. + * "str" - a raw text string, nothing special going on here + * "int" - an integer + * "float" - a float + + #2 Default values + These are the values the option will take if it's not called on the command line or + in the config file. Defaults need to have the proper data type, of course, and for + plural options they have to be lists. + + #3 Short names + Options can have a short, single letter alias. These are prefixed by a single dash on the command + line and don't appear in the config file at all. Again, booleans take special treatment: -a is False, + while +a is True. + Short names can be grouped: -a -b becomes -ab (both are False), or +a +b becomes +ab (both will be True). + If an option requires a parameter (e.g. -f filename), it can still be grouped, it just has to be the + rightmost option in the group. Whether the group starts with a + or a - doesn't matter. + E.g. -af filename is the same as -a -f filename and +af filename is the same as +a -f filename. + + #4 Callbacks + An option with a callback will invoke it if it appears on the command line (both +a and -a). This is + usually done for options like --help (internally), --list-modules, --show-something and similar. + The callback will be passed one argument: the main instance. + """ + + for option in self.options: + if name == option.name: + self._print("option §B--§r%s§/ already exists" %(name), prefix.fail, exc=GeneralError) + + if option.short_name and short_name == option.short_name: + self._print("option §B--§r%s§/'s short name §r%s§/ is already taken by §B--§y%s§/" %(name, short_name, option.name), prefix.fail, exc=GeneralError) + + if short_name and len(short_name) > 1: + self._print("short name must be a single character; option §B--§r%s§/ has §r%s§/"%(name, short_name), prefix.fail, exc=GeneralError) + + if callback: + use_cfg_file = False + + if dtype not in ["bool", "str", "int", "float"]: + self._print("option §B--§r%s§/'s dtype is not of a supported type" %(name), prefix.fail, exc=GeneralError) + + v = _verify_type(default, dtype, plural) + + if not v: + if plural: + type_desc = "a list of §m%s§rs" %(dtype) + else: + if dtype == "int": + type_desc = "an int" + else: + type_desc = "a %s" %(dtype) + + self._print("option §B--§r%s§/'s default must be §y%s§/" %(name, type_desc), prefix.fail, exc=GeneralError) + + o = Option(name, dtype, default, description, short_name, plural, use_cfg_file, callback, hidden) + self.options.append(o) + + def parse(self, cmdline=None, reset_to_default=False): + """ + Parses the cfg file and the command line and sets the internal cfg state accordingly. + """ + + ptr = 0 + parsing_plural = False + unparsed = [] + callbacks = [] + + if reset_to_default: + self.reset_to_default() + + if cmdline is None: + cmdline = self.cmdline + + while ptr < len(cmdline): + to_parse = [] + word = cmdline[ptr] + + if len(word) > 2 and word[0:2] == "--": + to_parse.append(word) + elif len(word) >= 2 and (word[0] == "+" or (word[0] == "-" and word[1] != "-")): + # expand and then parse + for s in word[1:]: + option = [opt for opt in self.options if opt.short_name == s] + + if option: + if word[0] == "+" or option[0].dtype != "bool": + to_parse.append("--{0.name}".format(option[0])) + else: + to_parse.append("--no-{0.name}".format(option[0])) + else: + self.unknowns.append(word[0] + s) + else: + unparsed.append(word) + + ptr += 1 + + if to_parse: + for w in to_parse: + if w.startswith("--no-"): + bool_on = False + name = w[5:] + else: + bool_on = True + name = w[2:] + + option = [opt for opt in self.options if opt.name == name] + + if option: + o = option[0] + + if o.callback: + callbacks.append(o.callback) + + if o.plural and o.source != "cmdline": + o.value = [] + + if o.dtype == "bool": + o.source = "cmdline" + + if o.plural: + o.value.append(bool_on) + else: + o.value = bool_on + else: + if not bool_on: + self._print("§B--no-§r%s§/ makes no sense because this option is of type §y%s§/" %(name, o.dtype), prefix.fail, exc=GeneralError) + + # This option takes arguments. These are all words at ptr and higher that don't begin with "--". + # If this option is part of a shortgroup it must be at its end. + + if to_parse.index(w) + 1 != len(to_parse): + self._print("option §B{0[0]}§r{1.short_name}§/ needs to be at the end of group §y{0}§/".format(word, o), prefix.fail, exc=GeneralError) + + got_something = False + + # NOTE it shouldn't be hard to implement specific plurality (how many words to chew) + # NOTE REPLY: I'm still not doing it. + while ptr < len(cmdline) and not cmdline[ptr].startswith("--"): + arg = cmdline[ptr] + ptr += 1 + + if o.dtype == "int": + try: + arg = int(arg) + except ValueError: + self._print("argument §r%s§/ passed to integer option §B--§y%s§/" %(arg, o.name), prefix.fail, exc=ValueError) + + elif o.dtype == "float": + try: + arg = float(arg) + except ValueError: + self._print("argument §r%s§/ passed to float option §B--§y%s§/" %(arg, o.name), prefix.fail, exc=ValueError) + + got_something = True + o.source = "cmdline" + + if o.plural: + o.value.append(arg) + else: + o.value = arg + break + + #if not got_something: + # self._print("option §B--§r%s§/ needs at least one argument" %(o.name), prefix.fail, exc=GeneralError) + + else: + self.unknowns.append(w) + + self.targets = [u for u in unparsed if u != "--"] + + if self.unknowns: + self._print("unknown options on the command line: §r%s§/" %("§/, §r".join(self.unknowns)), prefix.fail, exc=GeneralError) + + # parse the config file + if self.cfg_file: + new_lines = [] + + for opt in [o for o in self.options if o.use_cfg_file]: + match = re.findall("^(#?)\s*%s\s*=\s*(.+)" %(opt.name), self.cfg_file.data, re.M) + + if match: + if opt.source == "default": + if not match[0][0]: # if it isn't commented out + d = match[0][1] + + opt.source = "config" + + if opt.plural: + opt.value = [] + + if opt.dtype != "str": + elements = [x.strip() for x in d[1:-1].split(",")] + else: + d = re.sub("\",\s*\"", "\",\"", d) # remove spaces + d = d[1:-1].strip()[1:-1] # remove [] and outermost "" + elements = ["\"%s\"" %(x) for x in d.split("\",\"")] # split and re-pack them with "" + + for element in elements: + try: + opt.value.append(_parse(element, opt.dtype)) + except GeneralError: + self._print("config file syntax error for option §B--§r%s§/" %(opt.name), prefix.fail, exc=GeneralError) + + else: + try: + opt.value = _parse(d, opt.dtype) + except GeneralError: + self._print("config file syntax error for option §B--§r%s§/" %(opt.name), prefix.fail, exc=GeneralError) + + else: + self._print("updating config file with option §B--§y%s§/" %(opt.name)) + new_lines.append("") + new_lines.append(paragraph(render(opt.description, colors=False), prefix="#", width=80)) + new_lines.append("# *** data type: %s" %(opt.dtype)) + + if opt.plural: + new_lines.append("# *** plural option") + + tmp = [] + for v in opt.default: + if opt.dtype == "str": + tmp.append("\"%s\"" %(v)) + else: + tmp.append(str(v)) + + str_value = "[%s]" %(", ".join(tmp)) + else: + if opt.dtype == "str" and opt.default is not None: + str_value = "\"%s\"" %(opt.default) + else: + str_value = str(opt.default) + + new_lines.append("#%s = %s\n" %(opt.name, str_value)) + + self.cfg_file.data += "\n".join(new_lines) + + # handle callbacks + for callback in callbacks: + callback(self) + + def dump_cfg(self): + """ + Print a summary of the current cfg state. + + DEPRECATED by --help + """ + + self._print("state dump", prefix.start) + + for opt in self.options: + if opt.source == "cmdline": + src = "§mcmdline§/" + elif opt.source == "config": + src = "§yconfig§/" + else: + src = "default" + + if opt.plural: + self._print("--§g%s§/ (%s): §b%s§/" %(opt.name, src, "§/, §b".join(str(x) for x in opt.value))) + else: + self._print("--§g%s§/ (%s): §b%s§/" %(opt.name, src, opt.value)) + + self._print("Targets: §B%s§/" %("§/, §B".join(self.targets))) + + self._print("state dump", prefix.done) + + def enable_help(self, short_name=None): + """ + Enable --help, --over-help and --help-over. If a short name is provided, it will be used for --help. + """ + + self.add_option("help", "bool", False, "Display this help message.", callback=self.help, short_name=short_name, use_cfg_file=False) + self.add_option("over-help", "bool", False, "Display general usage information for OverCore.", callback=self.help_over, use_cfg_file=False) + self.add_option("help-over", "bool", False, "Display general usage information for OverCore.", callback=self.help_over, use_cfg_file=False, hidden=True) + + def add_help(self, chapter, paragraphs): + self.help_texts.append((chapter, paragraphs)) + + def help(self, *dummy): + _print_help(self, self.help_texts, list_options=True) + + 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): + """ + Just a container. Move along... + """ + + self.name = name + self.dtype = dtype # bool, int, str, float + self.default = default + self.description = description + self.short_name = short_name + self.plural = plural + self.use_cfg_file = use_cfg_file + self.callback = callback + self.hidden = hidden + + self.source = "default" + self.value = default diff --git a/src-00_core/src/core.50-misc.pyx b/src-00_core/src/core.50-misc.pyx new file mode 100644 index 0000000..d4070c5 --- /dev/null +++ b/src-00_core/src/core.50-misc.pyx @@ -0,0 +1,340 @@ +# 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 "" %(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 diff --git a/src-00_core/src/core.51-import.pyx b/src-00_core/src/core.51-import.pyx new file mode 100644 index 0000000..b62dea8 --- /dev/null +++ b/src-00_core/src/core.51-import.pyx @@ -0,0 +1,31 @@ +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 diff --git a/src-00_core/src/interface/core.pxd b/src-00_core/src/interface/core.pxd new file mode 100644 index 0000000..cc6e086 --- /dev/null +++ b/src-00_core/src/interface/core.pxd @@ -0,0 +1,2 @@ +cdef class Managed: + pass