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/expectations/metrics/column_aggregate_metrics/column_histogram.py b/great_expectations/expectations/metrics/column_aggregate_metrics/column_histogram.py
--- a/great_expectations/expectations/metrics/column_aggregate_metrics/column_histogram.py
+++ b/great_expectations/expectations/metrics/column_aggregate_metrics/column_histogram.py
@@ -1,6 +1,7 @@
 import copy
 import logging
-from typing import Any, Dict
+import traceback
+from typing import Any, Dict, List
 
 import numpy as np
 import pandas as pd
@@ -18,11 +19,40 @@
 from great_expectations.expectations.metrics.column_aggregate_metric_provider import (
     ColumnAggregateMetricProvider,
 )
-from great_expectations.expectations.metrics.import_manager import Bucketizer, F, sa
+from great_expectations.expectations.metrics.import_manager import (
+    Bucketizer,
+    F,
+    sa,
+    sparktypes,
+)
 from great_expectations.expectations.metrics.metric_provider import metric_value
 
 logger = logging.getLogger(__name__)
 
+try:
+    from sqlalchemy.exc import ProgrammingError
+    from sqlalchemy.sql import Select
+except ImportError:
+    logger.debug(
+        "Unable to load SqlAlchemy context; install optional sqlalchemy dependency for support"
+    )
+    ProgrammingError = None
+    Select = None
+
+try:
+    from sqlalchemy.engine.row import Row
+except ImportError:
+    try:
+        from sqlalchemy.engine.row import RowProxy
+
+        Row = RowProxy
+    except ImportError:
+        logger.debug(
+            "Unable to load SqlAlchemy Row class; please upgrade you sqlalchemy installation to the latest version."
+        )
+        RowProxy = None
+        Row = None
+
 
 class ColumnHistogram(ColumnAggregateMetricProvider):
     metric_name = "column.histogram"
@@ -69,10 +99,32 @@ def _sqlalchemy(
         column = accessor_domain_kwargs["column"]
         bins = metric_value_kwargs["bins"]
 
+        """return a list of counts corresponding to bins"""
         case_conditions = []
         idx = 0
         if isinstance(bins, np.ndarray):
             bins = bins.tolist()
+        elif isinstance(bins, int):
+            sqlalchemy_engine = execution_engine.engine
+            query: Select = (
+                sa.select(sa.column(column))
+                .distinct()
+                .where(sa.column(column) != None)
+                .order_by(sa.column(column).asc())
+                .select_from(selectable)
+            )
+            try:
+                rows: List[Row] = sqlalchemy_engine.execute(query).fetchall()
+                row: Row
+                column_values: np.ndarray = np.asarray([row[0] for row in rows])
+                bins = np.histogram_bin_edges(column_values, bins=bins)
+                bins = bins.tolist()
+            except ProgrammingError as pe:
+                exception_message: str = "An SQL syntax Exception occurred."
+                exception_traceback: str = traceback.format_exc()
+                exception_message += f'{type(pe).__name__}: "{str(pe)}".  Traceback: "{exception_traceback}".'
+                logger.error(exception_message)
+                raise pe
         else:
             bins = list(bins)
 
@@ -173,10 +225,27 @@ def _spark(
         df, _, accessor_domain_kwargs = execution_engine.get_compute_domain(
             domain_kwargs=metric_domain_kwargs, domain_type=MetricDomainTypes.COLUMN
         )
-        bins = metric_value_kwargs["bins"]
         column = metric_domain_kwargs["column"]
+        bins = metric_value_kwargs["bins"]
 
         """return a list of counts corresponding to bins"""
+        if isinstance(bins, np.ndarray):
+            bins = bins.tolist()
+        elif isinstance(bins, int):
+            rows: List[sparktypes.Row] = (
+                df.select(column)
+                .distinct()
+                .where(F.col(column).isNotNull())
+                .orderBy(F.col(column).asc())
+                .collect()
+            )
+            row: sparktypes.Row
+            column_values: np.ndarray = np.asarray([row[column] for row in rows])
+            bins = np.histogram_bin_edges(column_values, bins=bins)
+            bins = bins.tolist()
+        else:
+            bins = list(bins)
+
         bins = list(
             copy.deepcopy(bins)
         )  # take a copy since we are inserting and popping


Test patch:
diff --git a/tests/expectations/metrics/test_core.py b/tests/expectations/metrics/test_core.py
--- a/tests/expectations/metrics/test_core.py
+++ b/tests/expectations/metrics/test_core.py
@@ -524,6 +524,94 @@ def test_quantiles_metric_spark(spark_session):
     assert results == {desired_metric.id: [1.0, 2.0, 3.0]}
 
 
+def test_column_histogram_metric_pd():
+    engine = build_pandas_engine(pd.DataFrame({"a": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]}))
+
+    metrics: dict = {}
+
+    table_columns_metric: MetricConfiguration
+    results: dict
+
+    table_columns_metric, results = get_table_columns_metric(engine=engine)
+    metrics.update(results)
+
+    desired_metric = MetricConfiguration(
+        metric_name="column.histogram",
+        metric_domain_kwargs={"column": "a"},
+        metric_value_kwargs={
+            "bins": 10,
+        },
+        metric_dependencies={
+            "table.columns": table_columns_metric,
+        },
+    )
+    results = engine.resolve_metrics(
+        metrics_to_resolve=(desired_metric,), metrics=metrics
+    )
+    metrics.update(results)
+    assert results == {desired_metric.id: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}
+
+
+def test_column_histogram_metric_sa(sa):
+    engine = build_sa_engine(pd.DataFrame({"a": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]}), sa)
+
+    metrics: dict = {}
+
+    table_columns_metric: MetricConfiguration
+    results: dict
+
+    table_columns_metric, results = get_table_columns_metric(engine=engine)
+    metrics.update(results)
+
+    desired_metric = MetricConfiguration(
+        metric_name="column.histogram",
+        metric_domain_kwargs={"column": "a"},
+        metric_value_kwargs={
+            "bins": 10,
+        },
+        metric_dependencies={
+            "table.columns": table_columns_metric,
+        },
+    )
+    results = engine.resolve_metrics(
+        metrics_to_resolve=(desired_metric,), metrics=metrics
+    )
+    metrics.update(results)
+    assert results == {desired_metric.id: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}
+
+
+def test_column_histogram_metric_spark(spark_session):
+    engine: SparkDFExecutionEngine = build_spark_engine(
+        spark=spark_session,
+        df=pd.DataFrame({"a": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]}),
+        batch_id="my_id",
+    )
+
+    metrics: dict = {}
+
+    table_columns_metric: MetricConfiguration
+    results: dict
+
+    table_columns_metric, results = get_table_columns_metric(engine=engine)
+    metrics.update(results)
+
+    desired_metric = MetricConfiguration(
+        metric_name="column.histogram",
+        metric_domain_kwargs={"column": "a"},
+        metric_value_kwargs={
+            "bins": 10,
+        },
+        metric_dependencies={
+            "table.columns": table_columns_metric,
+        },
+    )
+    results = engine.resolve_metrics(
+        metrics_to_resolve=(desired_metric,), metrics=metrics
+    )
+    metrics.update(results)
+    assert results == {desired_metric.id: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}
+
+
 def test_max_metric_column_exists_pd():
     df = pd.DataFrame({"a": [1, 2, 3, 3, None]})
     batch = Batch(data=df)