Repository: unionai-oss/pandera

Issue:
SchemaModel not able to use registered checks
**Describe the bug**
`pa.SchemaModel` is not able to use custom checks. 

- [*] I have checked that this issue has not already been reported.
- [*] I have confirmed this bug exists on the latest version of pandas.
- [ ] (optional) I have confirmed this bug exists on the master branch of pandas.

```python

import pandas as pd
import pandera as pa
from pandera import Field
import pandera.extensions as extensions
from pandera.typing import Series


@extensions.register_check_method(statistics=["min_value", "max_value"])
def is_between(pandas_obj, *, min_value, max_value):
    return (min_value <= pandas_obj) & (pandas_obj <= max_value)


df = pd.DataFrame([10], columns=["col"])

schema = pa.DataFrameSchema(
    {"col": pa.Column(int, pa.Check.is_between(min_value=1, max_value=9))}
)

try:
    schema.validate(df)
except pa.errors.SchemaError:
    print("fails as expected")


class Schema(pa.SchemaModel):
    col: Series[int] = Field(is_between={"min_value": 1, "max_value": 9})


Schema.validate(df)
print("does not fail as expected")


```

#### Expected behavior
I would expect Schema.validate(df) to cause an error.

#### Desktop (please complete the following information):

 - Windows
 - Pandera 0.6.0
 


Discussion:

Resolving patch:
diff --git a/pandera/extensions.py b/pandera/extensions.py
--- a/pandera/extensions.py
+++ b/pandera/extensions.py
@@ -139,9 +139,13 @@ def validate_check_kwargs(check_kwargs):
             return check_kwargs
 
         @register_check_statistics(statistics)
-        def check_method(cls, **kwargs):
+        def check_method(cls, *args, **kwargs):
             """Wrapper function that serves as the Check method."""
             stats, check_kwargs = {}, {}
+
+            if args:
+                stats = dict(zip(statistics, args))
+
             for k, v in kwargs.items():
                 if k in statistics:
                     stats[k] = v
diff --git a/pandera/model_components.py b/pandera/model_components.py
--- a/pandera/model_components.py
+++ b/pandera/model_components.py
@@ -15,6 +15,7 @@
 )
 
 from .checks import Check
