def _validate_test_data_for_zone( self, zone_name: str, items: List[TestItem], ) -> int: """Compare the given test 'items' generatd by TestDataGenerator (using pytz) with the expected datetime components from ZoneSpecifier. Returns the number of errors. """ zone_info = self.zone_infos[zone_name] zone_specifier = ZoneSpecifier( zone_info_data=zone_info, viewing_months=self.viewing_months, debug=self.debug_specifier, in_place_transitions=self.in_place_transitions, optimize_candidates=self.optimize_candidates) num_errors = 0 for item in items: if self.year is not None and self.year != item.y: continue # Print out diagnostics if mismatch detected or if debug flag given unix_seconds = item.epoch + SECONDS_SINCE_UNIX_EPOCH ldt = datetime.utcfromtimestamp(unix_seconds) header = (f'======== Testing {zone_name}; ' f'at {_test_item_to_string(item)}w; ' f'utc {ldt}; ' f'epoch {item.epoch}; ' f'unix {unix_seconds}') if self.debug_specifier: logging.info(header) info = zone_specifier.get_timezone_info_for_seconds(item.epoch) if not info: logging.info("timezone info not found") continue is_matched = info.total_offset == item.total_offset status = '**Matched**' if is_matched else '**Mismatched**' ace_time_string = to_utc_string(info.utc_offset, info.dst_offset) utc_string = to_utc_string(item.total_offset - item.dst_offset, item.dst_offset) body = (f'{status}: ' f'AceTime({ace_time_string}); ' f'Expected({utc_string})') if is_matched: if self.debug_specifier: logging.info(body) zone_specifier.print_matches_and_transitions() else: num_errors += 1 if not self.debug_specifier: logging.error(header) logging.error(body) zone_specifier.print_matches_and_transitions() return num_errors
class acetz(tzinfo): """An implementation of datetime.tzinfo using the ZoneSpecifier class from AceTime/tools. """ def __init__(self, zone_info: ZoneInfo): self.zone_info = zone_info self.zs = ZoneSpecifier(zone_info, use_python_transition=True) def utcoffset(self, dt: Optional[datetime]) -> timedelta: assert dt info = self.zs.get_timezone_info_for_datetime(dt) if not info: raise Exception(f'Unknown timezone info for ' f'{dt.year:04}-{dt.month:02}-{dt.day:02} ' f'{dt.hour:02}:{dt.minute:02}:{dt.second:02}') return timedelta(seconds=info.total_offset) def dst(self, dt: Optional[datetime]) -> timedelta: assert dt offset_info = self.zs.get_timezone_info_for_datetime(dt) if not offset_info: raise Exception(f'Unknown timezone info for ' f'{dt.year:04}-{dt.month:02}-{dt.day:02} ' f'{dt.hour:02}:{dt.minute:02}:{dt.second:02}') return timedelta(seconds=offset_info.dst_offset) def tzname(self, dt: Optional[datetime]) -> str: assert dt offset_info = self.zs.get_timezone_info_for_datetime(dt) if not offset_info: raise Exception(f'Unknown timezone info for ' f'{dt.year:04}-{dt.month:02}-{dt.day:02} ' f'{dt.hour:02}:{dt.minute:02}:{dt.second:02}') return offset_info.abbrev def fromutc(self, dt: Optional[datetime]) -> datetime: """Override the default implementation in tzinfo which does not make sense for acetz. The 'dt' passed into this function from datetime.astimezone() is a weird one: the components are in UTC time, but the timezone is the target tzinfo, in other words, the same acetz as self. Warning: Do NOT call dt.isoformat() from this method, because it causes an infinite recursion as it tries to figure out the UTC offset. Use {dt.date()} and {dt.time()} instead. """ if not isinstance(dt, datetime): raise TypeError("fromutc() requires a datetime argument") if dt.tzinfo is not self: raise ValueError("dt.tzinfo is not self") # Extract the epoch_seconds of the source 'dt' assert dt is not None utcdt = dt.replace(tzinfo=timezone.utc) unix_seconds = int(utcdt.timestamp()) epoch_seconds = unix_seconds - SECONDS_SINCE_UNIX_EPOCH # Search the transitions for the matching Transition offset_info = self.zs.get_timezone_info_for_seconds(epoch_seconds) if not offset_info: raise ValueError(f"transition not found for {epoch_seconds}") # Convert the date/time fields into local date/time and attach # the current acetz object. newutcdt = utcdt + timedelta(seconds=offset_info.total_offset) newdt = newutcdt.replace(tzinfo=self, fold=offset_info.fold) return newdt def zone_specifier(self) -> ZoneSpecifier: return self.zs