-
-
Notifications
You must be signed in to change notification settings - Fork 3k
More improvements to stubgen #7951
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
Merged
Merged
Changes from all commits
Commits
Show all changes
38 commits
Select commit
Hold shift + click to select a range
037ce58
Continue operation even if imported module raises SystemExit
JukkaL 86e9ad4
Use separate process for module introspection
JukkaL 9923a7a
Recover from helper process death
JukkaL 0722c7d
Display message when we start processing ASTs
JukkaL db0a8d5
Automatically skip 'setup' modules and other non-library modules
JukkaL ca1923a
Add additional vendored package naming convention
JukkaL 7e10f4c
Fallback to finding module using sys.path if import fails
JukkaL 8e1b5b8
Import all names imported from the current package by default
JukkaL 0728e70
Silently ignore invalid docstring signatures
JukkaL 67e2db4
Also use Python 2 sys.path to find files
JukkaL 2cb948c
Do not generate invalid argument names from docstrings
JukkaL 514ae1d
Be more aggressive about ignoring various dunders
JukkaL b818592
Avoid generating both an attribute and a property definition
JukkaL 6237ec4
Avoid None return type for some methods that should always return a v…
JukkaL b4c8f21
Fix indent of named tuples
JukkaL c0dda3b
Fix some imports from vendorex six.moves
JukkaL 0d6e545
Fall back to Any for a dynamic named tuple definition
JukkaL 97f2bf4
Don't crash on invalid property definition
JukkaL 2affb99
Clear decorators if we skip a function
JukkaL 29acbed
Fix @classmethod without corresponding 'def' in C module
JukkaL 75a094b
Catch more bad types and bad arguments names in C stubs
JukkaL fb95598
Fix stripping of C type names
JukkaL 17440b4
Misc tweaks
JukkaL 7968a6a
Fixes
JukkaL 73fb604
Fix lint
JukkaL 2cc05d5
'None' is a valid type
JukkaL 7e3826c
Update test case
JukkaL e8a1498
Fix Python 3.5
JukkaL 9634aa9
Another Python 3.5 fix
JukkaL 18591d3
Raise exception if subprocess gets stuck
JukkaL 99c6dad
Attempt to fix compiled
JukkaL 78771f4
Attempt to fix compiled
JukkaL 1a098c6
Attempt to fix compiled
JukkaL d35ec7b
Fix unused import
JukkaL d892a7a
Fix test case on Windows
JukkaL 4724d00
Pass sys.path to the worker function
JukkaL 1bc5af7
Respond to feedback
JukkaL fd088b1
Skip test case on Windows
JukkaL File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
"""Basic introspection of modules.""" | ||
|
||
from typing import List, Optional, Union | ||
from types import ModuleType | ||
from multiprocessing import Process, Queue | ||
import importlib | ||
import inspect | ||
import os | ||
import pkgutil | ||
import queue | ||
import sys | ||
|
||
|
||
class ModuleProperties: | ||
def __init__(self, | ||
name: str, | ||
file: Optional[str], | ||
path: Optional[List[str]], | ||
all: Optional[List[str]], | ||
is_c_module: bool, | ||
subpackages: List[str]) -> None: | ||
self.name = name # __name__ attribute | ||
self.file = file # __file__ attribute | ||
self.path = path # __path__ attribute | ||
self.all = all # __all__ attribute | ||
self.is_c_module = is_c_module | ||
self.subpackages = subpackages | ||
|
||
|
||
def is_c_module(module: ModuleType) -> bool: | ||
if module.__dict__.get('__file__') is None: | ||
# Could be a namespace package. These must be handled through | ||
# introspection, since there is no source file. | ||
return True | ||
return os.path.splitext(module.__dict__['__file__'])[-1] in ['.so', '.pyd'] | ||
|
||
|
||
class InspectError(Exception): | ||
pass | ||
|
||
|
||
def get_package_properties(package_id: str) -> ModuleProperties: | ||
"""Use runtime introspection to get information about a module/package.""" | ||
try: | ||
package = importlib.import_module(package_id) | ||
except BaseException as e: | ||
raise InspectError(str(e)) | ||
name = getattr(package, '__name__', None) | ||
file = getattr(package, '__file__', None) | ||
path = getattr(package, '__path__', None) # type: Optional[List[str]] | ||
if not isinstance(path, list): | ||
path = None | ||
pkg_all = getattr(package, '__all__', None) | ||
if pkg_all is not None: | ||
try: | ||
pkg_all = list(pkg_all) | ||
except Exception: | ||
pkg_all = None | ||
is_c = is_c_module(package) | ||
|
||
if path is None: | ||
# Object has no path; this means it's either a module inside a package | ||
# (and thus no sub-packages), or it could be a C extension package. | ||
if is_c: | ||
# This is a C extension module, now get the list of all sub-packages | ||
# using the inspect module | ||
subpackages = [package.__name__ + "." + name | ||
for name, val in inspect.getmembers(package) | ||
if inspect.ismodule(val) | ||
and val.__name__ == package.__name__ + "." + name] | ||
else: | ||
# It's a module inside a package. There's nothing else to walk/yield. | ||
subpackages = [] | ||
else: | ||
all_packages = pkgutil.walk_packages(path, prefix=package.__name__ + ".", | ||
onerror=lambda r: None) | ||
subpackages = [qualified_name for importer, qualified_name, ispkg in all_packages] | ||
return ModuleProperties(name=name, | ||
file=file, | ||
path=path, | ||
all=pkg_all, | ||
is_c_module=is_c, | ||
subpackages=subpackages) | ||
|
||
|
||
def worker(tasks: 'Queue[str]', | ||
results: 'Queue[Union[str, ModuleProperties]]', | ||
sys_path: List[str]) -> None: | ||
"""The main loop of a worker introspection process.""" | ||
sys.path = sys_path | ||
while True: | ||
mod = tasks.get() | ||
try: | ||
prop = get_package_properties(mod) | ||
except InspectError as e: | ||
results.put(str(e)) | ||
continue | ||
results.put(prop) | ||
|
||
|
||
class ModuleInspect: | ||
"""Perform runtime introspection of modules in a separate process. | ||
|
||
Reuse the process for multiple modules for efficiency. However, if there is an | ||
error, retry using a fresh process to avoid cross-contamination of state between | ||
modules. | ||
|
||
We use a separate process to isolate us from many side effects. For example, the | ||
import of a module may kill the current process, and we want to recover from that. | ||
|
||
Always use in a with statement for proper clean-up: | ||
|
||
with ModuleInspect() as m: | ||
p = m.get_package_properties('urllib.parse') | ||
""" | ||
|
||
def __init__(self) -> None: | ||
self._start() | ||
|
||
def _start(self) -> None: | ||
self.tasks = Queue() # type: Queue[str] | ||
self.results = Queue() # type: Queue[Union[ModuleProperties, str]] | ||
self.proc = Process(target=worker, args=(self.tasks, self.results, sys.path)) | ||
self.proc.start() | ||
self.counter = 0 # Number of successfull roundtrips | ||
|
||
def close(self) -> None: | ||
"""Free any resources used.""" | ||
self.proc.terminate() | ||
|
||
def get_package_properties(self, package_id: str) -> ModuleProperties: | ||
"""Return some properties of a module/package using runtime introspection. | ||
|
||
Raise InspectError if the target couldn't be imported. | ||
""" | ||
self.tasks.put(package_id) | ||
res = self._get_from_queue() | ||
if res is None: | ||
# The process died; recover and report error. | ||
self._start() | ||
raise InspectError('Process died when importing %r' % package_id) | ||
if isinstance(res, str): | ||
# Error importing module | ||
if self.counter > 0: | ||
# Also try with a fresh process. Maybe one of the previous imports has | ||
# corrupted some global state. | ||
self.close() | ||
self._start() | ||
return self.get_package_properties(package_id) | ||
raise InspectError(res) | ||
self.counter += 1 | ||
return res | ||
|
||
def _get_from_queue(self) -> Union[ModuleProperties, str, None]: | ||
"""Get value from the queue. | ||
|
||
Return the value read from the queue, or None if the process unexpectedly died. | ||
""" | ||
max_iter = 100 | ||
n = 0 | ||
while True: | ||
if n == max_iter: | ||
raise RuntimeError('Timeout waiting for subprocess') | ||
try: | ||
return self.results.get(timeout=0.05) | ||
except queue.Empty: | ||
if not self.proc.is_alive(): | ||
return None | ||
n += 1 | ||
|
||
def __enter__(self) -> 'ModuleInspect': | ||
return self | ||
|
||
def __exit__(self, *args: object) -> None: | ||
self.close() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
What if the extra item is also decorated (with some unrelated decorator)? Maybe add a test for this?