Optionally surface exceptions that happen under executors by reading
[python_utils.git] / dateparse / dateparse_utils.py
index ad92ccd5d8d3e672bc0adc55ff2197912a833455..21fdb832b5c556317989e4f9855dae8daae67552 100755 (executable)
@@ -1,5 +1,10 @@
 #!/usr/bin/env python3
 
+"""
+Parse dates in a variety of formats.
+
+"""
+
 import datetime
 import functools
 import holidays  # type: ignore
@@ -15,10 +20,13 @@ import pytz
 
 import acl
 import bootstrap
-from decorator_utils import decorate_matching_methods_with
+from datetime_utils import (
+    TimeUnit, n_timeunits_from_base, datetime_to_date, date_to_datetime
+)
 from dateparse.dateparse_utilsLexer import dateparse_utilsLexer  # type: ignore
 from dateparse.dateparse_utilsListener import dateparse_utilsListener  # type: ignore
 from dateparse.dateparse_utilsParser import dateparse_utilsParser  # type: ignore
+import decorator_utils
 
 
 logger = logging.getLogger(__name__)
@@ -47,7 +55,6 @@ def debug_parse(enter_or_exit_f: Callable[[Any, Any], None]):
 class ParseException(Exception):
     """An exception thrown during parsing because of unrecognized input."""
     def __init__(self, message: str) -> None:
-        logger.error(message)
         self.message = message
 
 
@@ -56,7 +63,6 @@ class RaisingErrorListener(antlr4.DiagnosticErrorListener):
     def syntaxError(
             self, recognizer, offendingSymbol, line, column, msg, e
     ):
