Repository: alteryx/woodwork (python)

Issue:
Datetime logical type transform does not throw TypeConversionError
When transforming data for a specific logical type with `LogicalType.transform`, if the transformation to the correct dtype is not possible, a `TypeConversionError` should be thrown. This is not currently the case with the Datetime logical type.

The following code, which should produce a woodwork `TypeConversionError`
```python
import pandas as pd
from woodwork.logical_types import Datetime

s = pd.Series(['a', 'b', 'c'])
Datetime().transform(s)
```
produces the following error: `ValueError: Given date string not likely a datetime.`

The base LogicalType class' transform catches any errors during the `astype` call and raises a TypeConversionError. The other logical types with their own transform functions might raise separate types of errors (ordinal with a data validation error, or latlong if the format does not match the required latlong format) before calling the super transform, but those are not related to the dtype change, so it makes sense that they're not `TypeConversionErrors`.

However with Datetime, we perform a separate astype call via the `to_datetime` function. This transformation was previously treated the same as the other dtype changes and, if an error occurred, was raised to the user as a `TypeConversionError`. Now, however, we do not have a try/except around the datetime-specific logic in `Datetime.transform`, so the pandas error is what we see.

We should make sure that a TypeConversionError is thrown in this case. 

Pull request description:
Raise type conversion error in datetime
Closes #991 by raising a type conversion error when a Datetime logical type cannot parse data to a datetime dtype.

Patch:
diff --git a/docs/source/release_notes.rst b/docs/source/release_notes.rst
index 47a731e..095cd66 100644
--- a/docs/source/release_notes.rst
+++ b/docs/source/release_notes.rst
@@ -9,6 +9,7 @@ Future Release
         * Pass additional progress information in callback functions (:pr:`979`)
         * Add the ability to generate optional extra stats with ``DataFrame.ww.describe_dict`` (:pr:`988`)
     * Fixes
+        * Raise type conversion error in ``Datetime`` logical type (:pr:`1001`)
     * Changes
         * Remove version restriction for dask requirements (:pr:`998`)
     * Documentation Changes
diff --git a/woodwork/exceptions.py b/woodwork/exceptions.py
index 75864a7..4f35f01 100644
--- a/woodwork/exceptions.py
+++ b/woodwork/exceptions.py
@@ -34,7 +34,11 @@ class TypingInfoMismatchWarning(UserWarning):
 
 
 class TypeConversionError(Exception):
-    pass
+    def __init__(self, series, new_dtype, logical_type):
+        message = f'Error converting datatype for {series.name} from type {str(series.dtype)} '
+        message += f'to type {new_dtype}. Please confirm the underlying data is consistent with '
+        message += f'logical type {logical_type}.'
+        super().__init__(message)
 
 
 class ParametersIgnoredWarning(UserWarning):
diff --git a/woodwork/logical_types.py b/woodwork/logical_types.py
index dccad7e..778430f 100644
--- a/woodwork/logical_types.py
+++ b/woodwork/logical_types.py
@@ -47,17 +47,15 @@ class LogicalType(object, metaclass=LogicalTypeMetaClass):
         new_dtype = self._get_valid_dtype(type(series))
         if new_dtype != str(series.dtype):
             # Update the underlying series
-            error_msg = f'Error converting datatype for {series.name} from type {str(series.dtype)} ' \
-                f'to type {new_dtype}. Please confirm the underlying data is consistent with ' \
-                f'logical type {type(self)}.'
+            error = TypeConversionError(series, new_dtype, type(self))
             try:
                 series = series.astype(new_dtype)
                 if str(series.dtype) != new_dtype:
                     # Catch conditions when Panads does not error but did not
                     # convert to the specified dtype (example: 'category' -> 'bool')
-                    raise TypeConversionError(error_msg)
+                    raise error
             except (TypeError, ValueError):
-                raise TypeConversionError(error_msg)
+                raise error
         return series
 
 
@@ -183,14 +181,17 @@ class Datetime(LogicalType):
         """Converts the series data to a formatted datetime."""
         new_dtype = self._get_valid_dtype(type(series))
         if new_dtype != str(series.dtype):
-            if dd and isinstance(series, dd.Series):
-                name = series.name
-                series = dd.to_datetime(series, format=self.datetime_format)
-                series.name = name
-            elif ks and isinstance(series, ks.Series):
-                series = ks.Series(ks.to_datetime(series.to_numpy(), format=self.datetime_format), name=series.name)
-            else:
-                series = pd.to_datetime(series, format=self.datetime_format)
+            try:
+                if dd and isinstance(series, dd.Series):
+                    name = series.name
+                    series = dd.to_datetime(series, format=self.datetime_format)
+                    series.name = name
+                elif ks and isinstance(series, ks.Series):
+                    series = ks.Series(ks.to_datetime(series.to_numpy(), format=self.datetime_format), name=series.name)
+                else:
+                    series = pd.to_datetime(series, format=self.datetime_format)
+            except (TypeError, ValueError):
+                raise TypeConversionError(series, new_dtype, type(self))
         return super().transform(series)
 
 


Test patch:
diff --git a/woodwork/tests/logical_types/test_logical_types.py b/woodwork/tests/logical_types/test_logical_types.py
index d4f1deb..d4f8a99 100644
--- a/woodwork/tests/logical_types/test_logical_types.py
+++ b/woodwork/tests/logical_types/test_logical_types.py
@@ -3,6 +3,7 @@ import re
 import pandas as pd
 import pytest
 
+from woodwork.exceptions import TypeConversionError
 from woodwork.logical_types import (
     Boolean,
     Categorical,
@@ -110,6 +111,17 @@ def test_datetime_transform(datetimes):
         assert str(transform.dtype) == 'datetime64[ns]'
 
 
+def test_datetime_conversion_error(sample_series):
+    if dd and isinstance(sample_series, dd.Series):
+        pytest.xfail('Dask does not show error until compute is made.')
+
+    dtype = str(sample_series.dtype)
+    match = f'Error converting datatype for sample_series from type {dtype} to type datetime64[ns]. '
+    match += 'Please confirm the underlying data is consistent with logical type Datetime.'
+    with pytest.raises(TypeConversionError, match=re.escape(match)):
+        Datetime().transform(sample_series)
+
+
 def test_ordinal_transform(sample_series):
     series_type = str(type(sample_series))
     dask = 'dask' in series_type