# -*- coding: utf-8 -*- #--------------------------------------------------------------------------- # Name: sphinxtools/utilities.py # Author: Andrea Gavana # # Created: 30-Nov-2010 # Copyright: (c) 2010-2016 by Total Control Software # License: wxWindows License #--------------------------------------------------------------------------- # Standard library imports import sys import os import codecs import shutil import re if sys.version_info < (3,): import cPickle as pickle from UserDict import UserDict string_base = basestring else: import pickle from collections import UserDict string_base = str # Phoenix-specific imports from .templates import TEMPLATE_CONTRIB from .constants import IGNORE, PUNCTUATION, MODULENAME_REPLACE from .constants import CPP_ITEMS, VERSION, VALUE_MAP from .constants import RE_KEEP_SPACES, EXTERN_INHERITANCE from .constants import DOXYROOT, SPHINXROOT, WIDGETS_IMAGES_ROOT # ----------------------------------------------------------------------- # class ODict(UserDict): """ An ordered dict (odict). This is a dict which maintains an order to its items; the order is rather like that of a list, in that new items are, by default, added to the end, but items can be rearranged. .. note:: Note that updating an item (setting a value where the key is already in the dict) is not considered to create a new item, and does not affect the position of that key in the order. However, if an item is deleted, then a new item with the same key is added, this is considered a new item. """ def __init__(self, dict = None): self._keys = [] UserDict.__init__(self, dict) def __delitem__(self, key): UserDict.__delitem__(self, key) self._keys.remove(key) def __setitem__(self, key, item): UserDict.__setitem__(self, key, item) if key not in self._keys: self._keys.append(key) def clear(self): UserDict.clear(self) self._keys = [] def copy(self): dict = UserDict.copy(self) dict._keys = self._keys[:] return dict def items(self): return list(zip(self._keys, list(self.values()))) def keys(self): return self._keys def popitem(self): try: key = self._keys[-1] except IndexError: raise KeyError('dictionary is empty') val = self[key] del self[key] return (key, val) def setdefault(self, key, failobj = None): UserDict.setdefault(self, key, failobj) if key not in self._keys: self._keys.append(key) def update(self, dict): UserDict.update(self, dict) for key in list(dict.keys()): if key not in self._keys: self._keys.append(key) def values(self): return list(map(self.get, self._keys)) # ----------------------------------------------------------------------- # def isNumeric(input_string): """ Checks if the string `input_string` actually represents a number. :param string `input_string`: any string. :rtype: `bool` :returns: ``True`` if the `input_string` can be converted to a number (any number), ``False`` otherwise. """ try: float(input_string) return True except ValueError: return False # ----------------------------------------------------------------------- # def countSpaces(text): """ Counts the number of spaces before and after a string. :param string `text`: any string. :rtype: `tuple` :returns: a tuple representing the number of spaces before and after the text. """ space_before = ' '*(len(text) - len(text.lstrip(' '))) space_after = ' '*(len(text) - len(text.rstrip(' '))) return space_before, space_after # ----------------------------------------------------------------------- # def underscore2Capitals(string): """ Replaces the underscore letter in a string with the following letter capitalized. :param string `string`: the string to be analyzed. :rtype: `string` """ items = string.split('_')[1:] newstr = '' for item in items: newstr += item.capitalize() return newstr # ----------------------------------------------------------------------- # def replaceCppItems(line): """ Replaces various C++ specific stuff with more Pythonized version of them. :param string `line`: any string. :rtype: `string` """ items = RE_KEEP_SPACES.split(line) newstr = [] for n, item in enumerate(items): if item in CPP_ITEMS: continue if 'wxString' in item: item = 'string' elif 'wxCoord' == item: item = 'int' elif 'time_t' == item: item = 'int' elif item == 'char': item = 'int' elif item == 'double': if len(items) > n+2 and not items[n+2].lower().startswith("click"): item = 'float' if len(item.replace('``', '')) > 2: if '*' in item: try: eval(item) item = item.replace('*', 'x') except: # Avoid replacing standalone '&&' and similar for cpp in CPP_ITEMS[0:2]: item = item.replace(cpp, '') newstr.append(item) newstr = ''.join(newstr) newstr = newstr.replace(' *)', ' )') return newstr # ----------------------------------------------------------------------- # def pythonizeType(ptype, is_param): """ Replaces various C++ specific stuff with more Pythonized version of them, for parameter lists and return types (i.e., the `:param:` and `:rtype:` ReST roles). :param string `ptype`: any string; :param bool `is_param`: ``True`` if this is a parameter description, ``False`` if it is a return type. :rtype: `string` """ from etgtools.tweaker_tools import removeWxPrefix if 'size_t' in ptype: ptype = 'int' elif 'wx.' in ptype: ptype = ptype[3:] # *** else: ptype = wx2Sphinx(replaceCppItems(ptype))[1] ptype = ptype.replace('::', '.').replace('*&', '') ptype = ptype.replace('int const', 'int') ptype = ptype.replace('Uint32', 'int').replace('**', '').replace('Int32', 'int') ptype = ptype.replace('FILE', 'file') ptype = ptype.replace('boolean', 'bool') for item in ['unsignedchar', 'unsignedint', 'unsignedlong', 'unsigned']: ptype = ptype.replace(item, 'int') ptype = ptype.strip() ptype = removeWxPrefix(ptype) if '. wx' in ptype: #*** ptype = ptype.replace('. wx', '.') plower = ptype.lower() if plower == 'double': ptype = 'float' if plower in ['string', 'char', 'artid', 'artclient']: ptype = 'string' if plower in ['coord', 'byte', 'fileoffset', 'short', 'time_t', 'intptr', 'uintptr', 'windowid']: ptype = 'int' if plower in ['longlong']: ptype = 'long' cpp = ['ArrayString', 'ArrayInt', 'ArrayDouble'] python = ['list of strings', 'list of integers', 'list of floats'] for c, p in zip(cpp, python): ptype = ptype.replace(c, p) if 'Image.' in ptype: ptype = ptype.split('.')[-1] if 'FileName' in ptype: ptype = 'string' if ptype.endswith('&'): ptype = ptype[0:-1] if ' ' not in ptype: ptype = ':class:`%s`'%ptype else: if is_param and '.' in ptype: modules = list(MODULENAME_REPLACE.values()) modules.sort() modules = modules[1:] if ptype.split('.')[0] + '.' in modules: ptype = ':ref:`%s`'%ptype return ptype # ----------------------------------------------------------------------- # def convertToPython(text): """ Converts the input `text` into a more ReSTified version of it. This involves the following steps: 1. Any C++ specific declaration (like ``unsigned``, ``size_t`` and so on is removed. 2. Lines starting with "Include file" or "#include" are ignored. 3. Uppercase constants (i.e., like ID_ANY, HORIZONTAL and so on) are converted into inline literals (i.e., ``ID_ANY``, ``HORIZONTAL``). 4. The "wx" prefix is removed from all the words in the input `text`. #*** :param string `text`: any string. :rtype: `string` """ from etgtools.tweaker_tools import removeWxPrefix from etgtools.item_module_map import ItemModuleMap newlines = [] unwanted = ['Include file', '#include'] for line in text.splitlines(): newline = [] for remove in unwanted: if remove in line: line = line[0:line.index(remove)] break spacer = ' '*(len(line) - len(line.lstrip())) line = replaceCppItems(line) for word in RE_KEEP_SPACES.split(line): if word == VERSION: newline.append(word) continue newword = word for s in PUNCTUATION: newword = newword.replace(s, "") if newword in VALUE_MAP: word = word.replace(newword, VALUE_MAP[newword]) newline.append(word) continue if newword not in IGNORE and not newword.startswith('wx.'): word = removeWxPrefix(word) newword = removeWxPrefix(newword) if "::" in word and not word.endswith("::"): # Bloody SetCursorEvent... word = word.replace("::wx", ".") # *** word = word.replace("::", ".") word = "`%s`" % word newline.append(word) continue if (newword.upper() == newword and newword not in PUNCTUATION and newword not in IGNORE and len(newword.strip()) > 1 and not isNumeric(newword) and newword not in ['DC', 'GCDC']): if '``' not in newword and '()' not in word and '**' not in word: word = word.replace(newword, "``%s``" % ItemModuleMap().get_fullname(newword)) word = word.replace('->', '.') newline.append(word) newline = spacer + ''.join(newline) newline = newline.replace(':see:', '.. seealso::') newline = newline.replace(':note:', '.. note::') newlines.append(newline) formatted = "\n".join(newlines) formatted = formatted.replace('\\', '\\\\') return formatted # ----------------------------------------------------------------------- # def findDescendants(element, tag, descendants=None): """ Finds and returns all descendants of a specific `xml.etree.ElementTree.Element` whose tag matches the input `tag`. :param xml.etree.ElementTree.Element `element`: the XML element we want to examine. :param string `tag`: the target tag we are looking for. :param list `descendants`: a list of already-found descendants or ``None`` if this is the first call to the function. :rtype: `list` .. note:: This is a recursive function, and it is only used in the `etgtools.extractors.py` script. """ if descendants is None: descendants = [] for childElement in element: if childElement.tag == tag: descendants.append(childElement) descendants = findDescendants(childElement, tag, descendants) return descendants # ----------------------------------------------------------------------- # def findControlImages(elementOrString): """ Given the input `element` (an instance of `xml.etree.ElementTree.Element` or a plain string) representing a Phoenix class description, this function will scan the doxygen image folder ``DOXYROOT`` to look for a widget screenshot. If this class indeed represents a widget and a screenshot is found, it is then copied to the appropriate Sphinx input folder ``WIDGETS_IMAGES_ROOT`` in one of its sub-folders (``wxmsw``, ``wxgtk``, ``wxmac``) depending on which platform the screenshot was taken. :param `elementOrString`: the XML element we want to examine (an instance of xml.etree.ElementTree.Element) or a plain string (usually for wx.lib). :rtype: `list` :returns: A list of image paths, every element of it representing a screenshot on a different platform. An empty list if returned if no screenshots have been found. .. note:: If a screenshot doesn't exist for one (or more) platform but it exists for others, the missing images will be replaced by the "no_appearance.png" file (you can find it inside the ``WIDGETS_IMAGES_ROOT`` folder. """ from etgtools.tweaker_tools import removeWxPrefix if isinstance(elementOrString, string_base): class_name = py_class_name = elementOrString.lower() else: element = elementOrString class_name = element.pyName if element.pyName else removeWxPrefix(element.name) py_class_name = wx2Sphinx(class_name)[1] class_name = class_name.lower() py_class_name = py_class_name.lower() image_folder = os.path.join(DOXYROOT, 'images') appearance = ODict() for sub_folder in ['wxmsw', 'wxmac', 'wxgtk']: png_file = class_name + '.png' appearance[sub_folder] = '' possible_image = os.path.join(image_folder, sub_folder, png_file) new_path = os.path.join(WIDGETS_IMAGES_ROOT, sub_folder) py_png_file = py_class_name + '.png' new_file = os.path.join(new_path, py_png_file) if os.path.isfile(new_file): appearance[sub_folder] = py_png_file elif os.path.isfile(possible_image): if not os.path.isdir(new_path): os.makedirs(new_path) if not os.path.isfile(new_file): shutil.copyfile(possible_image, new_file) appearance[sub_folder] = py_png_file if not any(list(appearance.values())): return [] for sub_folder, image in list(appearance.items()): if not image: appearance[sub_folder] = '../no_appearance.png' return list(appearance.values()) # ----------------------------------------------------------------------- # def makeSummary(class_name, item_list, template, kind, add_tilde=True): """ This function generates a table containing a method/property name and a shortened version of its docstrings. :param string `class_name`: the class name containing the method/property lists; :param list `item_list`: a list of tuples like `(method/property name, short docstrings)`; :param string `template`: the template to use (from `sphinxtools/templates.py`, can be the ``TEMPLATE_METHOD_SUMMARY`` or the ``TEMPLATE_PROPERTY_SUMMARY``; :param string `kind`: can be ``:meth:`` or ``:attr:`` or ``:ref:`` or ``:mod:``; :param bool `add_tilde`: ``True`` to add the ``~`` character in front of the first summary table column, ``False`` otherwise. :rtype: `string` """ maxlen = 0 for method, simple_docs in item_list: substr = ':%s:`~%s`'%(kind, method) maxlen = max(maxlen, len(substr)) maxlen = max(80, maxlen) summary = '='*maxlen + ' ' + '='*80 + "\n" format = '%-' + str(maxlen) + 's %s' for method, simple_docs in item_list: if add_tilde: substr = ':%s:`~%s`'%(kind, method) else: substr = ':%s:`%s`'%(kind, method) new_docs = simple_docs if kind == 'meth': regex = re.findall(r':meth:\S+', simple_docs) for regs in regex: if '.' in regs: continue meth_name = regs[regs.index('`')+1:regs.rindex('`')] newstr = ':meth:`~%s.%s`'%(class_name, meth_name) new_docs = new_docs.replace(regs, newstr, 1) if '===' in new_docs: new_docs = '' elif new_docs.rstrip().endswith(':'): # Avoid Sphinx warnings new_docs = new_docs.rstrip(':') summary += format%(substr, new_docs) + '\n' summary += '='*maxlen + ' ' + '='*80 + "\n" return template % summary # ----------------------------------------------------------------------- # header = """\ .. wxPython Phoenix documentation This file was generated by Phoenix's sphinx generator and associated tools, do not edit by hand. Copyright: (c) 2011-2016 by Total Control Software License: wxWindows License """ def writeSphinxOutput(stream, filename, append=False): """ Writes the text contained in the `stream` to the `filename` output file. :param StringIO.StringIO `stream`: the stream where the text lives. :param string `filename`: the output file we want to write the text in; :param bool `append`: ``True`` to append to the file, ``False`` to simply write to it. """ text_file = os.path.join(SPHINXROOT, filename) text = stream.getvalue() mode = 'a' if append else 'w' with codecs.open(text_file, mode, encoding='utf-8') as fid: if mode == 'w': fid.write(header) fid.write('.. include:: headings.inc\n\n') fid.write(text) # ----------------------------------------------------------------------- # def chopDescription(text): """ Given the (possibly multiline) input text, this function will get the first non-blank line up to the next newline character. :param string `text`: any string. :rtype: `string` """ description = '' for line in text.splitlines(): line = line.strip() if not line or line.startswith('..') or line.startswith('|'): continue description = line break return description # ----------------------------------------------------------------------- # class PickleFile(object): """ A class to help simplify loading and saving data to pickle files. """ def __init__(self, fileName): self.fileName = fileName def __enter__(self): self.read() return self def __exit__(self, exc_type, exc_val, exc_tb): self.write(self.items) def read(self): if os.path.isfile(self.fileName): with open(self.fileName, 'rb') as fid: items = pickle.load(fid) else: items = {} self.items = items return items def write(self, items): with open(self.fileName, 'wb') as fid: pickle.dump(items, fid) # ----------------------------------------------------------------------- # def pickleItem(description, current_module, name, kind): """ This function pickles/unpickles a dictionary containing class names as keys and class brief description (chopped docstrings) as values to build the main class index for Sphinx **or** the Phoenix standalone function names as keys and their full description as values to build the function page. This step is necessary as the function names/description do not come out in alphabetical order from the ``etg`` process. :param string `description`: the function/class description. :param string `current_module`: the harmonized module name for this class or function (see ``MODULENAME_REPLACE`` in `sphinxtools/constants.py`). :param string `name`: the function/class name. :param string `kind`: can be `function` or `class`. """ if kind == 'function': pickle_file = os.path.join(SPHINXROOT, current_module + 'functions.pkl') else: pickle_file = os.path.join(SPHINXROOT, current_module + '1moduleindex.pkl') with PickleFile(pickle_file) as pf: pf.items[name] = description # ----------------------------------------------------------------------- # def pickleClassInfo(class_name, element, short_description): """ Saves some information about a class in a pickle-compatible file., i.e. the list of methods in that class and its super-classes. :param string `class_name`: the name of the class we want to pickle; :param xml.etree.ElementTree.Element `element`: the XML element we want to examine; :param string `short_description`: the class short description (if any). """ method_list, bases = [], [] for method, description in element.method_list: method_list.append(method) for base in element.bases: bases.append(wx2Sphinx(base)[1]) pickle_file = os.path.join(SPHINXROOT, 'class_summary.pkl') with PickleFile(pickle_file) as pf: pf.items[class_name] = (method_list, bases, short_description) # ----------------------------------------------------------------------- # def pickleFunctionInfo(fullname, short_description): """ Saves the short description for each function, used for generating the summary pages later. """ pickle_file = os.path.join(SPHINXROOT, 'function_summary.pkl') with PickleFile(pickle_file) as pf: pf.items[fullname] = short_description # ----------------------------------------------------------------------- # def wx2Sphinx(name): """ Converts a wxWidgets specific string into a Phoenix-ReST-ready string. :param string `name`: any string. """ from etgtools.tweaker_tools import removeWxPrefix if '<' in name: name = name[0:name.index('<')] name = name.strip() newname = fullname = removeWxPrefix(name) if '.' in newname and len(newname) > 3: lookup, remainder = newname.split('.') remainder = '.%s' % remainder else: lookup = newname remainder = '' from etgtools.item_module_map import ItemModuleMap imm = ItemModuleMap() if lookup in imm: fullname = imm[lookup] + lookup + remainder return newname, fullname # ----------------------------------------------------------------------- # RAW_1 = """ %s.. raw:: html %s
""" def formatContributedSnippets(kind, contrib_snippets): """ This method will include and properly ReST-ify contributed snippets of wxPython code (at the moment only 2 snippets are available), by including the Python code into the ReST files and allowing the user to show/hide the snippets using a JavaScript "Accordion" script thanks to the ``.. raw::`` directive (default for snippets is to be hidden). :param string `kind`: can be "method", "function" or "class" depending on the current item being scanned by the `sphinxgenerator.py` tool; :param list `contrib_snippets`: a list of file names (with the ``*.py`` extension) containing the contributed snippets of code. Normally these snippets live in the ``SPHINXROOT/rest_substitutions/snippets/python/contrib`` folder. """ spacer = '' if kind == 'function': spacer = 3*' ' elif kind == 'method': spacer = 6*' ' if kind == 'class': text = TEMPLATE_CONTRIB else: text = '\n' + spacer + '|contributed| **Contributed Examples:**\n\n' for indx, snippet in enumerate(contrib_snippets): with open(snippet, 'rt') as fid: lines = fid.readlines() user = lines[0].replace('##', '').strip() onlyfile = os.path.split(snippet)[1] download = os.path.join(SPHINXROOT, '_downloads', onlyfile) if not os.path.isfile(download): shutil.copyfile(snippet, download) text += RAW_1%(spacer, spacer) text += '\n' + spacer + 'Example %d - %s (:download:`download <_downloads/%s>`):\n\n'%(indx+1, user, onlyfile) text += spacer + '.. literalinclude:: _downloads/%s\n'%onlyfile text += spacer + ' :lines: 2-\n\n' text += RAW_2%(spacer, spacer) text += '\n\n%s|\n\n'%spacer return text def formatExternalLink(fullname, inheritance=False): """ Analyzes the input `fullname` string to check whether a class description is actually coming from an external documentation tool (like http://docs.python.org/library/ or http://docs.scipy.org/doc/numpy/reference/generated/). If the method finds such an external link, the associated inheritance diagram (if `inheritance` is ``True``) or the ``:ref:`` directive are modified accordingly to link it to the correct external documentation. :param string `fullname`: the fully qualified name for a class, method or function, i.e. `exceptions.Exception` or `threading.Thread`; :param bool `inheritance`: ``True`` if the call is coming from :mod:`inheritance`, ``False`` otherwise. """ if fullname.count('.') == 0: if not inheritance: return ':class:`%s`'%fullname return '' parts = fullname.split('.') possible_external = parts[-2] + '.' real_name = '.'.join(parts[-2:]) if possible_external.startswith('_'): # funny ctypes... possible_external = possible_external[1:] real_name = real_name[1:] if possible_external not in EXTERN_INHERITANCE: if not inheritance: return ':class:`%s`'%fullname return '' base_address = EXTERN_INHERITANCE[possible_external] if 'numpy' in real_name: htmlpage = '%s.html#%s'%(real_name.lower(), real_name) else: htmlpage = '%shtml#%s'%(possible_external.lower(), real_name) if inheritance: full_page = '"%s"'%(base_address + htmlpage) else: full_page = '`%s <%s>`_'%(fullname, base_address + htmlpage) return full_page def isPython3(): """ Returns ``True`` if we are using Python 3.x. """ return sys.version_info >= (3, )