-        logger.error(msg)
         raise ParseException(msg)
 
     def reportAmbiguity(
@@ -77,15 +83,18 @@ class RaisingErrorListener(antlr4.DiagnosticErrorListener):
         pass
 
 
-@decorate_matching_methods_with(
+@decorator_utils.decorate_matching_methods_with(
     debug_parse,
     acl=acl.StringWildcardBasedACL(
         allowed_patterns=[
             'enter*',
             'exit*',
         ],
-        denied_patterns=None,
-        order_to_check_allow_deny=acl.ACL_ORDER_DENY_ALLOW,
+        denied_patterns=[
+            'enterEveryRule',
+            'exitEveryRule'
+        ],
+        order_to_check_allow_deny=acl.Order.DENY_ALLOW,
         default_answer=False
     )
 )
@@ -105,7 +114,6 @@ class DateParser(dateparse_utilsListener):
         idea of "now" so that the code can be more easily unittested.
         Leave as None for real use cases.
         """
-        from datetime_utils import TimeUnit
         self.month_name_to_number = {
             'jan': 1,
             'feb': 2,
@@ -189,6 +197,8 @@ class DateParser(dateparse_utilsListener):
 
         This is the main entrypoint to this class for caller code.
         """
+        date_string = date_string.strip()
+        date_string = re.sub('\s+', ' ', date_string)
         self._reset()
         listener = RaisingErrorListener()
         input_stream = antlr4.InputStream(date_string)
@@ -231,7 +241,6 @@ class DateParser(dateparse_utilsListener):
 
     def _reset(self):
         """Reset at init and between parses."""
-        from datetime_utils import datetime_to_date
         if self.override_now_for_test_purposes is None:
             self.now_datetime = datetime.datetime.now()
             self.today = datetime.date.today()
@@ -245,6 +254,7 @@ class DateParser(dateparse_utilsListener):
         self.datetime: Optional[datetime.datetime] = None
         self.context: Dict[str, Any] = {}
         self.timedelta = datetime.timedelta(seconds=0)
+        self.saw_overt_year = False
 
     @staticmethod
     def _normalize_special_day_name(name: str) -> str:
@@ -259,16 +269,15 @@ class DateParser(dateparse_utilsListener):
         name = name.replace('washi', 'presi')
         return name
 
-    def _figure_out_date_unit(self, orig: str) -> int:
+    def _figure_out_date_unit(self, orig: str) -> TimeUnit:
         """Figure out what unit a date expression piece is talking about."""
-        from datetime_utils import TimeUnit
         if 'month' in orig:
             return TimeUnit.MONTHS
         txt = orig.lower()[:3]
         if txt in self.day_name_to_number:
-            return(self.day_name_to_number[txt])
+            return(TimeUnit(self.day_name_to_number[txt]))
         elif txt in self.delta_unit_to_constant:
-            return(self.delta_unit_to_constant[txt])
+            return(TimeUnit(self.delta_unit_to_constant[txt]))
         raise ParseException(f'Invalid date unit: {orig}')
 
     def _figure_out_time_unit(self, orig: str) -> int:
@@ -297,8 +306,10 @@ class DateParser(dateparse_utilsListener):
         next_last = self.context.get('special_next_last', '')
         if next_last == 'next':
             year += 1
+            self.saw_overt_year = True
         elif next_last == 'last':
             year -= 1
+            self.saw_overt_year = True
 
         # Holiday names
         if name == 'easte':
@@ -357,6 +368,9 @@ class DateParser(dateparse_utilsListener):
             raise ParseException('Missing day')
         if 'year' not in self.context:
             self.context['year'] = self.today.year
+            self.saw_overt_year = False
+        else:
+            self.saw_overt_year = True
 
         # Handling "ides" and "nones" requires both the day and month.
         if (
@@ -382,7 +396,7 @@ class DateParser(dateparse_utilsListener):
             tz = pytz.timezone(txt)
             if tz is not None:
                 return tz
-        except:
+        except Exception:
             pass
 
         # Try dateutil
@@ -390,7 +404,7 @@ class DateParser(dateparse_utilsListener):
             tz = dateutil.tz.gettz(txt)
             if tz is not None:
                 return tz
-        except:
+        except Exception:
             pass
 
         # Try constructing an offset in seconds
@@ -403,7 +417,7 @@ class DateParser(dateparse_utilsListener):
                 offset = sign * (hour * 60 * 60) + sign * (minute * 60)
                 tzoffset = dateutil.tz.tzoffset(txt, offset)
                 return tzoffset
-        except:
+        except Exception:
             pass
         return None
 
@@ -469,9 +483,6 @@ class DateParser(dateparse_utilsListener):
 
     def exitDateExpr(self, ctx: dateparse_utilsParser.DateExprContext) -> None:
         """When we leave the date expression, populate self.date."""
-        from datetime_utils import (
-            n_timeunits_from_base, datetime_to_date, date_to_datetime
-        )
         if 'special' in self.context:
             self.date = self._parse_special_date(self.context['special'])
         else:
@@ -507,14 +518,13 @@ class DateParser(dateparse_utilsListener):
         unit = self.context['delta_unit']
         dt = n_timeunits_from_base(
             count,
-            unit,
+            TimeUnit(unit),
             date_to_datetime(self.date)
         )
         self.date = datetime_to_date(dt)
 
     def exitTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext) -> None:
         # Simple time?
-        from datetime_utils import TimeUnit
         self.time = datetime.time(
             self.context['hour'],
             self.context['minute'],
@@ -575,7 +585,7 @@ class DateParser(dateparse_utilsListener):
             unit = self._figure_out_date_unit(
                 ctx.deltaUnit().getText().lower()
             )
-        except:
+        except Exception:
             raise ParseException(f'Invalid Delta +/-: {ctx.getText()}')
         else:
             self.context['delta_int'] = n
@@ -586,7 +596,7 @@ class DateParser(dateparse_utilsListener):
     ) -> None:
         try:
             unit = self._figure_out_date_unit(ctx.getText().lower())
-        except:
+        except Exception:
             raise ParseException(f'Bad delta unit: {ctx.getText()}')
         else:
             self.context['delta_unit'] = unit
@@ -596,7 +606,7 @@ class DateParser(dateparse_utilsListener):
     ) -> None:
         try:
             txt = ctx.getText().lower()
-        except:
+        except Exception:
             raise ParseException(f'Bad next/last: {ctx.getText()}')
         if (
                 'month' in self.context or
@@ -611,11 +621,13 @@ class DateParser(dateparse_utilsListener):
             self.context['day'] = self.now_datetime.day
             self.context['month'] = self.now_datetime.month
             self.context['year'] = self.now_datetime.year
+            self.saw_overt_year = True
         elif txt[:4] == 'last':
             self.context['delta_int'] = -1
             self.context['day'] = self.now_datetime.day
             self.context['month'] = self.now_datetime.month
             self.context['year'] = self.now_datetime.year
+            self.saw_overt_year = True
         else:
             raise ParseException(f'Bad next/last: {ctx.getText()}')
 
@@ -631,7 +643,7 @@ class DateParser(dateparse_utilsListener):
                 ctx.deltaTimeUnit().getText().lower()
             )
             self.context['time_delta_unit'] = unit
-        except:
+        except Exception:
             raise ParseException(f'Bad delta unit: {ctx.getText()}')
         if 'time_delta_before_after' not in self.context:
             raise ParseException(
@@ -641,7 +653,6 @@ class DateParser(dateparse_utilsListener):
     def exitDeltaTimeFraction(
             self, ctx: dateparse_utilsParser.DeltaTimeFractionContext
     ) -> None:
-        from datetime_utils import TimeUnit
         try:
             txt = ctx.getText().lower()[:4]
             if txt == 'quar':
@@ -656,7 +667,7 @@ class DateParser(dateparse_utilsListener):
                 ] = TimeUnit.MINUTES
             else:
                 raise ParseException(f'Bad time fraction {ctx.getText()}')
-        except:
+        except Exception:
             raise ParseException(f'Bad time fraction {ctx.getText()}')
 
     def exitDeltaBeforeAfter(
@@ -664,7 +675,7 @@ class DateParser(dateparse_utilsListener):
     ) -> None:
         try:
             txt = ctx.getText().lower()
-        except:
+        except Exception:
             raise ParseException(f'Bad delta before|after: {ctx.getText()}')
         else:
             self.context['delta_before_after'] = txt
@@ -674,7 +685,7 @@ class DateParser(dateparse_utilsListener):
     ) -> None:
         try:
             txt = ctx.getText().lower()
