Repository: pydantic/pydantic

Issue:
Model signatures contain reserved words.
### Checks

* [x] I added a descriptive title to this issue
* [x] I have searched (google, github) for similar issues and couldn't find anything
* [x] I have read and followed [the docs](https://pydantic-docs.helpmanual.io/) and still think this is a bug

# Bug

When creating a model with a field that has an alias that is a python reserved word, the signature uses the reserved word even when `allow_population_by_field_name` is true.

I expect the signature to use the field name in this case. I came across this issue when generating tests using hypothesis. This uses the model signature and was creating invalid test code as it was using the reserved words in the signature.

```
pydantic version: 1.9.0
pydantic compiled: False
python version: 3.9.0 (tags/v3.9.0:9cf6752, Oct  5 2020, 15:34:40) [MSC v.1927 64 bit (AMD64)]
platform: Windows-10-10.0.19041-SP0
optional deps. installed: ['devtools', 'dotenv', 'email-validator', 'typing-extensions']
```

```py
from pydantic import BaseModel, Field
from inspect import signature

class Foo(BaseModel):
    from_: str = Field(..., alias='from')

    class Config:
        allow_population_by_field_name = True

str(signature(Foo))
```

> '(*, from: str) -> None'

# Suggestion

In `utils.generate_model_signature()` we have these two checks.

```py
elif not param_name.isidentifier():
    if allow_names and field_name.isidentifier():
```

I propose replacing these with calls to the following (new) function.

```py
def is_valid_identifier(identifier: str) -> bool:
    """
    Checks that a string is a valid identifier and not a reserved word.
    :param identifier: The identifier to test.
    :return: True if the identifier is valid.
    """
    return identifier.isidentifier() and not keyword.iskeyword(identifier)
```

I believe that this behaviour is closer to what a user would expect in the common case that they are generating models from a schema containing python reserved words.





Discussion:

Resolving patch:
diff --git a/pydantic/utils.py b/pydantic/utils.py
--- a/pydantic/utils.py
+++ b/pydantic/utils.py
@@ -1,3 +1,4 @@
+import keyword
 import warnings
 import weakref
 from collections import OrderedDict, defaultdict, deque
@@ -56,6 +57,7 @@
     'lenient_isinstance',
     'lenient_issubclass',
     'in_ipython',
+    'is_valid_identifier',
     'deep_update',
     'update_not_none',
     'almost_equal_floats',
@@ -192,6 +194,15 @@ def in_ipython() -> bool:
         return True
 
 
+def is_valid_identifier(identifier: str) -> bool:
+    """
+    Checks that a string is a valid identifier and not a Python keyword.
+    :param identifier: The identifier to test.
+    :return: True if the identifier is valid.
+    """
+    return identifier.isidentifier() and not keyword.iskeyword(identifier)
+
+
 KeyType = TypeVar('KeyType')
 
 
@@ -244,8 +255,8 @@ def generate_model_signature(
             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():
+            elif not is_valid_identifier(param_name):
+                if allow_names and is_valid_identifier(field_name):
                     param_name = field_name
                 else:
                     use_var_kw = True


Test patch:
diff --git a/tests/test_model_signature.py b/tests/test_model_signature.py
--- a/tests/test_model_signature.py
+++ b/tests/test_model_signature.py
@@ -84,6 +84,16 @@ class Config:
     assert _equals(str(signature(Foo)), '(*, foo: str) -> None')
 
 
+def test_does_not_use_reserved_word():
+    class Foo(BaseModel):
+        from_: str = Field(..., alias='from')
+
+        class Config:
+            allow_population_by_field_name = True
+
+    assert _equals(str(signature(Foo)), '(*, from_: str) -> None')
+
+
 def test_extra_allow_no_conflict():
     class Model(BaseModel):
         spam: str