Repository: unionai-oss/pandera

Issue:
Lazy evaluation fails to identify series errors
**Describe the bug**
When evaluating a pandas Series using a simple SeriesSchema, non-lazy validation correctly identifies problems but lazy validation does not do so.

- [x] I have checked that this issue has not already been reported.
- [x] 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.

**Note**: Please read [this guide](https://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports) detailing how to provide the necessary information for us to reproduce your bug.

#### Code Sample, a copy-pastable example

```python
import pandera as pa
import pandas as pd
print(pd.__version__)
print(pa.__version__)
schema = pa.SeriesSchema(pa.Int)
series = pd.Series(list(range(10)) + [0.1])
try:
    schema.validate(series, lazy=True)
    print("ERROR: lazy failed")
except:
    print("OK   : lazy worked")
try:
    schema.validate(series)
    print("ERROR: non-lazy failed")
except:
    print("OK   : non-lazy worked")
```

#### Expected behavior
The above sample prints out 

```
OK   : lazy worked
OK   : non-lazy worked
```

#### Actual behavior

The above sample prints out 

```
ERROR: lazy failed
OK   : non-lazy worked
```

#### Desktop (please complete the following information):

 - OS: linux 5.9.0-0.bpo.5-amd64
 - Browser firefox
 - Version 
    pandas 1.2.2
    pandera 0.6.1

#### Screenshots
NA

#### Additional context
Add any other context about the problem here.



Discussion:
this is probably related to #408, will merge these two issues since the fix to both of these should probably be in one PR

Resolving patch:
diff --git a/pandera/schema_components.py b/pandera/schema_components.py
--- a/pandera/schema_components.py
+++ b/pandera/schema_components.py
@@ -540,6 +540,11 @@ def coerce(self):
         """Whether or not to coerce data types."""
         return self._coerce or any(index.coerce for index in self.indexes)
 
+    @coerce.setter
+    def coerce(self, value: bool) -> None:
+        """Set coerce attribute."""
+        self._coerce = value
+
     def coerce_dtype(self, obj: pd.MultiIndex) -> pd.MultiIndex:
         """Coerce type of a pd.Series by type specified in pandas_dtype.
 
@@ -611,9 +616,7 @@ def validate(
         # pylint: disable=too-many-locals
         if self.coerce:
             try:
-                check_obj.index = self.coerce_dtype(
-                    check_obj.index if inplace else check_obj.index
-                )
+                check_obj.index = self.coerce_dtype(check_obj.index)
             except errors.SchemaErrors as err:
                 if lazy:
                     raise
@@ -624,9 +627,9 @@ def validate(
         # DataFrameSchema.validate call. Need to fix this by having MultiIndex
         # not inherit from DataFrameSchema.
         self_copy = deepcopy(self)
-        self_copy._coerce = False
+        self_copy.coerce = False
         for index in self_copy.indexes:
-            index._coerce = False
+            index.coerce = False
 
         # rename integer-based column names in case of duplicate index names,
         # with at least one named index.
diff --git a/pandera/schemas.py b/pandera/schemas.py
--- a/pandera/schemas.py
+++ b/pandera/schemas.py
@@ -185,6 +185,11 @@ def coerce(self):
         """Whether to coerce series to specified type."""
         return self._coerce
 
+    @coerce.setter
+    def coerce(self, value: bool) -> None:
+        """Set coerce attribute"""
+        self._coerce = value
+
     @property
     def ordered(self):
         """Whether or not to validate the columns order."""
@@ -313,7 +318,7 @@ def _coerce_dtype(self, obj: pd.DataFrame) -> pd.DataFrame:
                 obj,
                 msg,
                 failure_cases=scalar_failure_case(str(obj.dtypes.to_dict())),
-                check=f"coerce_dtype({self.pdtype.str_alias})",
+                check=f"coerce_dtype('{self.pdtype.str_alias}')",
             ) from exc
 
     def coerce_dtype(self, obj: pd.DataFrame) -> pd.DataFrame:
@@ -1528,6 +1533,11 @@ def coerce(self) -> bool:
         """Whether to coerce series to specified type."""
         return self._coerce
 
+    @coerce.setter
+    def coerce(self, value: bool) -> None:
+        """Set coerce attribute."""
+        self._coerce = value
+
     @property
     def name(self) -> Union[str, None]:
         """Get SeriesSchema name."""
@@ -1592,8 +1602,8 @@ def coerce_dtype(self, obj: Union[pd.Series, pd.Index]) -> pd.Series:
                 self,
                 obj,
                 msg,
-                failure_cases=scalar_failure_case(obj.dtype),
-                check=f"coerce_dtype({self.dtype})",
+                failure_cases=scalar_failure_case(str(obj.dtype)),
+                check=f"coerce_dtype('{self.dtype}')",
             ) from exc
 
     @property
@@ -1668,12 +1678,15 @@ def validate(
                 self._name,
                 series.name,
             )
-            raise errors.SchemaError(
-                self,
-                check_obj,
-                msg,
-                failure_cases=scalar_failure_case(series.name),
-                check=f"column_name('{self._name}')",
+            error_handler.collect_error(
+                "wrong_field_name",
+                errors.SchemaError(
+                    self,
+                    check_obj,
+                    msg,
+                    failure_cases=scalar_failure_case(series.name),
+                    check=f"field_name('{self._name}')",
+                ),
             )
 
         series_dtype = series.dtype
@@ -1762,9 +1775,6 @@ def validate(
                 ),
             )
 
-        if not self.checks:
-            return check_obj
-
         check_results = []
         if isinstance(check_obj, pd.Series):
             check_obj, check_args = series, [None]
@@ -1967,25 +1977,28 @@ def validate(
             except errors.SchemaError as exc:
                 error_handler.collect_error("dtype_coercion_error", exc)
 
-        if self.index is not None and (self.index.coerce or self.coerce):
-            try:
-                check_obj.index = self.index.coerce_dtype(check_obj.index)
-            except errors.SchemaError as exc:
-                error_handler.collect_error("dtype_coercion_error", exc)
-
         # validate index
         if self.index:
+            # coerce data type using index schema copy to prevent mutation
+            # of original index schema attribute.
+            _index = copy.deepcopy(self.index)
+            _index.coerce = _index.coerce or self.coerce
             try:
-                self.index(check_obj, head, tail, sample, random_state, lazy)
+                check_obj = _index(
+                    check_obj, head, tail, sample, random_state, lazy, inplace
+                )
+            except errors.SchemaError as exc:
+                error_handler.collect_error("dtype_coercion_error", exc)
             except errors.SchemaErrors as err:
                 for schema_error_dict in err.schema_errors:
                     error_handler.collect_error(
                         "index_check", schema_error_dict["error"]
                     )
-
         # validate series
         try:
-            super().validate(check_obj, head, tail, sample, random_state, lazy)
+            super().validate(
+                check_obj, head, tail, sample, random_state, lazy, inplace
+            )
         except errors.SchemaErrors as err:
             for schema_error_dict in err.schema_errors:
                 error_handler.collect_error(


Test patch:
diff --git a/tests/core/test_schemas.py b/tests/core/test_schemas.py
--- a/tests/core/test_schemas.py
+++ b/tests/core/test_schemas.py
@@ -193,6 +193,11 @@ def test_dataframe_pandas_dtype_coerce():
     with pytest.raises(ValueError):
         schema._coerce_dtype(df)
 
+    # test setting coerce as false at the dataframe level no longer coerces
+    # columns to int
+    schema.coerce = False
+    assert (schema(df).dtypes == "float64").all()
+
 
 def test_dataframe_coerce_regex():
     """Test dataframe pandas dtype coercion for regex columns"""
@@ -877,6 +882,10 @@ def test_add_and_remove_columns():
 
     assert schema4 == expected_schema_4 == schema1
 
+    # test raising error if column name is not in the schema
+    with pytest.raises(errors.SchemaInitError):
+        schema2.remove_columns(["foo", "bar"])
+
 
 def test_schema_get_dtype():
     """Test that schema dtype and get_dtype methods handle regex columns."""
@@ -1001,6 +1010,11 @@ def test_rename_columns():
     with pytest.raises(errors.SchemaInitError):
         schema_original.rename_columns({"foo": "bar"})
 
+    # Test raising error if new column name is already in schema
+    for rename_dict in [{"col1": "col2"}, {"col2": "col1"}]:
+        with pytest.raises(errors.SchemaInitError):
+            schema_original.rename_columns(rename_dict)
+
 
 @pytest.mark.parametrize(
     "select_columns, schema",
@@ -1175,8 +1189,67 @@ def test_lazy_dataframe_scalar_false_check(schema_cls, data):
 @pytest.mark.parametrize(
     "schema, data, expectation",
     [
+        # case: series name doesn't match schema name
+        [
+            SeriesSchema(name="foobar"),
+            pd.Series(range(3)),
+            {
+                "data": pd.Series(range(3)),
+                "schema_errors": {
+                    "SeriesSchema": {"field_name('foobar')": [None]},
+                },
+            },
+        ],
+        # case: series type doesn't match schema type
+        [
+            SeriesSchema(int),
+            pd.Series([0.1]),
+            {
+                "data": pd.Series([0.1]),
+                "schema_errors": {
+                    "SeriesSchema": {"pandas_dtype('int64')": ["float64"]},
+                },
+            },
+        ],
+        # case: series index doesn't satisfy schema index
+        [
+            SeriesSchema(index=Index(int)),
+            pd.Series([1, 2, 3], index=list("abc")),
+            {
+                "data": pd.Series([1, 2, 3], index=list("abc")),
+                "schema_errors": {
+                    "Index": {"pandas_dtype('int64')": ["object"]},
+                },
+            },
+        ],
+        # case: SeriesSchema data-type coercion error
+        [
+            SeriesSchema(float, coerce=True),
+            pd.Series(["1", "foo", "bar"]),
+            {
+                "data": pd.Series(["1", "foo", "bar"]),
+                "schema_errors": {
+                    "SeriesSchema": {
+                        "pandas_dtype('float64')": ["object"],
+                        "coerce_dtype('float64')": ["object"],
+                    },
+                },
+            },
+        ],
+        # case: series index coercion error
+        [
+            SeriesSchema(index=Index(int, coerce=True)),
+            pd.Series([1, 2, 3], index=list("abc")),
+            {
+                "data": pd.Series([1, 2, 3], index=list("abc")),
+                "schema_errors": {
+                    "Index": {"coerce_dtype('int64')": ["object"]},
+                },
+            },
+        ],
+        # case: series type and check doesn't satisfy schema
         [
-            SeriesSchema(Int, checks=Check.greater_than(0)),
+            SeriesSchema(int, checks=Check.greater_than(0)),
             pd.Series(["a", "b", "c"]),
             {
                 "data": pd.Series(["a", "b", "c"]),
@@ -1185,8 +1258,8 @@ def test_lazy_dataframe_scalar_false_check(schema_cls, data):
                     "SeriesSchema": {
                         # check name -> failure cases
                         "greater_than(0)": [
-                            "TypeError(\"'>' not supported between instances of "
-                            "'str' and 'int'\")",
+                            "TypeError(\"'>' not supported between instances "
+                            "of 'str' and 'int'\")",
                             # TypeError raised in python=3.5
                             'TypeError("unorderable types: str() > int()")',
                         ],
@@ -1195,6 +1268,7 @@ def test_lazy_dataframe_scalar_false_check(schema_cls, data):
                 },
             },
         ],
+        # case: multiple series checks don't satisfy schema
         [
             Column(
                 Int,
@@ -1236,8 +1310,8 @@ def test_lazy_dataframe_scalar_false_check(schema_cls, data):
                 ),
             ),
             {
-                # expect that the data in the SchemaError is the pd.MultiIndex cast
-                # into a DataFrame
+                # expect that the data in the SchemaError is the pd.MultiIndex
+                # cast into a DataFrame
                 "data": pd.DataFrame(
                     {"column": [1, 2, 3]},
                     index=pd.MultiIndex.from_arrays(