+from .errors import SchemaInitError
 from .schema_components import (
     Column,
     Index,
@@ -151,11 +152,21 @@ def Field(
 
     *new in 0.5.0*
 
-    Some arguments apply only to number dtypes and some apply only to ``str``.
-    See the :ref:`User Guide <schema_models>` for more.
+    Some arguments apply only to numeric dtypes and some apply only to ``str``.
+    See the :ref:`User Guide <schema_models>` for more information.
 
-    :param alias: The public name of the column/index.
+    The keyword-only arguments from ``eq`` to ``str_startswith`` are dispatched
+    to the built-in `~pandera.checks.Check` methods.
 
+    :param nullable: whether or not the column/index is nullable.
+    :param allow_duplicates: whether or not to accept duplicate values.
+    :param coerce: coerces the data type if ``True``.
+    :param regex: whether or not the field name or alias is a regex pattern.
+    :param ignore_na: whether or not to ignore null values in the checks.
+    :param raise_warning: raise a warning instead of an Exception.
+    :param n_failure_cases: report the first n unique failure cases. If None,
+        report all failure cases.
+    :param alias: The public name of the column/index.
     :param check_name: Whether to check the name of the column/index during
         validation. `None` is the default behavior, which translates to `True`
         for columns and multi-index, and to `False` for a single index.
@@ -170,11 +181,21 @@ def Field(
     }
     args = locals()
     checks = []
-    for arg_name, check_constructor in _check_dispatch.items():
-        arg_value = args[arg_name]
+
+    check_dispatch = _check_dispatch()
+    for key in kwargs:
+        if key not in check_dispatch:
+            raise SchemaInitError(
+                f"custom check '{key}' is not available. Make sure you use "
+                "pandera.extensions.register_check_method decorator to "
+                "register your custom check method."
+            )
+
+    for arg_name, check_constructor in check_dispatch.items():
+        arg_value = args.get(arg_name, kwargs.get(arg_name))
         if arg_value is None:
             continue
-        if arg_name in {"in_range", "str_length"}:
+        if isinstance(arg_value, dict):
             check_ = check_constructor(**arg_value, **check_kwargs)
         else:
             check_ = check_constructor(arg_value, **check_kwargs)
@@ -191,23 +212,24 @@ def Field(
     )
 
 
-_check_dispatch = {
-    "eq": Check.equal_to,
-    "ne": Check.not_equal_to,
-    "gt": Check.greater_than,
-    "ge": Check.greater_than_or_equal_to,
-    "lt": Check.less_than,
-    "le": Check.less_than_or_equal_to,
-    "in_range": Check.in_range,
-    "isin": Check.isin,
-    "notin": Check.notin,
-    "str_contains": Check.str_contains,
-    "str_endswith": Check.str_endswith,
-    "str_matches": Check.str_matches,
-    "str_length": Check.str_length,
-    "str_startswith": Check.str_startswith,
-    **Check.REGISTERED_CUSTOM_CHECKS,
-}
+def _check_dispatch():
+    return {
+        "eq": Check.equal_to,
+        "ne": Check.not_equal_to,
+        "gt": Check.greater_than,
+        "ge": Check.greater_than_or_equal_to,
+        "lt": Check.less_than,
+        "le": Check.less_than_or_equal_to,
+        "in_range": Check.in_range,
+        "isin": Check.isin,
+        "notin": Check.notin,
+        "str_contains": Check.str_contains,
+        "str_endswith": Check.str_endswith,
+        "str_matches": Check.str_matches,
+        "str_length": Check.str_length,
+        "str_startswith": Check.str_startswith,
+        **Check.REGISTERED_CUSTOM_CHECKS,
+    }
 
 
 class CheckInfo:  # pylint:disable=too-few-public-methods


Test patch:
diff --git a/tests/test_extensions.py b/tests/test_extensions.py
--- a/tests/test_extensions.py
+++ b/tests/test_extensions.py
@@ -233,6 +233,7 @@ def custom_ge_check(pandas_obj, *, min_value):
 
 def test_schema_model_field_kwarg(custom_check_teardown):
     """Test that registered checks can be specified in a Field."""
+    # pylint: disable=missing-class-docstring,too-few-public-methods
 
     @extensions.register_check_method(
         statistics=["val"],
@@ -242,6 +243,14 @@ def test_schema_model_field_kwarg(custom_check_teardown):
     def custom_gt(pandas_obj, val):
         return pandas_obj > val
 
+    @extensions.register_check_method(
+        statistics=["min_value", "max_value"],
+        supported_types=(pd.Series, pd.DataFrame),
+        check_type="vectorized",
+    )
+    def custom_in_range(pandas_obj, min_value, max_value):
+        return (min_value <= pandas_obj) & (pandas_obj <= max_value)
+
     class Schema(pa.SchemaModel):
         """Schema that uses registered checks in Field."""
 
@@ -250,14 +259,8 @@ class Schema(pa.SchemaModel):
             custom_in_range={"min_value": -10, "max_value": 10}
         )
 
-    # checks are even accessible in the schema before being registered
-    @extensions.register_check_method(
-        statistics=["val"],
-        supported_types=(pd.Series, pd.DataFrame),
-        check_type="vectorized",
-    )
-    def custom_in_range(pandas_obj, min_value, max_value):
-        return (min_value <= pandas_obj) & (pandas_obj <= max_value)
+        class Config:
+            coerce = True
 
     data = pd.DataFrame(
         {
@@ -268,8 +271,34 @@ def custom_in_range(pandas_obj, min_value, max_value):
     Schema.validate(data)
 
     for invalid_data in [
-        pd.DataFrame({"col1": [0], "col2": [-10]}),
-        pd.DataFrame({"col1": [1000], "col2": [-100]}),
+        pd.DataFrame({"col1": [0], "col2": [-10.0]}),
+        pd.DataFrame({"col1": [1000], "col2": [-100.0]}),
     ]:
         with pytest.raises(pa.errors.SchemaError):
             Schema.validate(invalid_data)
+
+
+def test_register_before_schema_definitions():
+    """Test that custom checks need to be registered before use."""
+    # pylint: disable=missing-class-docstring,too-few-public-methods
+    # pylint: disable=function-redefined
+
+    with pytest.raises(
+        pa.errors.SchemaInitError,
+        match="custom check 'custom_eq' is not available",
+    ):
+
+        class Schema(pa.SchemaModel):
+            col: pa.typing.Series[int] = pa.Field(custom_eq=1)
+
+    with pytest.raises(AttributeError):
+        pa.Check.custom_eq(1)
+
+    @extensions.register_check_method(statistics=["val"])
+    def custom_eq(pandas_obj, val):
+        return pandas_obj == val
+
+    class Schema(pa.SchemaModel):  # noqa F811
+        col: pa.typing.Series[int] = pa.Field(custom_eq=1)
+
+    pa.Check.custom_eq(1)