src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Pydantic/PydanticUtils.py

branch
eric7
changeset 11143
ef75c265ab47
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Pydantic/PydanticUtils.py	Sat Feb 22 18:04:02 2025 +0100
@@ -0,0 +1,370 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2025 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing utility functions for the PydanticVisitor class.
+"""
+
+import ast
+
+#######################################################################
+## adapted from: flake8-pydantic v0.4.0
+##
+## Original: Copyright (c) 2023 Victorien
+#######################################################################
+
+
+def getDecoratorNames(decoratorList):
+    """
+    Function to extract the set of decorator names.
+
+    @param decoratorList list of decorators to be processed
+    @type list of ast.expr
+    @return set containing the decorator names
+    @rtype set of str
+    """
+    names = set()
+
+    for dec in decoratorList:
+        if isinstance(dec, ast.Call):
+            names.add(
+                dec.func.attr if isinstance(dec.func, ast.Attribute) else dec.func.id
+            )
+        elif isinstance(dec, ast.Name):
+            names.add(dec.id)
+        elif isinstance(dec, ast.Attribute):
+            names.add(dec.attr)
+
+    return names
+
+
+def _hasPydanticModelBase(node, *, includeRootModel):
+    """
+    Function to check, if a class definition inherits from Pydantic model classes.
+
+    @param node reference to the node to be be analyzed
+    @type ast.ClassDef
+    @keyparam includeRootModel flag indicating to include the root model
+    @type bool
+    @return flag indicating that the class definition inherits from a Pydantic model
+        class
+    @rtype bool
+    """
+    modelClassNames = {"BaseModel"}
+    if includeRootModel:
+        modelClassNames.add("RootModel")
+
+    for base in node.bases:
+        if isinstance(base, ast.Name) and base.id in modelClassNames:
+            return True
+        if isinstance(base, ast.Attribute) and base.attr in modelClassNames:
+            return True
+    return False
+
+
+def _hasModelConfig(node):
+    """
+    Function to check, if the class has a `model_config` attribute set.
+
+    @param node reference to the node to be be analyzed
+    @type ast.ClassDef
+    @return flag indicating that the class has a `model_config` attribute set
+    @rtype bool
+    """
+    for stmt in node.body:
+        if (
+            isinstance(stmt, ast.AnnAssign)
+            and isinstance(stmt.target, ast.Name)
+            and stmt.target.id == "model_config"
+        ):
+            ##~ model_config: ... = ...
+            return True
+
+        if isinstance(stmt, ast.Assign) and any(
+            t.id == "model_config" for t in stmt.targets if isinstance(t, ast.Name)
+        ):
+            ##~ model_config = ...
+            return True
+
+    return False
+
+
+PYDANTIC_FIELD_ARGUMENTS = {
+    "default",
+    "default_factory",
+    "alias",
+    "alias_priority",
+    "validation_alias",
+    "title",
+    "description",
+    "examples",
+    "exclude",
+    "discriminator",
+    "json_schema_extra",
+    "frozen",
+    "validate_default",
+    "repr",
+    "init",
+    "init_var",
+    "kw_only",
+    "pattern",
+    "strict",
+    "gt",
+    "ge",
+    "lt",
+    "le",
+    "multiple_of",
+    "allow_inf_nan",
+    "max_digits",
+    "decimal_places",
+    "min_length",
+    "max_length",
+    "union_mode",
+}
+
+
+def _hasFieldFunction(node):
+    """
+    Function to check, if the class has a field defined with the `Field` function.
+
+    @param node reference to the node to be be analyzed
+    @type ast.ClassDef
+    @return flag indicating that the class has a field defined with the `Field` function
+    @rtype bool
+    """
+    if any(
+        isinstance(stmt, (ast.Assign, ast.AnnAssign))
+        and isinstance(stmt.value, ast.Call)
+        and (
+            (
+                isinstance(stmt.value.func, ast.Name) and stmt.value.func.id == "Field"
+            )  # f = Field(...)
+            or (
+                isinstance(stmt.value.func, ast.Attribute)
+                and stmt.value.func.attr == "Field"
+            )  # f = pydantic.Field(...)
+        )
+        and all(
+            kw.arg in PYDANTIC_FIELD_ARGUMENTS
+            for kw in stmt.value.keywords
+            if kw.arg is not None
+        )
+        for stmt in node.body
+    ):
+        return True
+
+    return False
+
+
+def _hasAnnotatedField(node):
+    """
+    Function to check if the class has a field making use of `Annotated`.
+
+    @param node reference to the node to be be analyzed
+    @type ast.ClassDef
+    @return flag indicating that the class has a field making use of `Annotated`
+    @rtype bool
+    """
+    for stmt in node.body:
+        if isinstance(stmt, ast.AnnAssign) and isinstance(
+            stmt.annotation, ast.Subscript
+        ):
+            if (
+                isinstance(stmt.annotation.value, ast.Name)
+                and stmt.annotation.value.id == "Annotated"
+            ):
+                ##~ f: Annotated[...]
+                return True
+
+            if (
+                isinstance(stmt.annotation.value, ast.Attribute)
+                and stmt.annotation.value.attr == "Annotated"
+            ):
+                ##~ f: typing.Annotated[...]
+                return True
+
+    return False
+
+
+PYDANTIC_DECORATORS = {
+    "computed_field",
+    "field_serializer",
+    "model_serializer",
+    "field_validator",
+    "model_validator",
+}
+
+
+def _hasPydanticDecorator(node):
+    """
+    Function to check, if the class makes use of Pydantic decorators, such as
+    `computed_field` or `model_validator`.
+
+    @param node reference to the node to be be analyzed
+    @type ast.ClassDef
+    @return flag indicating that the class makes use of Pydantic decorators, such as
+        `computed_field` or `model_validator`.
+    @rtype bool
+    """
+    for stmt in node.body:
+        if isinstance(stmt, ast.FunctionDef):
+            decoratorNames = getDecoratorNames(stmt.decorator_list)
+            if PYDANTIC_DECORATORS & decoratorNames:
+                return True
+    return False
+
+
+PYDANTIC_METHODS = {
+    "model_construct",
+    "model_copy",
+    "model_dump",
+    "model_dump_json",
+    "model_json_schema",
+    "model_parametrized_name",
+    "model_rebuild",
+    "model_validate",
+    "model_validate_json",
+    "model_validate_strings",
+}
+
+
+def _hasPydanticMethod(node: ast.ClassDef) -> bool:
+    """
+    Function to check, if the class overrides any of the Pydantic methods, such as
+    `model_dump`.
+
+    @param node reference to the node to be be analyzed
+    @type ast.ClassDef
+    @return flag indicating that class overrides any of the Pydantic methods, such as
+        `model_dump`
+    @rtype bool
+    """
+    if any(
+        isinstance(stmt, ast.FunctionDef)
+        and (
+            stmt.name.startswith(("__pydantic_", "__get_pydantic_"))
+            or stmt.name in PYDANTIC_METHODS
+        )
+        for stmt in node.body
+    ):
+        return True
+
+    return False
+
+
+def isPydanticModel(node, *, includeRootModel=True):
+    """
+    Function to determine if a class definition is a Pydantic model.
+
+    Multiple heuristics are use to determine if this is the case:
+    - The class inherits from `BaseModel` (or `RootModel` if `includeRootModel` is
+      `True`).
+    - The class has a `model_config` attribute set.
+    - The class has a field defined with the `Field` function.
+    - The class has a field making use of `Annotated`.
+    - The class makes use of Pydantic decorators, such as `computed_field` or
+      `model_validator`.
+    - The class overrides any of the Pydantic methods, such as `model_dump`.
+
+    @param node reference to the node to be be analyzed
+    @type ast.ClassDef
+    @keyparam includeRootModel flag indicating to include the root model
+        (defaults to True)
+    @type bool (optional)
+    @return flag indicating a Pydantic model class
+    @rtype bool
+    """
+    if not node.bases:
+        return False
+
+    return (
+        _hasPydanticModelBase(node, includeRootModel=includeRootModel)
+        or _hasModelConfig(node)
+        or _hasFieldFunction(node)
+        or _hasAnnotatedField(node)
+        or _hasPydanticDecorator(node)
+        or _hasPydanticMethod(node)
+    )
+
+
+def isDataclass(node):
+    """
+    Function to check, if a class is a dataclass.
+
+    @param node reference to the node to be be analyzed
+    @type ast.ClassDef
+    @return flag indicating that the class is a dataclass.
+    @rtype bool
+    """
+    """Determine if a class is a dataclass."""
+
+    return bool(
+        {"dataclass", "pydantic_dataclass"} & getDecoratorNames(node.decorator_list)
+    )
+
+
+def isFunction(node, functionName):
+    """
+    Function to check, if a function call is referencing a given function name.
+
+    @param node reference to the node to be be analyzed
+    @type ast.Call
+    @param functionName name of the function to check for
+    @type str
+    @return flag indicating that the function call is referencing the given function
+        name
+    @rtype bool
+    """
+    return (isinstance(node.func, ast.Name) and node.func.id == functionName) or (
+        isinstance(node.func, ast.Attribute) and node.func.attr == functionName
+    )
+
+
+def isName(node, name):
+    """
+    Function to check, if an expression is referencing a given name.
+
+    @param node reference to the node to be be analyzed
+    @type ast.expr
+    @param name name to check for
+    @type str
+    @return flag indicating that the expression is referencing teh given name
+    @rtype bool
+    """
+    return (isinstance(node, ast.Name) and node.id == name) or (
+        isinstance(node, ast.Attribute) and node.attr == name
+    )
+
+
+def extractAnnotations(node):
+    """
+    Function to extract the annotations of an expression.
+
+    @param node reference to the node to be be processed
+    @type ast.expr
+    @return set containing the annotation names
+    @rtype set[str]
+    """
+    annotations = set()
+
+    if isinstance(node, ast.Name):
+        ##~ foo: date = ...
+        annotations.add(node.id)
+
+    elif isinstance(node, ast.BinOp):
+        ##~ foo: date | None = ...
+        annotations |= extractAnnotations(node.left)
+        annotations |= extractAnnotations(node.right)
+
+    elif isinstance(node, ast.Subscript):
+        ##~ foo: dict[str, date]
+        ##~ foo: Annotated[list[date], ...]
+        if isinstance(node.slice, ast.Tuple):
+            for elt in node.slice.elts:
+                annotations |= extractAnnotations(elt)
+        if isinstance(node.slice, ast.Name):
+            annotations.add(node.slice.id)
+
+    return annotations

eric ide

mercurial