""" sphinx.pycode ~~~~~~~~~~~~~ Utilities parsing and analyzing Python code. :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re from io import StringIO from os import path from zipfile import ZipFile from sphinx.errors import PycodeError from sphinx.pycode.parser import Parser from sphinx.util import get_module_source, detect_encoding if False: # For type annotation from typing import Any, Dict, IO, List, Tuple # NOQA class ModuleAnalyzer: # cache for analyzer objects -- caches both by module and file name cache = {} # type: Dict[Tuple[str, str], Any] @classmethod def for_string(cls, string, modname, srcname=''): # type: (str, str, str) -> ModuleAnalyzer return cls(StringIO(string), modname, srcname, decoded=True) @classmethod def for_file(cls, filename, modname): # type: (str, str) -> ModuleAnalyzer if ('file', filename) in cls.cache: return cls.cache['file', filename] try: with open(filename, 'rb') as f: obj = cls(f, modname, filename) cls.cache['file', filename] = obj except Exception as err: if '.egg' + path.sep in filename: obj = cls.cache['file', filename] = cls.for_egg(filename, modname) else: raise PycodeError('error opening %r' % filename, err) return obj @classmethod def for_egg(cls, filename, modname): # type: (str, str) -> ModuleAnalyzer SEP = re.escape(path.sep) eggpath, relpath = re.split('(?<=\\.egg)' + SEP, filename) try: with ZipFile(eggpath) as egg: code = egg.read(relpath).decode() return cls.for_string(code, modname, filename) except Exception as exc: raise PycodeError('error opening %r' % filename, exc) @classmethod def for_module(cls, modname): # type: (str) -> ModuleAnalyzer if ('module', modname) in cls.cache: entry = cls.cache['module', modname] if isinstance(entry, PycodeError): raise entry return entry try: type, source = get_module_source(modname) if type == 'string': obj = cls.for_string(source, modname) else: obj = cls.for_file(source, modname) except PycodeError as err: cls.cache['module', modname] = err raise cls.cache['module', modname] = obj return obj def __init__(self, source, modname, srcname, decoded=False): # type: (IO, str, str, bool) -> None self.modname = modname # name of the module self.srcname = srcname # name of the source file # cache the source code as well pos = source.tell() if not decoded: self.encoding = detect_encoding(source.readline) source.seek(pos) self.code = source.read().decode(self.encoding) else: self.encoding = None self.code = source.read() # will be filled by parse() self.attr_docs = None # type: Dict[Tuple[str, str], List[str]] self.tagorder = None # type: Dict[str, int] self.tags = None # type: Dict[str, Tuple[str, int, int]] def parse(self): # type: () -> None """Parse the source code.""" try: parser = Parser(self.code, self.encoding) parser.parse() self.attr_docs = {} for (scope, comment) in parser.comments.items(): if comment: self.attr_docs[scope] = comment.splitlines() + [''] else: self.attr_docs[scope] = [''] self.tags = parser.definitions self.tagorder = parser.deforders except Exception as exc: raise PycodeError('parsing %r failed: %r' % (self.srcname, exc)) def find_attr_docs(self): # type: () -> Dict[Tuple[str, str], List[str]] """Find class and module-level attributes and their documentation.""" if self.attr_docs is None: self.parse() return self.attr_docs def find_tags(self): # type: () -> Dict[str, Tuple[str, int, int]] """Find class, function and method definitions and their location.""" if self.tags is None: self.parse() return self.tags