=== added directory 'doc' === modified file 'makedocs.py' --- makedocs.py 2007-02-05 22:39:48 +0000 +++ makedocs.py 2007-02-10 19:23:01 +0000 @@ -1,13 +1,37 @@ -""" Create documentation """ - -import inspect, os +""" Create documentation. Takes two optional arguments -- the CSS file to reference in the generated HTML, and a JS file to load. + +Limitations: + + Can't link to superclasses in other modules + + No support for ordered lists (not massively important) +""" + +import inspect, os, sys +# pil stuff for inheritance diagrams +import ImageFont, ImageDraw, ImageChops, Image + +docDirectory = "doc" moduleNames = [ "pykhtml", "pykhtml.dom" ] +moduleForceFirstItems = { + "pykhtml" : [ + "Browser", + "startEventLoop", + "stopEventLoop", + "timer", + "init" + ], + "pykhtml.dom" : [ + "Document", + "Node", + "Element" + ] +} + allowableSpecialMethods = [ "__init__", "__del__", @@ -15,12 +39,56 @@ "__repr__" ] -itemToClassList = {} - +classToClassList = {} + +# dirty hack so that qt/kdecore/khtml aren't loaded when pykhtml calls 'import ...' +sys.modules["qt"] = sys.modules["kdecore"] = {} +# fake the KHTML interface +class Dummy(object): + pass +khtml = Dummy() +dom = Dummy() +dom.DOMString = None +dom.EventListener = int +khtml.DOM = dom +sys.modules["khtml"] = khtml +# end dirty hack + +def autocrop(image, backgroundColor=(255, 255, 255)): + """ crop an image based on alpha. """ + background = Image.new("RGB", image.size, backgroundColor) + return image.crop(ImageChops.difference(image, background).getbbox()) + +def makeDiagram(cls): + #image = Image.new("RGB", (300, 300)) + pass + + +def myImport(name): + """ Imports a module given a name in the form 'foo' or 'foo.bar' """ + module = __import__(name) + if name.count("."): + subModuleName = name.split(".")[1] + module = getattr(module, subModuleName) + return module def getDoc(obj): return "\n".join(x.strip() for x in (obj.__doc__ or "").split("\n")) +def functionSignature(func): + name = func.__name__ + args = list(func.func_code.co_varnames[:func.func_code.co_argcount]) + for i in xrange(len(func.func_defaults or [])): + val = func.func_defaults[i] + args[-(i + 1)] += "=%s" % repr(val) + return "%s(%s)" % (name, ", ".join(args)) + +def propertySignature(prop, name): + signature = "%s (read-only property)" % name + if prop.fset is not None: + signature = "%s (property)" % name + return signature + def documentClass(cls, name, itemsToExamine, documentedItems): doc = getDoc(cls) ## add sub methods @@ -35,33 +103,211 @@ classItemNames.insert(0, x) l = [] documentedItems.append(l) - ## add our documentation here - l.append((1, name, doc)) - ## + l.append((1, name, doc, cls)) + classToClassList[cls] = l for name in classItemNames: item = getattr(cls, name) - itemToClassList[item] = l - itemsToExamine.append((name, item)) - + # add a reference to the class as an extra param + itemsToExamine.append((name, item, cls)) + +def getMethodImplementor(meth, cls): + name = meth.__name__ + im_func = meth.im_func + node = cls + implementor = cls + while node is not None: + newNode = None + for base in node.__bases__: + try: + method = getattr(base, name) + except AttributeError: + pass + else: + if hasattr(method, "im_func") and method.im_func == im_func: + implementor = newNode = base + node = newNode + return implementor.__name__ + +def getPropertyImplementor(prop, name, cls): + fget = prop.fget + node = cls + implementor = cls + while node is not None: + newNode = None + for base in node.__bases__: + if hasattr(base, name) and getattr(base, name).fget == fget: + implementor = newNode = base + node = newNode + return implementor.__name__ + # go up through bases checking for the existence of meth.im_func + # in the + +#def getPropertyImplementor(prop, cls): + #return getMethodImplementor(prop.fget, cls) class GenerateHtml: + html = """ + + + + %(modulename)s module documentation + + %(stylesheet)s + %(script)s + + +
+
%(modules)s
+
+
+

