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-12-31 20:15:42 -06:00
:copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS.
2008-12-29 13:22:18 -06:00
:license: BSD, see LICENSE for details.
"""
import re
import tokenize
2019-12-26 10:37:13 -06:00
from importlib import import_module
2020-05-24 05:18:21 -05:00
from inspect import Signature
from io import StringIO
from os import path
from typing import Any, Dict, IO, List, Tuple, Optional
from zipfile import ZipFile
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
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) from 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) from 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":
return cls(StringIO(string), modname, srcname)
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:
with tokenize.open(filename) as f:
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) from 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) from 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:
2019-12-26 10:37:13 -06:00
obj = cls.for_string(source, modname, filename or '<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) -> 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
self.code = source.read()
2010-01-13 16:43:43 -06:00
# will be filled by parse()
self.annotations = None # type: Dict[Tuple[str, str], str]
self.attr_docs = None # type: Dict[Tuple[str, str], List[str]]
2020-04-26 07:42:55 -05:00
self.finals = None # type: List[str]
2020-05-24 05:18:21 -05:00
self.overloads = None # type: Dict[str, List[Signature]]
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:
parser = Parser(self.code)
2017-07-09 11:20:19 -05:00
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] = ['']
self.annotations = parser.annotations
2020-04-26 07:42:55 -05:00
self.finals = parser.finals
2020-05-24 05:18:21 -05:00
self.overloads = parser.overloads
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)) from 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