-        except:
+        except Exception:
             raise ParseException(f'Bad delta before|after: {ctx.getText()}')
         else:
             self.context['time_delta_before_after'] = txt
@@ -734,7 +745,7 @@ class DateParser(dateparse_utilsListener):
                 self.context['month'] = month
                 self.context['day'] = 1
             self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
-        except:
+        except Exception:
             raise ParseException(
                 f'Invalid nthWeekday expression: {ctx.getText()}'
             )
@@ -748,7 +759,7 @@ class DateParser(dateparse_utilsListener):
     def exitNth(self, ctx: dateparse_utilsParser.NthContext) -> None:
         try:
             i = self._get_int(ctx.getText())
-        except:
+        except Exception:
             raise ParseException(f'Bad nth expression: {ctx.getText()}')
         else:
             self.context['nth'] = i
@@ -766,7 +777,7 @@ class DateParser(dateparse_utilsListener):
                 raise ParseException(
                     f'Bad first|last expression: {ctx.getText()}'
                 )
-        except:
+        except Exception:
             raise ParseException(f'Bad first|last expression: {ctx.getText()}')
         else:
             self.context['nth'] = txt
@@ -775,7 +786,7 @@ class DateParser(dateparse_utilsListener):
         try:
             dow = ctx.getText().lower()[:3]
             dow = self.day_name_to_number.get(dow, None)
-        except:
+        except Exception:
             raise ParseException('Bad day of week')
         else:
             self.context['dow'] = dow
@@ -799,7 +810,7 @@ class DateParser(dateparse_utilsListener):
                 raise ParseException(
                     f'Bad dayOfMonth expression: {ctx.getText()}'
                 )
-        except:
+        except Exception:
             raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
         self.context['day'] = day
 
