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/exceptions/exceptions.py b/great_expectations/exceptions/exceptions.py
--- a/great_expectations/exceptions/exceptions.py
+++ b/great_expectations/exceptions/exceptions.py
@@ -435,6 +435,12 @@ class GeCloudError(GreatExpectationsError):
     """
 
 
+class GeCloudConfigurationError(GreatExpectationsError):
+    """
+    Error finding and verifying the required configuration values when preparing to connect to GX Cloud
+    """
+
+
 class DatabaseConnectionError(GreatExpectationsError):
     """Error connecting to a database including during an integration test."""
 
diff --git a/great_expectations/util.py b/great_expectations/util.py
--- a/great_expectations/util.py
+++ b/great_expectations/util.py
@@ -51,6 +51,7 @@
 from pkg_resources import Distribution
 
 from great_expectations.exceptions import (
+    GeCloudConfigurationError,
     PluginClassNotFoundError,
     PluginModuleNotFoundError,
 )
@@ -1673,7 +1674,7 @@ def get_context(
     ge_cloud_base_url: Optional[str] = None,
     ge_cloud_access_token: Optional[str] = None,
     ge_cloud_organization_id: Optional[str] = None,
-    ge_cloud_mode: bool = False,
+    ge_cloud_mode: Optional[bool] = None,
 ) -> Union["DataContext", "BaseDataContext", "CloudDataContext"]:
     """
     Method to return the appropriate DataContext depending on parameters and environment.
@@ -1697,6 +1698,15 @@ def get_context(
         ge_cloud_access_token, ge_cloud_organization_id) are passed in as parameters to get_context(), configured as
         environment variables, or in a .conf file, then get_context() will return a CloudDataContext.
 
+
+    +-----------------------+---------------------+---------------+
+    |  get_context params   |    Env Not Config'd |  Env Config'd |
+    +-----------------------+---------------------+---------------+
+    | ()                    | Local               | Cloud         |
+    | (ge_cloud_mode=True)  | Exception!          | Cloud         |
+    | (ge_cloud_mode=False) | Local               | Local         |
+    +-----------------------+---------------------+---------------+
+
     TODO: This method will eventually return FileDataContext and EphemeralDataContext, rather than DataContext and Base
 
     Args:
