summaryrefslogtreecommitdiff
path: root/bps/unstable
diff options
context:
space:
mode:
Diffstat (limited to 'bps/unstable')
-rw-r--r--bps/unstable/__init__.py1084
-rw-r--r--bps/unstable/ansi.py683
-rw-r--r--bps/unstable/bpsdoc/__init__.py8
-rw-r--r--bps/unstable/bpsdoc/ast/layout.html29
-rw-r--r--bps/unstable/bpsdoc/ast/static/ast-website.css733
-rw-r--r--bps/unstable/bpsdoc/ast/static/ast.css153
-rw-r--r--bps/unstable/bpsdoc/ast/static/bg_top.jpgbin0 -> 357 bytes
-rw-r--r--bps/unstable/bpsdoc/ast/theme.conf3
-rw-r--r--bps/unstable/bpsdoc/cloud/layout.html51
-rw-r--r--bps/unstable/bpsdoc/cloud/static/ast-website.css733
-rw-r--r--bps/unstable/bpsdoc/cloud/static/ast.css_t248
-rw-r--r--bps/unstable/bpsdoc/cloud/static/bg_top.jpgbin0 -> 357 bytes
-rw-r--r--bps/unstable/bpsdoc/cloud/static/header.pngbin0 -> 330 bytes
-rw-r--r--bps/unstable/bpsdoc/cloud/static/header.svg101
-rw-r--r--bps/unstable/bpsdoc/cloud/theme.conf35
-rw-r--r--bps/unstable/bpsdoc/index_styles.py69
-rw-r--r--bps/unstable/bpsdoc/make_helper.py149
-rw-r--r--bps/unstable/bpsdoc/nested_sections.py95
-rw-r--r--bps/unstable/bpsdoc/relbar_toc.py32
-rw-r--r--bps/unstable/softref.py847
-rw-r--r--bps/unstable/winconsole.py406
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> &raquo; </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
new file mode 100644
index 0000000..c1e5775
--- /dev/null
+++ b/bps/unstable/bpsdoc/ast/static/bg_top.jpg
Binary files differ
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 ' &nbsp; / &nbsp; ' or footdelim %}
+
+{# make root link redirectable #}
+{% block rootrellink %}
+ <li><a href="{{ pathto(theme_roottarget) }}">{{project}} Documentation</a> &raquo; </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 %}&copy; <a href="{{ path }}">Copyright</a> {{ copyright }}.{% endtrans %}
+ {%- else %}
+ {% trans copyright=copyright|e %}&copy; 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
new file mode 100644
index 0000000..c1e5775
--- /dev/null
+++ b/bps/unstable/bpsdoc/cloud/static/bg_top.jpg
Binary files differ
diff --git a/bps/unstable/bpsdoc/cloud/static/header.png b/bps/unstable/bpsdoc/cloud/static/header.png
new file mode 100644
index 0000000..c4f729e
--- /dev/null
+++ b/bps/unstable/bpsdoc/cloud/static/header.png
Binary files differ
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
+#=========================================================