@@ -816,7 +827,7 @@ class DateParser(dateparse_utilsListener):
                 raise ParseException(
                     f'Bad monthName expression: {ctx.getText()}'
                 )
-        except:
+        except Exception:
             raise ParseException(f'Bad monthName expression: {ctx.getText()}')
         else:
             self.context['month'] = month
@@ -830,7 +841,7 @@ class DateParser(dateparse_utilsListener):
                 raise ParseException(
                     f'Bad monthNumber expression: {ctx.getText()}'
                 )
-        except:
+        except Exception:
             raise ParseException(
                 f'Bad monthNumber expression: {ctx.getText()}'
             )
@@ -842,9 +853,10 @@ class DateParser(dateparse_utilsListener):
             year = self._get_int(ctx.getText())
             if year < 1:
                 raise ParseException(f'Bad year expression: {ctx.getText()}')
-        except:
+        except Exception:
             raise ParseException(f'Bad year expression: {ctx.getText()}')
         else:
+            self.saw_overt_year = True
             self.context['year'] = year
 
     def exitSpecialDateMaybeYearExpr(
@@ -853,7 +865,7 @@ class DateParser(dateparse_utilsListener):
         try:
             special = ctx.specialDate().getText().lower()
             self.context['special'] = special
-        except:
+        except Exception:
             raise ParseException(
                 f'Bad specialDate expression: {ctx.specialDate().getText()}'
             )
@@ -866,7 +878,7 @@ class DateParser(dateparse_utilsListener):
                     self.context['special_next_last'] = 'next'
                 elif mod.LAST() is not None:
                     self.context['special_next_last'] = 'last'
-        except:
+        except Exception:
             raise ParseException(
                 f'Bad specialDateNextLast expression: {ctx.getText()}'
             )
@@ -874,13 +886,12 @@ class DateParser(dateparse_utilsListener):
     def exitNFoosFromTodayAgoExpr(
         self, ctx: dateparse_utilsParser.NFoosFromTodayAgoExprContext
     ) -> None:
-        from datetime_utils import n_timeunits_from_base
         d = self.now_datetime
         try:
             count = self._get_int(ctx.unsignedInt().getText())
             unit = ctx.deltaUnit().getText().lower()
             ago_from_now = ctx.AGO_FROM_NOW().getText()
-        except:
+        except Exception:
             raise ParseException(
                 f'Bad NFoosFromTodayAgoExpr: {ctx.getText()}'
             )
@@ -891,7 +902,7 @@ class DateParser(dateparse_utilsListener):
         unit = self._figure_out_date_unit(unit)
         d = n_timeunits_from_base(
             count,
-            unit,
+            TimeUnit(unit),
             d)
         self.context['year'] = d.year
         self.context['month'] = d.month
@@ -900,7 +911,6 @@ class DateParser(dateparse_utilsListener):
     def exitDeltaRelativeToTodayExpr(
         self, ctx: dateparse_utilsParser.DeltaRelativeToTodayExprContext
     ) -> None:
-        from datetime_utils import n_timeunits_from_base
         d = self.now_datetime
         try:
             mod = ctx.thisNextLast()
@@ -915,14 +925,14 @@ class DateParser(dateparse_utilsListener):
                     f'Bad This/Next/Last modifier: {mod}'
                 )
             unit = ctx.deltaUnit().getText().lower()
-        except:
+        except Exception:
             raise ParseException(
                 f'Bad DeltaRelativeToTodayExpr: {ctx.getText()}'
             )
         unit = self._figure_out_date_unit(unit)
         d = n_timeunits_from_base(
             count,
-            unit,
+            TimeUnit(unit),
             d)
         self.context['year'] = d.year
         self.context['month'] = d.month
@@ -933,7 +943,7 @@ class DateParser(dateparse_utilsListener):
     ) -> None:
         try:
             txt = ctx.specialTime().getText().lower()
-        except:
+        except Exception:
             raise ParseException(
                 f'Bad special time expression: {ctx.getText()}'
             )
