Repository: great-expectations/great_expectations

Issue:
Bug in expect_column_values_to_be_of_type for PyAthena SQL dialect
In combination with PyAthena, I am unable to use the "expect_column_values_to_be_of_type" expectation when checking whether a column is of STRINGTYPE.

**To Reproduce**
1. Construct a SqlAlchemyDataset where the dialect is "module 'pyathena.sqlalchemy_athena'". Ensure that at least one column is of String type.
2. Run expect_column_values_to_be_of_type(col_name, "STRINGTYPE").
3. Observe that the resulting error is "TypeError: issubclass() arg 2 must be a class or tuple of classes".

**Expected behavior**
I would expect the method to check against the String class, not an instance of String().

**Environment:**
 - Great Expectations Version 0.13.19
 - PyAthena 2.3.0

**Additional Information**
Running the following code indicates why the error is occurring.

```python
from pyathena import sqlalchemy_athena

inspect.isclass(sqlalchemy_athena.STRINGTYPE)

inspect.isclass(sqlalchemy_athena.BIGINT)
```

The first function call returns False whereas the second call returns True.  The end result is that in great_expectations/dataset/sqlalchemy_dataset.py line 1622 the getattr call return an instance if the type specified is STRINGTYPE.

```python
success = issubclass(col_type, getattr(type_module, type_))
```

**Potential fix**
Replace the else clause with the following:

```python
type_module = self._get_dialect_type_module()
potential_type = getattr(type_module, type_)
if not inspect.isclass(potential_type):
    real_type = type(potential_type)
else:
    real_type = potential_type
success = issubclass(col_type, real_type)
```


Discussion:
Hi @BenGale93 - thanks so much for raising this issue, and apologies for delays! 

This is something that we will not be able to prioritize work on in the immediate future. That said, it looks like you are most of the way to a fix, and we always welcome contributions. I'm happy to prioritize a PR review if you would like to make a contribution, and I can also offer additional guidance as needed. Is that something that would interest you?
Hi @talagluck it will be my fist open source pull request so apologies for any faux pas in advance. I'll set one up as soon as possible.
Thanks, @BenGale93! No worries at all, I'm happy to help wherever I can. Looking forward!