@@ -1710,7 +1720,7 @@ def get_context(
         ge_cloud_base_url (str): url for ge_cloud endpoint.
         ge_cloud_access_token (str): access_token for ge_cloud account.
         ge_cloud_organization_id (str): org_id for ge_cloud account.
-        ge_cloud_mode (bool): bool flag to specify whether to run GE in cloud mode (default is False).
+        ge_cloud_mode (bool): bool flag to specify whether to run GE in cloud mode (default is None).
 
     Returns:
         DataContext. Either a DataContext, BaseDataContext, or CloudDataContext depending on environment and/or
@@ -1723,14 +1733,16 @@ def get_context(
         DataContext,
     )
 
-    if (
-        CloudDataContext.is_ge_cloud_config_available(
-            ge_cloud_base_url=ge_cloud_base_url,
-            ge_cloud_access_token=ge_cloud_access_token,
-            ge_cloud_organization_id=ge_cloud_organization_id,
-        )
-        or ge_cloud_mode
-    ):
+    # First, check for ge_cloud conditions
+
+    config_available = CloudDataContext.is_ge_cloud_config_available(
+        ge_cloud_base_url=ge_cloud_base_url,
+        ge_cloud_access_token=ge_cloud_access_token,
+        ge_cloud_organization_id=ge_cloud_organization_id,
+    )
+
+    # If config available and not explicitly disabled
+    if config_available and ge_cloud_mode is not False:
         return CloudDataContext(
             project_config=project_config,
             runtime_environment=runtime_environment,
@@ -1739,17 +1751,25 @@ def get_context(
             ge_cloud_access_token=ge_cloud_access_token,
             ge_cloud_organization_id=ge_cloud_organization_id,
         )
-    elif project_config is not None:
+
+    if ge_cloud_mode and not config_available:
+        raise GeCloudConfigurationError(
+            "GE Cloud Mode enabled, but missing env vars: GE_CLOUD_ORGANIZATION_ID, GE_CLOUD_ACCESS_TOKEN"
+        )
+
+    # Second, check for which type of local
+
+    if project_config is not None:
         return BaseDataContext(
             project_config=project_config,
             context_root_dir=context_root_dir,
             runtime_environment=runtime_environment,
         )
-    else:
-        return DataContext(
-            context_root_dir=context_root_dir,
-            runtime_environment=runtime_environment,
-        )
+
+    return DataContext(
+        context_root_dir=context_root_dir,
+        runtime_environment=runtime_environment,
+    )
 
 
 def is_sane_slack_webhook(url: str) -> bool:


Test patch:
diff --git a/tests/data_context/test_get_data_context.py b/tests/data_context/test_get_data_context.py
--- a/tests/data_context/test_get_data_context.py
+++ b/tests/data_context/test_get_data_context.py
@@ -10,9 +10,28 @@
 from great_expectations.exceptions import ConfigNotFoundError
 from tests.test_utils import working_directory
 
+GE_CLOUD_PARAMS_ALL = {
+    "ge_cloud_base_url": "http://hello.com",
+    "ge_cloud_organization_id": "bd20fead-2c31-4392-bcd1-f1e87ad5a79c",
+    "ge_cloud_access_token": "i_am_a_token",
+}
+GE_CLOUD_PARAMS_REQUIRED = {
+    "ge_cloud_organization_id": "bd20fead-2c31-4392-bcd1-f1e87ad5a79c",
+    "ge_cloud_access_token": "i_am_a_token",
+}
+
+
+@pytest.fixture()
+def set_up_cloud_envs(monkeypatch):
+    monkeypatch.setenv("GE_CLOUD_BASE_URL", "http://hello.com")
+    monkeypatch.setenv(
+        "GE_CLOUD_ORGANIZATION_ID", "bd20fead-2c31-4392-bcd1-f1e87ad5a79c"
+    )
+    monkeypatch.setenv("GE_CLOUD_ACCESS_TOKEN", "i_am_a_token")
+
 
 @pytest.mark.unit
-def test_config_returns_base_context():
+def test_base_context():
     config: DataContextConfig = DataContextConfig(
         config_version=3.0,
         plugins_directory=None,
@@ -32,7 +51,40 @@ def test_config_returns_base_context():
 
 
 @pytest.mark.unit
-def test_empty_call_returns_data_context(tmp_path: pathlib.Path):
+def test_base_context__with_overridden_yml(tmp_path: pathlib.Path):
+    project_path = tmp_path / "empty_data_context"
+    project_path.mkdir()
+    project_path_str = str(project_path)
+    gx.data_context.DataContext.create(project_path_str)
+    context_path = project_path / "great_expectations"
+    context = gx.get_context(context_root_dir=context_path)
+    assert isinstance(context, DataContext)
+    assert context.expectations_store_name == "expectations_store"
+
+    config: DataContextConfig = DataContextConfig(
+        config_version=3.0,
+        plugins_directory=None,
+        evaluation_parameter_store_name="new_evaluation_parameter_store",
+        expectations_store_name="new_expectations_store",
+        datasources={},
+        stores={
+            "new_expectations_store": {"class_name": "ExpectationsStore"},
+            "new_evaluation_parameter_store": {
+                "class_name": "EvaluationParameterStore"
+            },
+            "new_validation_result_store": {"class_name": "ValidationsStore"},
+        },
+        validations_store_name="new_validation_result_store",
+        data_docs_sites={},
+        validation_operators={},
+    )
+    context = gx.get_context(project_config=config, context_root_dir=context_path)
+    assert isinstance(context, BaseDataContext)
+    assert context.expectations_store_name == "new_expectations_store"
+
+
+@pytest.mark.unit
+def test_data_context(tmp_path: pathlib.Path):
     project_path = tmp_path / "empty_data_context"
     project_path.mkdir()
     project_path_str = str(project_path)
@@ -42,7 +94,7 @@ def test_empty_call_returns_data_context(tmp_path: pathlib.Path):
 
 
 @pytest.mark.unit
-def test_context_root_dir_returns_data_context(
+def test_data_context_root_dir_returns_data_context(
     tmp_path: pathlib.Path,
 ):
     project_path = tmp_path / "empty_data_context"
@@ -54,13 +106,7 @@ def test_context_root_dir_returns_data_context(
 
 
 @pytest.mark.unit
-def test_context_invalid_root_dir_gives_error():
-    with pytest.raises(ConfigNotFoundError):
-        gx.get_context(context_root_dir="i/dont/exist")
-
-
-@pytest.mark.unit
-def test_config_returns_base_context_invalid_root_dir():
+def test_base_context_invalid_root_dir():
     config: DataContextConfig = DataContextConfig(
         config_version=3.0,
         plugins_directory=None,
@@ -82,99 +128,56 @@ def test_config_returns_base_context_invalid_root_dir():
     )
 
 
-@pytest.mark.unit
-def test_config_returns_base_context_overrides_yml(tmp_path: pathlib.Path):
-    project_path = tmp_path / "empty_data_context"
-    project_path.mkdir()
-    project_path_str = str(project_path)
-    gx.data_context.DataContext.create(project_path_str)
-    context_path = project_path / "great_expectations"
-    context = gx.get_context(context_root_dir=context_path)
-    assert isinstance(context, DataContext)
-    assert context.expectations_store_name == "expectations_store"
-
-    config: DataContextConfig = DataContextConfig(
-        config_version=3.0,
-        plugins_directory=None,
-        evaluation_parameter_store_name="new_evaluation_parameter_store",
-        expectations_store_name="new_expectations_store",
-        datasources={},
-        stores={
-            "new_expectations_store": {"class_name": "ExpectationsStore"},
-            "new_evaluation_parameter_store": {
-                "class_name": "EvaluationParameterStore"
-            },
-            "new_validation_result_store": {"class_name": "ValidationsStore"},
-        },
-        validations_store_name="new_validation_result_store",
-        data_docs_sites={},
-        validation_operators={},
-    )
-    context = gx.get_context(project_config=config, context_root_dir=context_path)
-    assert isinstance(context, BaseDataContext)
-    assert context.expectations_store_name == "new_expectations_store"
-
-
+@pytest.mark.parametrize("ge_cloud_mode", [True, None])
 @pytest.mark.cloud
-def test_cloud_config_in_env_returns_cloud_context(
-    monkeypatch, empty_ge_cloud_data_context_config
+def test_cloud_context_env(
+    set_up_cloud_envs, empty_ge_cloud_data_context_config, ge_cloud_mode
 ):
-    monkeypatch.setenv("GE_CLOUD_BASE_URL", "http://hello.com")
-    monkeypatch.setenv(
-        "GE_CLOUD_ORGANIZATION_ID", "bd20fead-2c31-4392-bcd1-f1e87ad5a79c"
-    )
-    monkeypatch.setenv("GE_CLOUD_ACCESS_TOKEN", "i_am_a_token")
     with mock.patch.object(
         CloudDataContext,
         "retrieve_data_context_config_from_ge_cloud",
         return_value=empty_ge_cloud_data_context_config,
     ):
         assert isinstance(
-            gx.get_context(),
+            gx.get_context(ge_cloud_mode=ge_cloud_mode),
             CloudDataContext,
         )
 
 
 @pytest.mark.cloud
-def test_most_cloud_config_in_param_base_url_in_env_returns_cloud_context(
-    monkeypatch, empty_ge_cloud_data_context_config
-):
-    monkeypatch.setenv("GE_CLOUD_BASE_URL", "http://hello.com")
-    with mock.patch.object(
-        CloudDataContext,
-        "retrieve_data_context_config_from_ge_cloud",
-        return_value=empty_ge_cloud_data_context_config,
-    ):
-        assert isinstance(
-            gx.get_context(
-                ge_cloud_organization_id="bd20fead-2c31-4392-bcd1-f1e87ad5a79c",
-                ge_cloud_access_token="i_am_a_token",
-            ),
-            CloudDataContext,
-        )
+def test_cloud_context_disabled(set_up_cloud_envs, tmp_path: pathlib.Path):
+    project_path = tmp_path / "empty_data_context"
+    project_path.mkdir()
+    project_path_str = str(project_path)
+    gx.data_context.DataContext.create(project_path_str)
+    with working_directory(project_path_str):
+        assert isinstance(gx.get_context(ge_cloud_mode=False), DataContext)
 
 
 @pytest.mark.cloud
-def test_all_cloud_config_in_param_returns_cloud_context(
+def test_cloud_missing_env_throws_exception(
     monkeypatch, empty_ge_cloud_data_context_config
 ):
+    with pytest.raises(Exception):
+        gx.get_context(ge_cloud_mode=True),
+
+
+@pytest.mark.parametrize("params", [GE_CLOUD_PARAMS_REQUIRED, GE_CLOUD_PARAMS_ALL])
+@pytest.mark.cloud
+def test_cloud_context_params(monkeypatch, empty_ge_cloud_data_context_config, params):
     with mock.patch.object(
         CloudDataContext,
         "retrieve_data_context_config_from_ge_cloud",
         return_value=empty_ge_cloud_data_context_config,
     ):
         assert isinstance(
-            gx.get_context(
-                ge_cloud_base_url="http://hello.com",
-                ge_cloud_organization_id="bd20fead-2c31-4392-bcd1-f1e87ad5a79c",
-                ge_cloud_access_token="i_am_a_token",
-            ),
+            gx.get_context(**params),
             CloudDataContext,
         )
 
 
 @pytest.mark.cloud
-def test_all_cloud_config_in_param_returns_cloud_context_and_in_memory_config_overrides(
+def test_cloud_context_with_in_memory_config_overrides(
     monkeypatch, empty_ge_cloud_data_context_config
 ):
     with mock.patch.object(
@@ -215,3 +218,9 @@ def test_all_cloud_config_in_param_returns_cloud_context_and_in_memory_config_ov
         )
         assert isinstance(context, CloudDataContext)
         assert context.expectations_store_name == "new_expectations_store"
+
+
+@pytest.mark.unit
+def test_invalid_root_dir_gives_error():
+    with pytest.raises(ConfigNotFoundError):
+        gx.get_context(context_root_dir="i/dont/exist")