@@ -954,7 +964,7 @@ class DateParser(dateparse_utilsListener):
         try:
             tz = ctx.tzExpr().getText()
             self.context['tz'] = self._parse_tz(tz)
-        except:
+        except Exception:
             pass
 
     def exitTwelveHourTimeExpr(
@@ -965,14 +975,14 @@ class DateParser(dateparse_utilsListener):
             while not hour[-1].isdigit():
                 hour = hour[:-1]
             hour = self._get_int(hour)
-        except:
+        except Exception:
             raise ParseException(f'Bad hour: {ctx.hour().getText()}')
         if hour <= 0 or hour > 12:
             raise ParseException(f'Bad hour (out of range): {hour}')
 
         try:
             minute = self._get_int(ctx.minute().getText())
-        except:
+        except Exception:
             minute = 0
         if minute < 0 or minute > 59:
             raise ParseException(f'Bad minute (out of range): {minute}')
@@ -980,7 +990,7 @@ class DateParser(dateparse_utilsListener):
 
         try:
             seconds = self._get_int(ctx.second().getText())
-        except:
+        except Exception:
             seconds = 0
         if seconds < 0 or seconds > 59:
             raise ParseException(f'Bad second (out of range): {seconds}')
@@ -988,7 +998,7 @@ class DateParser(dateparse_utilsListener):
 
         try:
             micros = self._get_int(ctx.micros().getText())
-        except:
+        except Exception:
             micros = 0
         if micros < 0 or micros > 1000000:
             raise ParseException(f'Bad micros (out of range): {micros}')
@@ -996,7 +1006,7 @@ class DateParser(dateparse_utilsListener):
 
         try:
             ampm = ctx.ampm().getText()
-        except:
+        except Exception:
             raise ParseException(f'Bad ampm: {ctx.ampm().getText()}')
         if hour == 12:
             hour = 0
@@ -1007,7 +1017,7 @@ class DateParser(dateparse_utilsListener):
         try:
             tz = ctx.tzExpr().getText()
             self.context['tz'] = self._parse_tz(tz)
-        except:
+        except Exception:
             pass
 
     def exitTwentyFourHourTimeExpr(
@@ -1018,7 +1028,7 @@ class DateParser(dateparse_utilsListener):
             while not hour[-1].isdigit():
                 hour = hour[:-1]
             hour = self._get_int(hour)
-        except:
+        except Exception:
             raise ParseException(f'Bad hour: {ctx.hour().getText()}')
         if hour < 0 or hour > 23:
             raise ParseException(f'Bad hour (out of range): {hour}')
@@ -1026,7 +1036,7 @@ class DateParser(dateparse_utilsListener):
 
         try:
             minute = self._get_int(ctx.minute().getText())
-        except:
+        except Exception:
             minute = 0
         if minute < 0 or minute > 59:
             raise ParseException(f'Bad minute (out of range): {ctx.getText()}')
@@ -1034,7 +1044,7 @@ class DateParser(dateparse_utilsListener):
 
         try:
             seconds = self._get_int(ctx.second().getText())
-        except:
+        except Exception:
             seconds = 0
         if seconds < 0 or seconds > 59:
             raise ParseException(f'Bad second (out of range): {ctx.getText()}')
@@ -1042,7 +1052,7 @@ class DateParser(dateparse_utilsListener):
 
         try:
             micros = self._get_int(ctx.micros().getText())
-        except:
+        except Exception:
             micros = 0
         if micros < 0 or micros >= 1000000:
             raise ParseException(f'Bad micros (out of range): {ctx.getText()}')
@@ -1051,10 +1061,11 @@ class DateParser(dateparse_utilsListener):
         try:
             tz = ctx.tzExpr().getText()
             self.context['tz'] = self._parse_tz(tz)
-        except:
+        except Exception:
             pass
 
 
 def main() -> None:
     parser = DateParser()
     for line in sys.stdin:
@@ -1072,5 +1083,4 @@ def main() -> None:
 
 
 if __name__ == "__main__":
-    main = bootstrap.initialize(main)
     main()