diff options
Diffstat (limited to 'bps/unstable')
21 files changed, 5459 insertions, 0 deletions
diff --git a/bps/unstable/__init__.py b/bps/unstable/__init__.py new file mode 100644 index 0000000..297d701 --- /dev/null +++ b/bps/unstable/__init__.py @@ -0,0 +1,1084 @@ +"""bps.unstable -- new, undocumented, and still developing additions to bps +""" +#========================================================= +#imports +#========================================================= +#core +import contextlib +import os +from logging import getLogger; log = getLogger(__name__) +import os.path +from warnings import warn +import re +#pkg +from bps.fs import getcwd, filepath +from bps.text import condense +from bps.meta import isstr, is_iter, get_module +from bps.basic import enum_slice +#local +__all__ = [ + 'smart_list_iter', + 'ellipsize', + 'protected_env', +] + +#========================================================= +#iteration +#========================================================= +##class BufferedIter(object): +## "return iterator which can be walked back, ala ungetch" +## def __init__(self, source): +## self._source = source +## self._itr = iter(source) +## self._buffer = [] +## +## def next(self): +## if self._buffer: +## return self._buffer.pop() +## return self._itr.next() +## +## def pushnext(self, value): +## self._buffer.append(value) + + +##def pairs(obj): +## "an attempt at a Lua-like pairs() iterator." +## if hasattr(obj, "iteritems"): +## return obj.iteritems() +## elif hasattr(obj, "__len__") and hasattr(obj,"__getitem__"): + ##if hasattr(obj, "_asdict") and hasattr(obj, "_fields"): + ## #probably a namedtuple... + ## return ( + ## (k,getattr(obj,k)) + ## for k in obj._fields + ## ) + ##return enumerate(obj) +## else: +## return ( +## (k,getattr(obj,k)) +## for k in dir(obj) +## if not k.startswith("_") +## ) +##TODO: maybe also an ipairs() + +def filter_in_place(func, target, invert=False): + "perform in-place filtering on a list, removing elements if they don't pass filter func" + #NOTE: length check delayed til after type identified, + # so that type errors won't get hidden for empty instances + # of unsupported types. + if not hasattr(target, "__len__"): + #target is iter, or something more obscure + raise TypeError, "cannot support types without length: %r" % (type(target),) + if not hasattr(target, "__iter__"): + #not sure what types could get here. but code relies on iteration + raise TypeError, "cannot support non-iterable types: %r" % (type(target),) + if hasattr(target, "__getitem__"): + if not hasattr(target, "__delitem__"): + #target is frozenset, str, tuple, etc + raise TypeError, "cannot support types without del: %r" % (type(target),) + if hasattr(target, "keys"): #xxx: is there better way to identifier mapping? + #target is dict-like + #NOTE: we build set of keys to delete rather than deleting + #as we go, since that set will always be smaller than set of all keys + pos = not invert + remove = set( + key for key in target + if pos ^ func(key) #NOTE: operates on key, not value + ) + for key in remove: + del target[key] + return + else: + #target is list-like (eg: list, array) + #FIXME: this assumes __len__ + __getitem__ - keys implies int indexes. + # might need better check of this assumption ('index' is reliable for list & array) + end = len(target) + if not end: + return + pos = 0 + while pos < end: + if invert ^ func(target[pos]): + pos += 1 + else: + del target[pos] + end -= 1 + return + elif hasattr(target, "difference_update"): + #assume it's a set + pos = not invert + remove = set( + elem for elem in target + if pos ^ func(elem) + ) + if remove: + target.difference_update(remove) + return + else: + #probably a frozenset + raise TypeError, "unsupported type: %r" % (type(target),) + +#rename to mutable_list_iter ? also, might be too complex, when filter_in_place might be better +class smart_list_iter(object): + """list iterator which handles certain list operations without disrupting iteration. + + The typical usage of this class is when you need to iterate + over a list, and delete selected elements as you go. + + The reason this is in unstable is that other use-cases may require + reworking the class, the delete() case is the only one + this class is currently being used for. + + Usage example:: + + >>> from bps.unstable import smart_list_iter + >>> a=[5,6,100,2,3,40,8] #given a list + >>> itr = smart_list_iter(a) #create the iterator + >>> for elem in itr: #iterate over it as normal + >>> print elem + >>> if elem > 30: #remove all elements over 30 + >>> itr.delete() #delete via the iterator, allowing it to continue w/o losing sync + >>> #all elements will be scanned + 5 + 6 + 100 + 2 + 3 + 40 + 8 + >>> a #but all ones called for delete will be replaced + [5,6,2,3,8] + + Public Attributes + ================= + + .. attribute:: pos + + Current position in list (-1 before iterator starts) + + .. attribute:: next_pos + + Next position in list (may be equal to len of list) + + .. attrbite:: target + + The list we're iterating over. + + Public Methods + ============== + + .. automethod:: delete + + .. automethod:: pop + + .. automethod:: append + + .. automethod:: insert + """ + def __init__(self, target, enum=False): + #XXX: reverse option? + self.target = target + self._enum = enum + self.pos = -1 + self._deleted = False #flag that current element was deleted + + def _get_next_pos(self): + pos = self.pos + if not self._deleted: + pos += 1 + return max(pos, len(self.target)) + next_pos = property(_get_next_pos) + + def __iter__(self): + return self + + def __length__(self): + pos = self.pos + if not self._deleted: + pos += 1 + return max(0, len(self.target)-pos) + + def next(self): + "return next item" + pos, deleted = self.pos, self._deleted + if not deleted: + pos += 1 + assert pos >= 0 + end = len(self.target) + if pos >= end: + raise StopIteration + if deleted: + self._deleted = False + else: + self.pos = pos + if self._enum: + return pos, self.target[pos] + else: + return self.target[pos] + + def delete(self): + "delete current entry" + pos = self.pos + if pos == -1: + raise IndexError, "not currently pointing to an element" + del self.target[pos] + self._deleted = True + + def pop(self, idx, relative=False): + "pop entry from list. if index not specified, pops current entry in iterator" + pos = self.pos + if relative: + idx += pos + elif idx < 0: + idx += len(self.target) + if idx < 0: + raise IndexError, "index too small" + value = self.target.pop(idx) + if idx < pos: + self.pos = pos-1 + elif idx == pos: + self._deleted = True + return value + + def append(self, value): + "quickly append to list" + return self.insert(len(self.target), value) + + def insert(self, idx, value, relative=False): + "insert entry into list. if relative=True, pos is relative to current entry in iterator" + pos = self.pos + if relative: + idx += pos + elif idx < 0: + idx += len(self.target) + if idx < 0: + raise IndexError, "index too small" + self.target.insert(idx, value) + if idx < pos: + self.pos = pos+1 + return idx + +#========================================================= +#text related +#========================================================= +def get_textblock_size(text, rstrip=False): + "return rows & cols used by text block" + if not text: + return 0,0 + textlines = text.split("\n") + rows = len(textlines) + if rstrip: + cols = max(len(line.rstrip()) for line in textlines) + else: + cols = max(len(line) for line in textlines) + return rows, cols + +def ellipsize(text, width, align=">", ellipsis="...", mode="plain", window=None): + """attempt to ellipsize text. + + :arg text: the string to ellipsize + :arg width: the maximum allowed width + :param align: + Where the ellipsis should be inserted. + + ============== ============================================ + Value Location + -------------- -------------------------------------------- + "<", "left" ellipsis inserted at left side of text + "^", "center" ellipsis inserted in center of text + ">", "right" ellipsis inserted at right side of text + (the default). + ============== ============================================ + + :param mode: + Select which algorithm is used to ellipsize. + Defaults to "smart". + + ============== ================================================ + Value Behavior + -------------- ------------------------------------------------ + "plain" Text will be clipped as needed, + regardless of spaces or other boundaries, + and the text will otherwise be left alone. + "smart" This function will attempt to remove extraneous + spaces and similar characters from the string, + and if ellipsis are needed, will prefer splitting + text at a space. + "filepath" Variant of the smart algorithm, this assumes + it's working with a local filepath, + and attempts to break on directory boundaries. + ============== ================================================ + + :param window: + For smart / filepath modes, this specifies + the maximum number of characters to search for a good break point + before giving up. + + :param ellipsis: + Optionally overide the text used as the ellipsis. + + Usage Example:: + + >>> from bps.text import ellipsize + >>> ellipsize("abc", 6) #this does nothing + 'abc' + >>> ellipsize("abcdefghi", 8) #suddenly, ellipsized + 'abcde...' + >>> ellipsize("abcdefghi", 8, "<") #other side + '...efghi' + """ + #XXX: write code to break up string into atomic parts, + #and override len() to handle them, + #so this can deal with VT100 codes and HTML + + #convert to string + if not isstr(text): + text = str(text) + + #pre-process string + if mode == "smart": + #smart mode will ALWAYS try to shrink + text = condense(text, " \t") + + #check if string fits w/in alloted space + tsize = len(text) + if tsize <= width: + return text + + #figure out how much we can keep + chars = width-len(ellipsis) + if chars < 0: + raise ValueError, "width must be larger than ellipsis!" + elif chars == 0: + return ellipsis + + #select boundary finder function + if mode in ("smart", "filepath"): + if mode == "smart": + bc = " \t" + if window is None: + window = 8 + else: + assert mode == "filepath" + bc = os.path.sep + if window is None: + window = 32 + def find_boundary(start, fwd): + "locate next boundary character in string" + #nested vars: 'text', 'tsize', 'bc', and 'window' + if fwd: + if window == -1: + end = None + else: + end = min(start+window, tsize) + last = None + for idx, c in enum_slice(text, start, end): + if c in bc: + last = idx + elif last is not None: + return last + return last + else: + if window == -1: + end = None + else: + end = start-window-1 + if end < 0: + end = None + log.debug("find_boundary rev: %r %r %r", text, start, end) + last = None + for idx, c in enum_slice(text, start, end, -1): + log.debug("checking %r %r last=%r", idx, c, last) + if c in bc: + last = idx + elif last is not None: + return last + return last + else: + def find_boundary(start, fwd): + return None + + #chop according to alignment + if align == "<" or align == "left": + #put ellipsis on left side, so pick at most {chars} + #characters from the right side of the string. + left = tsize-chars + b = find_boundary(left, True) + if b is not None: + left = b #want text[left] to be the boundary char + result = ellipsis + text[left:] + + elif align == "^" or align == "center": + #'left' is left end-point + #'right' is right start-point + #result is [0:left] ... [right:tsize] + if mode == "plain": + #quick simple centering + left = chars//2 + right = tsize-chars+left + else: + #much more tricky... start in center of string, + #and move left/right until boundaries are large enough. + left = center = tsize//2 + right = center-1 + left_active = right_active = True + first = True + diff = tsize+2-chars + while left_active and right_active: + #move left to next boundary + if left_active: + assert left > 0 + b = find_boundary(left-1, False) + if b is None: + left_active = False + else: + left = b + left_active = (left > 0) + #check if we're done + if not first and right-left<diff: + break + #move right to next boundary + if right_active: + assert right < tsize-1 + b = find_boundary(right+1, True) + if b is None: + right_active = False + else: + right = b + right_active = (right < tsize-1) + #check if we're done + if right-left < diff: + break + #move boundaries outward again + first = False + #check if we failed. + if right-left < diff: + #moved out to best boundary we could find, + #and nothing doing. so fall back to + #quick simple centering + left = chars//2 + right = tsize-chars+left + else: + left += 1 #so we include left char + assert left>=0 and left < tsize + assert right>left and right <= tsize + assert left+tsize-right <= chars + result = text[:left] + ellipsis + text[right:] + + #TODO: could have "edge"/"<>" alignment, where center is kept + else: + assert align == ">" or align == "right" + right = chars-1 + b = find_boundary(right, False) + log.debug("text=%r right=%r b=%r", text, right, b) + if b is not None: + right = b + result = text[:right+1] + ellipsis + + #just a final check + assert len(result) <= width + return result + +#========================================================= +#text - email +#========================================================= + +#regexp for strict checking of local part +#TODO: support backslash escapes, quoted mode +_re_email_local = re.compile(r""" + ^( + ( + \w | [-!#$%&'*+/=?^`{|}~.] + )+ + )$ + """, re.X|re.U) + +#regexp for strict checking of domain part +#TODO: support ipv6 in quotes too +_re_email_domain = re.compile(r""" + ^( + ## match ip address within brackets (rare but in std) + \[ [0-2]?\d\d \. [0-2]?\d\d \. [0-2]?\d\d \. [0-2]?\d\d \] + | + ## match domain name + ( + [.-] + | + ## note: since \w matches underscore along with alphanum, + ## we have to use neg-lookahead to prevent underscore from matching + (?!_) \w + )+ + )$ + """, re.X|re.U) + +_dws_re = re.compile(r"\s{2,}") + +def parse_email_addr(value, strict=True, strip=True, allow_empty=False, unquote_name=True, clarify=False): + """parse email address into constituent parts. + + This function takes a provided email address, + and splits it into the display name, the local name, and the domain name. + While this function has a lot of options for controlling precisely + how it parses email addresses, the basic usage is:: + + >>> from bps.unstable import parse_email_addr + >>> parse_email_addr("joe@bob.com") + (None, 'joe', 'bob.com') + >>> parse_email_addr("Joe Smith <joe@bob.com>") + ('Joe Smith','joe','bob.com') + >>> parse_email_addr("joe@") + ValueError: domain part of email address must not be empty: 'joe@' + + :arg value: + This should be a string containing the email address to be parsed. + Extranous spaces around the address will be automatically stripped. + + :param strict: + By default this function is strict, and raises :exc:`ValueError` + if the local name or domain name violates various email address rules + (see :func:`validate_email_parts`). + + If ``strict=False``, this function will only throw as :exc:`ValueError` + only for mismatched ``<>`` around an email, or if the ``@`` is missing. + + :param strip: + By default, extraneous white space is stripped from the address + before parsing, and from the parts after they have been parsed, + to help normalize unpredictable user input. Set ``strip=False`` to disable. + + :param allow_empty: + By default, an empty string is considered an invalid email. + If ``allow_empty=True``, passing in an empty string + will result in the tuple ``(None,None,None)`` being returned. + This can be detected easily because in all other cases, + the domain part will be a non-empty string. + + :param unquote_name: + A common convention is to surround display names in quotes + (eg ``"John Doe" <jdoe@foo.com>``). By default, this function + will strip the quotes out, and report the raw name. + To disable this, set ``unquote_name=False``, + and the raw name string will be returned. + + :param clarify: + If enabled via ``clarify=True``, and the address cannot be parsed + as provided, parse_email_addr will search for obfuscated email address + features, such as ``@`` being written as ``(at)``, and attempt + to restore and parse the original address. This is particularly useful + when standardizing user input. + + This feature is disabled by default, since it may not always + return the right results. + + :returns: + This returns a tuple ``(name, local, domain)``: + + * ``name`` contains the display name, or ``None`` if the display name was empty / missing. + * ``local`` contains the local part of the address (or ``None`` if allow_empty is ``True``). + * ``domain`` contains the domain part of the address (or ``None`` if allow_empty is ``True``). + + :raises ValueError: + if the address cannot be parsed as an email address, + or if the components of the address violate rfc specs + (see the ``strict`` parameter for more). + + .. note:: + This function (mostly) complies with the relevant rfcs, such as http://tools.ietf.org/html/rfc3696. + Deviations include: + + * it doesn't support quoted local names (eg ``"John Doe"@foo.com``) + * it doesn't support backslash escaping in the local name (eg ``User\<\>Name@foo.com``). + * it allows any alphanumeric unicode/locale defined character, not just a-z, 0-9. + + """ + #initial setup + if value is None: + if allow_empty: + return (None,None,None) + else: + raise ValueError, "not a valid email address: %r" % (value,) + if strip: + addr = value.strip() + else: + addr = value + + #extract name part + if '<' in addr: + if addr[-1] != '>': + raise ValueError, "malformed braces in email address: %r" % (value,) + name, addr = addr[:-1].rsplit("<",1) + if strip: + name = name.strip() + elif name[-1] == ' ': #at least strip right most space + name = name[:-1] + if unquote_name: + if name.startswith('"') and name.endswith('"'): + name = name[1:-1] + if strip: + name = name.strip() + elif name.startswith("'") and name.endswith("'"): + name = name[1:-1] + if strip: + name = name.strip() + if not name: + name = None + elif strip and ' ' in name: + name = _dws_re.sub(" ", name) + if strip: + addr = addr.strip() + elif '>' in addr: + raise ValueError, "malformed braces in email address: %r" % (value,) + else: + name = None + + #split local & domain parts + if not addr and allow_empty: + return None, None, None + elif '@' in addr: + local, domain = addr.rsplit('@',1) + if strip: + local = local.rstrip() + domain = domain.lstrip() + elif clarify: + #let's try some alternates + def helper(addr): + try: + result = parse_email_addr(addr, strict=False, strip=True, clarify=False) + return result[1:3] + except ValueError: + return None, None + while True: + if '(at)' in addr: + tmp = re.sub(r"\s*\(at\)\s*","@",addr) + tmp = re.sub(r"\s*\(dot\)\s*",".",tmp) + local, domain = helper(tmp) + if domain: + break + if '[at]' in addr: + tmp = re.sub(r"\s*\[at\]\s*","@",addr) + tmp = re.sub(r"\s*\[dot\]\s*",".",tmp) + local, domain = helper(tmp) + if domain: + break + if ' at ' in addr: + tmp = re.sub(r"\s* at \s*","@",addr) + tmp = re.sub(r"\s* dot \s*",".",tmp) + local, domain = helper(tmp) + if domain: + break + raise ValueError, "not a valid email address: %r" % (value,) + else: + raise ValueError, "not a valid email address: %r" % (value,) + + #validate parts and return + validate_email_parts(name, local, domain, strict=strict, _value=value) + return name, local, domain + +def validate_email_parts(name, local, domain, strict=True, _value=None): + """validates the three components of an email address (``Name <local @ domain>``). + + :arg name: the display name component, or ``None``. + :arg local: the local part component + :arg domain: the domain part component + + :param strict: + By default, this function checks that the parts conform to the rfc, + and don't contain any forbidden characters or character sequences. + + By default this function is strict, and raises :exc:`ValueError` + if the local name or domain name contain invalid characters; + contain invalid character sequences (such as ".."); or + if the address violates various email part size rules. + + If ``strict=False``, the only checks made are that local & domain + are non-empty strings. + + :param _value: + Override the value that's displayed in error messages. + This is mainly used internally by :func:`parse_email_address`. + + :returns: + ``True`` on success; raises ValueError upon failure. + """ + if _value is None: + _value = (name,local,domain) + + if not local: + raise ValueError, "empty local part in email address: %r" % (_value,) + if not domain: + raise ValueError, "empty domain part in email address: %r" % (_value,) + if not strict: + return True + + if not _re_email_local.match(local): + raise ValueError, "invalid characters in local part of email address: %r" % (_value,) + if '..' in local or local[0] == '.' or local[-1] == '.': + raise ValueError, "invalid periods in local part of email address: %r" %(_value,) + + #XXX: split into is_valid_hostname? + if not _re_email_domain.match(domain): + raise ValueError, "invalid characters in domain part of email address: %r" % (_value,) + if '..' in domain or domain[0] == '.': + raise ValueError, "invalid periods in domain part of email address: %r" %(_value,) + if domain[0] == '-' or domain[-1] == '-' or '-.' in domain or '.-' in domain: + raise ValueError, "invalid hyphens in domain part of email address: %r" %(_value,) + ##if len(domain) < (3 if domain[-1] == '.' else 2): + ## raise ValueError, "domain part of email address is too small: %r" % (value,) + + if len(local) > 64: + raise ValueError, "local part of email address is too long: %r" % (_value,) + if len(domain) > 255: + raise ValueError, "domain part of email address is too long: %r" % (_value,) + + return True + +def compile_email_addr(name, local, domain, strict=True, quote_name=True): + """return formatted email address. + + this function takes the components of an email address, + and formats them correctly into a single string, + after validating them. + + :arg name: the display name component, or ``None``. + :arg local: the local part component + :arg domain: the domain part component + + :param strict: + whether strict validation is enabled + (see :func:`validate_email_parts`) + + :param quote_name: + whether the name part is automatically + put inside double-quotes when formatting. + + :returns: + email address as single string. + """ + validate_email_parts(name, local, domain, strict=strict) + if name: + if quote_name: + ##if '"' in name: + ## name = name.replace('"',"'") + return '"%s" <%s@%s>' % (name,local,domain) + else: + return '%s <%s@%s>' % (name,local,domain) + else: + return '%s@%s' % (local,domain) + +def norm_email_addr(value, strict=True, allow_empty=False, quote_name=True, clarify=False): + """normalize email address string. + + This uses :func:`parse_email_addr` and :func:`compile_email_addr` + in order to parse, validate, normalize, and reassemble + any email address passed into it. + + :arg value: raw email address + :param strict: + whether strict checking of email format is enabled + (see :func:`validate_email_parts`). + :param allow_empty: + By default, empty strings will cause a :exc:`ValueError`. + If ``True``, empty strings will be returned as ``None``. + :param quote_name: + By default, the name portion will have double-quotes + added around it if they are missing. + Set to ``False`` to preserve original name. + + :returns: + normalized email address, with extraneous spaces removed; + or raises ValueError if address was invalid. + """ + n,l,d = parse_email_addr(value, + strict=strict, + strip=True, + allow_empty=allow_empty, + unquote_name=quote_name, + clarify=clarify, + ) + if d is None: + assert allow_empty + return None + return compile_email_addr(n,l,d, strict=False, quote_name=quote_name) + +#========================================================= +#functional code +#========================================================= + +##class compose(object): +## """ +## function composition +## +## usage: +## fc = compose(f0, f1, ... fn) +## assert fc(*a,**k) == f0(f1(...fn(*a,**k)) +## """ +## def __new__(cls, *funcs): +## #calc true content from funcs +## assert isinstance(funcs, tuple) +## content = [] +## for func in funcs: +## assert callable(func) +## if func == IdentityFunc: +## #this one should be ignored. +## continue +## if isinstance(compose): +## content += compose.func_seq +## else: +## content.append(func) +## #return appropiate object based on content +## if len(content) == 0: +## return IdentityFunc +## elif len(content) == 1: +## #no need to compose +## return content[0] +## else: +## #build compose object +## self = object.__new__(cls) +## content.reverse() +## self.func_seq = tuple(content) +## return self +## +## def __call__(self, *args, **kwds): +## gen = iter(self.func_seq) +## result = gen.next()(*args,**kwds) +## for fn in gen: +## result = fn(result) +## return result + +##_NameCount = {} +##def composeClass(bases, name=Undef, kwds=None): +## """ +## returns a new class built out of the bases provided. +## given bases = [b1, b2, b3], +## the resulting class expects arguments in the form of... +## +## cls( a1, a2, a3) +## where aN are all tuples, dicts, or ArgKwds. +## only the first contructor b1.__new__(*args,**kwds) from a1 +## each bN.__new__(*args,**kwds) from aN +## +## xxx: not done with this! +## """ +## global _NameCount +## +## initseq = [] +## for pos, base in enumerate(bases): +## for i, cls in initseq: +## if issubclass(base,cls): +## break +## else: +## initseq.append((pos,base)) +## +## def newfn(cls, *aks): +## return cls.__bases__[0].__new__(aks[0]) +## +## def initfn(self, *aks): +## for pos, cls in initseq: +## if pos > len(aks): +## args = [] +## kwds = {} +## else: +## ak = aks[pos] +## if isinstance(ak, tuple): +## args = ak +## kwds = {} +## elif isinstance(ak, dict): +## args = [] +## kwds = ak +## else: +## args, kwds = ak.args, ak.kwds +## cls.__init__(*args, **kwds) +## outkwds = {"__new__":newfn, "__init__": initfn} +## if kwds: +## outkwds.update(kwds) +## +## if name is Undef: +## name = "_comp_".join([cls.__name__ for cls in bases]) +## count = _NameCount[name] = _NameCount.get(name,0)+1 +## if count > 1: +## name += "_%d" % (count,) +## +## return type(name,bases,outkwds) + +#========================================================= +#fs/env related +#========================================================= +@contextlib.contextmanager +def protected_env(*keys, **opts): + "context manager which restores cwd & specified environment keys" + cwd = opts.pop("cwd", False) + if opts: + raise TypeError, "unknown kwd options: %r" % (opts,) + if cwd: + c = getcwd() + assert c.isabs + if keys: + env = os.environ + o = dict((k,env.get(k)) for k in keys) + f = [] #list of files to purge + try: + yield f + finally: + for name in f: + filepath(name).discard() + if cwd: + c.chdir() + if keys: + for k, v in o.iteritems(): + if v is None: + if k in env: + del env[k] + else: + env[k] = v + +#========================================================= +#unused fs code that might be useful in the future +#========================================================= + +##def getDir(path, separator="\x00"): +## return join(separator, os.path.listdir(path)) + +##def getDirHash(path): +## return sha.new(getDir(path)).digest() + +##def getUrl(url, **kwds): +## """getUrl(url, **kwds) -> str +## wrapper for urllib, behaves like getFile. +## keyword args translated to cgi params. +## uses 'post' method. +## xxx: swallows all exceptions +## """ +## try: +## if len(kwds): +## fh = urllib.urlopen(url, urllib.urlencode(kwds)) +## else: +## fh = urllib.urlopen(url) +## except: +## return None +## try: +## return fh.read() +## finally: +## fh.close() + +##def getModUrl(srcUrl, tick=None, rawDate=False, dateFmt="%a, %d %b %Y %H:%M:%S %Z"): +## """ +## ok, data, tick = getModUrl(url,tick=None) +## """ +## print srcUrl, tick +## if tick is None: +## fh = urllib2.urlopen(srcUrl) +## else: +## if isinstance(tick, (int,float,long)): +## tick = time.strftime(dateFmt,time.gmtime(tick)) +## try: +## fh = urllib2.urlopen( +## urllib2.Request(srcUrl,None,{'If-Modified-Since': tick}) +## ) +## except urllib2.HTTPError, e: +## if e.code == 304: # not modified +## return False, None, tick +## else: +## raise e +## data = fh.read() +## tick = fh.headers['Last-Modified'] +## if not rawDate: +## tick = time.mktime(time.strptime(tick, dateFmt)) +## fh.close() +## return True, data, tick + +#========================================================= +#version string parsing - stopgap until PEP386 verlib is in stdlib +#========================================================= +FINAL_MARKER = ('f',) + +VERSION_RE = re.compile(r''' + ^ + (?P<release> + (?P<version>\d+\.\d+) # minimum 'N.N' + (?P<extraversion>(?:\.\d+)*) # any number of extra '.N' segments + ) + (?: + (?P<prerel>[abc]|rc) # 'a'=alpha, 'b'=beta, 'c'=release candidate + # 'rc'= alias for release candidate + (?P<prerelversion>\d+(?:\.\d+)*) + )? + (?P<postdev> + (?: \.post (?P<post>\d+) )? + (?: \.dev (?P<dev>\d+) )? + )? + $''', re.VERBOSE) + +def main_version(verstr, str=False): + "return version as ``(major,minor)`` tuple" + version = parse_version(verstr)[0][:2] + if str: + return "%d.%d" % version + else: + return version + +def release_version(verstr, str=False): + "return version+extraversion as ``(major,minor,...)`` tuple" + version = parse_version(verstr)[0] + if str: + return ".".join(str(n) for n in version) + else: + return version + +def get_module_release(modname, str=False): + "return release version given module name" + mod = get_module(modname) + return release_version(mod.__version__, str=str) + +def parse_version(verstr): + "parse version into parts per PEP386" + match = VERSION_RE.search(verstr) + if not match: + raise ValueError, "version string doesn't conform to PEP386: %r" % (verstr,) + groups = match.groupdict() + + def parse_numdots(s, minsize=0): + assert minsize >= 0 + nums = [] + for n in s.split("."): + if len(n) > 1 and n.startswith("0"): + raise ValueError("cannot have leading zero in version string segment: %r in %r" % (n, verstr)) + nums.append(int(n)) + if len(nums) > minsize: + while len(nums) > minsize and nums[-1] == 0: + nums.pop() + elif len(nums) < minsize: + nums.extend([0] * (minsize-len(nums))) + assert len(nums) >= minsize + return nums + + # main version + block = tuple(parse_numdots(groups['release'], 2)) + parts = [block] + + # prerelease + prerel = groups.get('prerel') + if prerel: + block = [prerel] + parse_numdots(groups['prerelversion'], 1) + parts.append(tuple(block)) + else: + parts.append(FINAL_MARKER) + + # postdev + if groups.get('postdev'): + post = groups.get('post') + dev = groups.get('dev') + postdev = [] + if post: + postdev.extend(FINAL_MARKER) + postdev.extend(['post', int(post)]) + if dev: + postdev.extend(FINAL_MARKER) + if dev: + postdev.extend(['dev', int(dev)]) + parts.append(tuple(postdev)) + else: + parts.append(FINAL_MARKER) + return tuple(parts) + +#========================================================= +#eof +#========================================================= diff --git a/bps/unstable/ansi.py b/bps/unstable/ansi.py new file mode 100644 index 0000000..3df3ed1 --- /dev/null +++ b/bps/unstable/ansi.py @@ -0,0 +1,683 @@ +"""ansi (aka vt100) control code handling + +this module contains some attempts at ANSI control code parsing, +with a focus on the color control codes. It also contains +some ctypes code for rendering said color codes onto windows consoles. +""" +#========================================================= +#imports +#========================================================= +#core +import sys +import os +import re +#pkg +from bps.logs.proxy_logger import log +from bps.meta import Params +#local +__all__ = [ + 'AnsiCode', 'is_ansi_code', + 'parse_ansi_string', + 'len_ansi_string', +] + +#========================================================= +#constants +#========================================================= +colors = ("black","red","green","yellow", "blue","magenta","cyan","white","default") + +def write_test_text(stream=sys.stdout): + "helper which writes some text to test ansi escape code handling" + def write(msg, *a, **k): + if a: msg %= a + if k: msg %= k + stream.write(msg) + + r8 = range(8) + [9] + hs = 16 + hfmt = "\x1b[0m%16s: " + fmt = "%-8s " * 8 + x = "-" * 7 + " " + + write("\x1b[2J\x1b[9999;9999H\x1b[3DBR\x1b[H") + + write("COLOR/STYLE CODES...\n") + write(" " * hs + " ") + for c in colors: + if not c: + continue + write("%-8s", c) + write("\n") + + write("-" * (hs+1) + " " + x * 9 + "\n") + + def write_row(name, seq): + write(hfmt % name) + for v in seq: + write("\x1b[%smTEST \x1b[0m",v) + write("\n") + + write_row("fg", ("3%s" % i for i in r8)) + #NOTE: 37 should basically be ignored + + write("\n") + write_row("unbold", ("1;3%s;22" % i for i in r8)) + write_row("bold", ("1;3%s" % i for i in r8)) + +## write("\n") +## write_row("unitalic", ("3;3%s;23" % i for i in r8)) +## write_row("italic", ("3;3%s" % i for i in r8)) + + #4/24 - underlined + + write("\n") + write_row("bg", ("4%s" % i for i in r8)) + write_row("bg+bold", ("1;4%s" % i for i in r8)) +# write("\n") +# write_row("under", ("4;3%s" % i for i in r8)) + + write("\n") + write_row("unblink", ("6;3%s;25" % i for i in r8)) + write_row("blink", ("5;3%s" % i for i in r8)) + write_row("blink+bold", ("6;1;3%s" % i for i in r8)) + + write("\n") + write_row("normal", ("7;3%s;27" % i for i in r8)) + write_row("reverse", ("7;3%s" % i for i in r8)) + write_row("bold+reverse", ("1;7;3%s" % i for i in r8)) #effective fg should be bold + write_row("blink+reverse", ("5;7;3%s" % i for i in r8)) + + write("\n") + write_row("visible", ("8;3%s;28" % i for i in r8)) + write_row("conceal", ("8;3%s" % i for i in r8)) #should display as solid BG block + + write("-" * (hs+1) + " " + x * 9 + "\n") + + write("\nCURSOR CONTROL CODES...\n") + write("1. UP-> <--\n") + write(" ERROR\x1b[1ATEST-UP\n2. ^ ^\n") + write("3. RIGHT ------\\/ \\/\n") + write("4. \x1b[13CTEST-RIGHT\n") + write("5. LEFT----> ERROR<--\x1b[12DTEST-LEFT\n") + write("6. DOWN----\\/ \\/\n7.\n ERROR \x1b[2A\x1b[1BTEST-DOWN\n8. /\\ /\\\n") + write("9. Far Right... \x1b[999C\x1b[13DTEST-FAR-RIGHT\nERROR\x1b[999D\x1b[1AA.\nB. \n") + +#========================================================= +#main classes +#========================================================= +#XXX: could remove the "CODESET." prefix from everything +class CODESET: + #the 'c0' set + #NOTE: this library ignores 0x09, 0x0A, 0x0D, and handles 0x1B specially + # xxx: could make this a special policy + C0 = "c0" + #range(0x00,0x20) + #identified by ESC + 0x21 + 0x40 + + #the 'c1' set + #NOTE: until CSTR support is added, this will parse CSTRs incorrectly + C1 = "c1" + # #ESC + range(0x40,0x60) + # #identified by ESC + 0x26 0x40 + # #8bit: 7bit identified by ESC + 0x20 + 0x46 + # #8bit: raw bytes in range(0x80,0xA0) + # #8bit: identified by ESC + 0x22 0x46 + + #control sequences + CSEQ = "cseq" + #ESC + 0x5b + P=range(0x30,0x40)? + I=range(0x20,0x30)? + F=range(0x40,0x7F) + # note F=range(0x70,0x7F) reserved for private/experimental use + #8bit: 0x9b instead of ESC + 0x5b + + #indep control funcs + ICF = "icf" + #ESC + range(0x60,0x7F) + + #code currently doesn't support parsing these, + ###control strings - meaning dependant on sender & receiver + ##CSTR = "cstr" + ## #startr + cstr + end + ST + ## #start: APC, DCS, OSC, PM, SOS - all defined in C1 + ## #cstr: (range(0x08,0x0D)|range(0x20,0x7F))? + ## # or any bitseq but SOS / ST + + values = (C0, C1, CSEQ, ICF) + +class AnsiError(ValueError): + "base for all ansi parsing errors" + +class AnsiParseError(AnsiError): + "error raised when parsing an incorrectly structured ansi control code" + +class AnsiCommandError(AnsiError): + "error raised when command contains invalid arguments" + +class AnsiCode(object): + "base class representing a vt100 control code" + #========================================================= + #instance attrs + #========================================================= + + #general + malformed = None #flag set if code is malformed: None if not malformed; if malformed, non-empty string containing error message + source = None #source string if set explicitly - use 'source' property + codeset = None #codeset this belongs to (one of CODESET.values) + #NOTE: this will be None for an instance IFF it's a "malformed" code + code = None #always contains code string w/ CODESET specific prefix & params removed + #see also <{codeset}_code> + + argstr = None #raw parameter string for CSEQ codes (empty string if no parms) + + #command specific attrs + args = None #generic tuple of parsed args + mode = None #used by some cseq commands which have a "mode" parameter + offset = None #used by some cseq commands which encode a single relative offset + row = None #used by some cseq commands which encode a absolute row + col = None #used by some cseq commands which encode a absolute col + + #========================================================= + #init + #========================================================= + def __init__(self, codeset, code, argstr=None, source=None, malformed=None): + if codeset is None: + assert code is None + assert argstr is None + else: + if codeset not in CODESET.values: + raise ValueError, "invalid codeset: %r" % (codeset,) + if not code: + raise ValueError, "code must be specified" + if malformed is True: + malformed = "<unknown reason>" + if malformed: + assert source + assert isinstance(malformed,str),"bad value: %r" % (malformed,) + self.malformed = malformed + self.codeset = codeset + self.code = code + self.source = source + if argstr is None and codeset == CODESET.CSEQ: + argstr = "" + self.argstr = argstr + + #run code-specific init func if present + if code: + func = self._get_init_func() + #XXX: not sure about this policy + if malformed: + try: + func() + except AnsiError, err: + self.malformed = "%s; %s" % (self.malformed, str(err)) + else: + func() + + def _get_init_func(self): + "retrieve code-specific init function" + codeset, code = self.codeset, self.code + name = "init_" + codeset + "_" + "_".join( + c if c.isalnum() + else "%02x" % (ord(c),) + for c in code + ) + func = getattr(self, name, None) + if func: + return func + name = "init_" + codeset + "_default" + func = getattr(self, name, None) + if func: + return func + return self.init_default + + @classmethod + def try_parse(cls, source): + "wrapper for :meth:`parse` which catches AnsiErrors" + try: + return True, cls.parse(source) + except AnsiError, err: + return False, err + + #XXX: flag controlling if argstr-related errors should be raised vs ignored vs turned into malformed? + + @classmethod + def parse(cls, source): + "parse control sequence; raises ValueError if format isn't right" + if not source: + raise AnsiParseError, "empty string is not a code" + elif source.startswith("\x1b"): + if len(source) < 2: + raise AnsiParseError, "too few characters in control code" + s1 = source[1] + if s1 == "[": + #parse cseq + if len(source) < 3: + raise AnsiParseError, "too few characters in control sequence" + code = source[-1] + if code < '\x40' or code >= '\x7F': + raise AnsiParseError, "invalid final character in control sequence" + idx = len(source)-2 + while idx > 1 and '\x20' <= source[idx] < '\x30': + idx -= 1 + code = source[idx+1:-1] + code + argstr = source[2:idx+1] + return cls(codeset=CODESET.CSEQ, code=code, + argstr=argstr, source=source) + elif s1 < '\x40': + #non-standard, but some legacy codes exist. + #TODO: should have init_c1_default issue warning + ##raise ValueError, "invalid control code" + if len(source) > 2: + #TODO: could be cstr instead + raise AnsiParseError, "too many characters in (c1) control code" + return cls(codeset=CODESET.C1, code=s1, source=source) + elif s1 < '\x60': + #parse c1 + if len(source) > 2: + #TODO: could be cstr instead + raise AnsiParseError, "too many characters in (c1) control code" + return cls(codeset=CODESET.C1, code=s1, source=source) + elif s1 < '\x7F': + #parse icf + if len(source) > 2: + raise AnsiParseError, "too many characters in (icf) control code" + return cls(codeset=CODESET.ICF, code=s1, source=source) + else: + raise AnsiParseError, "invalid control code" + elif len(source) == 1 and source < '\x20': + return cls(codeset=CODESET.C0, code=source, source=source) + else: + raise AnsiParseError, "unknown control code" + + #========================================================= + #python protocol + #========================================================= + def __str__(self): + "use source code came from, or render it as ansi string" + if self.source is None: + return self.render() + else: + return self.source + + def render(self): + "render string from components" + cs = self.codeset + if cs == CODESET.CSEQ: + return "\x1b[" + self.argstr + self.code + elif self.codeset == CODESET.C1 or self.codeset == CODESET.ICF: + return "\x1b" + self.code + elif not self.codeset: + return "" + else: + assert self.codeset == CODESET.C0 + return self.code + + def __repr__(self): + p = Params(self.codeset, self.code) + if self.codeset == CODESET.CSEQ and self.argstr: + p.append(self.argstr) + if self.source is not None and self.source != self.render(): + p.append(source=self.source) + malformed = self.malformed + if malformed: + ##if ';' in malformed: + ## #strip out init_xxx level errors that were added + ## malformed = malformed[:malformed.index(";")] + p.append(malformed=malformed) + return "AnsiCode(%s)" % p + + def __eq__(self, other): + if is_ansi_code(other): + #XXX: deal w/ malformed - probably should compare 'source' attrs + return self.codeset == other.codeset and \ + self.code == other.code and self.argstr == other.argstr + return False + + def __ne__(self, other): + return not self.__eq__(other) + + #========================================================= + #malformed helpers + #========================================================= + @classmethod + def create_malformed(cls, source, reason=None): + "helper to create a MalformedAnsiCode" + return cls(None, None, malformed=reason or True, source=source) + + def is_malformed(self): + return bool(self.malformed) + + def get_malformed_reasons(self): + if self.malformed: + return (self.malformed,) +## return self.malformed.split(";") + else: + return () + + #========================================================= + #codeset & code examination + #========================================================= + def get_c0_code(self): + return self.code if self.codeset == CODESET.C0 else None + c0_code = property(get_c0_code) + + def get_c1_code(self): + return self.code if self.codeset == CODESET.C1 else None + c1_code = property(get_c1_code) + + def get_cseq_code(self): + return self.code if self.codeset == CODESET.CSEQ else None + cseq_code = property(get_cseq_code) + + def get_icf_code(self): + return self.code if self.codeset == CODESET.ICF else None + icf_code = property(get_icf_code) + + ##def get_cstr_code(self): + ## return self.code if self.codeset == CODESET.CSTR else None + ##cstr_code = property(get_cstr_code) + + #========================================================= + #argstr parsing + #========================================================= + def parse_cseq_int_args(self): + "return argstr as ints separated by semicolons ala CSEQ" + if not self.argstr: + return () + def cast_int(x): + try: + return int(x) + except ValueError: + raise AnsiParseError("argstr contains non-integer: %r" % (self.argstr,)) + return tuple(cast_int(x) for x in self.argstr.split(";")) + + def _tma_error(self): + return AnsiCommandError("too many arguments for command sequence: %r" % (str(self),)) + + def _wna_error(self): + return AnsiCommandError("wrong number of arguments for command sequence: %r" % (str(self),)) + + #========================================================= + #c0, c1 init helpers + #========================================================= + def init_default(self): + #generic fallback + pass + + def init_c0_1b(self): + #forbidden, since this signals start of c1, icf, or cseq + raise AnsiParseError, "raw 'ESC' is not a valid control code" + + def init_c1_5b(self): + #forbidden, since this signals start of cseq + raise AnsiParseError, "raw 'ESC' + '[' is not a valid control code" + + #========================================================= + #cseq init helpers + #========================================================= + def init_cseq_default(self): + #by default, parse argstr as ints (if present at all) + self.args = self.parse_cseq_int_args() + + def init_cseq_A(self): + args = self.args = self.parse_cseq_int_args() + if not args: + self.offset = 1 + elif len(args) == 1: + self.offset, = args + else: + raise self._tma_error() + init_cseq_D = init_cseq_C = init_cseq_B = init_cseq_A + + def init_cseq_f(self): + self.code = "H" + self.init_cseq_H() + + def init_cseq_H(self): + #TODO: support row or col being None + args = self.args = self.parse_cseq_int_args() + if not args: + self.col = self.row = 0 + elif len(args) == 2: + self.col, self.row = args + else: + raise self._wna_error() + + def init_cseq_J(self): + args = self.args = self.parse_cseq_int_args() + if not args: + self.mode = 0 + elif len(args) == 1: + self.mode, = args + ##if not (0 <= self.mode < 3): + ## raise AnsiCommandError, "unknown clear-line mode: %r" % (str(self),) + else: + raise self._tma_error() + + def init_cseq_K(self): + args = self.args = self.parse_cseq_int_args() + if not args: + self.mode = 0 + elif len(args) == 1: + self.mode, = args + ##if not (0 <= self.mode < 3): + ## raise AnsiCommandError, "unknown clear-screen mode: %r" % (str(self),) + else: + raise self._tma_error() + + def init_cseq_m(self): + #ensure args are parseable + args = self.args = self.parse_cseq_int_args() + ##if not args: + ## raise AnsiCommandError, "no styles listed: %r" % (str(self),) + ##if any(x < 0 or x > 100 for x in args): + ## raise AnsiCommandError, "style value out of bounds: %r" % (str(self),) + + #========================================================= + #eoc + #========================================================= + +#========================================================= +#utilities +#========================================================= + +def is_ansi_code(obj): + "check if object is a AnsiCode object" + return hasattr(obj,"codeset") and hasattr(obj,"code") + +def is_malformed_ansi_code(obj): + return is_ansi_code(obj) and obj.is_malformed() + +def len_ansi_string(source): + """return effective text length of a ansi string. + + .. todo:: + decide on whether cursor control codes should result in error, + warning, or be ignored. for now, naively counting chars. + """ + count = 0 + for elem in parse_ansi_string(source, rtype=iter): + if not is_ansi_code(elem): + count += len(elem) + return count + +def strip_ansi_string(source): + "strip ansi escape codes out of string" + return "".join( + elem + for elem in parse_ansi_string(source, rtype=iter, + malformed_codes="parse") + if not is_ansi_code(elem) + ) + +def parse_ansi_string(source, rtype=list, malformed_codes="ignore"): + """parse string, yeilding chunks of raw text and ansi control codes. + + :arg source: + source string to parse + + :param rtype: + optionally you can specify the return type of this function; + the common values are ``list``, and ``iter``. + + :param malformed_codes: + this sets the policy for how this function + handles malformed command codes. + + * ``ignore`` (the default) -- malformed codes are ignored, and kept as literal text. + * ``parse`` -- malformed codes are parsed and returned + in :class:`AnsiCode` instances which have no code or codeset specified. + * ``strip`` -- malformed codes are removed entirely. + * ``raise`` -- malformed codes cause a ValueError to be raised. + + :returns: + this returns a list of 1 or more elements, + which are either raw strings, or :class:`AnsiCode` instances. + + """ + assert malformed_codes in ("ignore","parse","strip","raise") + if malformed_codes == "strip": + result = ( + elem + for elem in _parse_ansi_helper(source, "parse") + if not is_malformed_ansi_code(elem) + ) + else: + result = _parse_ansi_helper(source, malformed_codes) + if rtype is iter: + return result + else: + return rtype(result) + +def _parse_ansi_helper(source, malformed_codes): + if not source: + yield "" + return + if malformed_codes == "raise": + def create_bad(source, reason): + raise ValueError, "%s: %r" % (reason, source) + create = AnsiCode.parse + elif malformed_codes == "ignore": + def create_bad(source,reason): + log.warning("ignoring malformed control code: %r: %r", reason, source) + return source + def create(source): + ok, result = AnsiCode.try_parse(source) + if ok: + return result + else: + log.warning("ignoring malformed control code: %r: %r", result, source) + return source + else: + assert malformed_codes == "parse" + create_bad = AnsiCode.create_malformed + def create(source): + ok, result = AnsiCode.try_parse(source) + if ok: + return result + else: + result = str(result) + log.warning("encounterd malformed control code: %r: %r", result, source) + return create_bad(source, result) + state = 0 + #0 - scanning raw text into buffer + #1 - saw ESC -- looking for next char + #2 - saw ESC+[ -- scanning cseq into buffer + buf = "" + for c in source: + if state == 1: + #parsing escape code + assert buf == "\x1b" + if c == '[': + #it's a cseq + buf += c + state = 2 + continue + else: + #assume it's a 2 char escape code (c1, icf) + buf += c + yield create(buf) + state = 0 + buf = "" + continue + + elif state == 2: + assert buf.startswith("\x1b[") + + if '\x20' <= c < '\x40': + #parse cseq param or intermediate byte + buf += c + continue + elif '\x40' <= c < '\x7F': + #parse cseq final byte + buf += c + yield create(buf) + buf = "" + state = 0 + continue + else: + #cseq should contain no other bytes, + #so something's invalid here + yield create_bad("\x1b[" + buf, "string contains unterminated control code") + #fall through to state 0, below + state = 0 + + #this is down here in case a higher state finishes early + if state == 0: + #parsing raw text + if c < '\x20': + #it's a c0 code... + if c == "\x1b": + #jump to escape handling (c1,icf,cseq) + if buf: + yield buf + buf = c + state = 1 + continue + elif c in '\r\n\t': + #treat these codes like regular characters. + #XXX: should caller be able to set policy? + buf +=c + continue + else: + #all others, yeild c0 code + if buf: + yield buf + buf = "" + yield create(c) + continue + else: + buf += c + continue + + if state == 0: + if buf: + yield buf + else: + yield create_bad(buf, "string ends with unterminated control code") + +#========================================================= +#streams +#========================================================= +class AnsiStripper(object): + "wraps another stream, removes ansi escape codes before writing to original stream" + stream = None + + def __init__(self, stream): + self.stream = stream + + def __getattr__(self, attr): + return getattr(self.stream, attr) + + def write(self, text): + write = self.stream.write + for elem in parse_ansi_string(text, rtype=iter, malformed_codes="parse"): + if not is_ansi_code(elem): + write(elem) + + def writelines(self, seq): + for elem in seq: + self.write(seq) + +#========================================================= +#eof +#========================================================= diff --git a/bps/unstable/bpsdoc/__init__.py b/bps/unstable/bpsdoc/__init__.py new file mode 100644 index 0000000..3048668 --- /dev/null +++ b/bps/unstable/bpsdoc/__init__.py @@ -0,0 +1,8 @@ +""" +This module contains a few small sphinx extensions. +They are mainly used to help with the generation +of BPS's own documentation, but some other projects +use them as well, so they are kept here. +""" +import os.path +theme_path = os.path.abspath(os.path.join(__file__,os.path.pardir)) diff --git a/bps/unstable/bpsdoc/ast/layout.html b/bps/unstable/bpsdoc/ast/layout.html new file mode 100644 index 0000000..3384e90 --- /dev/null +++ b/bps/unstable/bpsdoc/ast/layout.html @@ -0,0 +1,29 @@ +{% extends "basic/layout.html" %} + +{# include release in root breadcrumb #} +{% block rootrellink %} + <li><a href="{{ pathto("index") }}"><b>{{project}} v{{release}}</b> Documentation</a> » </li> +{% endblock %} + +{# have logo go to index, not TOC #} +{# FIXME: is there a way to read conf.py setting, so user can set index_doc or we can read master_doc? #} +{%- block sidebarlogo %} + {%- if logo %} + <p class="logo"><a href="{{ pathto("index") }}"> + <img class="logo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/> + </a></p> + {%- endif %} +{%- endblock %} + +{# give relbars separate classes for themeing #} +{%- block relbar1 %} + <div class="relbar-top"> + {{ super() }} + </div> +{% endblock %} + +{%- block relbar2 %} + <div class="relbar-bottom"> + {{ super() }} + </div> +{% endblock %} diff --git a/bps/unstable/bpsdoc/ast/static/ast-website.css b/bps/unstable/bpsdoc/ast/static/ast-website.css new file mode 100644 index 0000000..d720321 --- /dev/null +++ b/bps/unstable/bpsdoc/ast/static/ast-website.css @@ -0,0 +1,733 @@ +/** + * Sphinx Doc Design + */ + +body { + font-family: sans-serif; + font-size: 100%; + background: #D4BCA5 url(bg_top.jpg) repeat-x; + color: #000; + margin: 0; + padding: 0; +} + +/* :::: LAYOUT :::: */ + +div.document { + background-color: #FBF8F4; + margin: auto; + max-width: 1024px; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 230px; +} + +div.body { + background-color: white; + padding: 20px 20px 30px 20px; + border-left: 2px dotted #F5F2EE; + min-height: 400px; +} + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; +} + +div.clearer { + clear: both; +} + +div.footer { + color: #fff; + width: 100%; + padding: 9px 0 9px 0; + text-align: center; + font-size: 75%; +} + +div.footer a { + color: #fff; + text-decoration: underline; +} + +div.footer sep +{ + font-weight: bold; + color: #854D15; +} + +div.relbar-top +{ + background-color: #FFFEFC; + border-bottom:5px solid #854D15; +} + +div.relbar-bottom div.related +{ + background-color: #FFFEFC; + border-bottom:5px solid #854D15; +} + +div.related { +/* background-color: #FFFEFC; + border-bottom:5px solid #854D15;*/ +/* color: #fff;*/ + line-height: 30px; + font-size: 90%; + margin: auto; + max-width: 1024px; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +div.related a { +/* color: white;*/ +} + +/* ::: TOC :::: */ +div.sphinxsidebar h3 { + font-family: 'Trebuchet MS', sans-serif; +/* color: white; */ + font-size: 1.3em; + font-weight: normal; + margin: 0; + padding: 0; + border-bottom:2px dotted #E3D4C5; + color: #D4BCA5; +} + +div.sphinxsidebar h3 a { + color: #D4BCA5; +/* color: white; */ +} + +div.sphinxsidebar h4 { + font-family: 'Trebuchet MS', sans-serif; +/* color: white; */ + font-size: 1.2em; + font-weight: normal; + margin: 5px 0 0 0; + padding: 0; + border-bottom:2px dotted #E3D4C5; + color: #D4BCA5; +} + +div.sphinxsidebar p { +/* color: white; */ +} + +div.sphinxsidebar p.topless { + margin: 5px 10px 10px 10px; +} + +div.sphinxsidebar ul { + margin: 10px; + padding: 0; + list-style: none; +/* color: white; */ +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar a { +/* color: #98dbcc; */ +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #D4BCA5; + font-family: sans-serif; + font-size: 1em; +} + +/* :::: MODULE CLOUD :::: */ +div.modulecloud { + margin: -5px 10px 5px 10px; + padding: 10px; + line-height: 160%; + border: 1px solid #cbe7e5; + background-color: #f2fbfd; +} + +div.modulecloud a { + padding: 0 5px 0 5px; +} + +/* :::: SEARCH :::: */ +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li div.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* :::: COMMON FORM STYLES :::: */ + +div.actions { + padding: 5px 10px 5px 10px; + border-top: 1px solid #cbe7e5; + border-bottom: 1px solid #cbe7e5; + background-color: #e0f6f4; +} + +form dl { + color: #333; +} + +form dt { + clear: both; + float: left; + min-width: 110px; + margin-right: 10px; + padding-top: 2px; +} + +input#homepage { + display: none; +} + +div.error { + margin: 5px 20px 0 0; + padding: 5px; + border: 1px solid #d00; + font-weight: bold; +} + +/* :::: INDEX PAGE :::: */ + +table.contentstable { + width: 90%; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* :::: INDEX STYLES :::: */ + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable dl, table.indextable dd { + margin-top: 0; + margin-bottom: 0; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +form.pfform { + margin: 10px 0 20px 0; +} + +/* :::: GLOBAL STYLES :::: */ + +.docwarning { + background-color: #ffe4e4; + padding: 10px; + margin: 0 -20px 0 -20px; + border-bottom: 1px solid #f66; +} + +p.subhead { + font-weight: bold; + margin-top: 20px; +} + +a { + color: #739BD5; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: 'Trebuchet MS', sans-serif; + background-color:#F5F0E9; + font-weight: normal; + color: black; + border-bottom:1px solid #D4BCA5; + margin: 20px 0 10px; + padding: 3px 0 3px 10px; +} + +div.body h1 { margin: -10px -10px 0; font-size: 200%; border: 1px solid #D4BCA5; } +div.body h2 { font-size: 160%; } +div.body h3 { font-size: 140%; border-color: #E3D4C5; } +div.body h4 { font-size: 120%; border-color: #E3D4C5; } +div.body h5 { font-size: 110%; border-color: #E3D4C5; } +div.body h6 { font-size: 100%; border-color: #E3D4C5; } + +a.headerlink { + color: #c60f0f; + font-size: 0.8em; + padding: 0 4px 0 4px; + text-decoration: none; + visibility: hidden; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink { + visibility: visible; +} + +a.headerlink:hover { + background-color: #c60f0f; + color: white; +} + +div.body p, div.body dd, div.body li { + text-align: justify; + line-height: 130%; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +ul.fakelist { + list-style: none; + margin: 10px 0 10px 20px; + padding: 0; +} + +.field-list ul { + padding-left: 1em; +} + +.first { + margin-top: 0 !important; +} + +/* "Footnotes" heading */ +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +/* Sidebars */ + +div.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px 7px 0 7px; + background-color: #ffe; + width: 40%; + float: right; +} + +p.sidebar-title { + font-weight: bold; +} + +/* "Topics" */ + +div.topic { + background-color: #eee; + border: 1px solid #ccc; + padding: 7px 7px 0 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* Admonitions */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +div.admonition dl { + margin-bottom: 0; +} + +div.admonition p.admonition-title + p { + display: inline; +} + +div.seealso { + background-color: #ffc; + border: 1px solid #ff6; +} + +div.warning { + background-color: #ffe4e4; + border: 1px solid #f66; +} + +div.note, +div.admonition-todo +{ + background-color: #fafafa; + border: 1px solid #eee; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +table.docutils { + border: 0; +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 0; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +table.docutils th.field-name +{ + position: absolute; + font-family: sans-serif; + font-weight: normal; +} + +table.docutils td.field-body +{ + padding: 1.5em 0 0 1.5em; +} + +table.docutils td.field-body dt +{ + font-style: italic; +} + +table.field-list td, table.field-list th { + border: 0 !important; +} + +table.footnote td, table.footnote th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +dl { + margin-bottom: 15px; + clear: both; +} + +dl.function > dt, +dl.attribute > dt, +dl.class > dt, +dl.exception > dt +{ + border-bottom: 3px dotted #E7EEF3; + padding: 1px 1px 1px 3px; +} + +dl.attribute > dt { border-color: #E9E4F3; } +dl.attribute > dt tt.descname { font-style: italic; } +dl.class > dt { border-color: #F3EAE7; } +dl.exception > dt { border-color: #F3EBDB; } + +tt.descclassname, tt.descname +{ + font-size: 120%; +} + +dl.exception > dd, +dl.class > dd, +dl.function > dd, +dl.attribute > dd +{ + padding-top: .1em; + padding-bottom: .75em; +} + +dd p { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +.refcount { + color: #060; +} + +dt:target, +.highlight { + background-color: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +th { + text-align: left; + padding-right: 5px; +} + +pre { + padding: 5px; + background-color: #F1FFD4; + color: #333; + border: 1px solid #D5E6B3; + border-left: none; + border-right: none; + overflow: auto; +} + +td.linenos pre { + padding: 5px 0px; + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + margin-left: 0.5em; +} + +table.highlighttable td { + padding: 0 0.5em 0 0.5em; +} + +tt { + background-color: #ECF0F3; + padding: 1px 2px 1px 2px; + font-size: 0.95em; +} + +tt.descname { + background-color: transparent; + font-weight: bold; + font-size: 1.2em; +} + +tt.descclassname { + background-color: transparent; +} + +tt.xref, a tt { + background-color: transparent; + font-weight: bold; +} + +.footnote:target { background-color: #ffa } + +h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.versionmodified { + font-style: italic; +} + +form.comment { + margin: 0; + padding: 10px 30px 10px 30px; + background-color: #eee; +} + +form.comment h3 { + background-color: #326591; + color: white; + margin: -10px -30px 10px -30px; + padding: 5px; + font-size: 1.4em; +} + +form.comment input, +form.comment textarea { + border: 1px solid #ccc; + padding: 2px; + font-family: sans-serif; + font-size: 100%; +} + +form.comment input[type="text"] { + width: 240px; +} + +form.comment textarea { + width: 100%; + height: 200px; + margin-bottom: 10px; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +img.math { + vertical-align: middle; +} + +div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +img.logo { + border: 0; +} + +/* :::: PRINT :::: */ +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0; + width : 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + div#comments div.new-comment-box, + #top-link { + display: none; + } +} diff --git a/bps/unstable/bpsdoc/ast/static/ast.css b/bps/unstable/bpsdoc/ast/static/ast.css new file mode 100644 index 0000000..2734b35 --- /dev/null +++ b/bps/unstable/bpsdoc/ast/static/ast.css @@ -0,0 +1,153 @@ +/***************************************************** + * Additional styles laid on top of default theme + *****************************************************/ + +@import url("default.css"); + +/***************************************************** + * limit page width to 1024px + *****************************************************/ + +div.related, div.document +{ + margin: 0 auto; + width: 1024px; +} + +div.relbar-top +{ + margin-top: 1em; +} + +/***************************************************** + * make sure small pages lay out right + *****************************************************/ +div.body +{ + min-height: 550px; +} + +/***************************************************** + * provide styling for TODO + *****************************************************/ +div.admonition.caution { + background-color: #FFF0E4; + border: 1px solid #FF9966; +} + +div.admonition-todo { + background-color: #EEE6E0; + border: 1px solid #D4CDC7; +} + +div#todos p.admonition-title +{ + font-weight: normal; + color: #AAA; + font-size: 70%; +} + +div#todos div.admonition-todo + p +{ + font-size: 70%; + text-align: right; + margin-top: -.5em; + margin-bottom: 1.5em; + color: #AAA; +} + +div#todos div.admonition-todo + p a +{ + font-size: 130%; +} + +/***************************************************** + * add more whitespace to definitions + *****************************************************/ +dl.function, +dl.method, +dl.attribute, +dl.class, +dl.exception, +dl.data +{ + margin-bottom: 1.5em; +} + +/***************************************************** + * add more whitespace to parameter lists + *****************************************************/ +td.field-body > ul.first.simple > li, +td.field-body > p.first +{ + margin-bottom: 1em; +} + +td.field-body > ul.first.simple > li > em, +td.field-body > em +{ + border-bottom: 1px solid #DEE6ED; + padding: 2px 4px; + background: #ECF0F3; +} + +/***************************************************** + * css colorization of object definitions, + * adds colored line underneath definition title. + * color scheme used: + * callables (function, method) - green + * attributes - purple + * classes - red + * exceptions - orange + * data - blue + *****************************************************/ + +dl.function > dt, +dl.method > dt, +dl.attribute > dt, +dl.class > dt, +dl.exception > dt, +dl.data > dt +{ + border-bottom: 2px solid #9AB9CE; + background: #E2ECF3; + padding: .1em 1px 1px 3px; +} + +dl.function > dt { border-color: #8BC38B; background: #D8E8D8; } +dl.method > dt { border-color: #AA96C2; background: #E8E1F2; } +dl.attribute > dt { border-color: #9996C2; background: #E7E6F6; } +dl.attribute > dt tt.descname { font-style: italic; } +dl.class > dt { border-color: #C8A69A; background: #E8DCD8; } +dl.exception > dt { border-color: #F8A69A; background: #F2E3E1; } + +/***************************************************** + * css colorization for index page, using styles + * provided by customize.py extension + *****************************************************/ + +table.indextable span.category +{ + font-size: 80%; + color: #84ADBE; +} + +table.indextable span.category.function { color: #8BC38B; } +table.indextable span.category.method { color: #AA96C2; } +table.indextable span.category.attribute { color: #9996C2; } +table.indextable span.category.class { color: #C8A69A; } +table.indextable span.category.module { color: #9AB9CE; } + +table.indextable span.subject +{ + font-weight: bold; +} + +table.indextable td > dl > dt +{ + margin-bottom: .5em; +} + +/***************************************************** + * EOF + *****************************************************/ diff --git a/bps/unstable/bpsdoc/ast/static/bg_top.jpg b/bps/unstable/bpsdoc/ast/static/bg_top.jpg Binary files differnew file mode 100644 index 0000000..c1e5775 --- /dev/null +++ b/bps/unstable/bpsdoc/ast/static/bg_top.jpg diff --git a/bps/unstable/bpsdoc/ast/theme.conf b/bps/unstable/bpsdoc/ast/theme.conf new file mode 100644 index 0000000..33a4bff --- /dev/null +++ b/bps/unstable/bpsdoc/ast/theme.conf @@ -0,0 +1,3 @@ +[theme] +inherit = default +stylesheet = ast.css diff --git a/bps/unstable/bpsdoc/cloud/layout.html b/bps/unstable/bpsdoc/cloud/layout.html new file mode 100644 index 0000000..597732b --- /dev/null +++ b/bps/unstable/bpsdoc/cloud/layout.html @@ -0,0 +1,51 @@ +{% extends "basic/layout.html" %} + +{%- set theme_roottarget = (theme_roottarget == "<master>" and master_doc or theme_roottarget) %} +{%- set theme_logotarget = (theme_logotarget == "<root>" and theme_roottarget or (theme_logotarget == "<master>" and master_doc or theme_logotarget)) %} +{%- set footdelim = footdelim is not defined and ' / ' or footdelim %} + +{# make root link redirectable #} +{% block rootrellink %} + <li><a href="{{ pathto(theme_roottarget) }}">{{project}} Documentation</a> » </li> +{% endblock %} + +{# make logo link redirectable #} +{%- block sidebarlogo %} + {%- if logo %} + <p class="logo"><a href="{{ pathto(theme_logotarget) }}"> + <img class="logo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/> + </a></p> + {%- endif %} +{%- endblock %} + +{# give relbars separate classes for themeing #} +{%- block relbar1 %} + <div class="relbar-top"> + {{ super() }} + </div> +{% endblock %} + +{%- block relbar2 %} + <div class="relbar-bottom"> + {{ super() }} + </div> +{% endblock %} + +{# add separators into footer #} +{%- block footer %} + <div class="footer"> + {%- if hasdoc('copyright') %} + {% trans path=pathto('copyright'), copyright=copyright|e %}© <a href="{{ path }}">Copyright</a> {{ copyright }}.{% endtrans %} + {%- else %} + {% trans copyright=copyright|e %}© Copyright {{ copyright }}.{% endtrans %} + {%- endif %} + {%- if last_updated %} + {{ footdelim }} + {% trans last_updated=last_updated|e %}Last updated on {{ last_updated }}.{% endtrans %} + {%- endif %} + {%- if show_sphinx %} + {{ footdelim }} + {% trans sphinx_version=sphinx_version|e %}Created using <a href="http://sphinx.pocoo.org/">Sphinx</a> {{ sphinx_version }}.{% endtrans %} + {%- endif %} + </div> +{%- endblock %} diff --git a/bps/unstable/bpsdoc/cloud/static/ast-website.css b/bps/unstable/bpsdoc/cloud/static/ast-website.css new file mode 100644 index 0000000..d720321 --- /dev/null +++ b/bps/unstable/bpsdoc/cloud/static/ast-website.css @@ -0,0 +1,733 @@ +/** + * Sphinx Doc Design + */ + +body { + font-family: sans-serif; + font-size: 100%; + background: #D4BCA5 url(bg_top.jpg) repeat-x; + color: #000; + margin: 0; + padding: 0; +} + +/* :::: LAYOUT :::: */ + +div.document { + background-color: #FBF8F4; + margin: auto; + max-width: 1024px; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 230px; +} + +div.body { + background-color: white; + padding: 20px 20px 30px 20px; + border-left: 2px dotted #F5F2EE; + min-height: 400px; +} + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; +} + +div.clearer { + clear: both; +} + +div.footer { + color: #fff; + width: 100%; + padding: 9px 0 9px 0; + text-align: center; + font-size: 75%; +} + +div.footer a { + color: #fff; + text-decoration: underline; +} + +div.footer sep +{ + font-weight: bold; + color: #854D15; +} + +div.relbar-top +{ + background-color: #FFFEFC; + border-bottom:5px solid #854D15; +} + +div.relbar-bottom div.related +{ + background-color: #FFFEFC; + border-bottom:5px solid #854D15; +} + +div.related { +/* background-color: #FFFEFC; + border-bottom:5px solid #854D15;*/ +/* color: #fff;*/ + line-height: 30px; + font-size: 90%; + margin: auto; + max-width: 1024px; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +div.related a { +/* color: white;*/ +} + +/* ::: TOC :::: */ +div.sphinxsidebar h3 { + font-family: 'Trebuchet MS', sans-serif; +/* color: white; */ + font-size: 1.3em; + font-weight: normal; + margin: 0; + padding: 0; + border-bottom:2px dotted #E3D4C5; + color: #D4BCA5; +} + +div.sphinxsidebar h3 a { + color: #D4BCA5; +/* color: white; */ +} + +div.sphinxsidebar h4 { + font-family: 'Trebuchet MS', sans-serif; +/* color: white; */ + font-size: 1.2em; + font-weight: normal; + margin: 5px 0 0 0; + padding: 0; + border-bottom:2px dotted #E3D4C5; + color: #D4BCA5; +} + +div.sphinxsidebar p { +/* color: white; */ +} + +div.sphinxsidebar p.topless { + margin: 5px 10px 10px 10px; +} + +div.sphinxsidebar ul { + margin: 10px; + padding: 0; + list-style: none; +/* color: white; */ +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar a { +/* color: #98dbcc; */ +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #D4BCA5; + font-family: sans-serif; + font-size: 1em; +} + +/* :::: MODULE CLOUD :::: */ +div.modulecloud { + margin: -5px 10px 5px 10px; + padding: 10px; + line-height: 160%; + border: 1px solid #cbe7e5; + background-color: #f2fbfd; +} + +div.modulecloud a { + padding: 0 5px 0 5px; +} + +/* :::: SEARCH :::: */ +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li div.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* :::: COMMON FORM STYLES :::: */ + +div.actions { + padding: 5px 10px 5px 10px; + border-top: 1px solid #cbe7e5; + border-bottom: 1px solid #cbe7e5; + background-color: #e0f6f4; +} + +form dl { + color: #333; +} + +form dt { + clear: both; + float: left; + min-width: 110px; + margin-right: 10px; + padding-top: 2px; +} + +input#homepage { + display: none; +} + +div.error { + margin: 5px 20px 0 0; + padding: 5px; + border: 1px solid #d00; + font-weight: bold; +} + +/* :::: INDEX PAGE :::: */ + +table.contentstable { + width: 90%; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* :::: INDEX STYLES :::: */ + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable dl, table.indextable dd { + margin-top: 0; + margin-bottom: 0; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +form.pfform { + margin: 10px 0 20px 0; +} + +/* :::: GLOBAL STYLES :::: */ + +.docwarning { + background-color: #ffe4e4; + padding: 10px; + margin: 0 -20px 0 -20px; + border-bottom: 1px solid #f66; +} + +p.subhead { + font-weight: bold; + margin-top: 20px; +} + +a { + color: #739BD5; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: 'Trebuchet MS', sans-serif; + background-color:#F5F0E9; + font-weight: normal; + color: black; + border-bottom:1px solid #D4BCA5; + margin: 20px 0 10px; + padding: 3px 0 3px 10px; +} + +div.body h1 { margin: -10px -10px 0; font-size: 200%; border: 1px solid #D4BCA5; } +div.body h2 { font-size: 160%; } +div.body h3 { font-size: 140%; border-color: #E3D4C5; } +div.body h4 { font-size: 120%; border-color: #E3D4C5; } +div.body h5 { font-size: 110%; border-color: #E3D4C5; } +div.body h6 { font-size: 100%; border-color: #E3D4C5; } + +a.headerlink { + color: #c60f0f; + font-size: 0.8em; + padding: 0 4px 0 4px; + text-decoration: none; + visibility: hidden; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink { + visibility: visible; +} + +a.headerlink:hover { + background-color: #c60f0f; + color: white; +} + +div.body p, div.body dd, div.body li { + text-align: justify; + line-height: 130%; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +ul.fakelist { + list-style: none; + margin: 10px 0 10px 20px; + padding: 0; +} + +.field-list ul { + padding-left: 1em; +} + +.first { + margin-top: 0 !important; +} + +/* "Footnotes" heading */ +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +/* Sidebars */ + +div.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px 7px 0 7px; + background-color: #ffe; + width: 40%; + float: right; +} + +p.sidebar-title { + font-weight: bold; +} + +/* "Topics" */ + +div.topic { + background-color: #eee; + border: 1px solid #ccc; + padding: 7px 7px 0 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* Admonitions */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +div.admonition dl { + margin-bottom: 0; +} + +div.admonition p.admonition-title + p { + display: inline; +} + +div.seealso { + background-color: #ffc; + border: 1px solid #ff6; +} + +div.warning { + background-color: #ffe4e4; + border: 1px solid #f66; +} + +div.note, +div.admonition-todo +{ + background-color: #fafafa; + border: 1px solid #eee; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +table.docutils { + border: 0; +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 0; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +table.docutils th.field-name +{ + position: absolute; + font-family: sans-serif; + font-weight: normal; +} + +table.docutils td.field-body +{ + padding: 1.5em 0 0 1.5em; +} + +table.docutils td.field-body dt +{ + font-style: italic; +} + +table.field-list td, table.field-list th { + border: 0 !important; +} + +table.footnote td, table.footnote th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +dl { + margin-bottom: 15px; + clear: both; +} + +dl.function > dt, +dl.attribute > dt, +dl.class > dt, +dl.exception > dt +{ + border-bottom: 3px dotted #E7EEF3; + padding: 1px 1px 1px 3px; +} + +dl.attribute > dt { border-color: #E9E4F3; } +dl.attribute > dt tt.descname { font-style: italic; } +dl.class > dt { border-color: #F3EAE7; } +dl.exception > dt { border-color: #F3EBDB; } + +tt.descclassname, tt.descname +{ + font-size: 120%; +} + +dl.exception > dd, +dl.class > dd, +dl.function > dd, +dl.attribute > dd +{ + padding-top: .1em; + padding-bottom: .75em; +} + +dd p { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +.refcount { + color: #060; +} + +dt:target, +.highlight { + background-color: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +th { + text-align: left; + padding-right: 5px; +} + +pre { + padding: 5px; + background-color: #F1FFD4; + color: #333; + border: 1px solid #D5E6B3; + border-left: none; + border-right: none; + overflow: auto; +} + +td.linenos pre { + padding: 5px 0px; + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + margin-left: 0.5em; +} + +table.highlighttable td { + padding: 0 0.5em 0 0.5em; +} + +tt { + background-color: #ECF0F3; + padding: 1px 2px 1px 2px; + font-size: 0.95em; +} + +tt.descname { + background-color: transparent; + font-weight: bold; + font-size: 1.2em; +} + +tt.descclassname { + background-color: transparent; +} + +tt.xref, a tt { + background-color: transparent; + font-weight: bold; +} + +.footnote:target { background-color: #ffa } + +h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.versionmodified { + font-style: italic; +} + +form.comment { + margin: 0; + padding: 10px 30px 10px 30px; + background-color: #eee; +} + +form.comment h3 { + background-color: #326591; + color: white; + margin: -10px -30px 10px -30px; + padding: 5px; + font-size: 1.4em; +} + +form.comment input, +form.comment textarea { + border: 1px solid #ccc; + padding: 2px; + font-family: sans-serif; + font-size: 100%; +} + +form.comment input[type="text"] { + width: 240px; +} + +form.comment textarea { + width: 100%; + height: 200px; + margin-bottom: 10px; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +img.math { + vertical-align: middle; +} + +div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +img.logo { + border: 0; +} + +/* :::: PRINT :::: */ +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0; + width : 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + div#comments div.new-comment-box, + #top-link { + display: none; + } +} diff --git a/bps/unstable/bpsdoc/cloud/static/ast.css_t b/bps/unstable/bpsdoc/cloud/static/ast.css_t new file mode 100644 index 0000000..d12a11f --- /dev/null +++ b/bps/unstable/bpsdoc/cloud/static/ast.css_t @@ -0,0 +1,248 @@ +/***************************************************** + * Additional styles laid on top of default theme + *****************************************************/ + +@import url("default.css"); + +/***************************************************** + * enforce max width and min height, and center document + *****************************************************/ + +div.related, div.document +{ + margin: 0 auto; + max-width: {{ theme_docwidth }}; +} + +div.relbar-top +{ + margin-top: 1em; +} + +div.body +{ + /* note: this is just a hack to prevent body from being shorter than sidebar */ + min-height: {{ theme_docheight }}; +} + +/***************************************************** + * restyle relbar & doc + *****************************************************/ + +div.relbar-top div.related +{ + border-radius: 8px 8px 0 0; + -moz-border-radius: 8px 8px 0 0; + -webkit-border-radius: 8px 8px 0 0; +} + +div.relbar-bottom div.related +{ + border-radius: 0 0 8px 8px; + -moz-border-radius: 0 0 8px 8px; + -webkit-border-radius: 0 0 8px 8px; +} + +/***************************************************** + * restyle the sidebar + *****************************************************/ +p.logo +{ + margin: 8px 0 0 0; + text-align: center; +} + +div.sphinxsidebar h3, +div.sphinxsidebar h4 +{ + font-size: 80%; + border-bottom: 1px dashed {{ theme_sidebartrimcolor }}; + margin-top: 24px; + margin-right: 16px; +} + +div.sphinxsidebar input +{ + border-color: {{ theme_sidebartrimcolor }}; +} + +div.sphinxsidebar .searchtip +{ + color: {{ theme_sidebartrimcolor }}; +} + +/***************************************************** + * restyle headers + *****************************************************/ + +div.body +{ + border-left: 1px solid {{theme_bodytrimcolor}}; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 +{ + border: 1px solid {{ theme_bodytrimcolor }}; + border-bottom: 1px solid {{ theme_headtrimcolor }}; + + margin-left: -15px; + margin-right: -15px; + + padding-top: 10px; + padding-bottom: 10px; + + border-radius: 10px; + -moz-border-radius: 10px; + -webkit-border-radius: 10px; +} + +div.body h3, +div.body h4, +div.body h5, +div.body h6 +{ + margin-left: -5px; + margin-right: -5px; +} + +div.body h1 +{ + border-top: 0; + padding: 20px; + margin-left: -20px; + margin-right: -20px; + text-align: center; + border-radius: 0 0 10px 10px; + -moz-border-radius: 0 0 10px 10px; + -webkit-border-radius: 0 0 10px 10px; +} + +/***************************************************** + * provide styling for TODO + *****************************************************/ +div.admonition.caution { + background-color: #FFF0E4; + border: 1px solid #FF9966; +} + +div.admonition-todo { + background-color: #EEE6E0; + border: 1px solid #D4CDC7; +} + +div#todos p.admonition-title +{ + font-weight: normal; + color: #AAA; + font-size: 70%; +} + +div#todos div.admonition-todo + p +{ + font-size: 70%; + text-align: right; + margin-top: -.5em; + margin-bottom: 1.5em; + color: #AAA; +} + +div#todos div.admonition-todo + p a +{ + font-size: 130%; +} + +/***************************************************** + * add more whitespace to definitions + *****************************************************/ +dl.function, +dl.method, +dl.attribute, +dl.class, +dl.exception, +dl.data +{ + margin-bottom: 1.5em; +} + +/***************************************************** + * add more whitespace to parameter lists + *****************************************************/ +td.field-body > ul.first.simple > li, +td.field-body > p.first +{ + margin-bottom: 1em; +} + +td.field-body > ul.first.simple > li > em, +td.field-body > em +{ + border-bottom: 1px solid #DEE6ED; + padding: 2px 4px; + background: #ECF0F3; +} + +/***************************************************** + * css colorization of object definitions, + * adds colored line underneath definition title. + * color scheme used: + * callables (function, method) - green + * attributes - purple + * classes - red + * exceptions - orange + * data - blue + *****************************************************/ + +dl.function > dt, +dl.method > dt, +dl.attribute > dt, +dl.class > dt, +dl.exception > dt, +dl.data > dt +{ + border-bottom: 2px solid #9AB9CE; + background: #E2ECF3; + padding: .1em 1px 1px 3px; +} + +dl.function > dt { border-color: #8BC38B; background: #D8E8D8; } +dl.method > dt { border-color: #AA96C2; background: #E8E1F2; } +dl.attribute > dt { border-color: #9996C2; background: #E7E6F6; } +dl.attribute > dt tt.descname { font-style: italic; } +dl.class > dt { border-color: #C8A69A; background: #E8DCD8; } +dl.exception > dt { border-color: #F8A69A; background: #F2E3E1; } + +/***************************************************** + * css colorization for index page, using styles + * provided by customize.py extension + *****************************************************/ + +table.indextable span.category +{ + font-size: 80%; + color: #84ADBE; +} + +table.indextable span.category.function { color: #8BC38B; } +table.indextable span.category.method { color: #AA96C2; } +table.indextable span.category.attribute { color: #9996C2; } +table.indextable span.category.class { color: #C8A69A; } +table.indextable span.category.module { color: #9AB9CE; } + +table.indextable span.subject +{ + font-weight: bold; +} + +table.indextable td > dl > dt +{ + margin-bottom: .5em; +} + +/***************************************************** + * EOF + *****************************************************/ diff --git a/bps/unstable/bpsdoc/cloud/static/bg_top.jpg b/bps/unstable/bpsdoc/cloud/static/bg_top.jpg Binary files differnew file mode 100644 index 0000000..c1e5775 --- /dev/null +++ b/bps/unstable/bpsdoc/cloud/static/bg_top.jpg diff --git a/bps/unstable/bpsdoc/cloud/static/header.png b/bps/unstable/bpsdoc/cloud/static/header.png Binary files differnew file mode 100644 index 0000000..c4f729e --- /dev/null +++ b/bps/unstable/bpsdoc/cloud/static/header.png diff --git a/bps/unstable/bpsdoc/cloud/static/header.svg b/bps/unstable/bpsdoc/cloud/static/header.svg new file mode 100644 index 0000000..b78c0a1 --- /dev/null +++ b/bps/unstable/bpsdoc/cloud/static/header.svg @@ -0,0 +1,101 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="256" + height="16" + id="svg4248" + version="1.1" + inkscape:version="0.47pre4 r22446" + inkscape:export-filename="/home/biscuit/dev/libs/bps/trunk/bps/unstable/bpsdoc/ast/static/header.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" + sodipodi:docname="New document 5"> + <defs + id="defs4250"> + <linearGradient + id="linearGradient4770"> + <stop + style="stop-color:#f2f2f2;stop-opacity:1;" + offset="0" + id="stop4772" /> + <stop + id="stop4780" + offset="0.49901354" + style="stop-color:#f2f2f2;stop-opacity:0.49803922;" /> + <stop + style="stop-color:#f2f2f2;stop-opacity:0;" + offset="1" + id="stop4774" /> + </linearGradient> + <inkscape:perspective + sodipodi:type="inkscape:persp3d" + inkscape:vp_x="0 : 526.18109 : 1" + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="744.09448 : 526.18109 : 1" + inkscape:persp3d-origin="372.04724 : 350.78739 : 1" + id="perspective4256" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient4770" + id="linearGradient4776" + x1="256" + y1="1052.3622" + x2="0" + y2="1052.3622" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(-256,0)" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#a5c2d9" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="1" + inkscape:pageshadow="2" + inkscape:zoom="3.959798" + inkscape:cx="105.7364" + inkscape:cy="4.5574751" + inkscape:document-units="px" + inkscape:current-layer="layer1" + showgrid="false" + inkscape:window-width="1920" + inkscape:window-height="1005" + inkscape:window-x="0" + inkscape:window-y="24" + inkscape:window-maximized="1" /> + <metadata + id="metadata4253"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(0,-1036.3622)"> + <rect + style="fill:url(#linearGradient4776);fill-opacity:1;stroke:none" + id="rect4768" + width="256" + height="16" + x="-256" + y="1036.3622" + transform="scale(-1,1)" /> + </g> +</svg> diff --git a/bps/unstable/bpsdoc/cloud/theme.conf b/bps/unstable/bpsdoc/cloud/theme.conf new file mode 100644 index 0000000..5b6bcfc --- /dev/null +++ b/bps/unstable/bpsdoc/cloud/theme.conf @@ -0,0 +1,35 @@ +[theme] +inherit = default +stylesheet = ast.css + +[options] + +roottarget = <master> +logotarget = <root> + +docwidth = 10.5in +docheight = 6in + +footerbgcolor = #1A4162 +footertextcolor = #B0B0B0 +doctrimcolor = #5682AD + +sidebarbgcolor = #F2F2F2 +sidebartextcolor = #777777 +sidebarlinkcolor = #003469 +sidebartrimcolor = #C0C0C0 + +relbarbgcolor = #5682AD +relbartextcolor = #ffffff +relbarlinkcolor = #ffffff +relbartrimcolor = #777777 + +bgcolor = #ffffff +textcolor = #000000 +linkcolor = #003469 + +headbgcolor = #A5C2D9 +headtextcolor = #000000 +headlinkcolor = #003469 +headtrimcolor = #A0A0A0 +bodytrimcolor = #D0D0D0 diff --git a/bps/unstable/bpsdoc/index_styles.py b/bps/unstable/bpsdoc/index_styles.py new file mode 100644 index 0000000..af7ec44 --- /dev/null +++ b/bps/unstable/bpsdoc/index_styles.py @@ -0,0 +1,69 @@ +""" +sphinx extension which intercepts & modifies the index page html. +all entries are wrapped in <span> elements +with class tags set to "category method" "category class", etc, +as appropriate for each entry. This allows colorization of the index +based on object type, making things an easier read. + +TODO: could improve style structure to make things more generically useful, +eg wrapping each entry in an "entry" span, tagged by type. +""" +from bps.develop import dbgcon +import re +from bps import * +from jinja2 import Markup as literal, escape + +prefix = r"^(?P<name>.*)\(" +suffix = r"\)$" +_attr_re = re.compile(prefix + r"(?P<left>)(?P<sub>.*)(?P<right> attribute)" + suffix) +_meth_re = re.compile(prefix + r"(?P<left>)(?P<sub>.*)(?P<right> method)" + suffix) +_fc_re = re.compile(prefix + r"(?P<left>class in |in module )(?P<sub>.*)(?P<right>)" + suffix) +_mod_re = re.compile(prefix + r"module" + suffix) + +def format_index_name(name): + while True: + m = _attr_re.match(name) + if m: + name, left, sub, right = m.group("name","left", "sub", "right") + type = "attribute" + break + m = _meth_re.match(name) + if m: + name, left, sub, right = m.group("name","left", "sub", "right") + type = "method" + break + m = _fc_re.match(name) + if m: + name, left, sub, right = m.group("name","left", "sub", "right") + if left.startswith("class"): + type = "class" + else: + type = "function" + break + m = _mod_re.match(name) + if m: + name = m.group("name") + left = "module" + sub = right = '' + type = "module" + break + return name + if sub: + sub = literal('<span class="subject">') + escape(sub) + literal("</span>") + cat = left + sub + right + return escape(name) + literal('<span class="category ' + type + '">(') + escape(cat) + literal(")</span>") + +def mangle_index(app, pagename, templatename, ctx, event_arg): + if pagename != "genindex": + return + fmt = format_index_name + for key, entries in ctx['genindexentries']: + for idx, entry in enumerate(entries): + name, (links, subitems) = entry + entries[idx] = fmt(name), (links, subitems) + for idx, entry in enumerate(subitems): + name, links = entry + subitems[idx] = fmt(name), links + +def setup(app): + app.connect('html-page-context', mangle_index) diff --git a/bps/unstable/bpsdoc/make_helper.py b/bps/unstable/bpsdoc/make_helper.py new file mode 100644 index 0000000..48ed2e3 --- /dev/null +++ b/bps/unstable/bpsdoc/make_helper.py @@ -0,0 +1,149 @@ +"""helper for quick cross-platform makefile for sphinx + +TODO: this was hacked up really quickly, could use a facelift. +""" +#=============================================================== +#imports +#=============================================================== +import os,sys +from bps import * +from string import Template +import subprocess +def sub(fmt, **kwds): + if not kwds: + kwds = globals() + return Template(fmt).substitute(**kwds) +__all__ = [ + "SphinxMaker", +] +#=============================================================== +#main class +#=============================================================== +class SphinxMaker(BaseClass): + #=============================================================== + #class attrs + #=============================================================== + # You can subclass these variables + #TODO: cmd line override support + SPHINXOPTS = [] + SPHINXBUILD = "sphinx-build" + PAPER = "letter" + + # Paths + BUILD = "_build" + STATIC = "_static" + + #internal opts + PAPEROPT_a4 = ["-D","latex_paper_size=a4"] + PAPEROPT_letter = ["-D","latex_paper_size=letter"] + #=============================================================== + #instance attrs + #=============================================================== + root_dir = None + conf_file = None + conf = None + + #=============================================================== + #frontend + #=============================================================== + def __init__(self, root=None): + if root is None: + root = sys.modules[self.__class__.__module__] + self.root_dir = filepath(root).abspath.dir + self.conf_file = self.root_dir / "conf.py" + if self.conf_file.ismissing: + raise RuntimeError, "conf file not found in root: %r" % (self.root_dir) + #XXX: load conf file? + + self.BUILD = filepath(self.BUILD) + self.STATIC = filepath(self.STATIC) + + @classmethod + def execute(cls, args=None, **kwds): + return cls(**kwds).run(args) + + def run(self, args=None): + if args is None: + args = sys.argv[1:] + self.root_dir.chdir() #due to relative paths like self.BUILD + for arg in args: + getattr(self,"target_"+arg)() + + #=============================================================== + #targets + #=============================================================== + def target_help(self): + print "Please use \`make <target>' where <target> is one of" + print " clean remove all compiled files" + print " html to make standalone HTML files" + print " http to serve standalone HTML files on port 8000" +# print " pickle to make pickle files" +# print " json to make JSON files" + print " htmlhelp to make HTML files and a HTML help project" +# print " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" +# print " changes to make an overview over all changed/added/deprecated items" +# print " linkcheck to check all external links for integrity" + + def target_clean(self): + BUILD = self.BUILD + if BUILD.exists: + BUILD.clear() + + def target_html(self): + #just in case htmldev was run + (self.BUILD / "html" / "_static" / "default.css").discard() + self.build("html") + + def target_htmlhelp(self): + self.build("htmlhelp") + + def target_http(self): + self.target_html() + path = self.BUILD.canonpath / "html" + path.chdir() + port = 8000 + print "Serving files from %r on port %r" % (path, port) + import SimpleHTTPServer as s + s.BaseHTTPServer.HTTPServer(('',port), s.SimpleHTTPRequestHandler).serve_forever() + + ##def target_latex(self): + ## build("latex") + ## print "Run \`make all-pdf' or \`make all-ps' in that directory to" \ + ## "run these through (pdf)latex." + ## + ##def target_pdf(): + ## assert os.name == "posix", "pdf build support not automated for your os" + ## build("latex") + ## target = BUILD / "latex" + ## target.chdir() + ## subprocess.call(['make', 'all-pdf']) + ## print "pdf built" + + #=============================================================== + #helpers + #=============================================================== + def build(self, name): + BUILD = self.BUILD + ALLSPHINXOPTS = self.get_sphinx_opts() + dt = BUILD / "doctrees"; dt.ensuredirs() + target = BUILD/ name; target.ensuredirs() + rc = subprocess.call([self.SPHINXBUILD, "-b", name] + ALLSPHINXOPTS + [ target ]) + if rc: + print "Sphinx-Build returned error, exiting." + sys.exit(rc) + print "Build finished. The %s pages are in %r." % (name, target,) + return target + + def get_paper_opts(self): + return getattr(self,"PAPER_" + self.PAPER, []) + + def get_sphinx_opts(self): + return ["-d", self.BUILD / "doctrees"] + self.get_paper_opts() + self.SPHINXOPTS + [ "." ] + + #=============================================================== + #eoc + #=============================================================== + +#=============================================================== +#eof +#=============================================================== diff --git a/bps/unstable/bpsdoc/nested_sections.py b/bps/unstable/bpsdoc/nested_sections.py new file mode 100644 index 0000000..516992e --- /dev/null +++ b/bps/unstable/bpsdoc/nested_sections.py @@ -0,0 +1,95 @@ +""" +This extension should be used in conjunction with autodoc. +It permits docstrings to have embedded rst section headers, +by translating them into indented paragraphs with +italicized section headers. + +TODO: make this more flexible and less hackneyed +""" +from bps.develop import dbgcon +import re +from bps import * + +def indent_sections(lines, reference_prefix=''): + "replaces any section headers with indented paragraphs" + end = len(lines)-1 + out = [] + + sections = [] + indent_char = ' ' * 4 + indent_level = 0 + SCHARS = '#*=-^"' + def get_level(c): + return SCHARS.index(c) + #FIXME: this doesn't detect double-barred sections + def detect_section(idx): + if idx == end: + return None + line = lines[idx].rstrip() + if not line or line.lstrip() != line: + return None + next = lines[idx+1].rstrip() + if next.lstrip() != next: + return None + for c in SCHARS: + if next.startswith(c * len(line)): + return c + return None + idx = 0 + while idx <= end: + line = lines[idx].rstrip() + if not line: + out.append("") + idx += 1 + continue + new_char = detect_section(idx) + if new_char: + new_level = get_level(new_char) + while sections and sections[-1] > new_level: + sections.pop() + if not sections or sections[-1] < new_level: + sections.append(new_level) + name = line.lower().strip().replace(" ", "-").replace("--", "-") + indent = indent_char * (indent_level-1) + #TODO: would be nice to add a special directive instead of **%s**, + # so that we could render appropriate html styling to the section header + out.extend([ + indent + ".. _%s:" % (reference_prefix + name), + "", + indent + "**%s**\n" % line.rstrip(), + ]) + idx += 2 #skip section header + indent_level = max(0, len(sections)) + continue + indent = indent_char * indent_level + out.append(indent + line) + idx += 1 + return out + +def _remove_oneline(name, lines): + #remove one-line description from top of module, if present, + #cause we don't want it being duplicated (should already be listed in module's header) + _title_re = re.compile(r""" + ^ \s* + ( {0} \s* -- \s* )? + [a-z0-9 _."']* + $ + """.format(re.escape(name)), re.X|re.I) + if len(lines) > 1 and _title_re.match(lines[0]) and lines[1].strip() == '': + del lines[:2] + +def mangle_docstrings(app, what, name, obj, options, lines): + if what == 'module': + _remove_oneline(name, lines) + elif what in ('class', 'exception', 'function', 'method'): + name = "%s.%s" % (obj.__module__, obj.__name__) + name = name.replace(".", "-").lower() + lines[:] = indent_sections(lines, reference_prefix=name + "-") + elif what in ('attribute',): + pass + else: + print "unknown what: %r %r" % (what, obj) + dbgcon() + +def setup(app): + app.connect('autodoc-process-docstring', mangle_docstrings) diff --git a/bps/unstable/bpsdoc/relbar_toc.py b/bps/unstable/bpsdoc/relbar_toc.py new file mode 100644 index 0000000..ace8730 --- /dev/null +++ b/bps/unstable/bpsdoc/relbar_toc.py @@ -0,0 +1,32 @@ +""" +automatically insert a "toc" entry into relbar for all pages +(ala old python documentation style) +""" +import re +from bps import * + +def insert_toc(app, pagename, templatename, ctx, event_arg): + links = ctx['rellinks'] + + #remove any existing toc (present on some pages) + for idx, elem in enumerate(links): + if elem[3] == "toc": + del links[idx] + break + + #place toc right after "next" / "previous" + idx = -1 + for idx, entry in enumerate(links): + if entry[3] in ("next","previous"): + break + else: + idx += 1 + + #insert our toc entry + path = filepath(ctx['pathto']("contents")).root + if path == '': + path = pagename + links.insert(idx, (path, "Table Of Contents", "C", "toc")) + +def setup(app): + app.connect('html-page-context', insert_toc) diff --git a/bps/unstable/softref.py b/bps/unstable/softref.py new file mode 100644 index 0000000..030d013 --- /dev/null +++ b/bps/unstable/softref.py @@ -0,0 +1,847 @@ +""" +soft reference implementation + +this module implements something similar to Java's soft references. +these are normal python references, but use reference count introspection +to remove the reference only no other references to the final object +are left, AND the object has not been used in a long enough time +to warrant removing the object. + +this is mainly useful when maintaining a cache of objects +which can be regenerated, but only through a (cpu) costly process. +it's generally useful to keep such objects around in a cache, +in case they are needed again, but for memory reasons, +long running processes will generally want to free unused +objects up after a time. + +this class provides ``softref()`` which provides access-time tracking, +allowing the reference to be freed up after it's remained unused +for an (application specified) amount of time. + +it also contains platform-specific code for detecting low memory conditions, +and freeing up softrefs more aggressively in this case, +in the hopes of staving off out-of-memory conditions. +""" +#================================================================================= +#imports +#================================================================================= +from __future__ import with_statement +#core +import logging +import sys +import threading +from itertools import count as itercount +from time import time as cur_time +import logging; log = logging.getLogger(__name__) +import UserDict +from weakref import ref as make_weakref +from warnings import warn +#site +#pkg +#local +log = logging.getLogger(__name__) +__all__ = [ + #main entry point + "softref", + + #collector control & configuration + "collect", "enable", "disable", "is_enabled", + "get_config", "set_config", + + #introspection + "get_softref_count", + "get_softrefs", + + #other helpers + 'SoftValueDict', +## 'KeyedSoftRef', +# 'get_memory_usage', +] + +#================================================================================= +#platform specific code for detecting memory levels +#================================================================================= + +#NOTE: this is mainly used a helper for the collector to detect low memory conditions. + +if sys.platform == "linux2": + #use /proc/meminfo + import re + + _memtotal_re = re.compile("^MemTotal:\s+(\d+)\s*kb$", re.I|re.M) + _memfree_re = re.compile("^MemFree:\s+(\d+)\s*kb$", re.I|re.M) + _buffers_re = re.compile("^Buffers:\s+(\d+)\s*kb$", re.I|re.M) + _cached_re = re.compile("^Cached:\s+(\d+)\s*kb$", re.I|re.M) + + def get_memory_usage(): + try: + with file("/proc/meminfo") as fh: + data = fh.read() + memtotal = int(_memtotal_re.search(data).group(1)) + memfree = int(_memfree_re.search(data).group(1)) + buffers = int(_buffers_re.search(data).group(1)) + cached = int(_cached_re.search(data).group(1)) + avail = memfree + buffers + cached + assert 0 <= avail <= memtotal + return memtotal, avail + except: + #this is a sign something has gone wrong :| + log.error("error reading /proc/meminfo", exc_info=True) + return (-1, -1) + +elif sys.platform == "win32": + #implementation taken from http://code.activestate.com/recipes/511491/ + #TODO: check if this will work with cygwin platform + import ctypes + kernel32 = ctypes.windll.kernel32 + c_ulong = ctypes.c_ulong + class MEMORYSTATUS(ctypes.Structure): + _fields_ = [ + ('dwLength', c_ulong), + ('dwMemoryLoad', c_ulong), + ('dwTotalPhys', c_ulong), + ('dwAvailPhys', c_ulong), + ('dwTotalPageFile', c_ulong), + ('dwAvailPageFile', c_ulong), + ('dwTotalVirtual', c_ulong), + ('dwAvailVirtual', c_ulong) + ] + def get_memory_usage(): + memoryStatus = MEMORYSTATUS() + memoryStatus.dwLength = ctypes.sizeof(MEMORYSTATUS) + kernel32.GlobalMemoryStatus(ctypes.byref(memoryStatus)) + #XXX: does availphy correspond properly to linux's free-buffers-cache ? + return (memoryStatus.dwTotalPhys//1024, memoryStatus.dwAvailPhys//1024) +else: + #TODO: would like to support more platforms (esp OS X) + warn("disabled low memory detection, not implemented for " + sys.platform + " platform") + def get_memory_usage(): + return (-1,-1) + +get_memory_usage.__doc__ = """return memory usage + +Return current memory usage as tuple +``(total physical memory, available physical memory)``. + +All measurments in kilobytes. + +Available physical memory counts +os buffers & cache as "available", +where possible for the implementation. + +Currently supports linux & win32 platforms, +if run on other platforms, will return +tuple where all values are -1. + +If an error occurs on a supported platform, +it will be logged, and a tuple of -1 values will be returned. +""" + +#================================================================================= +#soft ref collector - implemented as singleton of private class +#================================================================================= + +#log used by collector +clog = logging.getLogger(__name__ + ".collector") + +#NOTE: design of this module attempts to put as much work in the collector, +# and as little work in the creation & access of the softrefs, +# since the collector is designed to run in another thread anyways. + +class _SoftRefCollector(object): + """tracks softref objects, handles collecting them when needed""" + #================================================================================= + #instance attrs + #================================================================================= + + #-------------------------------------------------------------------- + #configuration + #-------------------------------------------------------------------- + default_min_age = 600 #default delay from last access time before a softref can be released + default_max_age = -1 #default delay from last access time before softref will be released even w/o lowmem condition + + collect_frequency = 300 #how often collector should run + + lowmem_abs = 25 * 1024 #lowmem level in kilobytes + lowmem_pct = .05 #lowmem level as % of total mem + #low memory is calculated as max(lowmem_abs, physmem * lowmem_pct) + #this way there's a floor to the lowmem level for low memory systems (eg <512mb), + #but it scales so lowmem doesn't get hit as often for systems with more memory (eg 4gb) + #values may need tuning + + #-------------------------------------------------------------------- + #softref state + #-------------------------------------------------------------------- + targetmap = None #map of id(target) -> [ weakref(softref(target)) ... ] + #since id will only be re-used once target is dereferenced, + #and that won't happen as long as softrefs exist, + #ids should never conflict + last_collect = 0 #timestamp of last time collect() ran + + #-------------------------------------------------------------------- + #threading + #-------------------------------------------------------------------- + lock = None #threading lock for instance state + thread = None #thread collector uses to run in the background + thread_stop = None #Event used to signal collector thread that it should halt + + #================================================================================= + #init + #================================================================================= + def __init__(self): + self.targetmap = {} + self.lock = threading.Lock() #lock for changes + #XXX: if softref's onrelease tried to create a new softref, + # it'll block on this lock.. in that case, this should be made to an RLock. + # if this happens, could either do it permanently, or make an use_rlock() method + self.thread_stop = threading.Event() + + #================================================================================= + #softref interface + #================================================================================= + def add(self, sref): + "add a new softref instance" + #NOTE: instances *must* be compatible with softref type, + # namely its' _target _atime min_age attrs + + #TODO: some target types (eg: int, str, bool, None) should never have softref expire, + # and we shouldn't even bother tracking them. should find nice way to have fallback in that case. + # bucket system would be good, could just never add them to initial bucket. + + target_id = id(sref._target) + sref_wr = make_weakref(sref) + targetmap = self.targetmap + with self.lock: + srlist = targetmap.get(target_id) + if srlist is None: + targetmap[target_id] = [ sref_wr ] + else: + srlist.append(sref_wr) + + #================================================================================= + #introspection + #================================================================================= + @property + def next_collect(self): + "time next collection is scheduled" + return self.last_collect + self.collect_frequency + + def count(self, target): + with self.lock: + srlist = self.targetmap.get(id(target)) + count = 0 + if srlist: + for sref_wr in srlist: + if sref_wr(): + count += 1 + return count + + def refs(self, target): + with self.lock: + srlist = self.targetmap.get(id(target)) + out = [] + if srlist: + for sref_wr in srlist: + sref = sref_wr() + if sref: + out.append(sref) + return out + + #================================================================================= + #collector + #================================================================================= + def collect(self): + #TODO: rework this scan into using generational buckets (ala gc module), + # keyed off of target id, that way long-lived ones don't have to be scanned as often. + with self.lock: + targetmap = self.targetmap + lowmem = self._check_lowmem() + clog.info("collecting soft refs... targets=%d lowmem=%r", len(targetmap), lowmem) + purge_keys = set() + cur = cur_time() + #call collect_entry for all of targetmap + helper = self._collect_entry + for target_id, srlist in targetmap.iteritems(): + if helper(target_id, srlist, cur, lowmem): + purge_keys.add(target_id) + #purge any keys we identified previously, just to free up even more memory + for target_id in purge_keys: + del targetmap[target_id] + self.last_collect = cur + count = len(purge_keys) + clog.info("released %d targets", count) + return count + + def _check_lowmem(self): + "check if we're running in low memory condition" + #TOOD: + # - sliding scale, doing more as lowmem approaches? ie, freeing only some eligible softrefs + # - have collector prefer softrefs w/ older atimes in that case + # - weighting for different instances / types, allowing "heavy" classes to be preferred to be freed? + # - generational buckets + # - other schemes? + total, free = get_memory_usage() + if total == -1: #not available or error occurred + clog.debug("disabling lowmem check") + #just to stop spamming, let's not call get mem again + self._check_lowmem = lambda : False + return False + threshold = int(max(self.lowmem_abs, total * self.lowmem_pct)) + clog.debug("system memory: total=%r lowmem_threshold=%r free=%r", total, threshold, free) + return free <= threshold + + def _collect_entry(self, target_id, srlist, cur, lowmem): + "run collect algorithm for specified entry, return True if it needs removing" + + #NOTE: could consolidate min_age, max_age (and maybe atime) + #into a single record stored w/in collector, + #instead of storing separately in each softref. + + #scan existing softrefs, working out latest atime & softrefcount + atime = 0 + min_age = self.default_min_age + max_age = self.default_max_age + srefs = [] #accumulate hard refs to sref objects, so weakrefs don't vanish while in this loop + for sref_wr in srlist: + sref = sref_wr() + if sref is None: + #TODO: could purge sref_wr here for extra mem, + #but probably efficient enough for most cases + #to just purge whole targetmap entry once target_wr is gone + continue + srefs.append(sref) + if sref._atime > atime: + atime = sref._atime + if sref.min_age > min_age: + min_age = sref.min_age + if sref.max_age is not None and (max_age == -1 or sref.max_age < max_age): + max_age = sref.max_age + + #check if any softrefs is still around + if not srefs: + #all references to softref objects dropped before they were purged + clog.debug("softrefs vanished: %r", target_id) + return True + + #decide if this one should be released yet + age = cur-atime + if age <= min_age: + return False + if not lowmem and (max_age == -1 or age < max_age): + return False + + #sanity check on target + assert all(id(sref._target) == target_id for sref in srefs), "targetmap corrupted: target=%r srefs=%r" % (target_id, srefs) + target = srefs[0]._target + + #now check how many hardrefs are out there, + #after ignoring the following hard refs: + # +1 reference in this frame's 'target' var + # +1 reference in getrefcount() call + # +N references held by '_target' attr of srefs + # any more, and it's external. + # any less, and it's runtimeerror, cause one of the above was missing. + offset = 2+len(srefs) + rc = sys.getrefcount(target) + if rc < offset: + raise RuntimeError, "too few references to target: %r rc=%r offset=%r" % (target, rc, offset) + if rc > offset: + #(rc-offset) hardrefs still out there, so don't purge softref + return False + + #ok, time to release softref + clog.info("releasing softrefs: %r", target) + for sref in reversed(srefs): #NOTE: reversed so handlers called LIFO, same as weakref module + sref._target = None #just so existing softrefs return None + h = sref._onrelease + if h is not None: + try: + h(sref) + except: + clog.error("error in softref onrelease callback: %r %r", target, onrelease) + sys.excepthook(*sys.exc_info()) + + #schedule whole entry for removal + return True + + #================================================================================= + #collector thread + #================================================================================= + def is_enabled(self): + "return true if collector thread is running, else false" + if not self.thread or not self.thread.isAlive(): + return False + if self.thread_stop.isSet(): + return None #special value indicating thread is still running but will stop soon. call disable() to ensure it's stopped. + return True + + def enable(self): + "start collector thread if not running" + if self.thread and self.thread.isAlive(): + if not self.thread_stop.isSet(): + return True + #wait til thread has exited + self.thread.join() + self.thread_stop.clear() + thread = threading.Thread(target=self._collector_loop, name="[softref collector]") + thread.setDaemon(True) + clog.debug("softref collector thread launched") + thread.run() + + def _collector_loop(self): + "main loop used by collector thread" + clog.info("softref collector thread started") + #XXX: should we check for errors and have a cooldown period before trying again? + while True: + #wait for stop event OR time for next collection + delay = max(.05, self.next_collect - cur_time()) + self.thread_stop.wait(delay) + + #check if we've been signalled to stop + if self.thread_stop.isSet(): + clog.info("softref collector thread stopped") + return + + #run collector + clog.info("collecting softrefs") + self.collect() + + def disable(self): + "stop collector thread if running" + #NOTE: this shouldn't be called if self.lock is held, + #otherwise might deadlock if we join while + #other thread is trying to acquire lock. + + #signal thread should stop + self.thread_stop.set() + + #then join til it does + if self.thread and self.thread.isAlive(): + self.thread.join() + self.thread = None + + #================================================================================= + #eoc + #================================================================================= + +#================================================================================= +#single collector and public interface to it +#================================================================================= + +_collector = _SoftRefCollector() + +#---------------------------------------------------------------------- +#configuration collector +#---------------------------------------------------------------------- +def set_config(default_max_age=None, default_min_age=None, collect_frequency=None, + lowmem_pct=None, lowmem_abs=None): + """update various collector config options. + + :kwd default_min_age: + change minimum age (seconds since last access) + before a softref is eligible to be released + by the collector. + + softrefs can specify this per-instance. + + defaults to 10 minutes. + + :kwd default_max_age: + change maximum age (seconds since last access) + before a softref will be purged by collector + even if there isn't a low memory condition. + + softrefs can specify this per-instance. + + defaults to -1 minutes, + which indicates there is no max age. + + :kwd collect_frequency: + how often collector thread calls collect(), + measured in seconds. + + defaults to every 5 minutes. + + :kwd lowmem_pct: + if free memory drops below this amount (as percent of total memory), + the collector considers the system to be low on memory, + and becomes agressive in purging softrefs. + + the actual low memory threshold is ``max(lowmem_abs, phymem * lowmem_pct)``, + providing a floor to the lowmem threshold so it will function acceptably + on systems with a small amount of physical memory. + + defaults to .05 percent (~100 Mb on a 2Gb system). + + :kwd lowmem_abs: + minimum free memory threshold (in kilobytes). + see lowmem_pct for more details. + + defaults to 25 Mb (~.05 percent on a 512 Mb system). + + .. note:: + default values subject to change as internal algorithm + is being refined (currently doesn't account for system memory usage, etc) + + most changes will take affect the next time collect() runs, + with the exception of collect_frequency, which will take effect + next time the collector thread wakes up. + """ + global _collector + if default_max_age is not None: + if default_max_age <= 0 and default_max_age != -1: + raise ValueError, "default_max_age must be -1 or positive value" + _collector.default_max_age = default_max_age + if default_min_age is not None: + if default_min_age < 0: + raise ValueError, "default_min_age must be >= 0" + _collector.default_min_age = default_min_age + if collect_frequency is not None: + if collector_frequency <= 0: + raise ValueError, "collector frequency must be > 0" + _collector.collect_frequency = collect_frequency + if lowmem_pct is not None: + if lowmem_pct < 0 or lowmem_pct >= 1: + raise ValueError, "lowmem_pct must be between [0.0,1.0)" + _collector.lowmem_pct = lowmem_pct + if lowmem_abs is not None: + if lowmem_abs < 0: + raise ValueError, "lowmem_abs must be >= 0" + _collector.lowmem_abs = lowmem_abs + +def get_config(): + "return dict of current collector config options, corresponding to :func:`set_config` kwds" + global _collector + return dict( + (k,getattr(_collector,k)) + for k in ["default_min_age", "default_max_age", "collect_frequency", + "lowmem_pct", "lowmem_abs"] + ) + +#---------------------------------------------------------------------- +#running collector +#---------------------------------------------------------------------- +def is_enabled(): + "check if softref collector thread is running" + return _collector.is_enabled() + +def enable(): + "ensure softref collector thread is running" + return _collector.enable() + +def disable(): + "ensure softref collector thread is not running (will block until thread terminates)" + return _collector.disable() + +def collect(): + "force a run of the softref collector immediately" + return _collector.collect() + +#---------------------------------------------------------------------- +#introspection of softref information +#---------------------------------------------------------------------- +def get_softref_count(target): + "return number of soft refs attached to target" + return _collector.count(target) + +def get_softrefs(target): + "return all softref instances attached to target" + return _collector.refs(target) + +def get_hardref_count(target): + "return number of hard refs attached to target (include 1 ref for this function call)" + rc = sys.getrefcount(target) + sc = get_softref_count(target) + return rc-sc-1 + +#================================================================================= +#softref constructor +#================================================================================= +class softref(object): + """create a softref to another object + + :arg target: object this should hold softref to + :arg onrelease: + optional callback to invoke ``onrelease(sref)`` + if softref to target is released before this object + is dereferenced. + :arg min_age: + override default min_age for this target + :arg max_age: + override default max_age for this target + + :returns: + a new softref instance. + calling it will return original target, or ``None``, + same as a weakref. + """ + + #TODO: provide hook which can prevent softref from being freed + #(eg if object shouldn't be freed if it's in a certain state, etc) + + #================================================================================= + #instance attrs + #================================================================================= + __slots__ = ["__weakref__", "_target", "_atime", "_onrelease", "min_age", "max_age"] + + #store quick links to collector + _collector_lock = _collector.lock + _collector_add = _collector.add + + #================================================================================= + #instance methods + #================================================================================= + #TODO: could override __new__ for cls=softref and only 'target' param, + # let it cache things for us. + + def __init__(self, target, onrelease=None, min_age=None, max_age=None): + self._target = target + self._onrelease = onrelease + self.min_age = min_age + self.max_age = max_age + self._atime = cur_time() + self._collector_add(self) #register new softref with collector + + ##@property + ##def atime(self): return self._atime + + ##def touch(self): + ## self._atime = cur_time() + + def __call__(self): + self._atime = cur_time() #NOTE: doing this outside lock cause it can't hurt, and might catch collector in it's tracks + + #NOTE: have to lock collector while we're creating new hardref, + #or collector might see N hard refs, this thread creates hard ref N+1, + #and then collector purges softref, causing this thread + #to have a hard ref after softref was purged (which is against + #how this module wants softrefs to behave) + with self._collector_lock: + return self._target + + def __repr__(self): + target = self._target + if target is None: + return "<softref at 0x%x; dead>"% (id(self),) + else: + return "<softref at 0x%x; to '%s' at 0x%x>" % (id(self), type(target).__name__, id(target)) + + def __eq__(self, other): + return self._target == other + + def __ne__(self, other): + return self._target != other + + #================================================================================= + #eoc + #================================================================================= + +#================================================================================= +#soft value dict +#================================================================================= +#NOTE: this is cloned from py26 weakref.WeakValueDict, and adapted for softrefs... + +class SoftValueDictionary(UserDict.UserDict): + """Mapping class that references values using softref. + + Entries in the dictionary will be discarded when no strong + reference to the value exists anymore + """ + # We inherit the constructor without worrying about the input + # dictionary; since it uses our .update() method, we get the right + # checks (if the other dictionary is a WeakValueDictionary, + # objects are unwrapped on the way out, and we always wrap on the + # way in). + + min_age = None + max_age = None + + def __init__(self, source=None, min_age=None, max_age=None): + if min_age: + self.min_age = min_age + if max_age: + self.max_age = max_age + def remove(sr, selfref=make_weakref(self)): + self = selfref() + if self is not None: + del self.data[sr.key] + self._remove = remove + if source is None: + args = () + else: + args = (source,) + UserDict.UserDict.__init__(self, *args) + + ##def touch(self, key): + ## "helper to update softref atime for value attached to key" + ## self.data[key].touch() + + def __getitem__(self, key): + o = self.data[key]() + if o is None: + raise KeyError, key + else: + return o + + def __contains__(self, key): + try: + o = self.data[key]() + except KeyError: + return False + return o is not None + + def has_key(self, key): + try: + o = self.data[key]() + except KeyError: + return False + return o is not None + + def __repr__(self): + return "<SoftValueDictionary at %s>" % id(self) + + def __setitem__(self, key, value): + self.data[key] = KeyedSoftRef(key, value, self._remove, self.min_age, self.max_age) + + def copy(self): + new = SoftValueDictionary() + new.min_age = self.min_age + new.max_age = self.max_age + for key, sr in self.data.items(): + o = sr() + if o is not None: + new[key] = o + return new + + def get(self, key, default=None): + try: + sr = self.data[key] + except KeyError: + return default + else: + o = sr() + if o is None: + # This should only happen + return default + else: + return o + + def items(self): + L = [] + for key, sr in self.data.items(): + o = sr() + if o is not None: + L.append((key, o)) + return L + + def iteritems(self): + for sr in self.data.itervalues(): + value = ws() + if value is not None: + yield ws.key, value + + def iterkeys(self): + return self.data.iterkeys() + + def __iter__(self): + return self.data.iterkeys() + + def itervaluerefs(self): + """Return an iterator that yields the weak references to the values. + + The references are not guaranteed to be 'live' at the time + they are used, so the result of calling the references needs + to be checked before being used. This can be used to avoid + creating references that will cause the garbage collector to + keep the values around longer than needed. + + """ + return self.data.itervalues() + + def itervalues(self): + for wr in self.data.itervalues(): + obj = wr() + if obj is not None: + yield obj + + def popitem(self): + while 1: + key, wr = self.data.popitem() + o = wr() + if o is not None: + return key, o + + def pop(self, key, *args): + try: + o = self.data.pop(key)() + except KeyError: + if args: + return args[0] + raise + if o is None: + raise KeyError, key + else: + return o + + def setdefault(self, key, default=None): + try: + wr = self.data[key] + except KeyError: + self.data[key] = KeyedSoftRef(key, default, self._remove, self.min_age, self.max_age) + return default + else: + return wr() + + def update(self, dict=None, **kwargs): + d = self.data + if dict is not None: + if not hasattr(dict, "items"): + dict = type({})(dict) + for key, o in dict.items(): + d[key] = KeyedSoftRef(key, o, self._remove, self.min_age, self.max_age) + if len(kwargs): + self.update(kwargs) + + def valuerefs(self): + """Return a list of weak references to the values. + + The references are not guaranteed to be 'live' at the time + they are used, so the result of calling the references needs + to be checked before being used. This can be used to avoid + creating references that will cause the garbage collector to + keep the values around longer than needed. + + """ + return self.data.values() + + def values(self): + L = [] + for wr in self.data.values(): + o = wr() + if o is not None: + L.append(o) + return L + +class KeyedSoftRef(softref): + """Specialized reference that includes a key corresponding to the value. + + This is used in the SoftValueDictionary to avoid having to create + a function object for each key stored in the mapping. A shared + callback object can use the 'key' attribute of a KeyedSoftRef instead + of getting a reference to the key from an enclosing scope. + + """ + + __slots__ = "key", + + def __new__(cls, key, target, onrelease=None, min_age=None, max_age=None): + self = softref.__new__(cls, target, onrelease, min_age, max_age) + self.key = key + return self + + def __init__(self, key, target, onrelease=None, min_age=None, max_age=None): + super(KeyedSoftRef, self).__init__(target, onrelease, min_age, max_age) + +#================================================================================= +#eof +#================================================================================= diff --git a/bps/unstable/winconsole.py b/bps/unstable/winconsole.py new file mode 100644 index 0000000..73a2193 --- /dev/null +++ b/bps/unstable/winconsole.py @@ -0,0 +1,406 @@ +"""collection of ctypes-based helpers for accessing the windows console. + +References +========== +* Windows API Reference -- http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winprog/winprog/windows_api_reference.asp +* recipe to set color attr -- http://code.activestate.com/recipes/496901/ +* recipe to get color attr -- http://code.activestate.com/recipes/440694/ +""" +#========================================================= +#imports +#========================================================= +#core +from logging import getLogger; log = getLogger(__name__) +import sys +import os +import re +if os.name == "nt": + #do thing conditionally so full docs can still be built under posix + import msvcrt + from ctypes import * + kernel32 = windll.kernel32 +else: + kernel32 = None +#pkg +from bps import * +from bps.numeric import limit +from bps.unstable import ansi +#local +__all__ = [ + 'print_ansi_string', +] + +#========================================================= +#misc constants +#========================================================= +FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000 + +#========================================================= +#constants from wincon.h +#========================================================= +##STD_INPUT_HANDLE = -10 +##STD_OUTPUT_HANDLE= -11 +##STD_ERROR_HANDLE = -12 + +FOREGROUND_BLUE = 0x01 # text color contains blue. +FOREGROUND_GREEN= 0x02 # text color contains green. +FOREGROUND_RED = 0x04 # text color contains red. +FOREGROUND_INTENSITY = 0x08 # text color is intensified. + +BACKGROUND_BLUE = 0x10 # background color contains blue. +BACKGROUND_GREEN= 0x20 # background color contains green. +BACKGROUND_RED = 0x40 # background color contains red. +BACKGROUND_INTENSITY = 0x80 # background color is intensified. + +class SMALL_RECT(Structure): + _fields_ = [("Left", c_short), + ("Top", c_short), + ("Right", c_short), + ("Bottom", c_short)] + +class COORD(Structure): + _fields_ = [("X", c_short), + ("Y", c_short)] + +class CONSOLE_SCREEN_BUFFER_INFO(Structure): + _fields_ = [("dwSize", COORD), + ("dwCursorPosition", COORD), + ("wAttributes", c_short), + ("srWindow", SMALL_RECT), + ("dwMaximumWindowSize", COORD)] + +#non-standard derived constants +FOREGROUND_WHITE = 0x07 +BACKGROUND_WHITE = 0x70 +FOREGROUND_ALL = 0x0F +BACKGROUND_ALL = 0xF0 +ALL_WHITE = 0x77 + +#map of ansi -> dos color values +# (dos flips R & B bits) +ansi_to_dos = [0,4,2,6,1,5,3,7] + +#========================================================= +# +#========================================================= +def _get_hnd(stream): + return msvcrt.get_osfhandle(stream.fileno()) + +##def get_console_size(file=sys.stdout): +## "get size of console attached to stream" +## info = CONSOLE_SCREEN_BUFFER_INFO() +## status = kernel32.GetConsoleScreenBufferInfo(hnd, byref(info)) +## + +def write_ansi_string(source, stream=sys.stdout): + "print string w/ embedded ansi escape codes to dos console" + return AnsiConsoleWriter(stream).write(source) + +def _clear_bits(attr, bits): + "clear specified bits in bitmask" + return (attr|bits)^bits + +def _swap_bgfg(attr): + "swap fg and bg color bits" + fg = (attr & FOREGROUND_WHITE) + bg = (attr & BACKGROUND_WHITE) + return (attr-fg-bg) | (bg>>4) |(fg<<4) + +def _pp_get_last_error(): + code = GetLastError() #ctypes wrapper for kernel32.GetLastError + if code < 1: + return None + else: + msg = FormatError(code) #ctypes wrapper for kernel32.FormatMessage + return "[%d] %s" % (code, msg) + +class AnsiConsoleWriter(object): + """wraps a stream attached to a windows console, + extracting any ansi escape codes, and implementing + them using the Windows Console API where possible. + + :arg stream: + open handle to console (usualy stdout / stderr). + by default, this writes to sys.stdout. + + .. note:: + Right now, only ansi text styles (code "m") are supported. + All others are ignored. In the future, there are plans + to support the cursor movement codes. + + .. warning:: + This will raise an EnvironmentError if the stream is not + attached to a console. Use :meth:`wrap` for a graceful + fallback if the stream is not a tty. + + .. warning:: + Reverse Video and Concealed Text styles utilitize + per-Writer state, which may not work with concurrent + changes to styles while either mode is enabled. + """ + #========================================================= + #instance attrs + #========================================================= + stream = None #stream we're wrapping + _hnd = None #windows fhandle for stream + _attrs = None #last attr state we read from console + _reverse = False #flag if reverse video is enabled + _conceal = None #flag bg if concealed text is enabled + _conceal_fg = None + + #========================================================= + #init + #========================================================= + + #XXX: classmethod such as "has_console(stream)" if isatty() isn't sufficient? + + def __init__(self, stream=None): + if stream is None: + stream = sys.stdout + if not stream.isatty(): + raise ValueError, "stream is not attached to a tty" + self._hnd = _get_hnd(stream) + assert isinstance(self._hnd, int) + self.stream = stream + + #========================================================= + #state management + #========================================================= + def _get_info(self): + info = CONSOLE_SCREEN_BUFFER_INFO() + ok = kernel32.GetConsoleScreenBufferInfo(self._hnd, byref(info)) + if ok: + return info + else: + log.error("failed to read screen buffer info: stream=%r error=%r", self.stream, _pp_get_last_error()) + return None + + def _update_state(self): + "update internal state from console" + info = self._get_info() + if info: + self._attrs = info.wAttributes + else: + self._attrs = FOREGROUND_WHITE + + def _apply_code(self, code): + "apply change requested by AnsiCode instance" + if code.cseq_code == "m": + self._apply_styles(code.args) + elif code.cseq_code == "A": + self._move_cursor(0, -code.offset) + elif code.cseq_code == "B": + self._move_cursor(0, code.offset) + elif code.cseq_code == "C": + self._move_cursor(code.offset,0) + elif code.cseq_code == "D": + self._move_cursor(-code.offset,0) + elif code.cseq_code == "H": + self._set_cursor(code.col, code.row) + #TODO: support abs vert & horiz csr movement codes + elif code.cseq_code == "J": + self._do_clear_screen(code.mode) + ##elif code.cseq_code == "K": + ## self._do_clear_line(code.mode) + else: + #TODO: we could support the cursor repositioning commands + log.debug("discarding unsupported ansi escape code: %r", code) + + def _do_clear_screen(self, mode): + if mode == 0: + #clear line -> bottom + info = self._get_info() + if not info: + return + cpos = info.dwCursorPosition + cx, cy = cpos.X, cpos.Y + c = COORD(0,cy) + d = c_short() + s = info.dwSize.X * (info.dwSize.Y-cy+1) + ok = kernel32.FillConsoleOutputCharacterA(self._hnd, 32, s, c, byref(d) ) + if not ok: + log.error("failed to clear screen: stream=%r error=%r", self.stream, _pp_get_last_error()) + elif mode == 1: + #clear top -> line + info = self._get_info() + if not info: + return + cpos = info.dwCursorPosition + cx, cy = cpos.X, cpos.Y + c = COORD(0,0) + d = c_short() + s = info.dwSize.X * (info.dwSize.Y-cy+1) + ok = kernel32.FillConsoleOutputCharacterA(self._hnd, 32, s, c, byref(d) ) + if not ok: + log.error("failed to clear screen: stream=%r error=%r", self.stream, _pp_get_last_error()) + elif mode == 2: + #clear all + info = self._get_info() + if not info: + return + c = COORD(0,0) + d = c_short() + s = info.dwSize.X * info.dwSize.Y + ok = kernel32.FillConsoleOutputCharacterA(self._hnd, 32, s, c, byref(d) ) + if not ok: + log.error("failed to clear screen: stream=%r error=%r", self.stream, _pp_get_last_error()) + else: + log.debug("unsupported J mode: %r", num) + + def _set_cursor(self, cx, cy): + info = self._get_info() + if not info: + return + bsize = info.dwSize + bx, by = bsize.X, bsize.Y + #FIXME: is windows relative to 0,0 or 1,1? cause H codes is 1,1 + #TODO: support single-dim movement when cx / cy is None + cx = limit(cx,0,bx-1) + cy = limit(cy,0,by-1) + #get csr position + cpos = COORD(cx,cy) + ok = kernel32.SetConsoleCursorPosition(self._hnd, cpos) + if not ok: + log.error("failed to set cursor position: stream=%r error=%r", self.stream, _pp_get_last_error()) + + def _move_cursor(self, rx, ry): + info = self._get_info() + if not info: + return + cpos = info.dwCursorPosition + cx, cy = cpos.X, cpos.Y + bsize = info.dwSize + bx, by = bsize.X, bsize.Y + cx = limit(cx+rx,0,bx-1) + cy = limit(cy+ry,0,by-1) + #get csr position + cpos = COORD(cx,cy) + ok = kernel32.SetConsoleCursorPosition(self._hnd, cpos) + if not ok: + log.error("failed to set cursor position: stream=%r error=%r", self.stream, _pp_get_last_error()) + + ##bufx = info.dwSize.X + ##bufy = into.dwSize.Y + ##curx = info.dwCursorPosition.X + ##cury = info.dwCursorPosition.Y + ##win = info.srWindow + ##l,t,r,b = win.Left, win.Top, win.Right, win.Bottom + ##ws = info.dwMaximumWindowSize + ##mx, my = ws.X, ws.Y + ##sizex = r-l+1 + ##sizey = b-t+1 + + def _apply_styles(self, values): + "apply values attached to ansi 'm' code" + clear = _clear_bits + + #load attrs, rearrange based on flags + attr = self._attrs + rev = self._reverse + if rev: #undo attr swap if reversed + attr = _swap_bgfg(attr) + conceal = self._conceal + if conceal: #restore orig bg color if concealed + attr = attr-(attr & FOREGROUND_ALL) + self._conceal_fg + + #make changes + for value in values: + if value == 0: + #reset all + attr = FOREGROUND_WHITE + rev = conceal = False + elif value == 1: + #enable bold + attr |= FOREGROUND_INTENSITY + #4,21 - underline + elif value == 5 or value == 6: + #enable blink (as background highlight) + attr |= BACKGROUND_INTENSITY + elif value == 7: + #reverse text mode + rev = True + elif value == 8: + #concealed text mode + conceal = True + + elif value == 22: + #disable bold + attr = clear(attr, FOREGROUND_INTENSITY) + elif value == 25: + #disable blink + attr = clear(attr, BACKGROUND_INTENSITY) + #24 - undo underline + elif value == 27: + #undo reverse text mode + rev = False + elif value == 28: + #undo concealed mode + conceal = False + + elif 30 <= value < 38 or value == 39: + #set fg color + if value == 39: #treat white as default + value = 37 + attr = clear(attr, FOREGROUND_WHITE) | ansi_to_dos[value-30] + elif 40 <= value < 48 or value == 49: + #set bg color + if value == 49: #treat black as default + value = 40 + attr = clear(attr, BACKGROUND_WHITE) | (ansi_to_dos[value-40]<<4) + else: + #we ignore all other attr codes + log.debug("ignoring unsupported ansi style attr: %r", value) + continue + + #rearrange attr based on flags + if conceal: + old = self._conceal_fg = attr & FOREGROUND_ALL + new = (attr&BACKGROUND_ALL)>>4 + attr = attr-old+new + if rev: #swap colors if reversed + attr = _swap_bgfg(attr) + + #now that we're done, try to update + assert isinstance(attr, int) + ok = kernel32.SetConsoleTextAttribute(self._hnd, attr) + if ok: + self._attrs = attr + self._reverse = rev + self._conceal = conceal + else: + log.error("failed to write attrstate to console: stream=%r error=%r", self.stream, _pp_get_last_error()) + + #========================================================= + #methods to proxy real stream + #========================================================= + def __getattr__(self, attr): + return getattr(self.stream, attr) + + def write(self, text): + self._update_state() + raw_write = self.stream.write + apply_code = self._apply_code + for elem in ansi.parse_ansi_string(text, rtype=iter, malformed_codes="ignore"): + if hasattr(elem, "code"): + apply_code(elem) + else: + raw_write(elem) + + def writelines(self, seq): + self._update_state() + raw_write = self.stream.write + apply_code = self._apply_code + for text in seq: + for elem in ansi.parse_ansi_string(text, rtype=iter, malformed_codes="ignore"): + if hasattr(elem, "code"): + apply_code(elem) + else: + raw_write(elem) + + #========================================================= + #eoc + #========================================================= + +#========================================================= +#eof +#========================================================= |
