From 9676e79c1cece0d76aa78fc3f00ea55e17f34434 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Tue, 22 Nov 2016 00:00:50 +0900 Subject: [PATCH 1/2] inheritance_diagram: Move _import_class_or_module() method to function --- sphinx/ext/inheritance_diagram.py | 93 ++++++++++--------- .../example/__init__.py | 1 + .../example/sphinx.py | 5 + tests/test_ext_inheritance_diagram.py | 47 +++++++++- 4 files changed, 99 insertions(+), 47 deletions(-) create mode 100644 tests/roots/test-ext-inheritance_diagram/example/__init__.py create mode 100644 tests/roots/test-ext-inheritance_diagram/example/sphinx.py diff --git a/sphinx/ext/inheritance_diagram.py b/sphinx/ext/inheritance_diagram.py index a06c4b17c3..e29d849e99 100644 --- a/sphinx/ext/inheritance_diagram.py +++ b/sphinx/ext/inheritance_diagram.py @@ -63,6 +63,52 @@ class_sig_re = re.compile(r'''^([\w.]*\.)? # module names ''', re.VERBOSE) +def import_classes(name, currmodule): + """Import a class using its fully-qualified *name*.""" + try: + path, base = class_sig_re.match(name).groups() + except (AttributeError, ValueError): + raise InheritanceException('Invalid class or module %r specified ' + 'for inheritance diagram' % name) + + fullname = (path or '') + base + path = (path and path.rstrip('.') or '') + + # two possibilities: either it is a module, then import it + try: + __import__(fullname) + todoc = sys.modules[fullname] + except ImportError: + # else it is a class, then import the module + if not path: + if currmodule: + # try the current module + path = currmodule + else: + raise InheritanceException( + 'Could not import class %r specified for ' + 'inheritance diagram' % base) + try: + __import__(path) + todoc = getattr(sys.modules[path], base) + except (ImportError, AttributeError): + raise InheritanceException( + 'Could not import class or module %r specified for ' + 'inheritance diagram' % (path + '.' + base)) + + # If a class, just return it + if inspect.isclass(todoc): + return [todoc] + elif inspect.ismodule(todoc): + classes = [] + for cls in todoc.__dict__.values(): + if inspect.isclass(cls) and cls.__module__ == todoc.__name__: + classes.append(cls) + return classes + raise InheritanceException('%r specified for inheritance diagram is ' + 'not a class or module' % name) + + class InheritanceException(Exception): pass @@ -88,56 +134,11 @@ class InheritanceGraph(object): raise InheritanceException('No classes found for ' 'inheritance diagram') - def _import_class_or_module(self, name, currmodule): - """Import a class using its fully-qualified *name*.""" - try: - path, base = class_sig_re.match(name).groups() - except (AttributeError, ValueError): - raise InheritanceException('Invalid class or module %r specified ' - 'for inheritance diagram' % name) - - fullname = (path or '') + base - path = (path and path.rstrip('.') or '') - - # two possibilities: either it is a module, then import it - try: - __import__(fullname) - todoc = sys.modules[fullname] - except ImportError: - # else it is a class, then import the module - if not path: - if currmodule: - # try the current module - path = currmodule - else: - raise InheritanceException( - 'Could not import class %r specified for ' - 'inheritance diagram' % base) - try: - __import__(path) - todoc = getattr(sys.modules[path], base) - except (ImportError, AttributeError): - raise InheritanceException( - 'Could not import class or module %r specified for ' - 'inheritance diagram' % (path + '.' + base)) - - # If a class, just return it - if inspect.isclass(todoc): - return [todoc] - elif inspect.ismodule(todoc): - classes = [] - for cls in todoc.__dict__.values(): - if inspect.isclass(cls) and cls.__module__ == todoc.__name__: - classes.append(cls) - return classes - raise InheritanceException('%r specified for inheritance diagram is ' - 'not a class or module' % name) - def _import_classes(self, class_names, currmodule): """Import a list of classes.""" classes = [] for name in class_names: - classes.extend(self._import_class_or_module(name, currmodule)) + classes.extend(import_classes(name, currmodule)) return classes def _class_info(self, classes, show_builtins, private_bases, parts): diff --git a/tests/roots/test-ext-inheritance_diagram/example/__init__.py b/tests/roots/test-ext-inheritance_diagram/example/__init__.py new file mode 100644 index 0000000000..2f85c08762 --- /dev/null +++ b/tests/roots/test-ext-inheritance_diagram/example/__init__.py @@ -0,0 +1 @@ +# example.py diff --git a/tests/roots/test-ext-inheritance_diagram/example/sphinx.py b/tests/roots/test-ext-inheritance_diagram/example/sphinx.py new file mode 100644 index 0000000000..5eb8a2291d --- /dev/null +++ b/tests/roots/test-ext-inheritance_diagram/example/sphinx.py @@ -0,0 +1,5 @@ +# example.sphinx + + +class DummyClass(object): + pass diff --git a/tests/test_ext_inheritance_diagram.py b/tests/test_ext_inheritance_diagram.py index 64446eed89..e641cc0de2 100644 --- a/tests/test_ext_inheritance_diagram.py +++ b/tests/test_ext_inheritance_diagram.py @@ -9,9 +9,54 @@ :license: BSD, see LICENSE for details. """ -from util import with_app +import sys +from util import with_app, rootdir, raises +from sphinx.ext.inheritance_diagram import InheritanceException, import_classes @with_app('html', testroot='ext-inheritance_diagram') def test_inheritance_diagram_html(app, status, warning): app.builder.build_all() + + +def test_import_classes(): + from sphinx.application import Sphinx, TemplateBridge + + try: + sys.path.append(rootdir / 'roots/test-ext-inheritance_diagram') + + # got exception for unknown class or module + raises(InheritanceException, import_classes, 'unknown', None) + raises(InheritanceException, import_classes, 'unknown.Unknown', None) + + # a module having no classes + classes = import_classes('sphinx', None) + assert classes == [] + + classes = import_classes('sphinx', 'foo') + assert classes == [] + + # all of classes in the module + classes = import_classes('sphinx.application', None) + assert set(classes) == set([Sphinx, TemplateBridge]) + + # specified class in the module + classes = import_classes('sphinx.application.Sphinx', None) + assert classes == [Sphinx] + + # specified class in current module + classes = import_classes('Sphinx', 'sphinx.application') + assert classes == [Sphinx] + + # ignore current module if name include the module name + raises(InheritanceException, import_classes, 'i18n.CatalogInfo', 'sphinx.util') + + # got exception for functions + raises(InheritanceException, import_classes, 'encode_uri', 'sphinx.util') + + # try to load example.sphinx, but inheritance_diagram imports sphinx instead + # refs: #3164 + classes = import_classes('sphinx', 'example') + assert classes == [] + finally: + sys.path.pop() From 52f54a637903a60cbda4074a2c903ed2b4528689 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Tue, 22 Nov 2016 16:32:03 +0900 Subject: [PATCH 2/2] Fix #3164: Change search order of ``sphinx.ext.inheritance_diagram`` --- CHANGES | 1 + sphinx/ext/inheritance_diagram.py | 80 ++++++++++++++------------- tests/test_ext_inheritance_diagram.py | 12 ++-- 3 files changed, 51 insertions(+), 42 deletions(-) diff --git a/CHANGES b/CHANGES index 9c6ca126bf..582f7a301e 100644 --- a/CHANGES +++ b/CHANGES @@ -15,6 +15,7 @@ Bugs fixed * #3093: gettext build broken on image node under ``note`` directive. * imgmath: crashes on showing error messages if image generation failed * #3117: LaTeX writer crashes if admonition is placed before first section title +* #3164: Change search order of ``sphinx.ext.inheritance_diagram`` Release 1.4.8 (released Oct 1, 2016) ==================================== diff --git a/sphinx/ext/inheritance_diagram.py b/sphinx/ext/inheritance_diagram.py index e29d849e99..eec56dc86a 100644 --- a/sphinx/ext/inheritance_diagram.py +++ b/sphinx/ext/inheritance_diagram.py @@ -58,51 +58,57 @@ from sphinx.util import force_decode from sphinx.util.compat import Directive -class_sig_re = re.compile(r'''^([\w.]*\.)? # module names - (\w+) \s* $ # class/final module name - ''', re.VERBOSE) +module_sig_re = re.compile(r'''^(?:([\w.]*)\.)? # module names + (\w+) \s* $ # class/final module name + ''', re.VERBOSE) + + +def try_import(objname): + """Import a object or module using *name* and *currentmodule*. + *name* should be a relative name from *currentmodule* or + a fully-qualified name. + + Returns imported object or module. If failed, returns None value. + """ + try: + __import__(objname) + return sys.modules.get(objname) + except ImportError: + modname, attrname = module_sig_re.match(objname).groups() + if modname is None: + return None + try: + __import__(modname) + return getattr(sys.modules.get(modname), attrname, None) + except ImportError: + return None def import_classes(name, currmodule): """Import a class using its fully-qualified *name*.""" - try: - path, base = class_sig_re.match(name).groups() - except (AttributeError, ValueError): - raise InheritanceException('Invalid class or module %r specified ' - 'for inheritance diagram' % name) + target = None - fullname = (path or '') + base - path = (path and path.rstrip('.') or '') + # import class or module using currmodule + if currmodule: + target = try_import(currmodule + '.' + name) - # two possibilities: either it is a module, then import it - try: - __import__(fullname) - todoc = sys.modules[fullname] - except ImportError: - # else it is a class, then import the module - if not path: - if currmodule: - # try the current module - path = currmodule - else: - raise InheritanceException( - 'Could not import class %r specified for ' - 'inheritance diagram' % base) - try: - __import__(path) - todoc = getattr(sys.modules[path], base) - except (ImportError, AttributeError): - raise InheritanceException( - 'Could not import class or module %r specified for ' - 'inheritance diagram' % (path + '.' + base)) + # import class or module without currmodule + if target is None: + target = try_import(name) - # If a class, just return it - if inspect.isclass(todoc): - return [todoc] - elif inspect.ismodule(todoc): + if target is None: + raise InheritanceException( + 'Could not import class or module %r specified for ' + 'inheritance diagram' % name) + + if inspect.isclass(target): + # If imported object is a class, just return it + return [target] + elif inspect.ismodule(target): + # If imported object is a module, return classes defined on it classes = [] - for cls in todoc.__dict__.values(): - if inspect.isclass(cls) and cls.__module__ == todoc.__name__: + for cls in target.__dict__.values(): + if inspect.isclass(cls) and cls.__module__ == target.__name__: classes.append(cls) return classes raise InheritanceException('%r specified for inheritance diagram is ' diff --git a/tests/test_ext_inheritance_diagram.py b/tests/test_ext_inheritance_diagram.py index e641cc0de2..ba912254a8 100644 --- a/tests/test_ext_inheritance_diagram.py +++ b/tests/test_ext_inheritance_diagram.py @@ -21,9 +21,11 @@ def test_inheritance_diagram_html(app, status, warning): def test_import_classes(): from sphinx.application import Sphinx, TemplateBridge + from sphinx.util.i18n import CatalogInfo try: sys.path.append(rootdir / 'roots/test-ext-inheritance_diagram') + from example.sphinx import DummyClass # got exception for unknown class or module raises(InheritanceException, import_classes, 'unknown', None) @@ -48,15 +50,15 @@ def test_import_classes(): classes = import_classes('Sphinx', 'sphinx.application') assert classes == [Sphinx] - # ignore current module if name include the module name - raises(InheritanceException, import_classes, 'i18n.CatalogInfo', 'sphinx.util') + # relative module name to current module + classes = import_classes('i18n.CatalogInfo', 'sphinx.util') + assert classes == [CatalogInfo] # got exception for functions raises(InheritanceException, import_classes, 'encode_uri', 'sphinx.util') - # try to load example.sphinx, but inheritance_diagram imports sphinx instead - # refs: #3164 + # import submodule on current module (refs: #3164) classes = import_classes('sphinx', 'example') - assert classes == [] + assert classes == [DummyClass] finally: sys.path.pop()