Repository: pydantic/pydantic

Issue:
Generate actual signature for BaseModel.__init__
# Feature Request
That idea came for me when I saw this issue:
https://github.com/tiangolo/fastapi/issues/318
And this particular comment that shows that standart inspect tools unable to determine signature of a pydantic model:
> @tiangolo, I had an opportunity to fiddle a little bit more with this today.
> Here is a full example of the different ways to specify parameters using depends: https://gist.github.com/Atheuz/075f4d8fe3b56d034741301ba2574ef1
> 
> You can run it using this command `uvicorn run:app --reload --host 0.0.0.0 --port 8080`
> 
> I essence: dataclass classes work if I specify `Query(None)` and `Depends`, pure classes work if I specify `Query(None)` and use `Depends`, and input parameters in the signature just works.
> 
> Pydantic classes do not work, at least in terms of the generated docs, it just says `data` instead of the expected `dt` and `to_sum`.
> 
> dataclasses in the generated docs:
> ![billede](https://user-images.githubusercontent.com/202696/59960977-0c7b6580-94d1-11e9-95ad-c89090f133f9.png)
> 
> pydantic in the generated docs:
> ![billede](https://user-images.githubusercontent.com/202696/59960978-18ffbe00-94d1-11e9-9869-524c738cd0e2.png)



This, however is not true for dataclasses, where `__init__` is generated on class creation.
```py
@dataclass
class Dataclass:
    id: int
    name: str = 'John'

class Model(BaseModel):
    id: int
    name: str = 'John'

help(Dataclass.__init__)
'''
Help on function __init__:
__init__(self, id: int, name: str = 'John') -> None
'''
help(Model.__init__)
'''
Help on function __init__ in module pydantic.main:
__init__(__pydantic_self__, **data: Any) -> None
    Initialize self.  See help(type(self)) for accurate signature.
'''
```

Of course, we cannot follow dataclasses in this part and generate `__init__` of `BaseModel`, since it will cause getting `TypeError` instead of `ValidationError` on missing values, etc., but we still can help inspect tools to see the right signature of our models.

```py
from functools import update_wrapper
import pydantic

def __init__(self, *, id: int, name: str = 'John') -> None: ...
update_wrapper(Model.__init__, __init__)
help(Model.__init__)
'''
Help on function __init__ in module __main__:
__init__(self, *, id: int, name: str = 'John') -> None
'''
```
# Possible approaches
I see this ways of creating typed `__init__`:
- Generate it as a string and use `eval()`
- Create function normally and modify its __code__ signature 
and then use `functools.update_wrapper(Model.__init__, typed_func)` 
# Example
```py
# using `Model` declared above

def f(): ...

f.__code__ = f.__code__.replace(   # __code__.replace() available only in python >= 3.8
    co_kwonlyargcount=len(Model.__fields__),
    co_argcount=1,
    co_varnames=('__pydantic_self__',) + tuple(Model.__fields__)
)

defaults, annotations = {}, {}
for name, field in Model.__fields__.items():
    if field.default:
        defaults[name] = field.default
    annotations[name] = field.type_
    
f.__kwdefaults__ = defaults
f.__annotations__ = annotations
help(f)
'''
Help on function f in module __main__:
f(__pydantic_self__, *, id: int, name: str = 'John')
'''
```







Comments:
Your approach sounds very good to me, PR welcome.

Questions:
* I assume this would not impact performance. Am I right?
* Are there any situations where this could cause problems or backwards incompatibility? E.g. do we need to wait for v2? I assume not.
* Are there any other places where this could help? E.g. ipython maybe? Presumably not pycharm, vscode or vim? 
Would it work to just set the `__signature__` attribute of the model `__init__`? This is enough to work with `inspect.signature`, which is used, for example, by FastAPI; I'm not sure how it relates to the other tools discussed above.

If you just need to update `__signature__`, I have a variety of examples doing this for various fastapi-related purposes that may be useful for reference.

See, for example, here:
https://gist.github.com/dmontagu/87e9d3d7795b14b63388d4b16054f0ff#file-fastapi_cbv-py-L37-L47

Also, I have worked through most of the config-related logic for what the generated signature should look like in the mypy plugin. So that might provide a good reference, e.g., if `Config.allow_extra` is `False` or similar, and you want to be really careful.
> I assume this would not impact performance. Am I right?

This has relatively small impact only on model creation.

> Are there any situations where this could cause problems or backwards incompatibility? E.g. do we need to wait for v2? I assume not.

If we implement this right (e. g. respect `__code__` differences between python versions), no, I don't think so.

### Question to think about: 
What if subclass of a model declares `__init__`? Would we merge it's signature, rewrite it or leave as is?
```py
class MyModel(BaseModel):
    id: int
    name: str = 'John'    

    def __init__(self, foo: int = 41, **data):
        self.foo = foo + 1 
        return super().__init__(**data)
```
> Would it work to just set the `__signature__` attribute of the model `__init__`? This is enough to work with `inspect.signature`, which is used, for example, by FastAPI; I'm not sure how it relates to the other tools discussed above.

@dmontagu , just to be clear, do you suggest construct new signature from scratch and set it to `__init__`, or generate function and copy its signature to `__init__`?
Generate a new signature from scratch; it's not too hard using the objects from the `inspect` module.
Sounds good. Having `__signature__` is enough for FastAPI to understand right parameters. 

However, I'm not sure if it's gonna work everywhere, as `__signature__` is not present in function type by default and used only in `inspect` module.

I think the best is to have all: valid `__wrapped__`, `__kwdefaults__`, `__annotations__` and `__signature__` together to make sure any kind of inspection will work properly.

Oh, looks like there's case when we can't use `__signature__`:
```py
        if not name.isidentifier():
>           raise ValueError('{!r} is not a valid parameter name'.format(name))
E           ValueError: 'this-is-funky' is not a valid parameter name
```
@MrMrRobat 
I think `this-is-funky` is an invalid signature name.
Is there a case to give a method an invalid name?
If I misread it, sorry. 
I'm assuming this is coming from an alias? Yeah, I'm generally against giving methods signatures based on aliases.

In general, aliases don't seem to play super nicely with existing python tools, a reason I heavily prefer having `allow_population_by_alias=True`.

I think part of the problem is that `__init__` is used for *both* parsing, where JSON schema conventions are important, *and* initializing in non-parsing contexts, where python conventions are important (e.g., for type checking, auto-docs-generation, IDEs, etc.).

The `construct` method mitigates this to some extent, but I think there is still some awkwardness, e.g., if you need your validators to run on the inputs. I think I personally would prefer if `__init__` always expected non-aliases, and aliases were only relevant for the `parse` methods.

(But I recognize obviously that's a huge breaking change from how things work now, so will probably never happen, and others may not feel the same way I do on this point anyway 😅.)
> I think `this-is-funky` is an invalid signature name.
> Is there a case to give a method an invalid name?

Not a method, but field. 

>I'm assuming this is coming from an alias?

In comes from test_create_model:
https://github.com/samuelcolvin/pydantic/blob/c71326d4a6898612597d3c647a4256f168818e30/tests/test_create_model.py#L127-L136

@MrMrRobat 
We should ignore an invalid field name to create `__signature__`  when using `create_model()`  with an invalid name.
Also, Someone may want to add valid field to `__sigunature___` on Model which is created by `create_model()`
```python
model = create_model('FooModel', **{'this-is-funky': (int, 1), 'bar': (int, ...)})
m = model(**{'this-is-funky': '123', 'bar': '456'})
assert m.dict() == {'this-is-funky': 123, 'bar': 456}

print(inspect.signature(model))
# > (**data: Any) -> None

print(model(bar='789'))
# > FooModel this-is-funky=1 bar=789

```

Patch:
diff --git a/docs/examples/models_signature.py b/docs/examples/models_signature.py
new file mode 100644
--- /dev/null
+++ b/docs/examples/models_signature.py
@@ -0,0 +1,10 @@
+import inspect
+from pydantic import BaseModel, Field
+
+class FooModel(BaseModel):
+    id: int
+    name: str = None
+    description: str = 'Foo'
+    apple: int = Field(..., alias='pear')
+
+print(inspect.signature(FooModel))
diff --git a/docs/examples/models_signature_custom_init.py b/docs/examples/models_signature_custom_init.py
new file mode 100644
--- /dev/null
+++ b/docs/examples/models_signature_custom_init.py
@@ -0,0 +1,13 @@
+import inspect
+
+from pydantic import BaseModel
+
+class MyModel(BaseModel):
+    id: int
+    info: str = 'Foo'
+
+    def __init__(self, id: int = 1, *, bar: str, **data) -> None:
+        """My custom init!"""
+        super().__init__(id=id, bar=bar, **data)
+
+print(inspect.signature(MyModel))
diff --git a/pydantic/main.py b/pydantic/main.py
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -18,9 +18,18 @@
 from .schema import model_schema
 from .types import PyObject, StrBytes
 from .typing import AnyCallable, AnyType, ForwardRef, is_classvar, resolve_annotations, update_field_forward_refs
-from .utils import GetterDict, Representation, ValueItems, lenient_issubclass, sequence_like, validate_field_name
+from .utils import (
+    GetterDict,
+    Representation,
+    ValueItems,
+    generate_model_signature,
+    lenient_issubclass,
+    sequence_like,
+    validate_field_name,
+)
 
 if TYPE_CHECKING:
+    from inspect import Signature
     from .class_validators import ValidatorListDict
     from .types import ModelOrDc
     from .typing import CallableGenerator, TupleGenerator, DictStrAny, DictAny, SetStr
@@ -156,6 +165,8 @@ def __new__(mcs, name, bases, namespace, **kwargs):  # noqa C901
         fields: Dict[str, ModelField] = {}
         config = BaseConfig
         validators: 'ValidatorListDict' = {}
+        fields_defaults: Dict[str, Any] = {}
+
         pre_root_validators, post_root_validators = [], []
         for base in reversed(bases):
             if issubclass(base, BaseModel) and base != BaseModel:
@@ -170,6 +181,9 @@ def __new__(mcs, name, bases, namespace, **kwargs):  # noqa C901
         vg = ValidatorGroup(validators)
 
         for f in fields.values():
+            if not f.required:
+                fields_defaults[f.name] = f.default
+
             f.set_config(config)
             extra_validators = vg.get_validators(f.name)
             if extra_validators:
@@ -196,13 +210,15 @@ def __new__(mcs, name, bases, namespace, **kwargs):  # noqa C901
                         and not lenient_issubclass(getattr(ann_type, '__origin__', None), Type)
                     ):
                         continue
-                    fields[ann_name] = ModelField.infer(
+                    fields[ann_name] = inferred = ModelField.infer(
                         name=ann_name,
                         value=value,
                         annotation=ann_type,
                         class_validators=vg.get_validators(ann_name),
                         config=config,
                     )
+                    if not inferred.required:
+                        fields_defaults[ann_name] = inferred.default
 
             for var_name, value in namespace.items():
                 if (
@@ -225,6 +241,8 @@ def __new__(mcs, name, bases, namespace, **kwargs):  # noqa C901
                             f'if you wish to change the type of this field, please use a type annotation'
                         )
                     fields[var_name] = inferred
+                    if not inferred.required:
+                        fields_defaults[var_name] = inferred.default
 
         _custom_root_type = ROOT_KEY in fields
         if _custom_root_type:
@@ -238,7 +256,7 @@ def __new__(mcs, name, bases, namespace, **kwargs):  # noqa C901
         new_namespace = {
             '__config__': config,
             '__fields__': fields,
-            '__field_defaults__': {n: f.default for n, f in fields.items() if not f.required},
+            '__field_defaults__': fields_defaults,
             '__validators__': vg.validators,
             '__pre_root_validators__': pre_root_validators + pre_rv_new,
             '__post_root_validators__': post_root_validators + post_rv_new,
@@ -247,7 +265,10 @@ def __new__(mcs, name, bases, namespace, **kwargs):  # noqa C901
             '__custom_root_type__': _custom_root_type,
             **{n: v for n, v in namespace.items() if n not in fields},
         }
-        return super().__new__(mcs, name, bases, new_namespace, **kwargs)
+
+        cls = super().__new__(mcs, name, bases, new_namespace, **kwargs)
+        cls.__signature__ = generate_model_signature(cls.__init__, fields, config)
+        return cls
 
 
 class BaseModel(metaclass=ModelMetaclass):
@@ -263,6 +284,7 @@ class BaseModel(metaclass=ModelMetaclass):
         __json_encoder__: Callable[[Any], Any] = lambda x: x
         __schema_cache__: 'DictAny' = {}
         __custom_root_type__: bool = False
+        __signature__: 'Signature'
 
     Config = BaseConfig
     __slots__ = ('__dict__', '__fields_set__')
@@ -274,6 +296,11 @@ class BaseModel(metaclass=ModelMetaclass):
     __repr__ = Representation.__repr__
 
     def __init__(__pydantic_self__, **data: Any) -> None:
+        """
+        Create a new model by parsing and validating input data from keyword arguments.
+
+        Raises ValidationError if the input data cannot be parsed to form a valid model.
+        """
         # Uses something other than `self` the first arg to allow "self" as a settable attribute
         if TYPE_CHECKING:
             __pydantic_self__.__dict__: Dict[str, Any] = {}
diff --git a/pydantic/utils.py b/pydantic/utils.py
--- a/pydantic/utils.py
+++ b/pydantic/utils.py
@@ -1,4 +1,5 @@
 import warnings
+from itertools import islice
 from types import GeneratorType
 from typing import (
     TYPE_CHECKING,
@@ -22,8 +23,10 @@
 from .version import version_info
 
 if TYPE_CHECKING:
-    from .main import BaseModel  # noqa: F401
+    from inspect import Signature
+    from .main import BaseModel, BaseConfig  # noqa: F401
     from .typing import AbstractSetIntStr, DictIntStrAny, IntStr, ReprArgs  # noqa: F401
+    from .fields import ModelField  # noqa: F401
     from .dataclasses import DataclassType  # noqa: F401
 
 __all__ = (
@@ -136,6 +139,51 @@ def almost_equal_floats(value_1: float, value_2: float, *, delta: float = 1e-8)
     return abs(value_1 - value_2) <= delta
 
 
+def generate_model_signature(
+    init: Callable[..., None], fields: Dict[str, 'ModelField'], config: Type['BaseConfig']
+) -> 'Signature':
+    """
+    Generate signature for model based on its fields
+    """
+    from inspect import Parameter, Signature, signature
+
+    present_params = signature(init).parameters.values()
+    merged_params: Dict[str, Parameter] = {}
+    var_kw = None
+    use_var_kw = False
+
+    for param in islice(present_params, 1, None):  # skip self arg
+        if param.kind is param.VAR_KEYWORD:
+            var_kw = param
+            continue
+        merged_params[param.name] = param
+
+    if var_kw:  # if custom init has no var_kw, fields which are not declared in it cannot be passed through
+        allow_names = config.allow_population_by_field_name
+        for field_name, field in fields.items():
+            param_name = field.alias
+            if field_name in merged_params or param_name in merged_params:
+                continue
+            elif not param_name.isidentifier():
+                if allow_names and field_name.isidentifier():
+                    param_name = field_name
+                else:
+                    use_var_kw = True
+                    continue
+
+            # TODO: replace annotation with actual expected types once #1055 solved
+            kwargs = {'default': field.default} if not field.required else {}
+            merged_params[param_name] = Parameter(param_name, Parameter.KEYWORD_ONLY, annotation=field.type_, **kwargs)
+
+    if config.extra is config.extra.allow:
+        use_var_kw = True
+
+    if var_kw and use_var_kw:
+        merged_params[var_kw.name] = var_kw
+
+    return Signature(parameters=list(merged_params.values()), return_annotation=None)
+
+
 def get_model(obj: Union[Type['BaseModel'], Type['DataclassType']]) -> Type['BaseModel']:
     from .main import BaseModel  # noqa: F811
 


Test patch:
diff --git a/tests/test_model_signature.py b/tests/test_model_signature.py
new file mode 100644
--- /dev/null
+++ b/tests/test_model_signature.py
@@ -0,0 +1,74 @@
+from inspect import signature
+from typing import Any, Iterable, Union
+
+from pydantic import BaseModel, Extra, Field, create_model
+
+
+def _equals(a: Union[str, Iterable[str]], b: Union[str, Iterable[str]]) -> bool:
+    """
+    compare strings with spaces removed
+    """
+    if isinstance(a, str) and isinstance(b, str):
+        return a.replace(' ', '') == b.replace(' ', '')
+    elif isinstance(a, Iterable) and isinstance(b, Iterable):
+        return all(_equals(a_, b_) for a_, b_ in zip(a, b))
+    else:
+        raise TypeError(f'arguments must be both strings or both lists, not {type(a)}, {type(b)}')
+
+
+def test_model_signature():
+    class Model(BaseModel):
+        a: float = Field(..., title='A')
+        b = Field(10)
+
+    sig = signature(Model)
+    assert sig != signature(BaseModel)
+    assert _equals(map(str, sig.parameters.values()), ('a: float', 'b: int = 10'))
+    assert _equals(str(sig), '(*, a: float, b: int = 10) -> None')
+
+
+def test_custom_init_signature():
+    class MyModel(BaseModel):
+        id: int
+        name: str = 'John Doe'
+        f__: str = Field(..., alias='foo')
+
+        class Config:
+            extra = Extra.allow
+
+        def __init__(self, id: int = 1, bar=2, *, baz: Any, **data):
+            super().__init__(id=id, **data)
+            self.bar = bar
+            self.baz = baz
+
+    sig = signature(MyModel)
+    assert _equals(
+        map(str, sig.parameters.values()),
+        ('id: int = 1', 'bar=2', 'baz: Any', "name: str = 'John Doe'", 'foo: str', '**data'),
+    )
+
+    assert _equals(str(sig), "(id: int = 1, bar=2, *, baz: Any, name: str = 'John Doe', foo: str, **data) -> None")
+
+
+def test_custom_init_signature_with_no_var_kw():
+    class Model(BaseModel):
+        a: float
+        b: int = 2
+        c: int
+
+        def __init__(self, a: float, b: int):
+            super().__init__(a=a, b=b, c=1)
+
+        class Config:
+            extra = Extra.allow
+
+    assert _equals(str(signature(Model)), '(a: float, b: int) -> None')
+
+
+def test_invalid_identifiers_signature():
+    model = create_model(
+        'Model', **{'123 invalid identifier!': Field(123, alias='valid_identifier'), '!': Field(0, alias='yeah')}
+    )
+    assert _equals(str(signature(model)), '(*, valid_identifier: int = 123, yeah: int = 0) -> None')
+    model = create_model('Model', **{'123 invalid identifier!': 123, '!': Field(0, alias='yeah')})
+    assert _equals(str(signature(model)), '(*, yeah: int = 0, **data: Any) -> None')