Repository: samuelcolvin/pydantic (python)

Issue:
Validate towards type hinted function signature without calling the function
### 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 couldn't find an answer
* [X] After submitting this, I commit to one of:
    * Look through open issues and helped at least one other person
    * Hit the "watch" button on this repo to receive notifications and I commit to help at least 2 people that ask questions in the future
    * Implement a Pull Request for a confirmed bug

<!-- Sorry to sound so draconian, but every second saved replying to issues is time spend improving pydantic :-) -->

# Question

Is there a way to validate data input compared to type hinted function signature, _without_ calling the function? I've seen https://pydantic-docs.helpmanual.io/usage/validation_decorator/ but that appears to do the validation check when calling the function.

**Example use case/context:**
User input (e.g. from a configuration file) is used to call different functions and/or initiate different classes. It would be nice to quickly provide the user with a collection of validation feedback in one go, and not having to wait on e.g. the first `X` function calls to be successful while `X+1` turns out to have invalid data input.

Pull request description:
feat: add `validate` method on function to validate input without calling it
<!-- 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 is a proposal after #2127 
I feel like it doesn't cost a lot for _pydantic_ to expose a method to validate inputs for a given function without actually calling it.

**If this feature is accepted, I'll add some documentation of course**

## Related issue number
closes #2127 

