diff --git a/IPython/core/completerlib.py b/IPython/core/completerlib.py index 7860cb6..0ca97e7 100644 --- a/IPython/core/completerlib.py +++ b/IPython/core/completerlib.py @@ -154,6 +154,17 @@ def is_importable(module, attr, only_modules): else: return not(attr[:2] == '__' and attr[-2:] == '__') +def is_possible_submodule(module, attr): + try: + obj = getattr(module, attr) + except AttributeError: + # Is possilby an unimported submodule + return True + except TypeError: + # https://github.com/ipython/ipython/issues/9678 + return False + return inspect.ismodule(obj) + def try_import(mod: str, only_modules=False) -> List[str]: """ @@ -172,7 +183,12 @@ def try_import(mod: str, only_modules=False) -> List[str]: completions.extend( [attr for attr in dir(m) if is_importable(m, attr, only_modules)]) - completions.extend(getattr(m, '__all__', [])) + m_all = getattr(m, "__all__", []) + if only_modules: + completions.extend(attr for attr in m_all if is_possible_submodule(m, attr)) + else: + completions.extend(m_all) + if m_is_init: completions.extend(module_list(os.path.dirname(m.__file__))) completions_set = {c for c in completions if isinstance(c, str)} diff --git a/IPython/core/tests/test_completerlib.py b/IPython/core/tests/test_completerlib.py index d112704..f87e784 100644 --- a/IPython/core/tests/test_completerlib.py +++ b/IPython/core/tests/test_completerlib.py @@ -157,6 +157,11 @@ def test_bad_module_all(): nt.assert_in('puppies', results) for r in results: nt.assert_is_instance(r, str) + + # bad_all doesn't contain submodules, but this completion + # should finish without raising an exception: + results = module_completion("import bad_all.") + nt.assert_equal(results, []) finally: sys.path.remove(testsdir) @@ -176,3 +181,14 @@ def test_module_without_init(): assert s == [] finally: sys.path.remove(tmpdir) + + +def test_valid_exported_submodules(): + """ + Test checking exported (__all__) objects are submodules + """ + results = module_completion("import os.pa") + # ensure we get a valid submodule: + nt.assert_in("os.path", results) + # ensure we don't get objects that aren't submodules: + nt.assert_not_in("os.pathconf", results)