"""
This module provides utilities for inspecting Python objects and retrieving their source code information.
It includes functionality to extract source file paths, line numbers, and source code for various Python objects
such as functions, classes, and methods. The module is particularly useful for code analysis, documentation
generation, and debugging purposes.
"""
import inspect
import os
import pathlib
from dataclasses import dataclass
from typing import Any, List, Optional
[docs]
@dataclass
class ObjectInspect:
"""
A dataclass that encapsulates inspection information about a Python object.
This class stores metadata about a Python object including its source file location,
line numbers, and source code. It provides convenient properties to access and
manipulate this information.
:param object: The Python object being inspected.
:type object: Any
:param source_file: The absolute path to the source file containing the object, or None if unavailable.
:type source_file: Optional[str]
:param start_line: The starting line number of the object in the source file, or None if unavailable.
:type start_line: Optional[int]
:param end_line: The ending line number of the object in the source file, or None if unavailable.
:type end_line: Optional[int]
:param source_lines: A list of source code lines for the object, or None if unavailable.
:type source_lines: Optional[List[str]]
"""
object: Any
source_file: Optional[str]
start_line: Optional[int]
end_line: Optional[int]
source_lines: Optional[List[str]]
[docs]
def __post_init__(self):
"""
Normalize and convert the source file path to an absolute path after initialization.
This method is automatically called after the dataclass is initialized. It ensures
that the source_file path is normalized, case-normalized, and converted to an
absolute path for consistency across different platforms.
"""
if self.source_file is not None:
self.source_file = os.path.normpath(os.path.normcase(os.path.abspath(self.source_file)))
@property
def name(self) -> Optional[str]:
"""
Get the name of the inspected object.
:return: The name of the object if it has a '__name__' attribute, otherwise None.
:rtype: Optional[str]
Example::
>>> def example_func():
... pass
>>> info = get_object_info(example_func)
>>> info.name
'example_func'
"""
return getattr(self.object, '__name__', None)
@property
def source_code(self) -> Optional[str]:
"""
Get the source code of the inspected object.
:return: The complete source code as a string if available, otherwise None.
:rtype: Optional[str]
Example::
>>> def example_func():
... return 42
>>> info = get_object_info(example_func)
>>> print(info.source_code)
def example_func():
return 42
"""
if self.has_source:
return ''.join(self.source_lines)
else:
return None
@property
def source_file_code(self) -> Optional[str]:
"""
Get the complete source code of the file containing the inspected object.
:return: The entire file content as a string if the source file is available, otherwise None.
:rtype: Optional[str]
Example::
>>> info = get_object_info(some_function)
>>> file_content = info.source_file_code # Gets entire file content
"""
if self.source_file is not None:
return pathlib.Path(self.source_file).read_text()
else:
return None
@property
def has_source(self) -> bool:
"""
Check whether source code lines are available for the inspected object.
:return: True if source lines are available, False otherwise.
:rtype: bool
Example::
>>> info = get_object_info(print) # Built-in function
>>> info.has_source
False
>>> def custom_func():
... pass
>>> info = get_object_info(custom_func)
>>> info.has_source
True
"""
return self.source_lines is not None
@property
def package_name(self) -> Optional[str]:
"""
Get the package name containing the inspected object.
This property retrieves the package name by analyzing the source file path.
It relies on the get_package_name function from the module utility.
:return: The package name if the source file is available, otherwise None.
:rtype: Optional[str]
Example::
>>> # Assuming the object is from package 'mypackage'
>>> info = get_object_info(some_function)
>>> info.package_name
'mypackage'
"""
from .module import get_package_name
if self.source_file is not None:
return get_package_name(self.source_file)
else:
return None
[docs]
def get_object_info(obj: Any) -> ObjectInspect:
"""
Retrieve comprehensive inspection information about a Python object.
This function attempts to extract source file location, line numbers, and source code
for the given object. If any information is unavailable (e.g., for built-in objects),
the corresponding fields will be set to None.
:param obj: The Python object to inspect (function, class, method, etc.).
:type obj: Any
:return: An ObjectInspect instance containing all available inspection information.
:rtype: ObjectInspect
Example::
>>> def example_function():
... '''A simple example function.'''
... return "Hello, World!"
>>> info = get_object_info(example_function)
>>> info.name
'example_function'
>>> info.has_source
True
>>> print(info.source_code) # doctest: +SKIP
def example_function():
'''A simple example function.'''
return "Hello, World!"
>>> # Built-in objects have limited information
>>> info = get_object_info(print)
>>> info.has_source
False
>>> info.source_file is None
True
"""
try:
source_file = inspect.getfile(obj)
except TypeError:
source_file = None
try:
source_lines, start_line = inspect.getsourcelines(obj)
end_line = start_line + len(source_lines) - 1
except TypeError:
source_lines, start_line, end_line = None, None, None
return ObjectInspect(
object=obj,
source_file=source_file,
start_line=start_line,
end_line=end_line,
source_lines=source_lines,
)