Old get_module_source API restored, new version moved to ModuleAnalyzer class, tests updated

This commit is contained in:
hkm 2019-12-25 22:29:20 +03:00
parent c4e60b5b9c
commit 0a982d5ebd
4 changed files with 81 additions and 24 deletions

View File

@ -11,8 +11,9 @@
import re import re
from io import StringIO from io import StringIO
from os import path from os import path
from typing import Any, Dict, IO, List, Tuple from typing import Any, Dict, IO, List, Tuple, Optional
from zipfile import ZipFile from zipfile import ZipFile
from importlib import import_module
from sphinx.errors import PycodeError from sphinx.errors import PycodeError
from sphinx.pycode.parser import Parser from sphinx.pycode.parser import Parser
@ -23,6 +24,55 @@ class ModuleAnalyzer:
# cache for analyzer objects -- caches both by module and file name # cache for analyzer objects -- caches both by module and file name
cache = {} # type: Dict[Tuple[str, str], Any] cache = {} # type: Dict[Tuple[str, str], Any]
@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
try:
source = loader.get_source(modname)
if source:
# no exception and not None - it must be module source
return filename, source
except ImportError as err:
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))
lfilename = filename.lower()
if lfilename.endswith('.pyo') or lfilename.endswith('.pyc'):
filename = filename[:-1]
if not path.isfile(filename) and path.isfile(filename + 'w'):
filename += 'w'
elif not (lfilename.endswith('.py') or lfilename.endswith('.pyw')):
raise PycodeError('source is not a .py file: %r' % filename)
elif ('.egg' + os.path.sep) in filename:
pat = '(?<=\\.egg)' + re.escape(os.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
@classmethod @classmethod
def for_string(cls, string: str, modname: str, srcname: str = '<string>' def for_string(cls, string: str, modname: str, srcname: str = '<string>'
) -> "ModuleAnalyzer": ) -> "ModuleAnalyzer":
@ -63,7 +113,7 @@ class ModuleAnalyzer:
return entry return entry
try: try:
filename, source = get_module_source(modname) filename, source = cls.get_module_source(modname)
if source is not None: if source is not None:
obj = cls.for_string(source, modname, filename if filename is not None else '<string>') obj = cls.for_string(source, modname, filename if filename is not None else '<string>')
elif filename is not None: elif filename is not None:

View File

@ -25,7 +25,7 @@ from hashlib import md5
from importlib import import_module from importlib import import_module
from os import path from os import path
from time import mktime, strptime from time import mktime, strptime
from typing import Any, Callable, Dict, IO, Iterable, Iterator, List, Pattern, Set, Tuple, Optional from typing import Any, Callable, Dict, IO, Iterable, Iterator, List, Pattern, Set, Tuple
from urllib.parse import urlsplit, urlunsplit, quote_plus, parse_qsl, urlencode from urllib.parse import urlsplit, urlunsplit, quote_plus, parse_qsl, urlencode
from docutils.utils import relative_path from docutils.utils import relative_path
@ -265,35 +265,31 @@ def save_traceback(app: "Sphinx") -> str:
return path return path
def get_module_source(modname: str) -> Tuple[Optional[str], Optional[str]]: def get_module_source(modname: str) -> Tuple[str, str]:
"""Try to find the source code for a module. """Try to find the source code for a module.
Returns ('filename', 'source'). One of it can be None if Can return ('file', 'filename') in which case the source is in the given
no filename or source found file, or ('string', 'source') which which case the source is the string.
""" """
try: try:
mod = import_module(modname) mod = import_module(modname)
except Exception as err: except Exception as err:
raise PycodeError('error importing %r' % modname, err) raise PycodeError('error importing %r' % modname, err)
loader = getattr(mod, '__loader__', None)
filename = getattr(mod, '__file__', None) filename = getattr(mod, '__file__', None)
if loader and getattr(loader, 'get_source', None): loader = getattr(mod, '__loader__', None)
# prefer Native loader, as it respects #coding directive if loader and getattr(loader, 'get_filename', None):
try:
source = loader.get_source(modname)
if source:
# no exception and not None - it must be module source
return filename, source
except ImportError as err:
pass # Try other "source-mining" methods
if filename is None and loader and getattr(loader, 'get_filename', None):
# have loader, but no filename
try: try:
filename = loader.get_filename(modname) filename = loader.get_filename(modname)
except ImportError as err: except Exception as err:
raise PycodeError('error getting filename for %r' % modname, err) raise PycodeError('error getting filename for %r' % filename, err)
if filename is None and loader:
try:
filename = loader.get_source(modname)
if filename:
return 'string', filename
except Exception as err:
raise PycodeError('error getting source for %r' % modname, err)
if filename is None: if filename is None:
# all methods for getting filename failed, so raise...
raise PycodeError('no source found for module %r' % modname) raise PycodeError('no source found for module %r' % modname)
filename = path.normpath(path.abspath(filename)) filename = path.normpath(path.abspath(filename))
lfilename = filename.lower() lfilename = filename.lower()
@ -307,11 +303,11 @@ def get_module_source(modname: str) -> Tuple[Optional[str], Optional[str]]:
pat = '(?<=\\.egg)' + re.escape(os.path.sep) pat = '(?<=\\.egg)' + re.escape(os.path.sep)
eggpath, _ = re.split(pat, filename, 1) eggpath, _ = re.split(pat, filename, 1)
if path.isfile(eggpath): if path.isfile(eggpath):
return filename, None return 'file', filename
if not path.isfile(filename): if not path.isfile(filename):
raise PycodeError('source file is not present: %r' % filename) raise PycodeError('source file is not present: %r' % filename)
return filename, None return 'file', filename
def get_full_modname(modname: str, attribute: str) -> str: def get_full_modname(modname: str, attribute: str) -> str:

View File

@ -10,12 +10,23 @@
import os import os
import sys import sys
import pytest
import sphinx import sphinx
from sphinx.pycode import ModuleAnalyzer from sphinx.pycode import ModuleAnalyzer
from sphinx.errors import PycodeError
SPHINX_MODULE_PATH = os.path.splitext(sphinx.__file__)[0] + '.py' SPHINX_MODULE_PATH = os.path.splitext(sphinx.__file__)[0] + '.py'
def test_ModuleAnalyzer_get_module_source():
assert ModuleAnalyzer.get_module_source('sphinx') == (sphinx.__file__, sphinx.__loader__.get_source('sphinx'))
# failed to obtain source information from builtin modules
with pytest.raises(PycodeError):
ModuleAnalyzer.get_module_source('builtins')
with pytest.raises(PycodeError):
ModuleAnalyzer.get_module_source('itertools')
def test_ModuleAnalyzer_for_string(): def test_ModuleAnalyzer_for_string():
analyzer = ModuleAnalyzer.for_string('print("Hello world")', 'module_name') analyzer = ModuleAnalyzer.for_string('print("Hello world")', 'module_name')

View File

@ -62,7 +62,7 @@ def test_display_chunk():
def test_get_module_source(): def test_get_module_source():
assert get_module_source('sphinx') == (sphinx.__file__, sphinx.__loader__.get_source('sphinx')) assert get_module_source('sphinx') == ('file', sphinx.__file__)
# failed to obtain source information from builtin modules # failed to obtain source information from builtin modules
with pytest.raises(PycodeError): with pytest.raises(PycodeError):