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/core/evaluation_parameters.py b/great_expectations/core/evaluation_parameters.py
--- a/great_expectations/core/evaluation_parameters.py
+++ b/great_expectations/core/evaluation_parameters.py
@@ -374,9 +374,30 @@ def parse_evaluation_parameter(
         return evaluation_parameters[L[0]]
 
     elif len(L) == 0 or L[0] != "Parse Failure":
+        # we have a stack to evaluate and there was no parse failure.
+        # iterate through values and look for URNs pointing to a store:
         for i, ob in enumerate(expr.exprStack):
             if isinstance(ob, str) and ob in evaluation_parameters:
                 expr.exprStack[i] = str(evaluation_parameters[ob])
+            elif isinstance(ob, str) and ob not in evaluation_parameters:
+                # try to retrieve this value from a store
+                try:
+                    res = ge_urn.parseString(ob)
+                    if res["urn_type"] == "stores":
+                        store = data_context.stores.get(res["store_name"])
+                        expr.exprStack[i] = str(
+                            store.get_query_result(
+                                res["metric_name"], res.get("metric_kwargs", {})
+                            )
+                        )  # value placed back in stack must be a string
+                    else:
+                        # handle other urn_types here, but note that validations URNs are being resolved elsewhere.
+                        pass
+                # graceful error handling for cases where the value in the stack isn't a URN:
+                except ParseException:
+                    pass
+                except AttributeError:
+                    pass
 
     else:
         err_str, err_line, err_col = L[-1]


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -4959,6 +4959,46 @@ def data_context_with_datasource_sqlalchemy_engine(empty_data_context, db_file):
     return context
 
 
+@pytest.fixture
+def data_context_with_query_store(
+    empty_data_context, titanic_sqlite_db_connection_string
+):
+    context = empty_data_context
+    config = yaml.load(
+        f"""
+    class_name: Datasource
+    execution_engine:
+        class_name: SqlAlchemyExecutionEngine
+        connection_string: {titanic_sqlite_db_connection_string}
+    data_connectors:
+        default_runtime_data_connector_name:
+            class_name: RuntimeDataConnector
+            batch_identifiers:
+                - default_identifier_name
+    """
+    )
+    context.add_datasource(
+        "my_datasource",
+        **config,
+    )
+    store_config = yaml.load(
+        f"""
+    class_name: SqlAlchemyQueryStore
+    credentials: 
+        connection_string: {titanic_sqlite_db_connection_string}
+    queries:
+        col_count:
+            query: "SELECT COUNT(*) FROM titanic;"
+            return_type: "scalar"
+        dist_col_count:
+            query: "SELECT COUNT(DISTINCT PClass) FROM titanic;"
+            return_type: "scalar"
+    """
+    )
+    context.add_store("my_query_store", store_config)
+    return context
+
+
 @pytest.fixture
 def ge_cloud_base_url():
     return "https://app.test.greatexpectations.io"
diff --git a/tests/core/test_evaluation_parameters.py b/tests/core/test_evaluation_parameters.py
--- a/tests/core/test_evaluation_parameters.py
+++ b/tests/core/test_evaluation_parameters.py
@@ -118,6 +118,59 @@ def test_parse_evaluation_parameter():
     )
 
 