%(modulename)s

+

%(moduledoc)s

+ %(content)s +
+
+ + +""" def __init__(self): - # [(name, data), ...] + # [(name, doc, data), ...] self.pages = [] - def generateModule(self, moduleName, structure): - ## 1 = class, 2 = method, 4 = property, 3 = function - self.pages.append((moduleName, structure)) + def generateModule(self, moduleName, moduleDoc, structure): + self.pages.append((moduleName, moduleDoc, structure)) + + def markup(self, s): + #s = s.replace("\n", "
") + #assert hasattr(self, "names") + if not s.strip(): + return s + if s.count("\n+ "): + lastLineWasInList = False + lines = s.split("\n") + for i, line in enumerate(lines): + if line.startswith("+"): + line = "
  • %s
  • " % line[2:] + if not lastLineWasInList: + line = "%s" % line + lastLineWasInList = False + s = "\n".join(lines).replace("\n", "") + # convert newlines to breaks + s = s.replace("\n", "
    ") + # and two dashes to an en dash + s = s.replace(" -- ", " – ") + # and finally (and most importantly), add a full stop at the end + s = "%s." % s.rstrip(".") + return s def write(self, directory): - for name, data in self.pages: - f = file(os.path.join(directory, name) + ".htm", "w") - f.write(str(data)) + # first, actually. We want to see all the 'names' present in the document so we can provide links to them if there's a reference in the form [foo] + fullNames = [] + for moduleName, doc, data in self.pages: + for item in data: + if isinstance(item, tuple) and item[0] == 4: + fullNames.append("%s.%s" % (moduleName, item[1].split("(")[0].strip())) + elif isinstance(item, list): + className = item[0][1] + fullNames.append("%s.%s" % (moduleName, className)) + for method in item[1:]: + fullNames.append("%s.%s.%s" % (moduleName, className, method[1].split("(")[0].strip())) + if len(self.pages) > 1: + moduleBar = '
    Modules:
    ' + else: + moduleBar = '' + # Generate the markup to link to other modules if there is > 1 + #print names + for moduleName, doc, data in self.pages: + # set up links + moduleNameLength = len(moduleName) + names = [x[moduleNameLength:].lstrip(".") if x.startswith(moduleName) else x for x in fullNames] + #print moduleName, names + # serialize the data to HTML + output = [] + append = output.append + ## type, name, doc + ## item[0]: 1 = class, 2 = method, 3 = property, 4 = function + for item in data: + if isinstance(item, tuple) and item[0] == 4: + # function + append('

    %s

    %s
    \n' % (item[1].split("(")[0], item[1], self.markup(item[2]))) + elif isinstance(item, list): + cls = item.pop(0) + inherits = "" + if cls[3].__bases__: + inherits = ' (inherits ' + inherits += ", ".join('%s' % x.__name__ for x in cls[3].__bases__) + inherits += ")" + append('

    class %s %s

    \n
    %s
    \n' % (cls[1], cls[1], inherits, self.markup(cls[2]))) + for method in item: + # check if this has been inherited + inheritedClassNameValue = "" + inheritedMessage = "" + # if the implementing class is not this one + if method[4] != cls[1]: + inheritedClassNameValue = "inherited " + inheritedMessage = '
    Inherited from [%s]
    ' % method[4] + if method[0] == 2: + append('

    %s

    %s
    %s
    \n' % (inheritedClassNameValue, cls[1], method[1].split("(")[0], method[1], inheritedMessage, self.markup(method[2]))) + elif method[0] == 3: + # change (property) into (...) + name = method[1] + assert name.count(" ") + name, tag = name.split(" ", 1) + assert tag.startswith("(") and tag.endswith(")") + tag = tag[1:-1] + name = '%s (%s)' % (name, tag) + append('

    %s

    %s
    %s
    \n' % (inheritedClassNameValue, cls[1], name.split(" ")[0], name, inheritedMessage, self.markup(method[2]))) + else: + OhMyGodStrangeThingsInTheDataStructure + # Finally, add all the links + content = "".join(output) + for name in names: + if content.count("[%s]" % name): + # deduce the link URL + url = "" + anchorName = name + # relative reference + if name.split(".")[0] not in moduleNames: + # if it's relative to a submodule, though + for possibleModule in moduleNames: + if possibleModule == "%s.%s" % (moduleName, name.split(".")[0]): + url = "%s.%s" % (moduleName, name.split(".")[0]) + ".htm" + anchorName = ".".join(name.split(".")[1:]) + else: + # absolute reference + for possibleModule in moduleNames: + if name.startswith(possibleModule) and hasattr(myImport(possibleModule), name.replace(possibleModule, "").lstrip(".").split(".")[0]): + url = possibleModule + ".htm" + #print "Setting url:", possibleModule + ".htm" + anchorName = name.replace(possibleModule, "").lstrip(".") + url += "#" + anchorName + print url + content = content.replace("[%s]" % name, '%s' % (url, name)) + # add the stylesheet link if necessary + stylesheet = "" + script = "" + if len(sys.argv) > 1: + if len(sys.argv) > 2: + script = "\n".join('' % x for x in sys.argv[2].split(",")) + stylesheet = "\n".join('' % x for x in sys.argv[1].split(",")) + # set the active module in the module bar + modulesMarkup = moduleBar.replace('"%s.htm"' % moduleName, '"%s.htm" class="here"' % moduleName) + d = {"modulename" : moduleName, "moduledoc" : doc, "content" : content, "stylesheet" : stylesheet, "script" : script, "modules" : modulesMarkup} + html = GenerateHtml.html % d + f = file(os.path.join(directory, moduleName) + ".htm", "w") + f.write(html) f.close() def main(): # turn the module names into modules + makeInheritanceDiagrams = "--diagrams" in sys.argv modules = [] ## Speed up inspect_isclass = inspect.isclass @@ -69,15 +315,17 @@ inspect_isfunction = inspect.isfunction _isinstance = isinstance _documentClass = documentClass - _itemToClassList = itemToClassList + _classToClassList = classToClassList _getattr = getattr ## End speed up + doneItems = {} generator = GenerateHtml() for moduleName in moduleNames: module = __import__(moduleName) if moduleName.count("."): subModuleName = moduleName.split(".")[1] module = _getattr(module, subModuleName) + moduleDoc = module.__doc__ itemsToExamine = [] documentedItems = [] for attributeName in dir(module): @@ -85,16 +333,56 @@ itemsToExamine.append((attributeName, _getattr(module, attributeName))) while itemsToExamine: - name, item = itemsToExamine.pop(0) + thingToExamine = itemsToExamine.pop(0) + name = thingToExamine[0] + item = thingToExamine[1] + extra = thingToExamine[2:] + # class if inspect_isclass(item) and item.__module__ in moduleNames: + print "Checking for %s.%s.png" % (moduleName, name) + if makeInheritanceDiagrams: + makeDiagram(item) + else: + assert os.path.exists(os.path.join(docDirectory, "%s.%s.png" % (moduleName, name))) _documentClass(item, name, itemsToExamine, documentedItems) - elif inspect_ismethod(item) or _isinstance(item, property): - _itemToClassList[item].append((2, name, getDoc(item))) - #_documentMethod(item, name, itemsToExamine, documentedItems) + # method (extra tuple items: class, definingClass) + elif inspect_ismethod(item): + cls = extra[0] + implementingClass = getMethodImplementor(item, cls) + _classToClassList[cls].append((2, functionSignature(item), getDoc(item), cls, implementingClass)) + # property (extra tuple items: class, definingClass) + elif _isinstance(item, property): + cls = extra[0] + implementingClass = getPropertyImplementor(item, name, cls) + #print "implementingClass:", implementingClass + _classToClassList[cls].append((3, propertySignature(item, name), getDoc(item), cls, implementingClass)) + # function elif inspect_isfunction(item) and item.__module__ in moduleNames: - documentedItems.append((4, name, getDoc(item))) - generator.generateModule(moduleName, documentedItems) - generator.write("doc") + documentedItems.append((4, functionSignature(item), getDoc(item))) + # re-sort data + if moduleName in moduleForceFirstItems: + order = moduleForceFirstItems[moduleName] + # pull the items we want out of the data structure into here, in the order that we want + dataHead = [None] * len(order) + for i in reversed(xrange(len(documentedItems))): + item = documentedItems[i] + if _isinstance(item, tuple): + # function will have the arguments added. Strip them + itemName = item[1].split("(")[0] + if itemName in order: + dataHead[order.index(itemName)] = item + documentedItems.pop(i) + elif _isinstance(item, list): + cls = documentedItems[i] + if cls[0][1] in order: + dataHead[order.index(cls[0][1])] = cls + documentedItems.pop(i) + if None in dataHead: + print dataHead + raise Exception("!! Error: Cannot move '%s' to the top of the documentation document: it doesn't exist!" % order[dataHead.index(None)]) + documentedItems = dataHead + documentedItems + generator.generateModule(moduleName, moduleDoc, documentedItems) + generator.write(docDirectory) if __name__ == "__main__": main() === modified file 'pykhtml/__init__.py' --- pykhtml/__init__.py 2007-02-05 22:39:48 +0000 +++ pykhtml/__init__.py 2007-02-10 19:23:01 +0000 @@ -3,7 +3,7 @@ import khtml, kdecore import sip # cast #from khtml.DOM import DOMString -from qt import * +import qt DOM = khtml.DOM DOMString = DOM.DOMString @@ -27,6 +27,7 @@ ## stupid to start Xvfb each time if os.name == "posix": def running(name): + """ Check whether a process of the given name is running """ procs = os.popen("ps -eo comm").read().strip().split("\n") return name in procs else: @@ -59,11 +60,11 @@ kdecore.KCmdLineArgs.init(sys.argv, "PyKHTML", "PyKHTML Library", "0.1") application = kdecore.KApplication() # the widget that will host the KHTMLParts - dialog = QDialog(None) + dialog = qt.QDialog(None) application.setMainWidget(dialog) if debugWithGUI: dialog.show() - dialog.layout = QVBoxLayout(dialog) + dialog.layout = qt.QVBoxLayout(dialog) def init(display=1, _sleep=1): """ Initiate the system if necessary (start Xvfb if it's not running, connect to it, start our program instance). This is called automatically when you create a Browser instance, so you shouldn't have to worry about it. You can specify use of a certain display by setting the `display` parameter. """ @@ -105,11 +106,13 @@ def timer(time, func): """ Call the given function after the alloted time. Requires that the PyKHTML event loop is running """ - QTimer.singleShot(int(time * 1000), func) + qt.QTimer.singleShot(int(time * 1000), func) class Browser(object): + """ A Browser is the main class you use to navigate around and visit different pages. Have a look at Browser.load and Browser.document to access basic use. """ def __init__(self): + """ Create a new Browser """ init() self.part = khtml.KHTMLPart(dialog) if debugWithGUI: @@ -132,14 +135,14 @@ def load(self, uri, callback): """ Load a webpage in the browser. It takes as parameters the URI of the page to load, and a callable object to call when the page has loaded. """ if self.loadFunction: - self.disconnect(self.part, SIGNAL("docCreated()"). self._slotDocCreated) + self.disconnect(self.part, qt.SIGNAL("docCreated()"). self._slotDocCreated) self.loadFunction = callback - self.connect(self.part, SIGNAL("docCreated()"), self._slotDocCreated) + self.connect(self.part, qt.SIGNAL("docCreated()"), self._slotDocCreated) self.location = uri def _slotDocCreated(self): self.part.executeScript(DOM.Node(), "window.alert = function() {}") - self.disconnect(self.part, SIGNAL("docCreated()"), self._slotDocCreated) + self.disconnect(self.part, qt.SIGNAL("docCreated()"), self._slotDocCreated) if not self.loadFunction: raise AttributeError("No load function callback present") func = self.loadFunction @@ -155,6 +158,6 @@ @property def document(self): - """ Get a reference to the document (dom.Document) for the currently loaded page. It contains all the tasty methods for walking the DOM tree like getElementById/getElementsByTagName and methods for browsing to other linked pages. """ + """ Get a reference to the document (see [dom.Document]) for the currently loaded page. It contains all the tasty methods for walking the DOM tree like getElementById / getElementsByTagName, and methods for browsing to other linked pages. """ return dom.Document(self.part.htmlDocument(), self) === modified file 'pykhtml/dom.py' --- pykhtml/dom.py 2007-02-05 22:39:48 +0000 +++ pykhtml/dom.py 2007-02-10 19:23:01 +0000 @@ -32,7 +32,7 @@ @property def children(self): - """ Get the children nodes of this """ + """ Get the children nodes of this node """ # cache the value if self.__children is None: l = self.__children = [] @@ -48,7 +48,7 @@ class Text(Node): - """ A text node lets you access the text in it using the Text.value attribute or by converting to a string with str() """ + """ A text node lets you access the text in it using the [Text.value] attribute or by converting to a string with str() """ def __init__(self, cTextNode): Node.__init__(self, cTextNode) @@ -60,6 +60,7 @@ @property def value(self): + """ Equivalent to str(textNode). Get the string this node represents """ return str(self._.nodeValue().string()) registerNode(3, Text) @@ -117,12 +118,12 @@ return str(e.nodeName().string()) def addEvent(self, eventName, func, capture=False): - """ This lets you listen for certain events as they occur on the current element. Only particularly useful when listening for load events really. """ + """ This lets you listen for certain events as they occur on the current element. Only particularly useful when listening for load events reaaally. """ listener = _CallbackEventListener(eventName, func) self._.addEventListener(DOMString(eventName), listener, capture) def removeEvent(self, eventName, func, capture=False): - """ Removes events that you've added with Element.addEvent """ + """ Removes events that you've added with [Element.addEvent] """ self._.removeEventListener(DOMString(eventName), _CallbackEventListener.getCallbackInstance(eventName, func), capture) _CallbackEventListener.remove(eventName, func) # -- important, we hook to the method not Element base class @@ -130,14 +131,14 @@ class Anchor(Element): - """ Anchor elements with an Anchor.href property """ + """ Anchor elements with an [Anchor.href] property """ def __init__(self, cAnchor): Element.__init__(self, cAnchor) self._ = sip.cast(self._, _DOM.HTMLAnchorElement) @property def href(self): - """ The anchors 'href' value. Returns the full URL pointed to by the href attribute """ + """ The anchor's 'href' value. Returns the full URL pointed to by the href attribute of this element """ return str(self._.href().string()) registerElement("A", Anchor) @@ -150,7 +151,7 @@ class Document(object): - """ Document object for accessing the DOM tree. Don't keep this object around, when the page changes it's invalidated. Just use browser.document. """ + """ Document object for accessing the DOM tree. Don't keep this object around, when the page changes it's invalidated. Just access it through [pykhtml.Browser.document] whenever you want it. """ def __init__(self, htmlDocument, browser): self._d = htmlDocument self.browser = browser @@ -185,8 +186,8 @@ def visit(self, text=None, callback=None, attributes=None, stripSpace=True): """ Visit a page pointed to by a certain link. This function searches for all links in the document that either: - - Match the given text (as a string or regular expression object) - - Match the attributes given (a dictionary mapping attribute name to attribute value, where the value is again either a string or regular expression object) + + Match the given text (as a string or regular expression object) + + Match the attributes given (a dictionary mapping attribute name to attribute value, where the value is again either a string or regular expression object) If the `stripSpace` attribute is True, when searching for a string match all whitespace is stripped from the item we are matching against """ _dlinks = self._d.links() possibleLinks = [_dlinks.item(i) for i in xrange(_dlinks.length())]