Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.10", "3.11", "3.12"]
python-version: ["3.10", "3.11", "3.12", "3.13"]

steps:
- name: Checkout code
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

0.2.2 is the first 0.2.x release published to PyPI. The 0.2.0 and 0.2.1 tags exist in git, but their publish runs were cancelled, so upgrading from PyPI goes straight from 0.1.16 to 0.2.2. The notes below cover every change in that jump.

## 0.2.5

Fixed:
- Inline field documentation (a string literal under a dataclass field) is extracted again on Python 3.13 and later. Class source lookup used `inspect._ClassFinder`, a private helper that Python 3.13 removed, so every dataclass came back with no field help. Classes are now located by qualified name from the module AST directly. CI now tests Python 3.13.

## 0.2.4

Fixed:
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "python-introspect"
version = "0.2.4"
version = "0.2.5"
description = "Pure Python introspection toolkit for function signatures, dataclasses, and type hints"
readme = "README.md"
requires-python = ">=3.10"
Expand All @@ -21,6 +21,7 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Utilities",
]
Expand Down
2 changes: 1 addition & 1 deletion src/python_introspect/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
type resolution for framework-specific types (lazy configs, proxies, etc.)
"""

__version__ = "0.2.4"
__version__ = "0.2.5"

from .signature_analyzer import (
SignatureAnalyzer,
Expand Down
41 changes: 27 additions & 14 deletions src/python_introspect/signature_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,26 @@ def type_hints(self) -> Dict[str, Any]:
)


def _class_node_for_qualname(tree: ast.AST, qualname: str) -> Optional[ast.ClassDef]:
"""Return the class statement whose qualified name is ``qualname``.

Function bodies contribute ``<locals>`` exactly as ``__qualname__`` does.
"""
pending = [(node, ()) for node in reversed(getattr(tree, 'body', ()))]
while pending:
node, scope = pending.pop()
if isinstance(node, ast.ClassDef):
node_scope = (*scope, node.name)
if '.'.join(node_scope) == qualname:
return node
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
node_scope = (*scope, node.name, '<locals>')
else:
node_scope = scope
pending.extend((child, node_scope) for child in reversed(list(ast.iter_child_nodes(node))))
return None


class ClassSourceDeclaration(NamedTuple):
"""Immutable located class source and its source-only documentation views."""

Expand Down Expand Up @@ -1196,20 +1216,13 @@ def _module_source_tree(source: str) -> ast.AST:
def _qualified_class_source(source: str, qualname: str) -> Optional[ClassSourceDeclaration]:
"""Use Python's qualified class/decorator locator on current source content."""
tree = SignatureAnalyzer._module_source_tree(source)
finder = inspect._ClassFinder(qualname)
try:
finder.visit(tree)
except inspect.ClassFoundException as found:
lines = source.splitlines(keepends=True)
first_line = found.args[0] + 1
block = ''.join(inspect.getblock(lines[found.args[0]:]))
class_node = next(
node for node in ast.walk(tree)
if isinstance(node, ast.ClassDef)
and min([node.lineno, *(decorator.lineno for decorator in node.decorator_list)]) == first_line
)
return ClassSourceDeclaration.from_block(block, class_node, first_line)
return None
class_node = _class_node_for_qualname(tree, qualname)
if class_node is None:
return None
first_line = min([class_node.lineno, *(decorator.lineno for decorator in class_node.decorator_list)])
lines = source.splitlines(keepends=True)
block = ''.join(inspect.getblock(lines[first_line - 1:]))
return ClassSourceDeclaration.from_block(block, class_node, first_line)

@staticmethod
def _extract_inline_field_docs(dataclass_type: type) -> Dict[str, str]:
Expand Down
Loading