Я создаю класс из словаря следующим образом:
class MyClass:
def __init__(self, dictionary):
for k, v in dictionary.items():
setattr(self, k, v)
Я пытаюсь понять, как я могу получить Intellisense для этого динамически сгенерированного класса. Большинство IDE могут читать файлы pyi для таких вещей.
Однако я не хочу записывать файл pyi вручную.
Можно ли создать экземпляр этого класса и программно записать из него файл pyi на диск?
У mypy есть инструмент stubgen, но я не могу понять, можно ли его использовать таким образом.
Могу ли я импортировать stubgen из mypy и как-то скормить его MyClass(<some dict>)
?
Зачем вообще нужен этот класс? Если это просто прямая копия диктофона, почему бы просто... не использовать этот диктофон?
... или types.SimpleNamespace
(для переменных ключей) или dataclasses.DataClass
(для фиксированных)?
@ Брайан, мой словарь меняется во время выполнения, но есть загруженный словарь по умолчанию, который был бы достаточно хорош для меня. Он встроен в базу кода, и мне интересно, могу ли я вызвать stubgen против него в setup.py или что-то в этом роде.
Программы статического анализа, такие как stubgen, являются неподходящим инструментом для анализа динамически заполняемого класса, потому что они не могут видеть исходный код вашего полностью сформированного класса, чтобы дать вам заглушку класса. Вы должны выполнить генерацию заглушки во время выполнения, запустив исходный код, чтобы сначала заполнить атрибуты экземпляра.
Допустим, у вас есть динамически заполняемый класс, как в вашем примере,
class MyClass:
def __init__(self, dictionary: dict[str, object]) -> None:
k: str
v: object
for k, v in dictionary.items():
setattr(self, k, v)
и вы передаете этот словарь конструктору,
import statistics
instance: MyClass = MyClass({"a": 1, "b": "my_string", "distribution": statistics.NormalDist(0.0, 1.0)})
и вы хотите, чтобы это было вашим выходом:
import statistics
class MyClass:
a: int
b: str
distribution: statistics.NormalDist
def __init__(self, dictionary: dict[str, object]) -> None:
...
Самый простой способ сгенерировать приведенный выше вывод — подключиться к созданию и инициализации экземпляра, чтобы вы не влияли на любые __new__
или __init__
связанные super
вызовы, которые уже существуют в вашем классе. Это можно сделать с помощью метода __call__
метакласса:
class _PostInitialisationMeta(type):
"""
Metaclass for classes subject to dynamic stub generation
"""
def __call__(
cls, dictionary: dict[str, object], *args: object, **kwargs: object
) -> object:
"""
Override instance creation and initialisation. Generate a string representing
the class's stub definition suitable for a `.pyi` file.
Parameters
----------
dictionary
Mapping from instance attribute names to attribute values
*args
**kwargs
Other positional and keyword arguments to the class's `__new__` and
`__init__` methods
Returns
-------
object
Created instance
"""
instance: object = super().__call__(dictionary, *args, **kwargs)
<generate string here>
return instance
Затем вы можете разобрать класс на абстрактное синтаксическое дерево , изменить дерево, добавив, удалив или преобразовав узлы, а затем распарсить преобразованное дерево. Вот одна из возможных реализаций с использованием ast.NodeVisitor стандартной библиотеки Python:
Только Python 3.9+
from __future__ import annotations
import ast
import inspect
import typing as t
if t.TYPE_CHECKING:
class _SupportsBodyStatements(t.Protocol):
body: list[ast.stmt]
_CLASS_TO_STUB_SOURCE_DICT: t.Final[dict[type, str]] = {}
class _PostInitialisationMeta(type):
"""
Metaclass for classes subject to dynamic stub generation
"""
def __call__(
cls, dictionary: dict[str, object], *args: object, **kwargs: object
) -> object:
"""
Override instance creation and initialisation. The first time an instance of a
class is created and initialised, cache a string representing the class's stub
definition suitable for a `.pyi` file.
Parameters
----------
dictionary
Mapping from instance attribute names to attribute values
*args
**kwargs
Other positional and keyword arguments to the class's `__new__` and
`__init__` methods
Returns
-------
object
Created instance
"""
instance: object = super().__call__(dictionary, *args, **kwargs)
_DynamicClassStubsGenerator.cache_stub_for_dynamic_class(cls, dictionary)
return instance
def _remove_docstring(node: _SupportsBodyStatements, /) -> None:
"""
Removes a docstring node if it exists in the given node's body
"""
first_node: ast.stmt = node.body[0]
if (
isinstance(first_node, ast.Expr)
and isinstance(first_node.value, ast.Constant)
and (type(first_node.value.value) is str)
):
node.body.pop(0)
def _replace_body_with_ellipsis(node: _SupportsBodyStatements, /) -> None:
"""
Replaces the body of a given node with a single `...`
"""
node.body[:] = [ast.Expr(ast.Constant(value=...))]
class _DynamicClassStubsGenerator(ast.NodeVisitor):
"""
Generate and cache stubs for class instances whose instance variables are populated
dynamically
"""
@classmethod
def cache_stub_for_dynamic_class(
StubsGenerator, Class: type, dictionary: dict[str, object], /
) -> None:
# Disallow stubs generation if the stub source is already generated
try:
_CLASS_TO_STUB_SOURCE_DICT[Class]
except KeyError:
pass
else:
return
# Get class's source code
src: str = inspect.getsource(Class)
module_tree: ast.Module = ast.parse(src)
class_statement: ast.stmt = module_tree.body[0]
assert isinstance(class_statement, ast.ClassDef)
# Strip unnecessary details from class body
stubs_generator: _DynamicClassStubsGenerator = StubsGenerator()
stubs_generator.visit(module_tree)
# Adds the following:
# - annotated instance attributes on the class body
# - import statements for non-builtins
# --------------------------------------------------
added_import_nodes: list[ast.stmt] = []
added_class_nodes: list[ast.stmt] = []
k: str
v: object
for k, v in dictionary.items():
value_type: type = type(v)
value_type_name: str = value_type.__qualname__
value_type_module_name: str = value_type.__module__
annotated_assignment_statement: ast.stmt = ast.parse(
f"{k}: {value_type_name}"
).body[0]
assert isinstance(annotated_assignment_statement, ast.AnnAssign)
added_class_nodes.append(annotated_assignment_statement)
if value_type_module_name != "builtins":
annotation_expression: ast.expr = (
annotated_assignment_statement.annotation
)
assert isinstance(annotation_expression, ast.Name)
annotation_expression.id = (
f"{value_type_module_name}.{annotation_expression.id}"
)
added_import_nodes.append(
ast.Import(names=[ast.alias(name=value_type_module_name)])
)
module_tree.body[:] = [*added_import_nodes, *module_tree.body]
class_statement.body[:] = [*added_class_nodes, *class_statement.body]
_CLASS_TO_STUB_SOURCE_DICT[Class] = ast.unparse(module_tree)
def visit_ClassDef(self, node: ast.ClassDef) -> None:
_remove_docstring(node)
node.keywords = [] # Clear metaclass and other keywords in class definition
self.generic_visit(node)
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
_replace_body_with_ellipsis(node)
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
_replace_body_with_ellipsis(node)
Затем вы можете запустить свой класс как обычно, а затем проверить, что хранится в кеше _CLASS_TO_STUB_SOURCE_DICT
:
class MyClass(metaclass=_PostInitialisationMeta):
def __init__(self, dictionary: dict[str, object]) -> None:
k: str
v: object
for k, v in dictionary.items():
setattr(self, k, v)
>>> MyClass({"a": 1, "b": "my_string", "distribution": statistics.NormalDist(0.0, 1.0)})
>>> src: str
>>> for src in _CLASS_TO_STUB_SOURCE_DICT.values():
... print(src)
...
import statistics
class MyClass:
a: int
b: str
distribution: statistics.NormalDist
def __init__(self, dictionary: dict[str, object]) -> None:
...
На практике файлы .pyi
формируют интерфейсы типов для каждого модуля, поэтому описанную выше реализацию нельзя использовать сразу, поскольку она предназначена только для класса. Вы также должны выполнять гораздо больше операций с другими типами узлов в вашем .pyi
-модуле, решать, что делать с неаннотированными узлами, повторным импортом и т. д., прежде чем записывать исходный код в .pyi
-файл. Здесь может пригодиться stubgen — он может анализировать статические части вашего модуля, и вы можете взять этот вывод и написать ast.NodeTransformer
для преобразования этого вывода в классы, которые вы сгенерировали динамически.
Я думаю, поскольку это будет довольно простой класс данных с одним или двумя методами, я мог бы просто создать контент вручную, учитывая, насколько это будет сложно. Хотя это очень полезно!
Атрибуты
MyClass
изменяются динамически или они фиксированы и заполняются только из словаря из соображений удобства?