+def test_query_store_results_in_evaluation_parameters(data_context_with_query_store):
+    TITANIC_ROW_COUNT = 1313  # taken from the titanic db conftest
+    DISTINCT_TITANIC_ROW_COUNT = 4
+
+    # parse_evaluation_parameters correctly resolves a stores URN
+    res1 = parse_evaluation_parameter(
+        parameter_expression="urn:great_expectations:stores:my_query_store:col_count",
+        evaluation_parameters=None,
+        data_context=data_context_with_query_store,
+    )
+    assert res1 == TITANIC_ROW_COUNT
+
+    # and can handle an operator
+    res2 = parse_evaluation_parameter(
+        parameter_expression="urn:great_expectations:stores:my_query_store:col_count * 2",
+        evaluation_parameters=None,
+        data_context=data_context_with_query_store,
+    )
+    assert res2 == TITANIC_ROW_COUNT * 2
+
+    # can even handle multiple operators
+    res3 = parse_evaluation_parameter(
+        parameter_expression="urn:great_expectations:stores:my_query_store:col_count * 0 + 100",
+        evaluation_parameters=None,
+        data_context=data_context_with_query_store,
+    )
+    assert res3 == 100
+
+    # allows stores URNs with functions
+    res4 = parse_evaluation_parameter(
+        parameter_expression="cos(urn:great_expectations:stores:my_query_store:col_count)",
+        evaluation_parameters=None,
+        data_context=data_context_with_query_store,
+    )
+    assert math.isclose(math.cos(TITANIC_ROW_COUNT), res4)
+
+    # multiple stores URNs can be used
+    res5 = parse_evaluation_parameter(
+        parameter_expression="urn:great_expectations:stores:my_query_store:col_count - urn:great_expectations:stores:my_query_store:dist_col_count",
+        evaluation_parameters=None,
+        data_context=data_context_with_query_store,
+    )
+    assert res5 == TITANIC_ROW_COUNT - DISTINCT_TITANIC_ROW_COUNT
+
+    # complex expressions can combine operators, urns, and functions
+    res6 = parse_evaluation_parameter(
+        parameter_expression="abs(-urn:great_expectations:stores:my_query_store:col_count - urn:great_expectations:stores:my_query_store:dist_col_count)",
+        evaluation_parameters=None,
+        data_context=data_context_with_query_store,
+    )
+    assert res6 == TITANIC_ROW_COUNT + DISTINCT_TITANIC_ROW_COUNT
+
+
 def test_parser_timing():
     """We currently reuse the parser, clearing the stack between calls, which is about 10 times faster than not
     doing so. But these operations are really quick, so this may not be necessary."""
diff --git a/tests/data_context/test_data_context.py b/tests/data_context/test_data_context.py
--- a/tests/data_context/test_data_context.py
+++ b/tests/data_context/test_data_context.py
@@ -9,9 +9,10 @@
 from ruamel.yaml import YAML
 
 import great_expectations.exceptions as ge_exceptions
-from great_expectations.checkpoint import Checkpoint
+from great_expectations.checkpoint import Checkpoint, SimpleCheckpoint
 from great_expectations.checkpoint.types.checkpoint_result import CheckpointResult
 from great_expectations.core import ExpectationConfiguration, expectationSuiteSchema
+from great_expectations.core.batch import BatchRequest, RuntimeBatchRequest
 from great_expectations.core.config_peer import ConfigOutputModes
 from great_expectations.core.expectation_suite import ExpectationSuite
 from great_expectations.core.run_identifier import RunIdentifier
@@ -2705,3 +2706,39 @@ def test_add_datasource_from_yaml_with_substitution_variables(
         ]
     )
     assert mock_emit.call_args_list == expected_call_args_list
+
+
+def test_stores_evaluation_parameters_resolve_correctly(data_context_with_query_store):
+    """End to end test demonstrating usage of Stores evaluation parameters"""
+    context = data_context_with_query_store
+    suite_name = "eval_param_suite"
+    context.create_expectation_suite(expectation_suite_name=suite_name)
+    batch_request = {
+        "datasource_name": "my_datasource",
+        "data_connector_name": "default_runtime_data_connector_name",
+        "data_asset_name": "DEFAULT_ASSET_NAME",
+        "batch_identifiers": {"default_identifier_name": "test123"},
+        "runtime_parameters": {"query": "select * from titanic"},
+    }
+    validator = context.get_validator(
+        batch_request=RuntimeBatchRequest(**batch_request),
+        expectation_suite_name=suite_name,
+    )
+    validator.expect_table_row_count_to_equal(
+        value={
+            # unnecessarily complex URN which should resolve to the actual row count.
+            "$PARAMETER": "abs(-urn:great_expectations:stores:my_query_store:col_count - urn:great_expectations:stores:my_query_store:dist_col_count) + 4"
+        }
+    )
+
+    checkpoint_config = {
+        "class_name": "SimpleCheckpoint",
+        "validations": [
+            {"batch_request": batch_request, "expectation_suite_name": suite_name}
+        ],
+    }
+    checkpoint = SimpleCheckpoint(
+        f"_tmp_checkpoint_{suite_name}", context, **checkpoint_config
+    )
+    checkpoint_result = checkpoint.run()
+    assert checkpoint_result.get("success") is True