pylint-dev / pylint

It's not just a linter that annoys you!
https://pylint.readthedocs.io/en/latest/
GNU General Public License v2.0
5.26k stars 1.12k forks source link

Force typing instead of inference #8230

Open alonme opened 1 year ago

alonme commented 1 year ago

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

Pierre-Sassoulas commented 1 year ago

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 ?

alonme commented 1 year ago

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 == ...,
)
DanielNoord commented 1 year ago

I would suggest maing this a pylint extension for now.

alonme commented 1 year ago

@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)
DanielNoord commented 1 year ago

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.

Pierre-Sassoulas commented 1 year ago

@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.

alonme commented 1 year ago

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,
)
Pierre-Sassoulas commented 6 months ago

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