Repository: pydantic/pydantic
Pull request: Feature/add anystr lower to config
<!-- Thank you for your contribution! -->
<!-- Unless your change is trivial, please create an issue to discuss the change before creating a PR -->
<!-- See https://pydantic-docs.helpmanual.io/contributing/ for help on Contributing -->

## Change Summary

This change adds the anystr_lower field to the BaseConfig class, allowing users to ensure all string fields are kept as lower case.

## Related issue number

fix #2134

## Checklist

* [x] Unit tests for the changes exist
* [x] Tests pass on CI and coverage remains at 100%
* [x] Documentation reflects the changes where applicable
* [x] `changes/<pull request or issue id>-<github username>.md` file added describing change

Problem statement:
Adding "anystr_lower" to config
# Feature Request

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
             pydantic version: 1.6.1
            pydantic compiled: True
                 install path: ***
               python version: 3.8.0 (v3.8.0:fa919fdf25, Oct 14 2019, 10:23:27)  [Clang 6.0 (clang-600.0.57)]
                     platform: macOS-10.15.7-x86_64-i386-64bit
     optional deps. installed: ['typing-extensions', 'email-validator']

```
Just like there is "anystr_strip_whitespace" in the config, which runs .strip() on any string value, I would like to add "anystr_lower" which would run .lower() on any string value.

I just find myself use .lower() on the strings in many validators in places in my code, so I thought it would be useful to add it to the config, I assume that was the use-case of "anystr_strip_whitespace" as well.

If this is fine I'll gladly implement it myself, looks like it's a small change.

Hints from issue discussion:

Commit message: added unit tests for anystr_lower config

Patch to review:
diff --git a/changes/2134-tayoogunbiyi.md b/changes/2134-tayoogunbiyi.md
new file mode 100644
index 00000000000..2491250db7d
--- /dev/null
+++ b/changes/2134-tayoogunbiyi.md
@@ -0,0 +1 @@
+* added `Config.anystr_lower` and `lower_str` kwarg to `constr`.
diff --git a/docs/usage/model_config.md b/docs/usage/model_config.md
index f847ad8c044..24219c95190 100644
--- a/docs/usage/model_config.md
+++ b/docs/usage/model_config.md
@@ -8,6 +8,9 @@ Options:
 **`anystr_strip_whitespace`**
 : whether to strip leading and trailing whitespace for str & byte types (default: `False`)
 
+**`anystr_lower`**
+: whether to make all characters lowercase for str & byte types (default: `False`)
+
 **`min_anystr_length`**
 : the min length for str & byte types (default: `0`)
 
diff --git a/pydantic/main.py b/pydantic/main.py
index 4834cbf12bf..c6faa25e514 100644
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -106,6 +106,7 @@ class Extra(str, Enum):
 
 class BaseConfig:
     title = None
+    anystr_lower = False
     anystr_strip_whitespace = False
     min_anystr_length = None
     max_anystr_length = None
diff --git a/pydantic/types.py b/pydantic/types.py
index b54182c7d5d..d006f0e2307 100644
--- a/pydantic/types.py
+++ b/pydantic/types.py
@@ -28,6 +28,7 @@
 from .validators import (
     bytes_validator,
     constr_length_validator,
+    constr_lower,
     constr_strip_whitespace,
     decimal_validator,
     float_validator,
@@ -112,6 +113,7 @@
 
 class ConstrainedBytes(bytes):
     strip_whitespace = False
+    lower_str = False
     min_length: OptionalInt = None
     max_length: OptionalInt = None
     strict: bool = False
@@ -124,6 +126,7 @@ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
     def __get_validators__(cls) -> 'CallableGenerator':
         yield strict_bytes_validator if cls.strict else bytes_validator
         yield constr_strip_whitespace
+        yield constr_lower
         yield constr_length_validator
 
 
@@ -223,6 +226,7 @@ def conset(item_type: Type[T], *, min_items: int = None, max_items: int = None)
 
 class ConstrainedStr(str):
     strip_whitespace = False
+    lower_str = False
     min_length: OptionalInt = None
     max_length: OptionalInt = None
     curtail_length: OptionalInt = None
@@ -239,6 +243,7 @@ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
     def __get_validators__(cls) -> 'CallableGenerator':
         yield strict_str_validator if cls.strict else str_validator
         yield constr_strip_whitespace
+        yield constr_lower
         yield constr_length_validator
         yield cls.validate
 
diff --git a/pydantic/validators.py b/pydantic/validators.py
index c47ee2f4f08..b33a4cc3d79 100644
--- a/pydantic/validators.py
+++ b/pydantic/validators.py
@@ -199,6 +199,10 @@ def anystr_strip_whitespace(v: 'StrBytes') -> 'StrBytes':
     return v.strip()
 
 
+def anystr_lower(v: 'StrBytes') -> 'StrBytes':
+    return v.lower()
+
+
 def ordered_dict_validator(v: Any) -> 'AnyOrderedDict':
     if isinstance(v, OrderedDict):
         return v
@@ -472,6 +476,13 @@ def constr_strip_whitespace(v: 'StrBytes', field: 'ModelField', config: 'BaseCon
     return v
 
 
+def constr_lower(v: 'StrBytes', field: 'ModelField', config: 'BaseConfig') -> 'StrBytes':
+    lower = field.type_.lower_str or config.anystr_lower
+    if lower:
+        v = v.strip()
+    return v
+
+
 def validate_json(v: Any, config: 'BaseConfig') -> Any:
     if v is None:
         # pass None through to other validators
@@ -542,6 +553,7 @@ def check(self, config: Type['BaseConfig']) -> bool:
         [
             str_validator,
             IfConfig(anystr_strip_whitespace, 'anystr_strip_whitespace'),
+            IfConfig(anystr_lower, 'anystr_lower'),
             IfConfig(anystr_length_validator, 'min_anystr_length', 'max_anystr_length'),
         ],
     ),
@@ -550,6 +562,7 @@ def check(self, config: Type['BaseConfig']) -> bool:
         [
             bytes_validator,
             IfConfig(anystr_strip_whitespace, 'anystr_strip_whitespace'),
+            IfConfig(anystr_lower, 'anystr_lower'),
             IfConfig(anystr_length_validator, 'min_anystr_length', 'max_anystr_length'),
         ],
     ),
diff --git a/tests/test_types.py b/tests/test_types.py
index 457817c8f27..b4a68470ae5 100644
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -1521,6 +1521,34 @@ class Config:
     assert m.bytes_check == b'  456  '
 
 
+def test_anystr_lower_enabled():
+    class Model(BaseModel):
+        str_check: str
+        bytes_check: bytes
+
+        class Config:
+            anystr_lower = True
+
+    m = Model(str_check='ABCDefG', bytes_check=b'abCD1Fg')
+
+    assert m.str_check == 'abcdefg'
+    assert m.bytes_check == b'abcd1fg'
+
+
+def test_anystr_lower_disabled():
+    class Model(BaseModel):
+        str_check: str
+        bytes_check: bytes
+
+        class Config:
+            anystr_lower = False
+
+    m = Model(str_check='ABCDefG', bytes_check=b'abCD1Fg')
+
+    assert m.str_check == 'ABCDefG'
+    assert m.bytes_check == b'abCD1Fg'
+
+
 @pytest.mark.parametrize(
     'type_,value,result',
     [


Code review comments:
diff_hunk: @@ -472,6 +476,13 @@ def constr_strip_whitespace(v: 'StrBytes', field: 'ModelField', config: 'BaseCon
     return v
 
 
+def constr_lower(v: 'StrBytes', field: 'ModelField', config: 'BaseConfig') -> 'StrBytes':
+    lower = field.type_.lower_str or config.anystr_lower
+    if lower:
+        v = v.strip()
path: pydantic/validators.py
text: @user1:
```suggestion
        v = v.lower()
```

@user1:
You should probably add a test for `constr`

@author:
thanks. I somehow missed that :+1: