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/data_context/data_context.py b/great_expectations/data_context/data_context.py
--- a/great_expectations/data_context/data_context.py
+++ b/great_expectations/data_context/data_context.py
@@ -13,6 +13,9 @@
 import webbrowser
 from typing import Dict, List, Optional, Union
 
+from marshmallow import ValidationError
+from ruamel.yaml import YAML, YAMLError
+
 import great_expectations.exceptions as ge_exceptions
 from great_expectations.core import ExpectationSuite, get_metric_kwargs_id
 from great_expectations.core.id_dict import BatchKwargs
@@ -55,8 +58,6 @@
 from great_expectations.render.renderer.site_builder import SiteBuilder
 from great_expectations.util import verify_dynamic_loading_support
 from great_expectations.validator.validator import Validator
-from marshmallow import ValidationError
-from ruamel.yaml import YAML, YAMLError
 
 try:
     from sqlalchemy.exc import SQLAlchemyError
@@ -159,13 +160,13 @@ def __init__(self, project_config, context_root_dir=None, runtime_environment=No
         self._init_stores(self._project_config_with_variables_substituted.stores)
 
         # Init validation operators
+        # NOTE - 20200522 - JPC - A consistent approach to lazy loading for plugins will be useful here, harmonizing
+        # the way that datasources, validation operators, site builders and other plugins are built.
         self.validation_operators = {}
         for (
             validation_operator_name,
             validation_operator_config,
-        ) in (
-            self._project_config_with_variables_substituted.validation_operators.items()
-        ):
+        ) in self._project_config.validation_operators.items():
             self.add_validation_operator(
                 validation_operator_name, validation_operator_config,
             )


Test patch:
diff --git a/tests/data_context/conftest.py b/tests/data_context/conftest.py
--- a/tests/data_context/conftest.py
+++ b/tests/data_context/conftest.py
@@ -13,7 +13,12 @@ def data_context_without_config_variables_filepath_configured(tmp_path_factory):
     context_path = os.path.join(project_path, "great_expectations")
     asset_config_path = os.path.join(context_path, "expectations")
 
-    create_data_context_files(context_path, asset_config_path, False)
+    create_data_context_files(
+        context_path,
+        asset_config_path,
+        ge_config_fixture_filename="great_expectations_basic_without_config_variables_filepath.yml",
+        config_variables_fixture_filename=None,
+    )
 
     return ge.data_context.DataContext(context_path)
 
@@ -25,30 +30,54 @@ def data_context_with_variables_in_config(tmp_path_factory):
     context_path = os.path.join(project_path, "great_expectations")
     asset_config_path = os.path.join(context_path, "expectations")
 
-    create_data_context_files(context_path, asset_config_path, True)
+    create_data_context_files(
+        context_path,
+        asset_config_path,
+        ge_config_fixture_filename="great_expectations_basic_with_variables.yml",
+        config_variables_fixture_filename="config_variables.yml",
+    )
+
+    return ge.data_context.DataContext(context_path)
+
+
+@pytest.fixture()
+def data_context_with_variables_in_config_exhaustive(tmp_path_factory):
+    # This data_context is *manually* created to have the config we want, vs created with DataContext.create
+    project_path = str(tmp_path_factory.mktemp("data_context"))
+    context_path = os.path.join(project_path, "great_expectations")
+    asset_config_path = os.path.join(context_path, "expectations")
+
+    create_data_context_files(
+        context_path,
+        asset_config_path,
+        ge_config_fixture_filename="great_expectations_basic_with_exhaustive_variables.yml",
+        config_variables_fixture_filename="config_variables_exhaustive.yml",
+    )
 
     return ge.data_context.DataContext(context_path)
 
 
 def create_data_context_files(
-    context_path, asset_config_path, with_config_variables_file
+    context_path,
+    asset_config_path,
+    ge_config_fixture_filename,
+    config_variables_fixture_filename=None,
 ):
-    if with_config_variables_file:
+    if config_variables_fixture_filename:
         os.makedirs(context_path, exist_ok=True)
         os.makedirs(os.path.join(context_path, "uncommitted"), exist_ok=True)
         copy_relative_path(
-            "../test_fixtures/config_variables.yml",
+            f"../test_fixtures/{config_variables_fixture_filename}",
             str(os.path.join(context_path, "uncommitted/config_variables.yml")),
         )
         copy_relative_path(
-            "../test_fixtures/great_expectations_basic_with_variables.yml",
+            f"../test_fixtures/{ge_config_fixture_filename}",
             str(os.path.join(context_path, "great_expectations.yml")),
         )
     else:
         os.makedirs(context_path, exist_ok=True)
         copy_relative_path(
-            "../test_fixtures/"
-            "great_expectations_basic_without_config_variables_filepath.yml",
+            f"../test_fixtures/{ge_config_fixture_filename}",
             str(os.path.join(context_path, "great_expectations.yml")),
         )
     create_common_data_context_files(context_path, asset_config_path)
diff --git a/tests/data_context/test_data_context_config_variables.py b/tests/data_context/test_data_context_config_variables.py
--- a/tests/data_context/test_data_context_config_variables.py
+++ b/tests/data_context/test_data_context_config_variables.py
@@ -1,11 +1,25 @@
 import os
 
-import pytest
 import great_expectations as ge
-from great_expectations.data_context.util import substitute_config_variable
+import pytest
+from great_expectations.data_context.types.base import (
+    DataContextConfig,
+    DataContextConfigSchema,
+)
+from great_expectations.data_context.util import (
+    file_relative_path,
+    substitute_config_variable,
+)
 from great_expectations.exceptions import InvalidConfigError, MissingConfigVariableError
+from ruamel.yaml import YAML, YAMLError
 from tests.data_context.conftest import create_data_context_files
 
+yaml = YAML()
+yaml.indent(mapping=2, sequence=4, offset=2)
+yaml.default_flow_style = False
+
+dataContextConfigSchema = DataContextConfigSchema()
+
 
 def test_config_variables_on_context_without_config_variables_filepath_configured(
     data_context_without_config_variables_filepath_configured,
@@ -98,6 +112,41 @@ def test_setting_config_variables_is_visible_immediately(
         del os.environ["replace_me_2"]
 
 
+def test_substituted_config_variables_not_written_to_file(tmp_path_factory):
+    # this test uses a great_expectations.yml with almost all values replaced
+    # with substitution variables
+
+    project_path = str(tmp_path_factory.mktemp("data_context"))
+    context_path = os.path.join(project_path, "great_expectations")
+    asset_config_path = os.path.join(context_path, "expectations")
+
+    create_data_context_files(
+        context_path,
+        asset_config_path,
+        ge_config_fixture_filename="great_expectations_basic_with_exhaustive_variables.yml",
+        config_variables_fixture_filename="config_variables_exhaustive.yml",
+    )
+
+    # load ge config fixture for expected
+    path_to_yml = (
+        "../test_fixtures/great_expectations_basic_with_exhaustive_variables.yml"
+    )
+    path_to_yml = file_relative_path(__file__, path_to_yml)
+    with open(path_to_yml) as data:
+        config_dict = yaml.load(data)
+    expected_config = DataContextConfig.from_commented_map(config_dict)
+    expected_config_dict = dataContextConfigSchema.dump(expected_config)
+
+    # instantiate data_context twice to go through cycle of loading config from file then saving
+    context = ge.data_context.DataContext(context_path)
+    context._save_project_config()
+    context_config_dict = dataContextConfigSchema.dump(
+        ge.data_context.DataContext(context_path)._project_config
+    )
+
+    assert context_config_dict == expected_config_dict
+
+
 def test_runtime_environment_are_used_preferentially(tmp_path_factory):
     value_from_environment = "from_environment"
     os.environ["replace_me"] = value_from_environment
@@ -108,7 +157,12 @@ def test_runtime_environment_are_used_preferentially(tmp_path_factory):
     project_path = str(tmp_path_factory.mktemp("data_context"))
     context_path = os.path.join(project_path, "great_expectations")
     asset_config_path = os.path.join(context_path, "expectations")
-    create_data_context_files(context_path, asset_config_path, True)
+    create_data_context_files(
+        context_path,
+        asset_config_path,
+        ge_config_fixture_filename="great_expectations_basic_with_variables.yml",
+        config_variables_fixture_filename="config_variables.yml",
+    )
 
     data_context = ge.data_context.DataContext(
         context_path, runtime_environment=runtime_environment
diff --git a/tests/test_fixtures/config_variables_exhaustive.yml b/tests/test_fixtures/config_variables_exhaustive.yml
new file mode 100644
--- /dev/null
+++ b/tests/test_fixtures/config_variables_exhaustive.yml
@@ -0,0 +1,45 @@
+a: great_expectations.datasource
+b: PandasDatasource
+c: PandasDataset
+d: SubdirReaderBatchKwargsGenerator
+e: ../data
+f: python
+g: SqlAlchemyDatasource
+h: SqlAlchemyDataset
+i: great_expectations.dataset
+j: great_expectations.datasource
+k:
+  drivername: postgres
+  host: localhost
+  port: '5432'
+  username: postgres
+  password: ''
+  database: postgres
+l: plugins/
+m: evaluation_parameter_store
+n: expectations_store
+o: validations_store
+p: SiteBuilder
+q: true
+r: TupleFilesystemStoreBackend
+s: uncommitted/data_docs/local_site/
+t: DefaultSiteIndexBuilder
+u: ExpectationsStore
+v: TupleFilesystemStoreBackend
+w: expectations/
+x: great_expectations.data_context.store
+y: EvaluationParameterStore
+z: ValidationsStore
+aa: ActionListValidationOperator
+bb: store_validation_result
+cc: StoreValidationResultAction
+dd: validations_store
+ee: notify_slack
+ff: SlackNotificationAction
+gg: www.fake-slack-webhook.net
+hh: all
+ii: great_expectations.render.renderer.slack_renderer
+jj: SlackRenderer
+kk: store_evaluation_params
+ll: StoreEvaluationParametersAction
+mm: evaluation_parameter_store
diff --git a/tests/test_fixtures/great_expectations_basic_with_exhaustive_variables.yml b/tests/test_fixtures/great_expectations_basic_with_exhaustive_variables.yml
new file mode 100644
--- /dev/null
+++ b/tests/test_fixtures/great_expectations_basic_with_exhaustive_variables.yml
@@ -0,0 +1,85 @@
+# This is a basic configuration for testing.
+# It has comments that should be preserved.
+config_version: 1.0
+# Here's a comment between the config version and the datassources
+datasources:
+  # For example, this one.
+  mydatasource: # This should stay by mydatasource
+    module_name: ${a}
+    class_name: ${b}
+    data_asset_type:
+      class_name: ${c}
+    batch_kwargs_generators:
+      # The name default is read if no datasource or generator is specified
+      mygenerator:
+        class_name: ${d}
+        base_directory: ${e}
+        reader_options:
+          sep:
+          engine: ${f}
+  my_postgres_db:
+    class_name: ${g}
+    data_asset_type:
+      class_name: ${h}
+      module_name: ${i}
+    module_name: ${j}
+    credentials: ${k}
+
+
+config_variables_file_path: uncommitted/config_variables.yml
+
+
+plugins_directory: ${l}
+evaluation_parameter_store_name: ${m}
+expectations_store_name: ${n}
+validations_store_name: ${o}
+
+data_docs_sites:
+  local_site:
+    class_name: ${p}
+    show_how_to_buttons: ${q}
+    store_backend:
+      class_name: ${r}
+      base_directory: ${s}
+    site_index_builder:
+      class_name: ${t}
+
+stores:
+  expectations_store:
+    class_name: ${u}
+    store_backend:
+      class_name: ${v}
+      base_directory: ${w}
+  evaluation_parameter_store:
+    module_name: ${x}
+    class_name: ${y}
+
+  validations_store:
+    class_name: ${z}
+
+validation_operators:
+  # Read about validation operators at: https://docs.greatexpectations.io/en/latest/guides/validation_operators.html
+  default:
+    class_name: ${aa}
+    action_list:
+      - name: ${bb}
+        action:
+          class_name: ${cc}
+          target_store_name: ${dd}
+      - name: ${ee}
+        action:
+          class_name: ${ff}
+          slack_webhook: ${gg}
+          notify_on: ${hh}
+          renderer:
+            module_name: ${ii}
+            class_name: ${jj}
+      - name: ${kk}
+        action:
+          class_name: ${ll}
+          target_store_name: ${mm}
+
+anonymous_usage_statistics:
+  data_context_id: d1aad86c-a205-49e6-95f7-b7c0a3635a43
+  enabled: False
+  usage_statistics_url: https://dev.stats.greatexpectations.io/great_expectations/v1/usage_statistics