Repository: great-expectations/great_expectations

Issue:
Use cleaner solution for non-truncating division in python 2
Prefer `from __future__ import division` to `1.*x/y`


Discussion:
Done inline during improve_partitions push.

Resolving patch:
diff --git a/great_expectations/util.py b/great_expectations/util.py
--- a/great_expectations/util.py
+++ b/great_expectations/util.py
@@ -71,7 +71,7 @@
     Table = None
     Select = None
 
-SINGULAR_TO_PLURAL_LOOKUP_DICT = {
+SINGULAR_TO_PLURAL_LOOKUP_DICT: dict = {
     "batch": "batches",
     "checkpoint": "checkpoints",
     "data_asset": "data_assets",
@@ -83,7 +83,7 @@
     "rendered_data_doc": "rendered_data_docs",
 }
 
-PLURAL_TO_SINGULAR_LOOKUP_DICT = {
+PLURAL_TO_SINGULAR_LOOKUP_DICT: dict = {
     "batches": "batch",
     "checkpoints": "checkpoint",
     "data_assets": "data_asset",
@@ -95,6 +95,8 @@
     "rendered_data_docs": "rendered_data_doc",
 }
 
+MAX_PROBABILISTIC_TEST_ASSERTION_RETRIES: int = 3
+
 
 def pluralize(singular_ge_noun):
     """
@@ -1397,3 +1399,43 @@ def import_make_url():
     else:
         from sqlalchemy.engine import make_url
     return make_url
+
+
+def probabilistic_test(
+    func: Callable = None,
+    max_num_retries: int = MAX_PROBABILISTIC_TEST_ASSERTION_RETRIES,
+) -> Callable:
+    @wraps(func)
+    def run_pytest_method(*args, **kwargs) -> None:
+        assertion_error: Optional[AssertionError] = None
+        error_message: Optional[str] = None
+
+        all_assertions_passed: bool = False
+
+        idx: int = 0
+        while idx < max_num_retries:
+            try:
+                func(*args, **kwargs)
+                all_assertions_passed = True
+            except AssertionError as e:
+                error_message = re.sub(r"\W+", " ", str(e)).strip()
+                logger.warning(
+                    f"""Attempt {idx + 1} to execute "{func}" failed with error "{error_message}".  Retrying."""
+                )
+                all_assertions_passed = False
+                assertion_error = e
+
+            if all_assertions_passed:
+                break
+
+            idx += 1
+
+        if not all_assertions_passed:
+            logger.error(
+                f"""Aborting trying to execute "{func}" (exceeded maximum allowed \
+{MAX_PROBABILISTIC_TEST_ASSERTION_RETRIES} attempts).  Error "{error_message}" is being raised.
+"""
+            )
+            raise assertion_error
+
+    return run_pytest_method
diff --git a/great_expectations/validator/validator.py b/great_expectations/validator/validator.py
--- a/great_expectations/validator/validator.py
+++ b/great_expectations/validator/validator.py
@@ -79,7 +79,7 @@
         "Unable to load pandas; install optional pandas dependency for support."
     )
 
-MAX_METRIC_COMPUTATION_RETRIES = 3
+MAX_METRIC_COMPUTATION_RETRIES: int = 3
 
 
 ValidationStatistics = namedtuple(


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -66,7 +66,7 @@
     get_dataset,
     get_sqlite_connection_url,
 )
-from great_expectations.util import is_library_loadable
+from great_expectations.util import is_library_loadable, probabilistic_test
 from tests.test_utils import create_files_in_directory
 
 RULE_BASED_PROFILER_MIN_PYTHON_VERSION: tuple = (3, 7)
@@ -227,6 +227,15 @@ def pytest_collection_modifyitems(config, items):
             item.add_marker(skip_docs_integration)
 
 
+@pytest.fixture
+def always_failing_test():
+    @probabilistic_test
+    def failing_test_func():
+        assert False, "Executing 'test_method()' failed."
+
+    return failing_test_func
+
+
 @pytest.fixture(autouse=True)
 def no_usage_stats(monkeypatch):
     # Do not generate usage stats from test runs
diff --git a/tests/integration/profiling/rule_based_profilers/test_profiler_user_workflows.py b/tests/integration/profiling/rule_based_profilers/test_profiler_user_workflows.py
--- a/tests/integration/profiling/rule_based_profilers/test_profiler_user_workflows.py
+++ b/tests/integration/profiling/rule_based_profilers/test_profiler_user_workflows.py
@@ -22,11 +22,17 @@
     ruleBasedProfilerConfigSchema,
 )
 from great_expectations.rule_based_profiler.rule_based_profiler import RuleBasedProfiler
+from great_expectations.util import probabilistic_test
 from great_expectations.validator.metric_configuration import MetricConfiguration
 from great_expectations.validator.validator import Validator
 
 yaml = YAML()
 
+# Measure of "closeness" between "actual" and "desired" is computed as: atol + rtol * abs(desired)
+# (see "https://numpy.org/doc/stable/reference/generated/numpy.testing.assert_allclose.html" for details).
+RTOL: float = 1.0e-7
+ATOL: float = 5.0e-2
+
 
 def test_alice_columnar_table_single_batch_batches_are_accessible(
     alice_columnar_table_single_batch_context,
@@ -924,6 +930,7 @@ def test_bobby_expect_column_values_to_be_between_auto_yes_default_profiler_conf
     }
 
 
+@probabilistic_test
 @pytest.mark.skipif(
     version.parse(np.version.version) < version.parse("1.21.0"),
     reason="requires numpy version 1.21.0 or newer",
@@ -1000,6 +1007,7 @@ def test_bobster_profiler_user_workflow_multi_batch_row_count_range_rule_bootstr
     )
 
 
+@probabilistic_test
 @pytest.mark.skipif(
     version.parse(np.version.version) < version.parse("1.21.0"),
     reason="requires numpy version 1.21.0 or newer",
@@ -1067,6 +1075,7 @@ def test_bobster_expect_table_row_count_to_be_between_auto_yes_default_profiler_
     )
 
 
+@probabilistic_test
 @pytest.mark.skipif(
     version.parse(np.version.version) < version.parse("1.21.0"),
     reason="requires numpy version 1.21.0 or newer",
@@ -1133,11 +1142,6 @@ def test_quentin_profiler_user_workflow_multi_batch_quantiles_value_ranges_rule(
         ]["tolls_amount"]
     )
 
-    # Measure of "closeness" between "actual" and "desired" is computed as: atol + rtol * abs(desired)
-    # (see "https://numpy.org/doc/stable/reference/generated/numpy.testing.assert_allclose.html" for details).
-    rtol: float = 1.0e-7
-    atol: float = 5.0e-2
-
     value_ranges: List[Tuple[Tuple[float, float]]]
     paired_quantiles: zip
     column_quantiles: List[List[Number]]
@@ -1159,12 +1163,13 @@ def test_quentin_profiler_user_workflow_multi_batch_quantiles_value_ranges_rule(
                 np.testing.assert_allclose(
                     actual=value_ranges[0][idx],
                     desired=value_ranges[1][idx],
-                    rtol=rtol,
-                    atol=atol,
-                    err_msg=f"Actual value of {value_ranges[0][idx]} differs from expected value of {value_ranges[1][idx]} by more than {atol + rtol * abs(value_ranges[1][idx])} tolerance.",
+                    rtol=RTOL,
+                    atol=ATOL,
+                    err_msg=f"Actual value of {value_ranges[0][idx]} differs from expected value of {value_ranges[1][idx]} by more than {ATOL + RTOL * abs(value_ranges[1][idx])} tolerance.",
                 )
 
 
+@probabilistic_test
 @pytest.mark.skipif(
     version.parse(np.version.version) < version.parse("1.21.0"),
     reason="requires numpy version 1.21.0 or newer",
@@ -1228,11 +1233,6 @@ def test_quentin_expect_column_quantile_values_to_be_between_auto_yes_default_pr
         "batch_id": "84000630d1b69a0fe870c94fb26a32bc",
     }
 
-    # Measure of "closeness" between "actual" and "desired" is computed as: atol + rtol * abs(desired)
-    # (see "https://numpy.org/doc/stable/reference/generated/numpy.testing.assert_allclose.html" for details).
-    rtol: float = 1.0e-7
-    atol: float = 5.0e-2
-
     value_ranges_expected: List[List[float]]
     value_ranges_computed: List[List[float]]
 
@@ -1260,9 +1260,9 @@ def test_quentin_expect_column_quantile_values_to_be_between_auto_yes_default_pr
             np.testing.assert_allclose(
                 actual=value_ranges[0][idx],
                 desired=value_ranges[1][idx],
-                rtol=rtol,
-                atol=atol,
-                err_msg=f"Actual value of {value_ranges[0][idx]} differs from expected value of {value_ranges[1][idx]} by more than {atol + rtol * abs(value_ranges[1][idx])} tolerance.",
+                rtol=RTOL,
+                atol=ATOL,
+                err_msg=f"Actual value of {value_ranges[0][idx]} differs from expected value of {value_ranges[1][idx]} by more than {ATOL + RTOL * abs(value_ranges[1][idx])} tolerance.",
             )
 
     parameter_builder_batch_request: dict
@@ -1373,9 +1373,9 @@ def test_quentin_expect_column_quantile_values_to_be_between_auto_yes_default_pr
             np.testing.assert_allclose(
                 actual=value_ranges[0][idx],
                 desired=value_ranges[1][idx],
-                rtol=rtol,
-                atol=atol,
-                err_msg=f"Actual value of {value_ranges[0][idx]} differs from expected value of {value_ranges[1][idx]} by more than {atol + rtol * abs(value_ranges[1][idx])} tolerance.",
+                rtol=RTOL,
+                atol=ATOL,
+                err_msg=f"Actual value of {value_ranges[0][idx]} differs from expected value of {value_ranges[1][idx]} by more than {ATOL + RTOL * abs(value_ranges[1][idx])} tolerance.",
             )
 
     # Use all batches, loaded by Validator, for estimating Expectation argument values.
diff --git a/tests/test_ge_utils.py b/tests/test_ge_utils.py
--- a/tests/test_ge_utils.py
+++ b/tests/test_ge_utils.py
@@ -560,11 +560,6 @@ def test_deep_filter_properties_iterable():
     assert d1_end == d1_end_expected
 
 
-def test_hyphen():
-    txt: str = "suite_validation_result"
-    assert hyphen(txt=txt) == "suite-validation-result"
-
-
 def test_deep_filter_properties_iterable_on_batch_request_dict():
     batch_request: dict = {
         "datasource_name": "df78ebde1957385a02d8736cd2c9a6d9",
@@ -584,3 +579,17 @@ def test_deep_filter_properties_iterable_on_batch_request_dict():
         "data_connector_name": "123a3221fc4b65014d061cce4a71782e",
         "data_asset_name": "eac128c5824b698c22b441ada61022d4",
     }
+
+
+def test_hyphen():
+    txt: str = "suite_validation_result"
+    assert hyphen(txt=txt) == "suite-validation-result"
+
+
+def test_probabilistic_test_decorator(
+    always_failing_test,
+):
+    with pytest.raises(AssertionError) as e:
+        always_failing_test()
+
+    assert "Executing 'test_method()' failed." in str(e.value)
diff --git a/tests/test_utils.py b/tests/test_utils.py
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -9,21 +9,6 @@
 import pandas as pd
 import pytest
 
-logger = logging.getLogger(__name__)
-
-try:
-    import sqlalchemy as sa
-    from sqlalchemy.exc import SQLAlchemyError
-
-except ImportError:
-    logger.debug(
-        "Unable to load SqlAlchemy context; install optional sqlalchemy dependency for support"
-    )
-    sa = None
-    reflection = None
-    Table = None
-    Select = None
-
 from great_expectations.data_context.store import (
     CheckpointStore,
     ProfilerStore,
@@ -44,9 +29,26 @@
 
 logger = logging.getLogger(__name__)
 
+try:
+    import sqlalchemy as sa
+    from sqlalchemy.exc import SQLAlchemyError
+
+except ImportError:
+    logger.debug(
+        "Unable to load SqlAlchemy context; install optional sqlalchemy dependency for support"
+    )
+    sa = None
+    reflection = None
+    Table = None
+    Select = None
+    SQLAlchemyError = None
+
+logger = logging.getLogger(__name__)
+
 
 # Taken from the following stackoverflow:
 # https://stackoverflow.com/questions/23549419/assert-that-two-dictionaries-are-almost-equal
+# noinspection PyPep8Naming
 def assertDeepAlmostEqual(expected, actual, *args, **kwargs):
     """
     Assert that two complex structures have almost equal contents.
@@ -450,6 +452,8 @@ def load_data_into_test_database(
     """
     import pandas as pd
 
+    connection = None
+
     if sa:
         engine = sa.create_engine(connection_string)
     else:
@@ -509,12 +513,12 @@ def check_athena_table_count(
             "Attempting to perform test on AWSAthena database, but unable to load SqlAlchemy context; "
             "install optional sqlalchemy dependency for support."
         )
-        return
+        return False
+
+    connection = None
     try:
-        athena_connection = engine.connect()
-        result = athena_connection.execute(
-            sa.text(f"SHOW TABLES in {db_name}")
-        ).fetchall()
+        connection = engine.connect()
+        result = connection.execute(sa.text(f"SHOW TABLES in {db_name}")).fetchall()
         return len(result) == expected_table_count
     except SQLAlchemyError as e:
         logger.error(
@@ -522,7 +526,7 @@ def check_athena_table_count(
         )
         raise
     finally:
-        athena_connection.close()
+        connection.close()
         engine.dispose()
 
 
@@ -538,15 +542,15 @@ def clean_athena_db(connection_string: str, db_name: str, table_to_keep: str) ->
             "install optional sqlalchemy dependency for support."
         )
         return
+
+    connection = None
     try:
-        athena_connection = engine.connect()
-        result = athena_connection.execute(
-            sa.text(f"SHOW TABLES in {db_name}")
-        ).fetchall()
+        connection = engine.connect()
+        result = connection.execute(sa.text(f"SHOW TABLES in {db_name}")).fetchall()
         for table_tuple in result:
             table = table_tuple[0]
             if table != table_to_keep:
-                athena_connection.execute(sa.text(f"DROP TABLE `{table}`;"))
+                connection.execute(sa.text(f"DROP TABLE `{table}`;"))
     finally:
-        athena_connection.close()
+        connection.close()
         engine.dispose()