## 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
  (see [changes/README.md](https://github.com/samuelcolvin/pydantic/blob/master/changes/README.md) for details)


Patch:
diff --git a/changes/1911-TrDex.md b/changes/1911-TrDex.md
new file mode 100644
index 000000000..997802283
--- /dev/null
+++ b/changes/1911-TrDex.md
@@ -0,0 +1,2 @@
+Add `rediss` (Redis over SSL) protocol to `RedisDsn`
+Allow URLs without `user` part (e.g., `rediss://:pass@localhost`)
\ No newline at end of file
diff --git a/changes/1960-EdwardBetts.md b/changes/1960-EdwardBetts.md
new file mode 100644
index 000000000..c424d4dde
--- /dev/null
+++ b/changes/1960-EdwardBetts.md
@@ -0,0 +1,1 @@
+Use % for percentage in string format of colors
diff --git a/changes/2127-PrettyWood.md b/changes/2127-PrettyWood.md
new file mode 100644
index 000000000..dd39881e6
--- /dev/null
+++ b/changes/2127-PrettyWood.md
@@ -0,0 +1,2 @@
+Add a bound method `validate` to functions decorated with `validate_arguments`
+to validate parameters without actually calling the function
\ No newline at end of file
diff --git a/changes/947-daviskirk.md b/changes/947-daviskirk.md
new file mode 100644
index 000000000..d50a1a1de
--- /dev/null
+++ b/changes/947-daviskirk.md
@@ -0,0 +1,1 @@
+Fix bug where generic models with fields where the typevar is nested in another type `a: List[T]` are considered to be concrete. This allows these models to be subclassed and composed as expected.
diff --git a/docs/examples/types_urls.py b/docs/examples/types_urls.py
index 9874d3a6e..fb49280ff 100644
--- a/docs/examples/types_urls.py
+++ b/docs/examples/types_urls.py
@@ -7,6 +7,7 @@ class MyModel(BaseModel):
 
 m = MyModel(url='http://www.example.com')
 print(m.url)
+
 try:
     MyModel(url='ftp://invalid.url')
 except ValidationError as e:
diff --git a/docs/examples/validation_decorator_validate.py b/docs/examples/validation_decorator_validate.py
new file mode 100644
index 000000000..849b8af0d
--- /dev/null
+++ b/docs/examples/validation_decorator_validate.py
@@ -0,0 +1,17 @@
+from pydantic import validate_arguments, ValidationError
+
+
+@validate_arguments
+def slow_sum(a: int, b: int) -> int:
+    print(f'Called with a={a}, b={b}')
+    return a + b
+
+
+slow_sum(1, 1)
+
+slow_sum.validate(2, 2)
+
+try:
+    slow_sum.validate(1, 'b')
+except ValidationError as exc:
+    print(exc)
diff --git a/docs/usage/types.md b/docs/usage/types.md
index 9a41fb1a7..eda606550 100644
--- a/docs/usage/types.md
+++ b/docs/usage/types.md
@@ -526,8 +526,8 @@ For URI/URL validation the following types are available:
 - `AnyHttpUrl`: schema `http` or `https`, TLD not required
 - `HttpUrl`: schema `http` or `https`, TLD required, max length 2083
 - `PostgresDsn`: schema `postgres` or `postgresql`, user info required, TLD not required
-- `RedisDsn`: schema `redis`, user info not required, tld not required (CHANGED: user info not required from
-  **v1.6** onwards)
+- `RedisDsn`: schema `redis` or `rediss`, user info not required, tld not required (CHANGED: user info
+  not required from **v1.6** onwards), user info may be passed without user part (e.g., `rediss://:pass@localhost`)
 - `stricturl`, method with the following keyword arguments:
     - `strip_whitespace: bool = True`
     - `min_length: int = 1`
diff --git a/docs/usage/validation_decorator.md b/docs/usage/validation_decorator.md
index 3161d0719..03578c826 100644
--- a/docs/usage/validation_decorator.md
+++ b/docs/usage/validation_decorator.md
@@ -29,9 +29,10 @@ As with the rest of *pydantic*, types can be coerced by the decorator before the
 _(This script is complete, it should run "as is")_
 
 A few notes:
-* through they're passed as strings `path` and `regex` are converted to a `Path` object and regex respectively,
-  by the decorator
-* `max` has no type annotation, so will be considered as `Any` by the decorator
+
+- though they're passed as strings, `path` and `regex` are converted to a `Path` object and regex respectively
+by the decorator
+- `max` has no type annotation, so will be considered as `Any` by the decorator
 
 Type coercion like this can be extremely helpful but also confusing or not desired,
 see [below](#coercion-and-strictness) for a discussion of `validate_arguments`'s limitations in this regard.
@@ -61,6 +62,17 @@ defined to return a function with the same signature as the function it decorate
 since we trick mypy into thinking the function returned by the decorator is the same as the function being
 decorated; access to the [raw function](#raw-function) or other attributes will require `type: ignore`.
 
+## Validate without calling the function
+
+By default, arguments validation is done by directly calling the decorated function with parameters.
+But what if you wanted to validate them without *actually* calling the function?
+To do that you can call the `validate` method bound to the decorated function.
+
+```py
+{!.tmp_examples/validation_decorator_validate.py!}
+```
+_(This script is complete, it should run "as is")_
+
 ## Raw function
 
 The raw function which was decorated is accessible, this is useful if in some scenarios you trust your input
diff --git a/pydantic/color.py b/pydantic/color.py
index 97fc76bf5..b074f2e31 100644
--- a/pydantic/color.py
+++ b/pydantic/color.py
@@ -156,10 +156,10 @@ class Color(Representation):
         """
         if self._rgba.alpha is None:
             h, s, li = self.as_hsl_tuple(alpha=False)  # type: ignore
-            return f'hsl({h * 360:0.0f}, {s * 100:0.0f}%, {li * 100:0.0f}%)'
+            return f'hsl({h * 360:0.0f}, {s:0.0%}, {li:0.0%})'
         else:
             h, s, li, a = self.as_hsl_tuple(alpha=True)  # type: ignore
-            return f'hsl({h * 360:0.0f}, {s * 100:0.0f}%, {li * 100:0.0f}%, {round(a, 2)})'
+            return f'hsl({h * 360:0.0f}, {s:0.0%}, {li:0.0%}, {round(a, 2)})'
 
     def as_hsl_tuple(self, *, alpha: Optional[bool] = None) -> HslColorTuple:
         """
diff --git a/pydantic/decorator.py b/pydantic/decorator.py
index 0d7ca8822..98bc4e21b 100644
--- a/pydantic/decorator.py
+++ b/pydantic/decorator.py
@@ -52,6 +52,7 @@ def validate_arguments(func: Optional['AnyCallableT'] = None, *, config: 'Config
             return vd.call(*args, **kwargs)
 
         wrapper_function.vd = vd  # type: ignore
+        wrapper_function.validate = vd.init_model_instance  # type: ignore
         wrapper_function.raw_function = vd.raw_function  # type: ignore
         wrapper_function.model = vd.model  # type: ignore
         return wrapper_function
@@ -134,9 +135,12 @@ class ValidatedFunction:
 
         self.create_model(fields, takes_args, takes_kwargs, config)
 
-    def call(self, *args: Any, **kwargs: Any) -> Any:
+    def init_model_instance(self, *args: Any, **kwargs: Any) -> BaseModel:
         values = self.build_values(args, kwargs)
-        m = self.model(**values)
+        return self.model(**values)
+
+    def call(self, *args: Any, **kwargs: Any) -> Any:
+        m = self.init_model_instance(*args, **kwargs)
         return self.execute(m)
 
     def build_values(self, args: Tuple[Any, ...], kwargs: Dict[str, Any]) -> Dict[str, Any]:
diff --git a/pydantic/generics.py b/pydantic/generics.py
index 76be7599b..be09b50a5 100644
--- a/pydantic/generics.py
+++ b/pydantic/generics.py
@@ -1,10 +1,14 @@
 import sys
+import typing
 from typing import (
     TYPE_CHECKING,
     Any,
     ClassVar,
     Dict,
     Generic,
+    Iterable,
+    Iterator,
+    List,
     Optional,
     Tuple,
     Type,
@@ -17,8 +21,8 @@ from typing import (
 from .class_validators import gather_all_validators
 from .fields import FieldInfo, ModelField
 from .main import BaseModel, create_model
-from .typing import get_origin
-from .utils import lenient_issubclass
+from .typing import display_as_type, get_args, get_origin, typing_base
+from .utils import all_identical, lenient_issubclass
 
 _generic_types_cache: Dict[Tuple[Type[Any], Union[Any, Tuple[Any, ...]]], Type[BaseModel]] = {}
 GenericModelT = TypeVar('GenericModelT', bound='GenericModel')
@@ -37,6 +41,16 @@ class GenericModel(BaseModel):
 
     # Setting the return type as Type[Any] instead of Type[BaseModel] prevents PyCharm warnings
     def __class_getitem__(cls: Type[GenericModelT], params: Union[Type[Any], Tuple[Type[Any], ...]]) -> Type[Any]:
+        """Instantiates a new class from a generic class `cls` and type variables `params`.
+
+        :param params: Tuple of types the class . Given a generic class
+            `Model` with 2 type variables and a concrete model `Model[str, int]`,
+            the value `(str, int)` would be passed to `params`.
+        :return: New model class inheriting from `cls` with instantiated
+            types described by `params`. If no parameters are given, `cls` is
+            returned as is.
+
+        """
         cached = _generic_types_cache.get((cls, params))
         if cached is not None:
             return cached
@@ -50,16 +64,24 @@ class GenericModel(BaseModel):
             raise TypeError(f'Type {cls.__name__} must inherit from typing.Generic before being parameterized')
 
         check_parameters_count(cls, params)
+        # Build map from generic typevars to passed params
         typevars_map: Dict[TypeVarType, Type[Any]] = dict(zip(cls.__parameters__, params))
+        if all_identical(typevars_map.keys(), typevars_map.values()) and typevars_map:
+            return cls  # if arguments are equal to parameters it's the same object
+
+        # Recursively walk class type hints and replace generic typevars
+        # with concrete types that were passed.
         type_hints = get_type_hints(cls).items()
         instance_type_hints = {k: v for k, v in type_hints if get_origin(v) is not ClassVar}
         concrete_type_hints: Dict[str, Type[Any]] = {
-            k: resolve_type_hint(v, typevars_map) for k, v in instance_type_hints.items()
+            k: replace_types(v, typevars_map) for k, v in instance_type_hints.items()
         }
 
+        # Create new model with original model as parent inserting fields with
+        # updated type hints.
         model_name = cls.__concrete_name__(params)
         validators = gather_all_validators(cls)
-        fields = _build_generic_fields(cls.__fields__, concrete_type_hints, typevars_map)
+        fields = _build_generic_fields(cls.__fields__, concrete_type_hints)
         model_module, called_globally = get_caller_frame_info()
         created_model = cast(
             Type[GenericModel],  # casting ensures mypy is aware of the __concrete__ and __parameters__ attributes
@@ -82,12 +104,20 @@ class GenericModel(BaseModel):
                 reference_name += '_'
 
         created_model.Config = cls.Config
-        concrete = all(not _is_typevar(v) for v in concrete_type_hints.values())
-        created_model.__concrete__ = concrete
-        if not concrete:
-            parameters = tuple(v for v in concrete_type_hints.values() if _is_typevar(v))
-            parameters = tuple({k: None for k in parameters}.keys())  # get unique params while maintaining order
-            created_model.__parameters__ = parameters
+
+        # Find any typevars that are still present in the model.
+        # If none are left, the model is fully "concrete", otherwise the new
+        # class is a generic class as well taking the found typevars as
+        # parameters.
+        new_params = tuple(
+            {param: None for param in iter_contained_typevars(typevars_map.values())}
+        )  # use dict as ordered set
+        created_model.__concrete__ = not new_params
+        if new_params:
+            created_model.__parameters__ = new_params
+
+        # Save created model in cache so we don't end up creating duplicate
+        # models that should be identical.
         _generic_types_cache[(cls, params)] = created_model
         if len(params) == 1:
             _generic_types_cache[(cls, params[0])] = created_model
@@ -95,19 +125,74 @@ class GenericModel(BaseModel):
 
     @classmethod
     def __concrete_name__(cls: Type[Any], params: Tuple[Type[Any], ...]) -> str:
+        """Compute class name for child classes.
+
+        :param params: Tuple of types the class . Given a generic class
+            `Model` with 2 type variables and a concrete model `Model[str, int]`,
+            the value `(str, int)` would be passed to `params`.
+        :return: String representing a the new class where `params` are
+            passed to `cls` as type variables.
+
+        This method can be overridden to achieve a custom naming scheme for GenericModels.
         """
-        This method can be overridden to achieve a custom naming scheme for GenericModels
-        """
-        param_names = [param.__name__ if hasattr(param, '__name__') else str(param) for param in params]
+        param_names = [display_as_type(param) for param in params]
         params_component = ', '.join(param_names)
         return f'{cls.__name__}[{params_component}]'
 
 
-def resolve_type_hint(type_: Any, typevars_map: Dict[Any, Any]) -> Type[Any]:
-    if get_origin(type_) and getattr(type_, '__parameters__', None):
-        concrete_type_args = tuple([typevars_map[x] for x in type_.__parameters__])
-        return type_[concrete_type_args]
-    return typevars_map.get(type_, type_)
+def replace_types(type_: Any, type_map: Dict[Any, Any]) -> Any:
+    """Return type with all occurances of `type_map` keys recursively replaced with their values.
+
+    :param type_: Any type, class or generic alias
+    :type_map: Mapping from `TypeVar` instance to concrete types.
+    :return: New type representing the basic structure of `type_` with all
+        `typevar_map` keys recursively replaced.
+
+    >>> replace_types(Tuple[str, Union[List[str], float]], {str: int})
+    Tuple[int, Union[List[int], float]]
+
+    """
+    if not type_map:
+        return type_
+
+    type_args = get_args(type_)
+    origin_type = get_origin(type_)
+
+    # Having type args is a good indicator that this is a typing module
+    # class instantiation or a generic alias of some sort.
+    if type_args:
+        resolved_type_args = tuple(replace_types(arg, type_map) for arg in type_args)
+        if all_identical(type_args, resolved_type_args):
+            # If all arguments are the same, there is no need to modify the
+            # type or create a new object at all
+            return type_
+        if origin_type is not None and isinstance(type_, typing_base) and not isinstance(origin_type, typing_base):
+            # In python < 3.9 generic aliases don't exist so any of these like `list`,
+            # `type` or `collections.abc.Callable` need to be translated.
+            # See: https://www.python.org/dev/peps/pep-0585
+            origin_type = getattr(typing, type_._name)
+        return origin_type[resolved_type_args]
+
+    # We handle pydantic generic models separately as they don't have the same
+    # semantics as "typing" classes or generic aliases
+    if not origin_type and lenient_issubclass(type_, GenericModel) and not type_.__concrete__:
+        type_args = type_.__parameters__
+        resolved_type_args = tuple(replace_types(t, type_map) for t in type_args)
+        if all_identical(type_args, resolved_type_args):
+            return type_
+        return type_[resolved_type_args]
+
+    # Handle special case for typehints that can have lists as arguments.
+    # `typing.Callable[[int, str], int]` is an example for this.
+    if isinstance(type_, (List, list)):
+        resolved_list = list(replace_types(element, type_map) for element in type_)
+        if all_identical(type_, resolved_list):
+            return type_
+        return resolved_list
+
+    # If all else fails, we try to resolve the type directly and otherwise just
+    # return the input with no modifications.
+    return type_map.get(type_, type_)
 
 
 def check_parameters_count(cls: Type[GenericModel], parameters: Tuple[Any, ...]) -> None:
@@ -118,27 +203,26 @@ def check_parameters_count(cls: Type[GenericModel], parameters: Tuple[Any, ...])
         raise TypeError(f'Too {description} parameters for {cls.__name__}; actual {actual}, expected {expected}')
 
 
+def iter_contained_typevars(v: Any) -> Iterator[TypeVarType]:
+    """Recursively iterate through all subtypes and type args of `v` and yield any typevars that are found."""
+    if isinstance(v, TypeVar):
+        yield v
+    elif hasattr(v, '__parameters__') and not get_origin(v) and lenient_issubclass(v, GenericModel):
+        yield from v.__parameters__
+    elif isinstance(v, Iterable):
+        for var in v:
+            yield from iter_contained_typevars(var)
+    else:
+        args = get_args(v)
+        for arg in args:
+            yield from iter_contained_typevars(arg)
+
+
 def _build_generic_fields(
     raw_fields: Dict[str, ModelField],
     concrete_type_hints: Dict[str, Type[Any]],
-    typevars_map: Dict[TypeVarType, Type[Any]],
 ) -> Dict[str, Tuple[Type[Any], FieldInfo]]:
-    return {
-        k: (_parameterize_generic_field(v, typevars_map), raw_fields[k].field_info)
-        for k, v in concrete_type_hints.items()
-        if k in raw_fields
-    }
-
-
-def _parameterize_generic_field(field_type: Type[Any], typevars_map: Dict[TypeVarType, Type[Any]]) -> Type[Any]:
-    if lenient_issubclass(field_type, GenericModel) and not field_type.__concrete__:
-        parameters = tuple(typevars_map.get(param, param) for param in field_type.__parameters__)
-        field_type = field_type[parameters]
-    return field_type
-
-
-def _is_typevar(v: Any) -> bool:
-    return isinstance(v, TypeVar)
+    return {k: (v, raw_fields[k].field_info) for k, v in concrete_type_hints.items() if k in raw_fields}
 
 
 def get_caller_frame_info() -> Tuple[Optional[str], bool]:
diff --git a/pydantic/networks.py b/pydantic/networks.py
index 2418e5b6c..8f44eedb7 100644
--- a/pydantic/networks.py
+++ b/pydantic/networks.py
@@ -43,7 +43,6 @@ __all__ = [
     'validate_email',
 ]
 
-
 _url_regex_cache = None
 _ascii_domain_regex_cache = None
 _int_domain_regex_cache = None
@@ -54,7 +53,7 @@ def url_regex() -> Pattern[str]:
     if _url_regex_cache is None:
         _url_regex_cache = re.compile(
             r'(?:(?P<scheme>[a-z][a-z0-9+\-.]+)://)?'  # scheme https://tools.ietf.org/html/rfc3986#appendix-A
-            r'(?:(?P<user>[^\s:/]+)(?::(?P<password>[^\s/]*))?@)?'  # user info
+            r'(?:(?P<user>[^\s:/]*)(?::(?P<password>[^\s/]*))?@)?'  # user info
             r'(?:'
             r'(?P<ipv4>(?:\d{1,3}\.){3}\d{1,3})|'  # ipv4
             r'(?P<ipv6>\[[A-F0-9]*:[A-F0-9:]+\])|'  # ipv6
@@ -147,8 +146,9 @@ class AnyUrl(str):
         url = scheme + '://'
         if user:
             url += user
-            if password:
-                url += ':' + password
+        if password:
+            url += ':' + password
+        if user or password:
             url += '@'
         url += host
         if port:
@@ -183,20 +183,7 @@ class AnyUrl(str):
         assert m, 'URL regex failed unexpectedly'
 
         parts = m.groupdict()
-        scheme = parts['scheme']
-        if scheme is None:
-            raise errors.UrlSchemeError()
-
-        if cls.allowed_schemes and scheme.lower() not in cls.allowed_schemes:
-            raise errors.UrlSchemePermittedError(cls.allowed_schemes)
-
-        port = parts['port']
-        if port is not None and int(port) > 65_535:
-            raise errors.UrlPortError()
-
-        user = parts['user']
-        if cls.user_required and user is None:
-            raise errors.UrlUserInfoError()
+        parts = cls.validate_parts(parts)
 
         host, tld, host_type, rebuild = cls.validate_host(parts)
 
@@ -205,18 +192,41 @@ class AnyUrl(str):
 
         return cls(
             None if rebuild else url,
-            scheme=scheme,
-            user=user,
+            scheme=parts['scheme'],
+            user=parts['user'],
             password=parts['password'],
             host=host,
             tld=tld,
             host_type=host_type,
-            port=port,
+            port=parts['port'],
             path=parts['path'],
             query=parts['query'],
             fragment=parts['fragment'],
         )
 
+    @classmethod
+    def validate_parts(cls, parts: Dict[str, str]) -> Dict[str, str]:
+        """
+        A method used to validate parts of an URL.
+        Could be overridden to set default values for parts if missing
+        """
+        scheme = parts['scheme']
+        if scheme is None:
+            raise errors.UrlSchemeError()
+
+        if cls.allowed_schemes and scheme.lower() not in cls.allowed_schemes:
+            raise errors.UrlSchemePermittedError(cls.allowed_schemes)
+
+        port = parts['port']
+        if port is not None and int(port) > 65_535:
+            raise errors.UrlPortError()
+
+        user = parts['user']
+        if cls.user_required and user is None:
+            raise errors.UrlUserInfoError()
+
+        return parts
+
     @classmethod
     def validate_host(cls, parts: Dict[str, str]) -> Tuple[str, Optional[str], str, bool]:
         host, tld, host_type, rebuild = None, None, None, False
@@ -279,7 +289,19 @@ class PostgresDsn(AnyUrl):
 
 
 class RedisDsn(AnyUrl):
-    allowed_schemes = {'redis'}
+    allowed_schemes = {'redis', 'rediss'}
+
+    @classmethod
+    def validate_parts(cls, parts: Dict[str, str]) -> Dict[str, str]:
+        defaults = {
+            'domain': 'localhost' if not (parts['ipv4'] or parts['ipv6']) else '',
+            'port': '6379',
+            'path': '/0',
+        }
+        for key, value in defaults.items():
+            if not parts[key]:
+                parts[key] = value
+        return super().validate_parts(parts)
 
 
 def stricturl(
diff --git a/pydantic/typing.py b/pydantic/typing.py
index e71228f67..16d27fc48 100644
--- a/pydantic/typing.py
+++ b/pydantic/typing.py
@@ -25,6 +25,12 @@ try:
 except ImportError:
     from typing import _Final as typing_base  # type: ignore
 
+try:
+    from typing import GenericAlias  # type: ignore
+except ImportError:
+    # python < 3.9 does not have GenericAlias (list[int], tuple[str, ...] and so on)
+    GenericAlias = ()
+
 
 if sys.version_info < (3, 7):
     if TYPE_CHECKING:
@@ -74,7 +80,7 @@ else:
     AnyCallable = TypingCallable[..., Any]
     NoArgAnyCallable = TypingCallable[[], Any]
 
-if sys.version_info < (3, 8):
+if sys.version_info < (3, 8):  # noqa: C901
     if TYPE_CHECKING:
         from typing_extensions import Literal
     else:  # due to different mypy warnings raised during CI for python 3.7 and 3.8
@@ -83,8 +89,34 @@ if sys.version_info < (3, 8):
         except ImportError:
             Literal = None
 
-    def get_args(t: Type[Any]) -> Tuple[Any, ...]:
-        return getattr(t, '__args__', ())
+    if sys.version_info < (3, 7):
+
+        def get_args(t: Type[Any]) -> Tuple[Any, ...]:
+            """Simplest get_args compatability layer possible.
+
+            The Python 3.6 typing module does not have `_GenericAlias` so
+            this won't work for everything. In particular this will not
+            support the `generics` module (we don't support generic models in
+            python 3.6).
+
+            """
+            return getattr(t, '__args__', ())
+
+    else:
+        from typing import _GenericAlias
+
+        def get_args(t: Type[Any]) -> Tuple[Any, ...]:
+            """Compatability version of get_args for python 3.7.
+
+            Mostly compatible with the python 3.8 `typing` module version
+            and able to handle almost all use cases.
+            """
+            if isinstance(t, _GenericAlias):
+                res = t.__args__
+                if t.__origin__ is Callable and res and res[0] is not Ellipsis:
+                    res = (list(res[:-1]), res[-1])
+                return res
+            return getattr(t, '__args__', ())
 
     def get_origin(t: Type[Any]) -> Optional[Type[Any]]:
         return getattr(t, '__origin__', None)
@@ -179,7 +211,7 @@ NoneType = None.__class__
 
 
 def display_as_type(v: Type[Any]) -> str:
-    if not isinstance(v, typing_base) and not isinstance(v, type):
+    if not isinstance(v, typing_base) and not isinstance(v, GenericAlias) and not isinstance(v, type):
         v = v.__class__
 
     if isinstance(v, type) and issubclass(v, Enum):
@@ -190,6 +222,10 @@ def display_as_type(v: Type[Any]) -> str:
         else:
             return 'enum'
 
+    if isinstance(v, GenericAlias):
+        # Generic alias are constructs like `list[int]`
+        return str(v).replace('typing.', '')
+
     try:
         return v.__name__
     except AttributeError:
diff --git a/pydantic/utils.py b/pydantic/utils.py
index c75f4e1cf..1a2c2cbe6 100644
--- a/pydantic/utils.py
+++ b/pydantic/utils.py
@@ -2,7 +2,7 @@ import warnings
 import weakref
 from collections import OrderedDict, defaultdict, deque
 from copy import deepcopy
-from itertools import islice
+from itertools import islice, zip_longest
 from types import BuiltinFunctionType, CodeType, FunctionType, GeneratorType, LambdaType, ModuleType
 from typing import (
     TYPE_CHECKING,
@@ -11,6 +11,7 @@ from typing import (
     Callable,
     Dict,
     Generator,
+    Iterable,
     Iterator,
     List,
     Mapping,
@@ -639,3 +640,21 @@ def is_valid_private_name(name: str) -> bool:
         '__orig_bases__',
         '__qualname__',
     }
+
+
+_EMPTY = object()
+
+
+def all_identical(left: Iterable[Any], right: Iterable[Any]) -> bool:
+    """Check that the items of `left` are the same objects as those in `right`.
+
+    >>> a, b = object(), object()
+    >>> all_identical([a, b, a], [a, b, a])
+    True
+    >>> all_identical([a, b, [a]], [a, b, [a]])  # new list object, while "equal" is not "identical"
+    False
+    """
+    for left_item, right_item in zip_longest(left, right, fillvalue=_EMPTY):
+        if left_item is not right_item:
+            return False
+    return True


Test patch:
diff --git a/tests/test_decorator.py b/tests/test_decorator.py
index 4710d5141..ed7efd3ba 100644
--- a/tests/test_decorator.py
+++ b/tests/test_decorator.py
@@ -318,3 +318,17 @@ def test_config_arbitrary_types_allowed():
             'ctx': {'expected_arbitrary_type': 'EggBox'},
         },
     ]
+
+
+def test_validate(mocker):
+    stub = mocker.stub(name='on_something_stub')
+
+    @validate_arguments
+    def func(s: str, count: int, *, separator: bytes = b''):
+        stub(s, count, separator)
+
+    func.validate('qwe', 2)
+    with pytest.raises(ValidationError):
+        func.validate(['qwe'], 2)
+
+    stub.assert_not_called()
diff --git a/tests/test_generics.py b/tests/test_generics.py
index ecd39b8f3..07847e471 100644
--- a/tests/test_generics.py
+++ b/tests/test_generics.py
@@ -1,11 +1,11 @@
 import sys
 from enum import Enum
-from typing import Any, ClassVar, Dict, Generic, List, Optional, Tuple, Type, TypeVar, Union
+from typing import Any, Callable, ClassVar, Dict, Generic, List, Optional, Sequence, Tuple, Type, TypeVar, Union
 
 import pytest
 
 from pydantic import BaseModel, Field, ValidationError, root_validator, validator
-from pydantic.generics import GenericModel, _generic_types_cache
+from pydantic.generics import GenericModel, _generic_types_cache, iter_contained_typevars, replace_types
 
 skip_36 = pytest.mark.skipif(sys.version_info < (3, 7), reason='generics only supported for python 3.7 and above')
 
@@ -17,7 +17,10 @@ def test_generic_name():
     class Result(GenericModel, Generic[data_type]):
         data: data_type
 
-    assert Result[List[int]].__name__ == 'Result[typing.List[int]]'
+    if sys.version_info >= (3, 9):
+        assert Result[list[int]].__name__ == 'Result[list[int]]'
+    assert Result[List[int]].__name__ == 'Result[List[int]]'
+    assert Result[int].__name__ == 'Result[int]'
 
 
 @skip_36
@@ -248,35 +251,6 @@ def test_generic_config():
         result.data = 2
 
 
-@skip_36
-def test_deep_generic():
-    T = TypeVar('T')
-    S = TypeVar('S')
-    R = TypeVar('R')
-
-    class OuterModel(GenericModel, Generic[T, S, R]):
-        a: Dict[R, Optional[List[T]]]
-        b: Optional[Union[S, R]]
-        c: R
-        d: float
-
-    class InnerModel(GenericModel, Generic[T, R]):
-        c: T
-        d: R
-
-    class NormalModel(BaseModel):
-        e: int
-        f: str
-
-    inner_model = InnerModel[int, str]
-    generic_model = OuterModel[inner_model, NormalModel, int]
-
-    inner_models = [inner_model(c=1, d='a')]
-    generic_model(a={1: inner_models, 2: None}, b=None, c=1, d=1.5)
-    generic_model(a={}, b=NormalModel(e=1, f='a'), c=1, d=1.5)
-    generic_model(a={}, b=1, c=1, d=1.5)
-
-
 @skip_36
 def test_enum_generic():
     T = TypeVar('T')
@@ -498,6 +472,26 @@ def test_partial_specification():
     ]
 
 
+@skip_36
+def test_partial_specification_with_inner_typevar():
+    AT = TypeVar('AT')
+    BT = TypeVar('BT')
+
+    class Model(GenericModel, Generic[AT, BT]):
+        a: List[AT]
+        b: List[BT]
+
+    partial_model = Model[str, BT]
+    assert partial_model.__concrete__ is False
+    concrete_model = partial_model[int]
+    assert concrete_model.__concrete__ is True
+
+    # nested resolution of partial models should work as expected
+    nested_resolved = concrete_model(a=[123], b=['456'])
+    assert nested_resolved.a == ['123']
+    assert nested_resolved.b == [456]
+
+
 @skip_36
 def test_partial_specification_name():
     AT = TypeVar('AT')
@@ -681,7 +675,11 @@ def test_generic_model_from_function_pickle_fail(create_module):
 
 
 @skip_36
-def test_generic_model_redefined_without_cache_fail(create_module):
+def test_generic_model_redefined_without_cache_fail(create_module, monkeypatch):
+
+    # match identity checker otherwise we never get to the redefinition check
+    monkeypatch.setattr('pydantic.generics.all_identical', lambda left, right: False)
+
     @create_module
     def module():
         from typing import Generic, TypeVar
@@ -756,3 +754,264 @@ def test_get_caller_frame_info_when_sys_getframe_undefined():
         assert get_caller_frame_info() == (None, False)
     finally:  # just to make sure we always setting original attribute back
         sys._getframe = getframe
+
+
+@skip_36
+def test_iter_contained_typevars():
+    T = TypeVar('T')
+    T2 = TypeVar('T2')
+
+    class Model(GenericModel, Generic[T]):
+        a: T
+
+    assert list(iter_contained_typevars(Model[T])) == [T]
+    assert list(iter_contained_typevars(Optional[List[Union[str, Model[T]]]])) == [T]
+    assert list(iter_contained_typevars(Optional[List[Union[str, Model[int]]]])) == []
+    assert list(iter_contained_typevars(Optional[List[Union[str, Model[T], Callable[[T2, T], str]]]])) == [T, T2, T]
+
+
+@skip_36
+def test_nested_identity_parameterization():
+    T = TypeVar('T')
+    T2 = TypeVar('T2')
+
+    class Model(GenericModel, Generic[T]):
+        a: T
+
+    assert Model[T][T][T] is Model
+    assert Model[T] is Model
+    assert Model[T2] is not Model
+
+
+@skip_36
+def test_replace_types():
+    T = TypeVar('T')
+
+    class Model(GenericModel, Generic[T]):
+        a: T
+
+    assert replace_types(T, {T: int}) is int
+    assert replace_types(List[Union[str, list, T]], {T: int}) == List[Union[str, list, int]]
+    assert replace_types(Callable, {T: int}) == Callable
+    assert replace_types(Callable[[int, str, T], T], {T: int}) == Callable[[int, str, int], int]
+    assert replace_types(T, {}) is T
+    assert replace_types(Model[List[T]], {T: int}) == Model[List[T]][int]
+    assert replace_types(T, {}) is T
+    assert replace_types(Type[T], {T: int}) == Type[int]
+    assert replace_types(Model[T], {T: T}) == Model[T]
+
+    if sys.version_info >= (3, 9):
+        # Check generic aliases (subscripted builtin types) to make sure they
+        # resolve correctly (don't get translated to typing versions for
+        # example)
+        assert replace_types(list[Union[str, list, T]], {T: int}) == list[Union[str, list, int]]
+
+
+@skip_36
+def test_replace_types_identity_on_unchanged():
+    T = TypeVar('T')
+    U = TypeVar('U')
+
+    type_ = List[Union[str, Callable[[list], Optional[str]], U]]
+    assert replace_types(type_, {T: int}) is type_
+
+
+@skip_36
+def test_deep_generic():
+    T = TypeVar('T')
+    S = TypeVar('S')
+    R = TypeVar('R')
+
+    class OuterModel(GenericModel, Generic[T, S, R]):
+        a: Dict[R, Optional[List[T]]]
+        b: Optional[Union[S, R]]
+        c: R
+        d: float
+
+    class InnerModel(GenericModel, Generic[T, R]):
+        c: T
+        d: R
+
+    class NormalModel(BaseModel):
+        e: int
+        f: str
+
+    inner_model = InnerModel[int, str]
+    generic_model = OuterModel[inner_model, NormalModel, int]
+
+    inner_models = [inner_model(c=1, d='a')]
+    generic_model(a={1: inner_models, 2: None}, b=None, c=1, d=1.5)
+    generic_model(a={}, b=NormalModel(e=1, f='a'), c=1, d=1.5)
+    generic_model(a={}, b=1, c=1, d=1.5)
+
+    assert InnerModel.__concrete__ is False
+    assert inner_model.__concrete__ is True
+
+
+@skip_36
+def test_deep_generic_with_inner_typevar():
+    T = TypeVar('T')
+
+    class OuterModel(GenericModel, Generic[T]):
+        a: List[T]
+
+    class InnerModel(OuterModel[T], Generic[T]):
+        pass
+
+    assert InnerModel[int].__concrete__ is True
+    assert InnerModel.__concrete__ is False
+
+    with pytest.raises(ValidationError):
+        InnerModel[int](a=['wrong'])
+    assert InnerModel[int](a=['1']).a == [1]
+
+
+@skip_36
+def test_deep_generic_with_referenced_generic():
+    T = TypeVar('T')
+    R = TypeVar('R')
+
+    class ReferencedModel(GenericModel, Generic[R]):
+        a: R
+
+    class OuterModel(GenericModel, Generic[T]):
+        a: ReferencedModel[T]
+
+    class InnerModel(OuterModel[T], Generic[T]):
+        pass
+
+    assert InnerModel[int].__concrete__ is True
+    assert InnerModel.__concrete__ is False
+
+    with pytest.raises(ValidationError):
+        InnerModel[int](a={'a': 'wrong'})
+    assert InnerModel[int](a={'a': 1}).a.a == 1
+
+
+@skip_36
+def test_deep_generic_with_referenced_inner_generic():
+    T = TypeVar('T')
+
+    class ReferencedModel(GenericModel, Generic[T]):
+        a: T
+
+    class OuterModel(GenericModel, Generic[T]):
+        a: Optional[List[Union[ReferencedModel[T], str]]]
+
+    class InnerModel(OuterModel[T], Generic[T]):
+        pass
+
+    assert InnerModel[int].__concrete__ is True
+    assert InnerModel.__concrete__ is False
+
+    with pytest.raises(ValidationError):
+        InnerModel[int](a=['s', {'a': 'wrong'}])
+    assert InnerModel[int](a=['s', {'a': 1}]).a[1].a == 1
+
+    assert InnerModel[int].__fields__['a'].outer_type_ == List[Union[ReferencedModel[int], str]]
+    assert (InnerModel[int].__fields__['a'].sub_fields[0].sub_fields[0].outer_type_.__fields__['a'].outer_type_) == int
+
+
+@skip_36
+def test_deep_generic_with_multiple_typevars():
+    T = TypeVar('T')
+    U = TypeVar('U')
+
+    class OuterModel(GenericModel, Generic[T]):
+        data: List[T]
+
+    class InnerModel(OuterModel[T], Generic[U, T]):
+        extra: U
+
+    ConcreteInnerModel = InnerModel[int, float]
+    assert ConcreteInnerModel.__fields__['data'].outer_type_ == List[float]
+    assert ConcreteInnerModel.__fields__['extra'].outer_type_ == int
+
+    assert ConcreteInnerModel(data=['1'], extra='2').dict() == {'data': [1.0], 'extra': 2}
+
+
+@skip_36
+def test_deep_generic_with_multiple_inheritance():
+    K = TypeVar('K')
+    V = TypeVar('V')
+    T = TypeVar('T')
+
+    class OuterModelA(GenericModel, Generic[K, V]):
+        data: Dict[K, V]
+
+    class OuterModelB(GenericModel, Generic[T]):
+        stuff: List[T]
+
+    class InnerModel(OuterModelA[K, V], OuterModelB[T], Generic[K, V, T]):
+        extra: int
+
+    ConcreteInnerModel = InnerModel[int, float, str]
+
+    assert ConcreteInnerModel.__fields__['data'].outer_type_ == Dict[int, float]
+    assert ConcreteInnerModel.__fields__['stuff'].outer_type_ == List[str]
+    assert ConcreteInnerModel.__fields__['extra'].outer_type_ == int
+
+    ConcreteInnerModel(data={1.1: '5'}, stuff=[123], extra=5).dict() == {
+        'data': {1: 5},
+        'stuff': ['123'],
+        'extra': 5,
+    }
+
+
+@skip_36
+def test_generic_with_referenced_generic_type_1():
+    T = TypeVar('T')
+
+    class ModelWithType(GenericModel, Generic[T]):
+        # Type resolves to type origin of "type" which is non-subscriptible for
+        # python < 3.9 so we want to make sure it works for other versions
+        some_type: Type[T]
+
+    class ReferenceModel(GenericModel, Generic[T]):
+        abstract_base_with_type: ModelWithType[T]
+
+    ReferenceModel[int]
+
+
+@skip_36
+def test_generic_with_referenced_nested_typevar():
+    T = TypeVar('T')
+
+    class ModelWithType(GenericModel, Generic[T]):
+        # Type resolves to type origin of "collections.abc.Sequence" which is
+        # non-subscriptible for
+        # python < 3.9 so we want to make sure it works for other versions
+        some_type: Sequence[T]
+
+    class ReferenceModel(GenericModel, Generic[T]):
+        abstract_base_with_type: ModelWithType[T]
+
+    ReferenceModel[int]
+
+
+@skip_36
+def test_generic_with_callable():
+    T = TypeVar('T')
+
+    class Model(GenericModel, Generic[T]):
+        # Callable is a test for any type that accepts a list as an argument
+        some_callable: Callable[[Optional[int], T], None]
+
+    Model[str].__concrete__ is True
+    Model.__concrete__ is False
+
+
+@skip_36
+def test_generic_with_partial_callable():
+    T = TypeVar('T')
+    U = TypeVar('U')
+
+    class Model(GenericModel, Generic[T, U]):
+        t: T
+        u: U
+        # Callable is a test for any type that accepts a list as an argument
+        some_callable: Callable[[Optional[int], str], None]
+
+    Model[str, U].__concrete__ is False
+    Model[str, U].__parameters__ == [U]
+    Model[str, int].__concrete__ is False
diff --git a/tests/test_networks.py b/tests/test_networks.py
index c69ba0a78..256fcd0ca 100644
--- a/tests/test_networks.py
+++ b/tests/test_networks.py
@@ -190,6 +190,13 @@ def test_user_no_password():
     assert url.host == 'example.org'
 
 
+def test_user_info_no_user():
+    url = validate_url('http://:password@example.org')
+    assert url.user == ''
+    assert url.password == 'password'
+    assert url.host == 'example.org'
+
+
 def test_at_in_path():
     url = validate_url('https://twitter.com/@handle')
     assert url.scheme == 'https'
@@ -321,21 +328,35 @@ def test_redis_dsns():
     class Model(BaseModel):
         a: RedisDsn
 
-    m = Model(a='redis://user:pass@localhost:5432/app')
-    assert m.a == 'redis://user:pass@localhost:5432/app'
+    m = Model(a='redis://user:pass@localhost:1234/app')
+    assert m.a == 'redis://user:pass@localhost:1234/app'
     assert m.a.user == 'user'
     assert m.a.password == 'pass'
 
+    m = Model(a='rediss://user:pass@localhost:1234/app')
+    assert m.a == 'rediss://user:pass@localhost:1234/app'
+
+    m = Model(a='rediss://:pass@localhost:1234')
+    assert m.a == 'rediss://:pass@localhost:1234/0'
+
     with pytest.raises(ValidationError) as exc_info:
         Model(a='http://example.org')
     assert exc_info.value.errors()[0]['type'] == 'value_error.url.scheme'
 
-    # password is not required for redis
-    m = Model(a='redis://localhost:5432/app')
-    assert m.a == 'redis://localhost:5432/app'
+    # Password is not required for Redis protocol
+    m = Model(a='redis://localhost:1234/app')
+    assert m.a == 'redis://localhost:1234/app'
     assert m.a.user is None
     assert m.a.password is None
 
+    # Only schema is required for Redis protocol. Otherwise it will be set to default
+    # https://www.iana.org/assignments/uri-schemes/prov/redis
+    m = Model(a='rediss://')
+    assert m.a.scheme == 'rediss'
+    assert m.a.host == 'localhost'
+    assert m.a.port == '6379'
+    assert m.a.path == '/0'
+
 
 def test_custom_schemes():
     class Model(BaseModel):
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 8dd736c38..1850c0f73 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -27,6 +27,7 @@ from pydantic.utils import (
     BUILTIN_COLLECTIONS,
     ClassAttribute,
     ValueItems,
+    all_identical,
     deep_update,
     get_model,
     import_string,
@@ -60,11 +61,18 @@ def test_import_no_attr():
     assert exc_info.value.args[0] == 'Module "os" does not define a "foobar" attribute'
 
 
-@pytest.mark.parametrize('value,expected', ((str, 'str'), ('string', 'str'), (Union[str, int], 'Union[str, int]')))
+@pytest.mark.parametrize(
+    'value,expected', ((str, 'str'), ('string', 'str'), (Union[str, int], 'Union[str, int]'), (list, 'list'))
+)
 def test_display_as_type(value, expected):
     assert display_as_type(value) == expected
 
 
+@pytest.mark.skipif(sys.version_info < (3, 9), reason='generic aliases are not available in python < 3.9')
+def test_display_as_type_generic_alias():
+    assert display_as_type(list[[Union[str, int]]]) == 'list[[Union[str, int]]]'
+
+
 def test_display_as_type_enum():
     class SubField(Enum):
         a = 1
@@ -446,3 +454,18 @@ def test_resolve_annotations_no_module():
     # TODO: is there a better test for this, can this case really happen?
     fr = ForwardRef('Foo')
     assert resolve_annotations({'Foo': ForwardRef('Foo')}, None) == {'Foo': fr}
+
+
+def test_all_identical():
+    a, b = object(), object()
+    c = [b]
+    assert all_identical([a, b], [a, b]) is True
+    assert all_identical([a, b], [a, b]) is True
+    assert all_identical([a, b, b], [a, b, b]) is True
+    assert all_identical([a, c, b], [a, c, b]) is True
+
+    assert all_identical([], [a]) is False, 'Expected iterables with different lengths to evaluate to `False`'
+    assert all_identical([a], []) is False, 'Expected iterables with different lengths to evaluate to `False`'
+    assert (
+        all_identical([a, [b], b], [a, [b], b]) is False
+    ), 'New list objects are different objects and should therefor not be identical.'