sphinx/sphinx/pycode/__init__.py

176 lines
6.6 KiB
Python
Raw Normal View History

2008-12-29 13:22:18 -06:00
"""
sphinx.pycode
~~~~~~~~~~~~~
Utilities parsing and analyzing Python code.
2019-01-02 01:00:30 -06:00
:copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS.
2008-12-29 13:22:18 -06:00
:license: BSD, see LICENSE for details.
"""
import re
from io import StringIO
from os import path
from typing import Any, Dict, IO, List, Tuple, Optional
from zipfile import ZipFile
from importlib import import_module
2010-01-13 16:43:43 -06:00
from sphinx.errors import PycodeError
2017-07-09 11:20:19 -05:00
from sphinx.pycode.parser import Parser
2019-12-26 10:28:16 -06:00
from sphinx.util import detect_encoding
2008-12-29 13:22:18 -06:00
class ModuleAnalyzer:
# cache for analyzer objects -- caches both by module and file name
2018-12-14 12:14:11 -06:00
cache = {} # type: Dict[Tuple[str, str], Any]
2008-12-29 13:22:18 -06:00
@staticmethod
def get_module_source(modname: str) -> Tuple[Optional[str], Optional[str]]:
"""Try to find the source code for a module.
Returns ('filename', 'source'). One of it can be None if
no filename or source found
"""
try:
mod = import_module(modname)
except Exception as err:
raise PycodeError('error importing %r' % modname, err)
loader = getattr(mod, '__loader__', None)
filename = getattr(mod, '__file__', None)
if loader and getattr(loader, 'get_source', None):
# prefer Native loader, as it respects #coding directive
2019-12-26 10:28:16 -06:00
try:
source = loader.get_source(modname)
if source:
# no exception and not None - it must be module source
return filename, source
2019-12-26 10:28:16 -06:00
except ImportError:
pass # Try other "source-mining" methods
if filename is None and loader and getattr(loader, 'get_filename', None):
# have loader, but no filename
try:
filename = loader.get_filename(modname)
except ImportError as err:
raise PycodeError('error getting filename for %r' % modname, err)
if filename is None:
# all methods for getting filename failed, so raise...
raise PycodeError('no source found for module %r' % modname)
filename = path.normpath(path.abspath(filename))
if filename.lower().endswith(('.pyo', '.pyc')):
filename = filename[:-1]
if not path.isfile(filename) and path.isfile(filename + 'w'):
filename += 'w'
elif not filename.lower().endswith(('.py', '.pyw')):
raise PycodeError('source is not a .py file: %r' % filename)
2019-12-26 10:28:16 -06:00
elif ('.egg' + path.sep) in filename:
pat = '(?<=\\.egg)' + re.escape(path.sep)
eggpath, _ = re.split(pat, filename, 1)
if path.isfile(eggpath):
return filename, None
if not path.isfile(filename):
raise PycodeError('source file is not present: %r' % filename)
return filename, None
2019-12-26 10:28:16 -06:00
2008-12-29 13:22:18 -06:00
@classmethod
def for_string(cls, string: str, modname: str, srcname: str = '<string>'
) -> "ModuleAnalyzer":
2018-10-15 12:17:32 -05:00
return cls(StringIO(string), modname, srcname, decoded=True)
2008-12-29 13:22:18 -06:00
@classmethod
def for_file(cls, filename: str, modname: str) -> "ModuleAnalyzer":
if ('file', filename) in cls.cache:
return cls.cache['file', filename]
2008-12-29 13:22:18 -06:00
try:
2017-12-15 06:11:38 -06:00
with open(filename, 'rb') as f:
2018-10-15 12:17:32 -05:00
obj = cls(f, modname, filename)
2017-12-15 06:11:38 -06:00
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
2008-12-29 13:22:18 -06:00
@classmethod
def for_egg(cls, filename: str, modname: 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)
2008-12-29 13:22:18 -06:00
@classmethod
def for_module(cls, modname: str) -> "ModuleAnalyzer":
if ('module', modname) in cls.cache:
entry = cls.cache['module', modname]
if isinstance(entry, PycodeError):
raise entry
return entry
try:
filename, source = cls.get_module_source(modname)
if source is not None:
obj = cls.for_string(source, modname, filename if filename is not None else '<string>')
elif filename is not None:
obj = cls.for_file(filename, modname)
except PycodeError as err:
cls.cache['module', modname] = err
raise
cls.cache['module', modname] = obj
return obj
2008-12-29 13:22:18 -06:00
def __init__(self, source: IO, modname: str, srcname: str, decoded: bool = False) -> None:
2017-07-09 11:20:19 -05:00
self.modname = modname # name of the module
self.srcname = srcname # name of the source file
2010-01-13 16:43:43 -06:00
# cache the source code as well
2017-07-09 11:20:19 -05:00
pos = source.tell()
if not decoded:
2017-07-09 11:20:19 -05:00
self.encoding = detect_encoding(source.readline)
source.seek(pos)
self.code = source.read().decode(self.encoding)
else:
self.encoding = None
2017-07-09 11:20:19 -05:00
self.code = source.read()
2010-01-13 16:43:43 -06:00
# will be filled by parse()
2018-12-14 12:14:11 -06:00
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) -> None:
2017-07-09 11:20:19 -05:00
"""Parse the source code."""
2009-01-10 13:34:26 -06:00
try:
2017-07-09 11:20:19 -05:00
parser = Parser(self.code, self.encoding)
parser.parse()
self.attr_docs = {}
for (scope, comment) in parser.comments.items():
2017-07-09 11:20:19 -05:00
if comment:
self.attr_docs[scope] = comment.splitlines() + ['']
else:
self.attr_docs[scope] = ['']
2017-07-09 11:20:19 -05:00
self.tags = parser.definitions
self.tagorder = parser.deforders
except Exception as exc:
raise PycodeError('parsing %r failed: %r' % (self.srcname, exc))
2017-07-09 11:20:19 -05:00
def find_attr_docs(self) -> Dict[Tuple[str, str], List[str]]:
"""Find class and module-level attributes and their documentation."""
2017-07-09 11:20:19 -05:00
if self.attr_docs is None:
self.parse()
return self.attr_docs
2008-12-29 13:22:18 -06:00
def find_tags(self) -> Dict[str, Tuple[str, int, int]]:
"""Find class, function and method definitions and their location."""
2017-07-09 11:20:19 -05:00
if self.tags is None:
self.parse()
2017-07-09 11:20:19 -05:00
return self.tags