Repository: unionai-oss/pandera

Issue:
@pa.dataframe_check doesn't actually support its check_kwargs
If you attempt to add additional args onto a @pa.dataframe_check they don't get passed threw to the underlying class method.

```
class BugTestSchema(pa.DataFrameModel):
    @pa.dataframe_check(env=DataLakeEnv.prod)
    def check_counterparty_combinations(cls, df, env: DataLakeEnv) -> Series[bool]:
        valid_combinations = get_counterparty_payment_terms(env=env)
        ...
```

"error": "Error while executing check function: TypeError(\"BaseCheckInfo.to_check.<locals>._adapter() got an unexpected keyword argument 'env'\")

There's some dynamic values testing that we're doing and at least being able to pass threw check_kwargs gets us the minimal functionality we need.

Bugfix PR to follow with a fix

Discussion:
danield-catalyst: signing went awry, will reopen, sorry about that
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/unionai-oss/pandera/pull/1934?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=unionai-oss) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 60.02%. Comparing base [(`812b2a8`)](https://app.codecov.io/gh/unionai-oss/pandera/commit/812b2a8af8c123c46caa348837da04e8c6573260?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=unionai-oss) to head [(`237b78c`)](https://app.codecov.io/gh/unionai-oss/pandera/commit/237b78c89b7c4ade097ffbbbcb05a2248a98e4f5?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=unionai-oss).
> Report is 199 commits behind head on main.

> :exclamation:  There is a different number of reports uploaded between BASE (812b2a8) and HEAD (237b78c). Click for more details.
> 
> <details><summary>HEAD has 31 uploads less than BASE</summary>
>
>| Flag | BASE (812b2a8) | HEAD (237b78c) |
>|------|------|------|
>||48|17|
></details>

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##             main    #1934       +/-   ##
===========================================
- Coverage   94.28%   60.02%   -34.27%     
===========================================
  Files          91      121       +30     
  Lines        7013     9375     +2362     
===========================================
- Hits         6612     5627      -985     
- Misses        401     3748     +3347     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/unionai-oss/pandera/pull/1934?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=unionai-oss).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=unionai-oss).


Patch:
diff --git a/pandera/api/base/model_components.py b/pandera/api/base/model_components.py
index 9c85b73..47ce8e4 100644
--- a/pandera/api/base/model_components.py
+++ b/pandera/api/base/model_components.py
@@ -136,8 +136,8 @@ class BaseCheckInfo:  # pylint:disable=too-few-public-methods
                 self.check_fn, "__name__", self.check_fn.__class__.__name__
             )
 
-        def _adapter(arg: Any) -> Union[bool, Iterable[bool]]:
-            return self.check_fn(model_cls, arg)
+        def _adapter(arg: Any, **kwargs) -> Union[bool, Iterable[bool]]:
+            return self.check_fn(model_cls, arg, **kwargs)
 
         return Check(_adapter, name=name, **self.check_kwargs)
 