Resolving patch:
diff --git a/great_expectations/dataset/sqlalchemy_dataset.py b/great_expectations/dataset/sqlalchemy_dataset.py
--- a/great_expectations/dataset/sqlalchemy_dataset.py
+++ b/great_expectations/dataset/sqlalchemy_dataset.py
@@ -1641,7 +1641,14 @@ def expect_column_values_to_be_of_type(
                 success = True
             else:
                 type_module = self._get_dialect_type_module()
-                success = issubclass(col_type, getattr(type_module, type_))
+                potential_type = getattr(type_module, type_)
+                # In the case of the PyAthena dialect we need to verify that
+                # the type returned is indeed a type and not an instance.
+                if not inspect.isclass(potential_type):
+                    real_type = type(potential_type)
+                else:
+                    real_type = potential_type
+                success = issubclass(col_type, real_type)
 
             return {"success": success, "result": {"observed_value": col_type.__name__}}
 
@@ -1689,7 +1696,13 @@ def expect_column_values_to_be_in_type_list(
             type_module = self._get_dialect_type_module()
             for type_ in type_list:
                 try:
-                    type_class = getattr(type_module, type_)
+                    potential_type = getattr(type_module, type_)
+                    # In the case of the PyAthena dialect we need to verify that
+                    # the type returned is indeed a type and not an instance.
+                    if not inspect.isclass(potential_type):
+                        type_class = type(potential_type)
+                    else:
+                        type_class = potential_type
                     types.append(type_class)
                 except AttributeError:
                     logger.debug("Unrecognized type: %s" % type_)
diff --git a/great_expectations/expectations/core/expect_column_values_to_be_in_type_list.py b/great_expectations/expectations/core/expect_column_values_to_be_in_type_list.py
--- a/great_expectations/expectations/core/expect_column_values_to_be_in_type_list.py
+++ b/great_expectations/expectations/core/expect_column_values_to_be_in_type_list.py
@@ -1,3 +1,4 @@
+import inspect
 import logging
 from typing import Dict, Optional
 
@@ -275,8 +276,14 @@ def _validate_sqlalchemy(
             type_module = _get_dialect_type_module(execution_engine=execution_engine)
             for type_ in expected_types_list:
                 try:
-                    type_class = getattr(type_module, type_)
-                    types.append(type_class)
+                    potential_type = getattr(type_module, type_)
+                    # In the case of the PyAthena dialect we need to verify that
+                    # the type returned is indeed a type and not an instance.
+                    if not inspect.isclass(potential_type):
+                        real_type = type(potential_type)
+                    else:
+                        real_type = potential_type
+                    types.append(real_type)
                 except AttributeError:
                     logger.debug("Unrecognized type: %s" % type_)
             if len(types) == 0:
diff --git a/great_expectations/expectations/core/expect_column_values_to_be_of_type.py b/great_expectations/expectations/core/expect_column_values_to_be_of_type.py
--- a/great_expectations/expectations/core/expect_column_values_to_be_of_type.py
+++ b/great_expectations/expectations/core/expect_column_values_to_be_of_type.py
@@ -1,3 +1,4 @@
+import inspect
 import logging
 from typing import Dict, Optional
 
@@ -285,7 +286,13 @@ def _validate_sqlalchemy(self, actual_column_type, expected_type, execution_engi
             types = []
             type_module = _get_dialect_type_module(execution_engine=execution_engine)
             try:
-                type_class = getattr(type_module, expected_type)
+                potential_type = getattr(type_module, expected_type)
+                # In the case of the PyAthena dialect we need to verify that
+                # the type returned is indeed a type and not an instance.
+                if not inspect.isclass(potential_type):
+                    type_class = type(potential_type)
+                else:
+                    type_class = potential_type
                 types.append(type_class)
             except AttributeError:
                 logger.debug("Unrecognized type: %s" % expected_type)


Test patch:
diff --git a/tests/dataset/test_sqlalchemydataset.py b/tests/dataset/test_sqlalchemydataset.py
--- a/tests/dataset/test_sqlalchemydataset.py
+++ b/tests/dataset/test_sqlalchemydataset.py
@@ -366,3 +366,33 @@ def test_expect_compound_columns_to_be_unique_with_no_rows(sa):
     dataset = SqlAlchemyDataset("test_sql_data", engine=engine)
 
     assert dataset.expect_compound_columns_to_be_unique(["col1", "col2"]).success
+
+
+def test_expect_column_values_to_be_of_type_string_dialect_pyathena(sa):
+    from pyathena import sqlalchemy_athena
+
+    engine = sa.create_engine("sqlite://")
+
+    data = pd.DataFrame({"col": ["test_val1", "test_val2"]})
+
+    data.to_sql(name="test_sql_data", con=engine, index=False)
+    dataset = SqlAlchemyDataset("test_sql_data", engine=engine)
+    dataset.dialect = sqlalchemy_athena
+
+    assert dataset.expect_column_values_to_be_of_type("col", type_="STRINGTYPE").success
+
+
+def test_expect_column_values_to_be_in_type_list_pyathena(sa):
+    from pyathena import sqlalchemy_athena
+
+    engine = sa.create_engine("sqlite://")
+
+    data = pd.DataFrame({"col": ["test_val1", "test_val2"]})
+
+    data.to_sql(name="test_sql_data", con=engine, index=False)
+    dataset = SqlAlchemyDataset("test_sql_data", engine=engine)
+    dataset.dialect = sqlalchemy_athena
+
+    assert dataset.expect_column_values_to_be_in_type_list(
+        "col", type_list=["STRINGTYPE", "BOOLEAN"]
+    ).success
diff --git a/tests/expectations/core/test_expect_column_values_to_be_in_type_list.py b/tests/expectations/core/test_expect_column_values_to_be_in_type_list.py
new file mode 100644
--- /dev/null
+++ b/tests/expectations/core/test_expect_column_values_to_be_in_type_list.py
@@ -0,0 +1,69 @@
+import pandas as pd
+
+from great_expectations.core.expectation_configuration import ExpectationConfiguration
+from great_expectations.core.expectation_validation_result import (
+    ExpectationValidationResult,
+)
+from great_expectations.execution_engine.sqlalchemy_execution_engine import (
+    SqlAlchemyBatchData,
+    SqlAlchemyExecutionEngine,
+)
+from great_expectations.expectations.core.expect_column_values_to_be_in_type_list import (
+    ExpectColumnValuesToBeInTypeList,
+)
+from great_expectations.validator.validator import Validator
+
+
+def test_expect_column_values_to_be_in_type_list_dialect_pyathena(sa):
+    from pyathena import sqlalchemy_athena
+
+    engine = sa.create_engine("sqlite://")
+
+    data = pd.DataFrame({"col": ["test_val1", "test_val2"]})
+
+    data.to_sql(name="test_sql_data", con=engine, index=False)
+
+    expectationConfiguration = ExpectationConfiguration(
+        expectation_type="expect_column_values_to_be_in_type_list",
+        kwargs={
+            "column": "col",
+            "type_list": ["STRINGTYPE", "BOOLEAN"],
+        },
+    )
+    expectation = ExpectColumnValuesToBeInTypeList(expectationConfiguration)
+    engine = SqlAlchemyExecutionEngine(engine=engine)
+    # Monkey-patch dialect for testing purposes.
+    engine.dialect_module = sqlalchemy_athena
+    engine.load_batch_data(
+        "my_id",
+        SqlAlchemyBatchData(execution_engine=engine, table_name="test_sql_data"),
+    )
+    result = expectation.validate(Validator(execution_engine=engine))
+
+    assert result == ExpectationValidationResult(
+        success=True,
+        expectation_config={
+            "expectation_type": "expect_column_values_to_be_in_type_list",
+            "kwargs": {
+                "column": "col",
+                "type_list": ["STRINGTYPE", "BOOLEAN"],
+            },
+            "meta": {},
+        },
+        result={
+            "element_count": 2,
+            "unexpected_count": 0,
+            "unexpected_percent": 0.0,
+            "partial_unexpected_list": [],
+            "missing_count": 0,
+            "missing_percent": 0.0,
+            "unexpected_percent_total": 0.0,
+            "unexpected_percent_nonmissing": 0.0,
+        },
+        exception_info={
+            "raised_exception": False,
+            "exception_traceback": None,
+            "exception_message": None,
+        },
+        meta={},
+    )
diff --git a/tests/expectations/core/test_expect_column_values_to_be_of_type.py b/tests/expectations/core/test_expect_column_values_to_be_of_type.py
new file mode 100644
--- /dev/null
+++ b/tests/expectations/core/test_expect_column_values_to_be_of_type.py
@@ -0,0 +1,69 @@
+import pandas as pd
+
+from great_expectations.core.expectation_configuration import ExpectationConfiguration
+from great_expectations.core.expectation_validation_result import (
+    ExpectationValidationResult,
+)
+from great_expectations.execution_engine.sqlalchemy_execution_engine import (
+    SqlAlchemyBatchData,
+    SqlAlchemyExecutionEngine,
+)
+from great_expectations.expectations.core.expect_column_values_to_be_of_type import (
+    ExpectColumnValuesToBeOfType,
+)
+from great_expectations.validator.validator import Validator
+
+
+def test_expect_column_values_to_be_of_type_string_dialect_pyathena(sa):
+    from pyathena import sqlalchemy_athena
+
+    engine = sa.create_engine("sqlite://")
+
+    data = pd.DataFrame({"col": ["test_val1", "test_val2"]})
+
+    data.to_sql(name="test_sql_data", con=engine, index=False)
+
+    expectationConfiguration = ExpectationConfiguration(
+        expectation_type="expect_column_values_to_be_of_type",
+        kwargs={
+            "column": "col",
+            "type_": "STRINGTYPE",
+        },
+    )
+    expectation = ExpectColumnValuesToBeOfType(expectationConfiguration)
+    engine = SqlAlchemyExecutionEngine(engine=engine)
+    # Monkey-patch dialect for testing purposes.
+    engine.dialect_module = sqlalchemy_athena
+    engine.load_batch_data(
+        "my_id",
+        SqlAlchemyBatchData(execution_engine=engine, table_name="test_sql_data"),
+    )
+    result = expectation.validate(Validator(execution_engine=engine))
+
+    assert result == ExpectationValidationResult(
+        success=True,
+        expectation_config={
+            "expectation_type": "expect_column_values_to_be_of_type",
+            "kwargs": {
+                "column": "col",
+                "type_": "STRINGTYPE",
+            },
+            "meta": {},
+        },
+        result={
+            "element_count": 2,
+            "unexpected_count": 0,
+            "unexpected_percent": 0.0,
+            "partial_unexpected_list": [],
+            "missing_count": 0,
+            "missing_percent": 0.0,
+            "unexpected_percent_total": 0.0,
+            "unexpected_percent_nonmissing": 0.0,
+        },
+        exception_info={
+            "raised_exception": False,
+            "exception_traceback": None,
+            "exception_message": None,
+        },
+        meta={},
+    )