From 35e1764025447b8e863a68c65a666836b5099a9d Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Thu, 6 Sep 2018 21:30:56 +0900 Subject: [PATCH] Fix #5290: autodoc: failed to analyze source code in egg package --- CHANGES | 1 + sphinx/pycode/__init__.py | 19 ++++++++++++- sphinx/util/__init__.py | 5 ++++ tests/roots/test-pycode-egg/conf.py | 8 ++++++ tests/roots/test-pycode-egg/index.rst | 2 ++ .../test-pycode-egg/sample-0.0.0-py3.7.egg | Bin 0 -> 1365 bytes tests/roots/test-pycode-egg/src/sample.py | 8 ++++++ tests/roots/test-pycode-egg/src/setup.py | 6 ++++ tests/test_autodoc.py | 23 ++++++++++++++++ tests/test_pycode.py | 26 ++++++++++++++++++ 10 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 tests/roots/test-pycode-egg/conf.py create mode 100644 tests/roots/test-pycode-egg/index.rst create mode 100644 tests/roots/test-pycode-egg/sample-0.0.0-py3.7.egg create mode 100644 tests/roots/test-pycode-egg/src/sample.py create mode 100644 tests/roots/test-pycode-egg/src/setup.py diff --git a/CHANGES b/CHANGES index 27d3d4ab9..301e5b2f6 100644 --- a/CHANGES +++ b/CHANGES @@ -38,6 +38,7 @@ Bugs fixed * autodoc: ImportError is replaced by AttributeError for deeper module * #2720, #4034: Incorrect links with ``:download:``, duplicate names, and parallel builds +* #5290: autodoc: failed to analyze source code in egg package Testing -------- diff --git a/sphinx/pycode/__init__.py b/sphinx/pycode/__init__.py index c4c055bf5..a97903a27 100644 --- a/sphinx/pycode/__init__.py +++ b/sphinx/pycode/__init__.py @@ -10,6 +10,9 @@ """ from __future__ import print_function +import re +from zipfile import ZipFile + from six import iteritems, BytesIO, StringIO from sphinx.errors import PycodeError @@ -42,9 +45,23 @@ class ModuleAnalyzer(object): obj = cls(f, modname, filename) # type: ignore cls.cache['file', filename] = obj except Exception as err: - raise PycodeError('error opening %r' % filename, err) + if '.egg/' 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: (unicode, unicode) -> ModuleAnalyzer + eggpath, relpath = re.split('(?<=\\.egg)/', filename) + try: + with ZipFile(eggpath) as egg: + code = egg.read(relpath).decode('utf-8') + 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 diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py index 29038978a..0c0c22c9c 100644 --- a/sphinx/util/__init__.py +++ b/sphinx/util/__init__.py @@ -314,6 +314,11 @@ def get_module_source(modname): filename += 'w' elif not (lfilename.endswith('.py') or lfilename.endswith('.pyw')): raise PycodeError('source is not a .py file: %r' % filename) + elif '.egg' in filename: + eggpath, _ = re.split('(?<=\\.egg)/', filename) + if path.isfile(eggpath): + return 'file', filename + if not path.isfile(filename): raise PycodeError('source file is not present: %r' % filename) return 'file', filename diff --git a/tests/roots/test-pycode-egg/conf.py b/tests/roots/test-pycode-egg/conf.py new file mode 100644 index 000000000..a8e25882b --- /dev/null +++ b/tests/roots/test-pycode-egg/conf.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +import os +import sys + +sys.path.insert(0, os.path.abspath('sample-0.0.0-py3.7.egg')) +master_doc = 'index' +extensions = ['sphinx.ext.autodoc'] diff --git a/tests/roots/test-pycode-egg/index.rst b/tests/roots/test-pycode-egg/index.rst new file mode 100644 index 000000000..affc7912a --- /dev/null +++ b/tests/roots/test-pycode-egg/index.rst @@ -0,0 +1,2 @@ +test-pycode-egg +=============== diff --git a/tests/roots/test-pycode-egg/sample-0.0.0-py3.7.egg b/tests/roots/test-pycode-egg/sample-0.0.0-py3.7.egg new file mode 100644 index 0000000000000000000000000000000000000000..719dbea5134b3cb53f7641e89600b66c094a2551 GIT binary patch literal 1365 zcmWIWW@Zs#U|`^2Sh!lv*TW+AWD1a%3B;U0T%4F&kdvxcP#GL{;p(MOk8}Pf&vGvN8m~ow6M0lu6g0*OUUC zumy+(f!NjEUDwmk&0jyj8^ZYPtLy3GspENt*IQTX+?n&6gAA@1KltQ*#&>IwhTj?8 z6P{0-PD{LCR17c3%*#s(TsqZh#r*B_>vzaccdyG^mFJjLrR8*D;}#X={Ty@G&z?1V z#>{4cuP1%{&T6||dNwI)PRx|Z0H$rcY7!GBM@*U)Kc!oM5zUSIo3CsL1iGyYh(Un} zcVn=BXppmOuwF?;i8k0NSG89>(el&P)MYSMa9a8D<*u2PnLmFq-g~!eUR~Zl3891k z7CN3=cd$xTR8^u#b0zD_O3js(C#Qb2oLPDEXoTq0C*nt*FPXOVSwjjVYA{V^yKck` zw3-o!Ws#kbl3I|Omy(*7Tp6E}nU`G*a?4{TG!ML==WPIrXLcYKL)KlAUl5;@T9%rF z@WNAc&B#`x#86dcfo^ePS}KNhbE~$*p8;CK2Xw6>kdBWps7y{w&Pa`q*M}yx4tOD=x&^l1`9Q{BzZi!&X588QncFeR}o=!r21Y4QZ}X!dCO=<)>iXr4L9 zGfPKbvhmQ#o`9nYvm`s_CQ5lVoXVCsA~8!+vT;_U)Qmr?ENnn2P&B05R5E{~m`Lig zsSJ`mN3|upA{wmfUaCz^d$#0-`qC7!=`WtDsXZ4De<3CHLQ?8wSj3E@vuD0ozHIqx zarI|nPnNtA4Y`+emSN^GzD*N2-?S*1DZE^MQsn5vH}Az|9=#A}5@VF^bJ@Vu87*VB z^QY!g%avt127h(V07IUUNrV}9UIT^)7%XW7QSh9HtPNYffM{Z1Skl-HWWrM*ESI2b zM^CZ{?fSq-fon%f!009*M>{CR!N8KnO~@u-OMmD_p(j6tQ6G?vLP?D1W*|EUlnP;B qNuw?kl80bv5?wobl0;~a2WlsnKm)v4*+5#@f$%EO%~_zl$N&J#W1`6b literal 0 HcmV?d00001 diff --git a/tests/roots/test-pycode-egg/src/sample.py b/tests/roots/test-pycode-egg/src/sample.py new file mode 100644 index 000000000..c4d3d61e8 --- /dev/null +++ b/tests/roots/test-pycode-egg/src/sample.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +#: constant on sample.py +CONSTANT = 1 + + +def hello(s): + print('Hello %s' % s) diff --git a/tests/roots/test-pycode-egg/src/setup.py b/tests/roots/test-pycode-egg/src/setup.py new file mode 100644 index 000000000..f23c80a06 --- /dev/null +++ b/tests/roots/test-pycode-egg/src/setup.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from setuptools import setup + + +setup(name='sample', + py_modules=['sample']) diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index cefceb833..e2dc37c56 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -1584,3 +1584,26 @@ def test_autodoc_default_options_with_values(app): assert ' list of weak references to the object (if defined)' not in actual assert ' .. py:method:: CustomIter.snafucate()' not in actual assert ' Makes this snafucated.' not in actual + + +@pytest.mark.sphinx('html', testroot='pycode-egg') +def test_autodoc_for_egged_code(app): + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'module', 'sample', options) + assert list(actual) == [ + '', + '.. py:module:: sample', + '', + '', + '.. py:data:: CONSTANT', + ' :module: sample', + ' :annotation: = 1', + '', + ' constant on sample.py', + ' ', + '', + '.. py:function:: hello(s)', + ' :module: sample', + '' + ] diff --git a/tests/test_pycode.py b/tests/test_pycode.py index b4385e8a6..2eab456bc 100644 --- a/tests/test_pycode.py +++ b/tests/test_pycode.py @@ -10,6 +10,7 @@ """ import os +import sys from six import PY2 @@ -47,6 +48,31 @@ def test_ModuleAnalyzer_for_module(): assert analyzer.encoding == 'utf-8' +def test_ModuleAnalyzer_for_file_in_egg(rootdir): + try: + path = rootdir / 'test-pycode-egg' / 'sample-0.0.0-py3.7.egg' + sys.path.insert(0, path) + + import sample + analyzer = ModuleAnalyzer.for_file(sample.__file__, 'sample') + docs = analyzer.find_attr_docs() + assert docs == {('', 'CONSTANT'): ['constant on sample.py', '']} + finally: + sys.path.pop(0) + + +def test_ModuleAnalyzer_for_module_in_egg(rootdir): + try: + path = rootdir / 'test-pycode-egg' / 'sample-0.0.0-py3.7.egg' + sys.path.insert(0, path) + + analyzer = ModuleAnalyzer.for_module('sample') + docs = analyzer.find_attr_docs() + assert docs == {('', 'CONSTANT'): ['constant on sample.py', '']} + finally: + sys.path.pop(0) + + def test_ModuleAnalyzer_find_tags(): code = ('class Foo(object):\n' # line: 1 ' """class Foo!"""\n'