diff --git a/pandera/api/pandas/model.py b/pandera/api/pandas/model.py
index 1887251..3975e43 100644
--- a/pandera/api/pandas/model.py
+++ b/pandera/api/pandas/model.py
@@ -207,7 +207,7 @@ class DataFrameModel(_DataFrameModel[pd.DataFrame, DataFrameSchema]):
         """
         schema = cls.to_schema()
         empty = pd.DataFrame(columns=schema.columns.keys()).astype(
-            {k: v.type for k, v in schema.dtypes.items()}
+            {k: v.type if v else None for k, v in schema.dtypes.items()}
         )
         table_schema = pd.io.json.build_table_schema(empty)
 
diff --git a/pandera/typing/pandas.py b/pandera/typing/pandas.py
index 91e7e92..b8f6ee2 100644
--- a/pandera/typing/pandas.py
+++ b/pandera/typing/pandas.py
@@ -198,24 +198,29 @@ class DataFrame(DataFrameBase, pd.DataFrame, Generic[T]):
                 "boolean": core_schema.bool_schema(),
                 "datetime": core_schema.datetime_schema(),
             }
-            return core_schema.no_info_plain_validator_function(
-                functools.partial(
-                    cls.pydantic_validate,
-                    schema_model=schema_model,
-                ),
-                json_schema_input_schema=core_schema.list_schema(
-                    core_schema.typed_dict_schema(
-                        {
-                            key: core_schema.typed_dict_field(
-                                type_map[
-                                    schema_json_columns[key]["items"]["type"]
-                                ]
-                            )
-                            for key in schema.columns.keys()
-                        },
-                    )
-                ),
+            function = functools.partial(
+                cls.pydantic_validate,
+                schema_model=schema_model,
             )
+            json_schema_input_schema = core_schema.list_schema(
+                core_schema.typed_dict_schema(
+                    {
+                        key: core_schema.typed_dict_field(
+                            type_map[schema_json_columns[key]["items"]["type"]]
+                        )
+                        for key in schema.columns.keys()
+                    },
+                )
+            )
+            try:
+                # json schema input schema is only available in
+                # pydantic_core >=2.30.0
+                return core_schema.no_info_plain_validator_function(
+                    function,
+                    json_schema_input_schema=json_schema_input_schema,
+                )
+            except TypeError:
+                return core_schema.no_info_plain_validator_function(function)
 
     else:
 


Test patch:
diff --git a/tests/core/test_model.py b/tests/core/test_model.py
index 7fcef2c..4b4854f 100644
--- a/tests/core/test_model.py
+++ b/tests/core/test_model.py
@@ -5,7 +5,8 @@ import os
 import re
 import runpy
 from copy import deepcopy
-from typing import Any, Generic, Iterable, List, Optional, TypeVar
+from enum import Enum
+from typing import Any, Generic, Iterable, List, Optional, TypeVar, Type
 
 import numpy as np
 import pandas as pd
@@ -640,6 +641,50 @@ def test_dataframe_check() -> None:
     assert len(e.value.message["DATA"]) == 1
 
 
+def test_dataframe_check_passthrough_kwargs() -> None:
+    """Test dataframe checks with passthrough kwargs."""
+
+    class MockTypesOne(Enum):
+        ONE = 1
+        TWO = 2
+        THREE = 3
+
+    class MockTypesTwo(Enum):
+        AVG = "avg"
+        SUM = "sum"
+        MEAN = "mean"
+
+    class Base(pa.DataFrameModel):
+        a: Series[int]
+        b: Series[str]
+
+        @pa.dataframe_check(column="a", enum=MockTypesOne)
+        @classmethod
+        def type_id_valid(
+            cls, df: pd.DataFrame, column: str, enum: Type[Enum]
+        ) -> Iterable[bool]:
+            return cls._field_in_enum(df, column, enum)  # type: ignore
+
+        @pa.dataframe_check(column="b", enum=MockTypesTwo)
+        @classmethod
+        def calc_type_valid(
+            cls, df: pd.DataFrame, column: str, enum: Type[Enum]
+        ) -> Iterable[bool]:
+            return cls._field_in_enum(df, column, enum)  # type: ignore
+
+        @classmethod
+        def _field_in_enum(
+            cls, df: pd.DataFrame, column: str, enum: Type[Enum]
+        ) -> Iterable[bool]:
+            return df[column].isin([member.value for member in enum])  # type: ignore
+
+    schema = Base.to_schema()
+    assert len(schema.checks) == 2
+
+    df = pd.DataFrame({"a": [1, 2], "b": ["avg", "mean"]})
+    schema.validate(df, lazy=True)
+
+
 def test_registered_dataframe_checks(
     extra_registered_checks: None,  # pylint: disable=unused-argument
 ) -> None:
@@ -1556,3 +1601,23 @@ def test_empty() -> None:
     df = Schema.empty()
     assert df.empty
     assert Schema.validate(df).empty  # type: ignore [attr-defined]
+
+
+def test_model_with_pydantic_base_model_with_df_init():
+    """
+    Test that a dataframe can be initialized within a pydantic base model that
+    defines the dataframe model as a class attribute.
+    """
+    from pydantic import BaseModel
+
+    # pylint: disable=unused-variable
+    class PydanticModel(BaseModel):
+
+        class PanderaDataFrameModel(pa.DataFrameModel):
+            """The DF we use to represent code/label/description associations."""
+
+            field: pa.typing.Series[Any]
+
+        df: pa.typing.DataFrame[PanderaDataFrameModel] = pd.DataFrame(
+            {"field": [1, 2, 3]}
+        )