pylint icon indicating copy to clipboard operation
pylint copied to clipboard

Force typing instead of inference

Open alonme opened this issue 1 year ago • 8 comments

Question

I have a use case in which i want to force pylint to use the type hint of a variable, instead of the inferred value.

For example i have the following code

number: int = "123"

pylint will treat number as a str, but i want it to treat it as an int.

Is this possible in any way?

Documentation for future user

If there is a feature like that, i couldn't find it easily in the docs

Additional context

No response

alonme avatar Feb 07 '23 21:02 alonme

We have #4813 to use typing when the inference fail, but there's no plan to use typing before inference. (Imo, if we did that we might ad well create a new program from scratch as inference is at the very core of what pylint do). Also I don't understand the use case, in your example number IS a string, treating it like an int will definitely cause issue, right ?

Pierre-Sassoulas avatar Feb 07 '23 21:02 Pierre-Sassoulas

Its a complex case, so i simplified it.

What i would like to do is to tell pylint to specifically use a type instead of infering.

maybe register_transform with inference_tip can do the trick? in the actual case, i am always assigning ... and then injecting the actual value later into the variable


Something like this i guess?

def infer_my_custom_call(call_node, context=None):
    # Do some transformation here
    return iter((call_node.annotation,))


astroid.MANAGER.register_transform(
    astroid.nodes.AnnAssign,
    inference_tip(infer_my_custom_call),
    predicate=lambda x: x.value == ...,
)

alonme avatar Feb 07 '23 22:02 alonme

I would suggest maing this a pylint extension for now.

DanielNoord avatar Feb 08 '23 15:02 DanielNoord

@DanielNoord not sure i understand. Are you saying this is achievable using an extension?

Any tips regarding how? I had no luck with the direction that i posted in the last comment

The following seems to work for the examples i currently have, but its kind of ugly

def pylint_cast(t):
    if False:  # pylint: disable=using-constant-test
        return t()
    return ...

number: int = pylint_cast(int)

alonme avatar Feb 08 '23 15:02 alonme

I don't really have an idea about whether this is feasible, I only know that I don't think we should spend time of maintainers (which is already sparse) to explore this. We simply have more pressing issues for us to focus on this future goal.

DanielNoord avatar Feb 08 '23 18:02 DanielNoord

@alonme there's a bunch of examples about inference tweaking in astroid's "brains" (inference tip) see https://github.com/PyCQA/astroid/tree/main/astroid/brain. You can pretty much do whatever you want for a specific lib this way. Your suggestion to permit to do it on a case by case basis on the user side maybe with pylint: inference:int or something similar is interesting imo, but also a LOT of design work and then of work. And as Daniel said we have a lot on our plate already.

Pierre-Sassoulas avatar Feb 08 '23 20:02 Pierre-Sassoulas

I created a plugin - which isn't perfect - but works for me for now.

couldn't get it too work with inference_tip so i used a regular transform

Of course i still believe this should become a feature

from typing import TYPE_CHECKING, Optional

import astroid
from astroid.builder import extract_node
from astroid.nodes.node_classes import AnnAssign, Const, Name, NodeNG, Subscript

if TYPE_CHECKING:
    from pylint.lint import PyLinter


def register(linter: "PyLinter") -> None:
    """This required method auto registers the checker during initialization.
    :param linter: The linter to register the checker to.
    """
    pass

def _is_assigning_ellipsis(node: AnnAssign):
    if isinstance(node.value, Const) and node.value.value is ...:
        return True

def _handle_subscript(node: Subscript):
    if node.value.name == "Optional":
        new_node_code = f"{_create_new_code(node.slice)} or None"
    elif node.value.name == "Union":
        types = [_create_new_code(s) for s in node.slice.elts]
        new_node_code = " or ".join(types)
    elif node.value.name == "List":
        inner_type = _create_new_code(node.slice)
        new_node_code = f"list({inner_type})"
    elif node.value.name == "Tuple":
        types = [_create_new_code(s) for s in node.slice.elts]
        new_node_code = f"tuple({', '.join(types)})"
    elif node.value.name == "Set":
        inner_type = _create_new_code(node.slice)
        new_node_code = f"Set({inner_type})"
    else:
        raise ValueError(f"Unhandled Subscript: {node.value.name}")

    return new_node_code


def _create_new_code(call_node: Optional[NodeNG]) -> str:
    if isinstance(call_node, Name):
        new_node_code = f"{call_node.name}()"

    elif isinstance(call_node, Subscript):
        new_node_code = _handle_subscript(call_node)

    else:
        raise ValueError(f"Unhandled node type: {call_node}")

    return new_node_code


def _transform_to_annotation_object(call_node: AnnAssign, context=None):
    """
    Transform an AnnAssign node to have a value based on the type annotation only
    """
    new_node_code = _create_new_code(call_node.annotation)
    new_node = extract_node(new_node_code)
    # Change the assignment to look as if its value is of the annotation class
    call_node.value = new_node


astroid.MANAGER.register_transform(
    AnnAssign,
    _transform_to_annotation_object,
    predicate=_is_assigning_ellipsis,
)

alonme avatar Mar 16 '23 08:03 alonme

Related (use typing when the inference fail): https://github.com/pylint-dev/pylint/issues/4813. It's a consensual first step imo.

Pierre-Sassoulas avatar Apr 01 '24 20:04 Pierre-Sassoulas