Repository navigation
gh-68441: Identify namespace packages in pydoc output #160793
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
| import os | ||
| import sys | ||
| import contextlib | ||
| import importlib.machinery | ||
| import importlib.util | ||
| import inspect | ||
| import io | ||
|
|
@@ -446,6 +447,127 @@ def test_text_doc(self): | |
| (inspect.getabsfile(pydoc_mod),)) | ||
| self.assertEqual(expected_text, result) | ||
|
|
||
| def test_namespace_package_file(self): | ||
| name = 'pydoc_namespace_package' | ||
| with tempfile.TemporaryDirectory() as tmpdir: | ||
| paths = [os.path.join(tmpdir, str(i)) for i in range(2)] | ||
| for path in paths: | ||
| os.makedirs(os.path.join(path, name, 'child')) | ||
| for count in (1, 2): | ||
| with self.subTest(portions=count): | ||
| with (import_helper.DirsOnSysPath(*paths[:count]), | ||
| import_helper.isolated_modules()): | ||
| sys.modules.pop(name, None) | ||
| sys.modules.pop(name + '.child', None) | ||
| for modname in (name, name + '.child'): | ||
| with self.subTest(module=modname): | ||
| module = importlib.import_module(modname) | ||
| self.assertIsNone(module.__file__) | ||
| self.assertEqual(len(module.__path__), count) | ||
| text, _ = get_pydoc_text(module) | ||
| html, _ = get_pydoc_html(module) | ||
| for kind, output, marker in ( | ||
| ('text', text, '\nFILE\n (namespace)'), | ||
| ('html', html, 'index</a><br>(namespace)'), | ||
| ): | ||
| with self.subTest(format=kind): | ||
| self.assertIn(marker, output) | ||
| self.assertNotIn('(built-in)', output) | ||
|
|
||
| def test_virtual_package_file(self): | ||
| name = 'pydoc_virtual_package' | ||
|
|
||
| class Loader: | ||
| def create_module(self, spec): | ||
| return None | ||
|
|
||
| def exec_module(self, module): | ||
| pass | ||
|
|
||
| class Finder: | ||
| def find_spec(self, fullname, path=None, target=None): | ||
| if fullname == name: | ||
| return importlib.util.spec_from_loader( | ||
| fullname, Loader(), is_package=True) | ||
|
|
||
| with (support.swap_attr(sys, 'meta_path', [Finder(), *sys.meta_path]), | ||
| import_helper.isolated_modules()): | ||
| module = importlib.import_module(name) | ||
| self.assertIsNone(module.__spec__.origin) | ||
| self.assertIsInstance(module.__spec__.loader, Loader) | ||
| self.assertNotIn('(namespace)', repr(module)) | ||
| text, _ = get_pydoc_text(module) | ||
| html, _ = get_pydoc_html(module) | ||
| for kind, output, marker in ( | ||
| ('text', text, '\nFILE\n (built-in)'), | ||
| ('html', html, 'index</a><br>(built-in)'), | ||
| ): | ||
| with self.subTest(format=kind): | ||
| self.assertIn(marker, output) | ||
| self.assertNotIn('(namespace)', output) | ||
|
|
||
| def test_namespace_package_custom_path_hook(self): | ||
| name = 'pydoc_hook_namespace' | ||
| path = os.path.abspath('pydoc_namespace_hook_path') | ||
|
|
||
| class Finder: | ||
| def find_spec(self, fullname, target=None): | ||
| if fullname == name: | ||
| spec = importlib.machinery.ModuleSpec( | ||
| fullname, None, is_package=True) | ||
| spec.submodule_search_locations = [path] | ||
| return spec | ||
|
|
||
| def path_hook(entry): | ||
| if entry != path: | ||
| raise ImportError | ||
| return Finder() | ||
|
|
||
| with (support.swap_attr(sys, 'path_hooks', [path_hook, *sys.path_hooks]), | ||
| support.swap_attr(sys, 'path_importer_cache', {}), | ||
| import_helper.DirsOnSysPath(path), | ||
| import_helper.isolated_modules()): | ||
| module = importlib.import_module(name) | ||
| self.assertIsInstance(module.__spec__.loader, | ||
| importlib.machinery.NamespaceLoader) | ||
| text, _ = get_pydoc_text(module) | ||
| html, _ = get_pydoc_html(module) | ||
| for kind, output, marker in ( | ||
| ('text', text, '\nFILE\n (namespace)'), | ||
| ('html', html, 'index</a><br>(namespace)'), | ||
| ): | ||
| with self.subTest(format=kind): | ||
| self.assertIn(marker, output) | ||
| self.assertNotIn('(built-in)', output) | ||
|
|
||
| def test_module_file_classification_controls(self): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The case from my comment on class Loader:
def create_module(self, spec): return None
def exec_module(self, module): pass
spec = importlib.util.spec_from_loader('pydoc_virtual_package', Loader(), is_package=True)
module = importlib.util.module_from_spec(spec)The spec-based check renders it (namespace), the loader check keeps (built-in). Either way an assertion here would make the choice explicit. |
||
| modules = [sys, pydoc_mod, test.test_pydoc] | ||
| module = types.ModuleType('pydoc_originless_module') | ||
| module.__spec__ = importlib.util.spec_from_loader( | ||
| module.__name__, loader=None) | ||
| modules.append(module) | ||
| for origin in (None, 'built-in', 'frozen'): | ||
| module = types.ModuleType('pydoc_fileless_module') | ||
| if origin is not None: | ||
| module.__spec__ = importlib.util.spec_from_loader( | ||
| module.__name__, loader=None, origin=origin, | ||
| is_package=True) | ||
| module.__path__ = [] | ||
| modules.append(module) | ||
| for module in modules: | ||
| with self.subTest(module=module.__name__, spec=module.__spec__): | ||
| text, _ = get_pydoc_text(module) | ||
| html, _ = get_pydoc_html(module) | ||
| self.assertNotIn('(namespace)', text) | ||
| self.assertNotIn('(namespace)', html) | ||
| if getattr(module, '__file__', None) is None: | ||
| self.assertIn('\nFILE\n (built-in)', text) | ||
| self.assertIn('index</a><br>(built-in)', html) | ||
| else: | ||
| filename = inspect.getabsfile(module) | ||
| self.assertIn('\nFILE\n ' + filename, text) | ||
| self.assertIn(urllib.request.pathname2url(filename), html) | ||
|
|
||
| def test_text_enum_member_with_value_zero(self): | ||
| # Test issue #20654 to ensure enum member with value 0 can be | ||
| # displayed. It used to throw KeyError: 'zero'. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| Identify namespace packages correctly instead of labeling them as built-in | ||
| modules in the text and HTML documentation generated by :mod:`pydoc`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
ModuleSpec.origindocs give namespace packages only as an example of when it isNone. It is alsoNonefor packages created by meta path importers that supplies its own loader and no origin.Those are virtual packages whose submodules come from the loader, not from directories, so they are not namespace packages and
repr()does not describe them as one. With this patchhelp()would reportFILE (namespace)for them: no more accurate than today's(built-in), but now a specific claim that is false.Every namespace package the import system creates, including ones from third-party path hooks that return loader-less specs, ends up with a
NamespaceLoader(see_init_module_attrs). Checking the loader would be narrower and consistent withrepr():Is there a case you had in mind where the spec-based rule is needed?