mkdocstrings / griffe

Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API.
https://mkdocstrings.github.io/griffe
ISC License
267 stars 38 forks source link

bug: A dataclass field with init=False should not be a parameter #233

Closed has2k1 closed 3 months ago

has2k1 commented 4 months ago

Here are failing tests.

from griffe.tests import temporary_visited_module

code = """
from dataclasses import dataclass, field

@dataclass
class PointA:
    x: float
    y: float
    z: float = field(init=False)

@dataclass(init=False)
class PointB:
    x: float
    y: float

@dataclass(init=False)
class PointC:
    x: float
    y: float = field(init=True)  # init=True has no effect
"""

with temporary_visited_module(code) as module:
    paramsA = module["PointA"].parameters
    paramsB = module["PointB"].parameters
    paramsC = module["PointC"].parameters

    assert "z" not in paramsA
    assert "x" not in paramsB and "y" not in paramsB
    assert "x" not in paramsC and "y" not in paramsC

Then there are the perhaps more complicated cases to deal with statically where the field class is wrapped inside another function e.g.

def no_init(default: T) -> T:
    """
    Set default value of a dataclass field that will not be __init__ed
    """
    return field(init=False, default=default)

@dataclass
class Point:
    x: float
    y: float = no_init(0)  # y is not a parameter
pawamoy commented 4 months ago

Parameters for dataclasses are currently dynamically created when accessing the class' parameters property. Seeing that the logic can get quite complex, and based on the fact that this parameters property cannot be easily extended by users or extensions, I think we should change the logic and generate an __init__ method object when visiting class decorated with @dataclasses.dataclass. Extensions for third-party libs (Pydantic, attrs, whatever) could then do the same, without any special code in Griffe itself.

has2k1 commented 4 months ago

I haven't looked at the how the parameters are generated, but I have a temporary solution that inspects ExprCall parameters and I only do the check for dataclasses.

Obviously this does not handle the cases when field(init=False) is hidden inside another function.

pawamoy commented 4 months ago

Yes, that's what we'll use in Griffe too (thanks for the link). Just not in the parameters property as it's not extensible :)

Regarding indirections (hiding the field call in other functions), that won't be supported statically, and users will have to rely on dynamic analysis.