diff --git a/CHANGELOG.md b/CHANGELOG.md index 99a018564..5640d01d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,7 +81,7 @@ See also https://github.com/neo4j/neo4j-python-driver/wiki for a full changelog. - Remove `ExperimentalWarning` and turn the few left instances of it into `PreviewWarning`. - Deprecate importing `PreviewWarning` from `neo4j`. Import it from `neo4j.warnings` instead. -- Make undocumented internal constants and helper functions private: +- Make undocumented internal constants, helper functions, and other items private: - `neo4j.api` - `DRIVER_BOLT` - `DRIVER_NEO4J` @@ -98,6 +98,23 @@ See also https://github.com/neo4j/neo4j-python-driver/wiki for a full changelog. - `ERROR_REWRITE_MAP` - `client_errors` - `transient_errors` + - `neo4j.time` + - `DATE_ISO_PATTERN` + - `TIME_ISO_PATTERN` + - `DURATION_ISO_PATTERN` + - `NANO_SECONDS` + - `AVERAGE_SECONDS_IN_MONTH` + - `AVERAGE_SECONDS_IN_DAY` + - `FORMAT_F_REPLACE` + - `IS_LEAP_YEAR` + - `DAYS_IN_YEAR` + - `DAYS_IN_MONTH` + - `round_half_to_even` + - `symmetric_divmod` + - `DateTimeType` + - `DateType` + - `TimeType` + - all other indirectly exposed items from imports (e.g. `re` as `neo4j.time.re`) - `neo4j.spatial` - `hydrate_point` - `dehydrate_point` @@ -111,6 +128,10 @@ See also https://github.com/neo4j/neo4j-python-driver/wiki for a full changelog. - `.default_host` - `.default_port` - `.default_target` +- Deprecate ClockTime and its accessors + - For each `neo4j.time.Date`, `neo4j.time.DateTime`, `neo4j.time.Time` + - `from_clock_time` and `to_clock_time` methods + - `neo4j.time.ClockTime` itself - Raise `ConfigurationError` instead of ignoring the routing context (URI query parameters) when creating a direct driver ("bolt[+s[sc]]://" scheme). - Change behavior of closed drivers: diff --git a/src/neo4j/_codec/hydration/v1/temporal.py b/src/neo4j/_codec/hydration/v1/temporal.py index 9f6e943b1..5a01b0d99 100644 --- a/src/neo4j/_codec/hydration/v1/temporal.py +++ b/src/neo4j/_codec/hydration/v1/temporal.py @@ -25,12 +25,12 @@ pd, ) from ....time import ( + _NANO_SECONDS, Date, DateTime, Duration, MAX_YEAR, MIN_YEAR, - NANO_SECONDS, Time, ) from ...packstream import Structure @@ -170,8 +170,8 @@ def seconds_and_nanoseconds(dt): if isinstance(dt, datetime): dt = DateTime.from_native(dt) zone_epoch = DateTime(1970, 1, 1, tzinfo=dt.tzinfo) - dt_clock_time = dt.to_clock_time() - zone_epoch_clock_time = zone_epoch.to_clock_time() + dt_clock_time = dt._to_clock_time() + zone_epoch_clock_time = zone_epoch._to_clock_time() t = dt_clock_time - zone_epoch_clock_time return t.seconds, t.nanoseconds @@ -226,7 +226,8 @@ def dehydrate_np_datetime(value): ) seconds = value.astype(np.dtype("datetime64[s]")).astype(int) nanoseconds = ( - value.astype(np.dtype("datetime64[ns]")).astype(int) % NANO_SECONDS + value.astype(np.dtype("datetime64[ns]")).astype(int) + % _NANO_SECONDS ) return Structure(b"d", seconds, nanoseconds) diff --git a/src/neo4j/_codec/hydration/v2/temporal.py b/src/neo4j/_codec/hydration/v2/temporal.py index 5c13c0bd3..4d8555a9f 100644 --- a/src/neo4j/_codec/hydration/v2/temporal.py +++ b/src/neo4j/_codec/hydration/v2/temporal.py @@ -21,9 +21,9 @@ from ...._optional_deps import pd from ....time import ( + _NANO_SECONDS, Date, DateTime, - NANO_SECONDS, Time, ) from ...packstream import Structure @@ -74,8 +74,8 @@ def seconds_and_nanoseconds(dt): dt = DateTime.from_native(dt) dt = dt.astimezone(pytz.UTC) utc_epoch = DateTime(1970, 1, 1, tzinfo=pytz.UTC) - dt_clock_time = dt.to_clock_time() - utc_epoch_clock_time = utc_epoch.to_clock_time() + dt_clock_time = dt._to_clock_time() + utc_epoch_clock_time = utc_epoch._to_clock_time() t = dt_clock_time - utc_epoch_clock_time return t.seconds, t.nanoseconds @@ -119,7 +119,7 @@ def dehydrate_pandas_datetime(value): :type value: pandas.Timestamp :returns: """ - seconds, nanoseconds = divmod(value.value, NANO_SECONDS) + seconds, nanoseconds = divmod(value.value, _NANO_SECONDS) tz = value.tzinfo if tz is None: diff --git a/src/neo4j/time/__init__.py b/src/neo4j/time/__init__.py index dfc2a4ac4..b830dd533 100644 --- a/src/neo4j/time/__init__.py +++ b/src/neo4j/time/__init__.py @@ -21,37 +21,44 @@ as a number of utility functions. """ -from __future__ import annotations +from __future__ import annotations as _ -import re +import re as _re from datetime import ( - date, - datetime, - time, - timedelta, - timezone, + date as _date, + datetime as _datetime, + time as _time, + timedelta as _timedelta, + timezone as _timezone, tzinfo as _tzinfo, ) -from functools import total_ordering -from re import compile as re_compile +from functools import total_ordering as _total_ordering +from re import compile as _re_compile from time import ( - gmtime, - mktime, - struct_time, + gmtime as _gmtime, + mktime as _mktime, + struct_time as _struct_time, ) from .. import _typing as _t +from .._warnings import deprecation_warn as _deprecation_warn from ._arithmetic import ( - round_half_to_even, - symmetric_divmod, + round_half_to_even as _round_half_to_even, + symmetric_divmod as _symmetric_divmod, ) from ._metaclasses import ( - DateTimeType, - DateType, - TimeType, + DateTimeType as _DateTimeType, + DateType as _DateType, + TimeType as _TimeType, ) +if _t.TYPE_CHECKING: + from typing_extensions import deprecated as _deprecated +else: + from .._warnings import deprecated as _deprecated + + __all__ = [ "MAX_INT64", "MAX_YEAR", @@ -69,8 +76,8 @@ ] -MIN_INT64 = -(2**63) -MAX_INT64 = (2**63) - 1 +MIN_INT64: _t.Final[int] = -(2**63) +MAX_INT64: _t.Final[int] = (2**63) - 1 #: The smallest year number allowed in a :class:`.Date` or :class:`.DateTime` #: object to be compatible with :class:`datetime.date` and @@ -82,21 +89,23 @@ #: :class:`datetime.datetime`. MAX_YEAR: _t.Final[int] = 9999 -DATE_ISO_PATTERN = re_compile(r"^(\d{4})-(\d{2})-(\d{2})$") -TIME_ISO_PATTERN = re_compile( +_DATE_ISO_PATTERN: _t.Final[_re.Pattern] = _re_compile( + r"^(\d{4})-(\d{2})-(\d{2})$" +) +_TIME_ISO_PATTERN: _t.Final[_re.Pattern] = _re_compile( r"^(\d{2})(:(\d{2})(:((\d{2})" r"(\.\d*)?))?)?(([+-])(\d{2}):(\d{2})(:((\d{2})(\.\d*)?))?)?$" ) -DURATION_ISO_PATTERN = re_compile( +_DURATION_ISO_PATTERN: _t.Final[_re.Pattern] = _re_compile( r"^P((\d+)Y)?((\d+)M)?((\d+)D)?" r"(T((\d+)H)?((\d+)M)?(((\d+)(\.\d+)?)?S)?)?$" ) -NANO_SECONDS = 1000000000 -AVERAGE_SECONDS_IN_MONTH = 2629746 -AVERAGE_SECONDS_IN_DAY = 86400 +_NANO_SECONDS: _t.Final[int] = 1000000000 +_AVERAGE_SECONDS_IN_MONTH: _t.Final[int] = 2629746 +_AVERAGE_SECONDS_IN_DAY: _t.Final[int] = 86400 -FORMAT_F_REPLACE = re.compile(r"(? 12: raise ValueError("Month out of range (1..12)") - days_in_month = DAYS_IN_MONTH[year, month] + days_in_month = _DAYS_IN_MONTH[year, month] if day in {days_in_month, -1}: return year, month, -1 if day in {days_in_month - 1, -2}: @@ -194,6 +203,7 @@ def _normalize_day(year, month, day): ) +# TODO: 7.0 - make private class ClockTime(tuple): """ A count of `seconds` and `nanoseconds`. @@ -209,26 +219,30 @@ class ClockTime(tuple): Note that the structure of a :class:`.ClockTime` object is similar to the ``timespec`` struct in C. + + .. deprecated:: 6.0 + :class:`ClockTime` is an implementation detail. + It and its related methods will be removed in a future version. """ def __new__(cls, seconds: float = 0, nanoseconds: int = 0) -> ClockTime: seconds, nanoseconds = divmod( - int(NANO_SECONDS * seconds) + int(nanoseconds), NANO_SECONDS + int(_NANO_SECONDS * seconds) + int(nanoseconds), _NANO_SECONDS ) return tuple.__new__(cls, (seconds, nanoseconds)) def __add__(self, other): if isinstance(other, (int, float)): - other = ClockTime(other) - if isinstance(other, ClockTime): - return ClockTime( + other = _ClockTime(other) + if isinstance(other, _ClockTime): + return _ClockTime( self.seconds + other.seconds, self.nanoseconds + other.nanoseconds, ) if isinstance(other, Duration): if other.months or other.days: raise ValueError("Cannot add Duration with months or days") - return ClockTime( + return _ClockTime( self.seconds + other.seconds, self.nanoseconds + int(other.nanoseconds), ) @@ -236,9 +250,9 @@ def __add__(self, other): def __sub__(self, other): if isinstance(other, (int, float)): - other = ClockTime(other) - if isinstance(other, ClockTime): - return ClockTime( + other = _ClockTime(other) + if isinstance(other, _ClockTime): + return _ClockTime( self.seconds - other.seconds, self.nanoseconds - other.nanoseconds, ) @@ -247,7 +261,7 @@ def __sub__(self, other): raise ValueError( "Cannot subtract Duration with months or days" ) - return ClockTime( + return _ClockTime( self.seconds - other.seconds, self.nanoseconds - int(other.nanoseconds), ) @@ -266,19 +280,19 @@ def nanoseconds(self): return self[1] -class Clock: +class _Clock: """ Accessor for time values. This class is fulfilled by implementations that subclass :class:`.Clock`. These implementations are contained within - the ``neo4j.time.clock_implementations`` module, and are not intended to be - accessed directly. + the ``neo4j.time._clock_implementations`` module, and are not intended to + be accessed directly. Creating a new :class:`.Clock` instance will produce the highest precision clock implementation available. - >>> clock = Clock() + >>> clock = _Clock() >>> type(clock) # doctest: +SKIP neo4j.time.clock_implementations.LibCClock >>> clock.local_time() # doctest: +SKIP @@ -290,12 +304,14 @@ class Clock: def __new__(cls): if cls.__implementations is None: # Find an available clock with the best precision - import neo4j.time._clock_implementations # noqa: F401 needed to make subclasses available + from . import ( # noqa: F401 needed to make subclasses available + _clock_implementations, + ) cls.__implementations = sorted( ( clock - for clock in Clock.__subclasses__() + for clock in _Clock.__subclasses__() if clock.available() ), key=lambda clock: clock.precision(), @@ -343,7 +359,7 @@ def local_offset(cls): # Adding and subtracting two days to avoid passing a pre-epoch time to # `mktime`, which can cause a `OverflowError` on some platforms (e.g., # Windows). - return ClockTime(-int(mktime(gmtime(172800))) + 172800) + return _ClockTime(-int(_mktime(_gmtime(172800))) + 172800) def local_time(self): """ @@ -372,13 +388,13 @@ def utc_time(self): if _t.TYPE_CHECKING: # make typechecker believe that Duration subclasses datetime.timedelta # https://github.com/python/typeshed/issues/8409#issuecomment-1197704527 - duration_base_class = timedelta + _duration_base_class = _timedelta else: - duration_base_class = object + _duration_base_class = object class Duration( # type: ignore[misc] - tuple[int, int, int, int], duration_base_class + tuple[int, int, int, int], _duration_base_class ): r""" A difference between two points in time. @@ -456,11 +472,11 @@ def __new__( + int(1000 * microseconds) + int(nanoseconds) ) - s, ns = symmetric_divmod(ns, NANO_SECONDS) + s, ns = _symmetric_divmod(ns, _NANO_SECONDS) tuple_ = (mo, d, s, ns) avg_total_seconds = ( - mo * AVERAGE_SECONDS_IN_MONTH - + d * AVERAGE_SECONDS_IN_DAY + mo * _AVERAGE_SECONDS_IN_MONTH + + d * _AVERAGE_SECONDS_IN_DAY + s - (1 if ns < 0 else 0) ) @@ -475,7 +491,7 @@ def __bool__(self) -> bool: __nonzero__ = __bool__ def __add__( # type: ignore[override] - self, other: Duration | timedelta + self, other: Duration | _timedelta ) -> Duration: """Add a :class:`.Duration` or :class:`datetime.timedelta`.""" if isinstance(other, Duration): @@ -485,7 +501,7 @@ def __add__( # type: ignore[override] seconds=self[2] + int(other.seconds), nanoseconds=self[3] + int(other.nanoseconds), ) - if isinstance(other, timedelta): + if isinstance(other, _timedelta): return Duration( months=self[0], days=self[1] + other.days, @@ -494,7 +510,7 @@ def __add__( # type: ignore[override] ) return NotImplemented - def __sub__(self, other: Duration | timedelta) -> Duration: + def __sub__(self, other: Duration | _timedelta) -> Duration: """Subtract a :class:`.Duration` or :class:`datetime.timedelta`.""" if isinstance(other, Duration): return Duration( @@ -503,7 +519,7 @@ def __sub__(self, other: Duration | timedelta) -> Duration: seconds=self[2] - int(other.seconds), nanoseconds=self[3] - int(other.nanoseconds), ) - if isinstance(other, timedelta): + if isinstance(other, _timedelta): return Duration( months=self[0], days=self[1] - other.days, @@ -527,10 +543,10 @@ def __mul__(self, other: float) -> Duration: # type: ignore[override] """ if isinstance(other, (int, float)): return Duration( - months=round_half_to_even(self[0] * other), - days=round_half_to_even(self[1] * other), - nanoseconds=round_half_to_even( - self[2] * NANO_SECONDS * other + self[3] * other + months=_round_half_to_even(self[0] * other), + days=_round_half_to_even(self[1] * other), + nanoseconds=_round_half_to_even( + self[2] * _NANO_SECONDS * other + self[3] * other ), ) return NotImplemented @@ -552,7 +568,7 @@ def __floordiv__(self, other: int) -> Duration: # type: ignore[override] return Duration( months=self[0] // other, days=self[1] // other, - nanoseconds=(self[2] * NANO_SECONDS + self[3]) // other, + nanoseconds=(self[2] * _NANO_SECONDS + self[3]) // other, ) return NotImplemented @@ -571,7 +587,7 @@ def __mod__(self, other: int) -> Duration: # type: ignore[override] return Duration( months=self[0] % other, days=self[1] % other, - nanoseconds=(self[2] * NANO_SECONDS + self[3]) % other, + nanoseconds=(self[2] * _NANO_SECONDS + self[3]) % other, ) return NotImplemented @@ -602,10 +618,10 @@ def __truediv__(self, other: float) -> Duration: # type: ignore[override] """ if isinstance(other, (int, float)): return Duration( - months=round_half_to_even(self[0] / other), - days=round_half_to_even(self[1] / other), - nanoseconds=round_half_to_even( - self[2] * NANO_SECONDS / other + self[3] / other + months=_round_half_to_even(self[0] / other), + days=_round_half_to_even(self[1] / other), + nanoseconds=_round_half_to_even( + self[2] * _NANO_SECONDS / other + self[3] / other ), ) return NotImplemented @@ -677,7 +693,7 @@ def from_iso_format(cls, s: str) -> Duration: :raises ValueError: if the string does not match the required format. """ - match = DURATION_ISO_PATTERN.match(s) + match = _DURATION_ISO_PATTERN.match(s) if match: ns = 0 if match.group(15): @@ -763,7 +779,7 @@ def years_months_days(self) -> tuple[int, int, int]: tuple of years, months and days. """ - years, months = symmetric_divmod(self[0], 12) + years, months = _symmetric_divmod(self[0], 12) return years, months, self[1] @property @@ -773,8 +789,8 @@ def hours_minutes_seconds_nanoseconds(self) -> tuple[int, int, int, int]: tuple of hours, minutes, seconds and nanoseconds. """ - minutes, seconds = symmetric_divmod(self[2], 60) - hours, minutes = symmetric_divmod(minutes, 60) + minutes, seconds = _symmetric_divmod(self[2], 60) + hours, minutes = _symmetric_divmod(minutes, 60) return hours, minutes, seconds, self[3] @@ -790,12 +806,12 @@ def hours_minutes_seconds_nanoseconds(self) -> tuple[int, int, int, int]: if _t.TYPE_CHECKING: # make typechecker believe that Date subclasses datetime.date # https://github.com/python/typeshed/issues/8409#issuecomment-1197704527 - date_base_class = date + _date_base_class = _date else: - date_base_class = object + _date_base_class = object -class Date(date_base_class, metaclass=DateType): +class Date(_date_base_class, metaclass=_DateType): """ Idealized date representation. @@ -864,11 +880,11 @@ def today(cls, tz: _tzinfo | None = None) -> Date: this to be restricted to years from 1970 through 2038. """ if tz is None: - return cls.from_clock_time(Clock().local_time(), UnixEpoch) + return cls._from_clock_time(_Clock().local_time(), UnixEpoch) else: return ( DateTime.utc_now() - .replace(tzinfo=timezone.utc) + .replace(tzinfo=_timezone.utc) .astimezone(tz) .date() ) @@ -876,7 +892,7 @@ def today(cls, tz: _tzinfo | None = None) -> Date: @classmethod def utc_today(cls) -> Date: """Get the current date as UTC local date.""" - return cls.from_clock_time(Clock().utc_time(), UnixEpoch) + return cls._from_clock_time(_Clock().utc_time(), UnixEpoch) @classmethod def from_timestamp( @@ -892,7 +908,7 @@ def from_timestamp( supported by the platform C localtime() function. It’s common for this to be restricted to years from 1970 through 2038. """ - return cls.from_native(datetime.fromtimestamp(timestamp, tz)) + return cls.from_native(_datetime.fromtimestamp(timestamp, tz)) @classmethod def utc_from_timestamp(cls, timestamp: float) -> Date: @@ -901,7 +917,7 @@ def utc_from_timestamp(cls, timestamp: float) -> Date: :returns: the `Date` as local date `Date` in UTC. """ - return cls.from_clock_time((timestamp, 0), UnixEpoch) + return cls._from_clock_time((timestamp, 0), UnixEpoch) @classmethod def from_ordinal(cls, ordinal: int) -> Date: @@ -921,7 +937,7 @@ def from_ordinal(cls, ordinal: int) -> Date: return ZeroDate elif ordinal < 0 or ordinal > 3652059: raise ValueError("Ordinal out of range (0..3652059)") - d = datetime.fromordinal(ordinal) + d = _datetime.fromordinal(ordinal) year, month, day = _normalize_day(d.year, d.month, d.day) return cls.__new(ordinal, year, month, day) @@ -960,7 +976,7 @@ def from_iso_format(cls, s: str) -> Date: :raises ValueError: if the string could not be parsed. """ - m = DATE_ISO_PATTERN.match(s) + m = _DATE_ISO_PATTERN.match(s) if m: year = int(m.group(1)) month = int(m.group(2)) @@ -969,7 +985,7 @@ def from_iso_format(cls, s: str) -> Date: raise ValueError("Date string must be in format YYYY-MM-DD") @classmethod - def from_native(cls, d: date) -> Date: + def from_native(cls, d: _date) -> Date: """ Convert from a native Python `datetime.date` value. @@ -977,7 +993,12 @@ def from_native(cls, d: date) -> Date: """ return Date.from_ordinal(d.toordinal()) + # TODO: 7.0 - remove public alias (copy over docstring) @classmethod + @_deprecated( + "ClockTime is an implementation detail. " + "It and its related methods will be removed in a future version." + ) def from_clock_time( cls, clock_time: ClockTime | tuple[float, int], @@ -989,9 +1010,21 @@ def from_clock_time( :param clock_time: the clock time as :class:`.ClockTime` or as tuple of (seconds, nanoseconds) :param epoch: the epoch to which `clock_time` is relative + + .. deprecated:: 6.0 + :class:`ClockTime` is an implementation detail. + It and its related methods will be removed in a future version. """ + return cls._from_clock_time(clock_time, epoch) + + @classmethod + def _from_clock_time( + cls, + clock_time: ClockTime | tuple[float, int], + epoch: DateTime, + ) -> Date: try: - clock_time = ClockTime(*clock_time) + clock_time = _ClockTime(*clock_time) except (TypeError, ValueError): raise ValueError( "Clock time must be a 2-tuple of (s, ns)" @@ -1012,7 +1045,7 @@ def is_leap_year(cls, year: int) -> bool: """ if year < MIN_YEAR or year > MAX_YEAR: raise ValueError(f"Year out of range ({MIN_YEAR}..{MAX_YEAR})") - return IS_LEAP_YEAR[year] + return _IS_LEAP_YEAR[year] @classmethod def days_in_year(cls, year: int) -> int: @@ -1026,7 +1059,7 @@ def days_in_year(cls, year: int) -> int: """ if year < MIN_YEAR or year > MAX_YEAR: raise ValueError(f"Year out of range ({MIN_YEAR}..{MAX_YEAR})") - return DAYS_IN_YEAR[year] + return _DAYS_IN_YEAR[year] @classmethod def days_in_month(cls, year: int, month: int) -> int: @@ -1044,7 +1077,7 @@ def days_in_month(cls, year: int, month: int) -> int: raise ValueError(f"Year out of range ({MIN_YEAR}..{MAX_YEAR})") if month < 1 or month > 12: raise ValueError("Month out of range (1..12)") - return DAYS_IN_MONTH[year, month] + return _DAYS_IN_MONTH[year, month] @classmethod def __calc_ordinal(cls, year, month, day): @@ -1052,7 +1085,7 @@ def __calc_ordinal(cls, year, month, day): day = cls.days_in_month(year, month) + int(day) + 1 # The built-in date class does this faster than a # long-hand pure Python algorithm could - return date(year, month, day).toordinal() + return _date(year, month, day).toordinal() # CLASS METHOD ALIASES # @@ -1183,7 +1216,7 @@ def __hash__(self): def __eq__(self, other: object) -> bool: """``==`` comparison with :class:`.Date` or :class:`datetime.date`.""" - if not isinstance(other, (Date, date)): + if not isinstance(other, (Date, _date)): # TODO: 6.0 - return NotImplemented for non-Date objects # return NotImplemented return False @@ -1196,9 +1229,9 @@ def __ne__(self, other: object) -> bool: # return NotImplemented return not self.__eq__(other) - def __lt__(self, other: Date | date) -> bool: + def __lt__(self, other: Date | _date) -> bool: """``<`` comparison with :class:`.Date` or :class:`datetime.date`.""" - if not isinstance(other, (Date, date)): + if not isinstance(other, (Date, _date)): # TODO: 6.0 - return NotImplemented for non-Date objects # return NotImplemented raise TypeError( @@ -1207,9 +1240,9 @@ def __lt__(self, other: Date | date) -> bool: ) return self.toordinal() < other.toordinal() - def __le__(self, other: Date | date) -> bool: + def __le__(self, other: Date | _date) -> bool: """``<=`` comparison with :class:`.Date` or :class:`datetime.date`.""" - if not isinstance(other, (Date, date)): + if not isinstance(other, (Date, _date)): # TODO: 6.0 - return NotImplemented for non-Date objects # return NotImplemented raise TypeError( @@ -1218,9 +1251,9 @@ def __le__(self, other: Date | date) -> bool: ) return self.toordinal() <= other.toordinal() - def __ge__(self, other: Date | date) -> bool: + def __ge__(self, other: Date | _date) -> bool: """``>=`` comparison with :class:`.Date` or :class:`datetime.date`.""" - if not isinstance(other, (Date, date)): + if not isinstance(other, (Date, _date)): # TODO: 6.0 - return NotImplemented for non-Date objects # return NotImplemented raise TypeError( @@ -1229,9 +1262,9 @@ def __ge__(self, other: Date | date) -> bool: ) return self.toordinal() >= other.toordinal() - def __gt__(self, other: Date | date) -> bool: + def __gt__(self, other: Date | _date) -> bool: """``>`` comparison with :class:`.Date` or :class:`datetime.date`.""" - if not isinstance(other, (Date, date)): + if not isinstance(other, (Date, _date)): # TODO: 6.0 - return NotImplemented for non-Date objects # return NotImplemented raise TypeError( @@ -1284,7 +1317,7 @@ def add_days(d, days): return NotImplemented @_t.overload # type: ignore[override] - def __sub__(self, other: Date | date) -> Duration: ... + def __sub__(self, other: Date | _date) -> Duration: ... @_t.overload def __sub__(self, other: Duration) -> Date: ... @@ -1300,7 +1333,7 @@ def __sub__(self, other): :raises ValueError: if the added duration has a time component. """ - if isinstance(other, (Date, date)): + if isinstance(other, (Date, _date)): return Duration(days=(self.toordinal() - other.toordinal())) try: return self.__add__(-other) @@ -1351,11 +1384,11 @@ def replace(self, **kwargs) -> Date: int(kwargs.get("day", self.__day)), ) - def time_tuple(self) -> struct_time: + def time_tuple(self) -> _struct_time: """Convert the date to :class:`time.struct_time`.""" _, _, day_of_week = self.year_week_day _, day_of_year = self.year_day - return struct_time( + return _struct_time( ( self.year, self.month, @@ -1378,20 +1411,34 @@ def to_ordinal(self) -> int: """ return self.__ordinal + # TODO: 7.0 - remove public alias (copy over docstring) + @_deprecated( + "ClockTime is an implementation detail. " + "It and its related methods will be removed in a future version." + ) def to_clock_time(self, epoch: Date | DateTime) -> ClockTime: """ - Convert the date to :class:`ClockTime` relative to `epoch`. + Convert the date to :class:`ClockTime` relative to `epoch`'s date. :param epoch: the epoch to which the date is relative + + :returns: the :class:`ClockTime` value. + + .. deprecated:: 6.0 + :class:`ClockTime` is an implementation detail. + It and its related methods will be removed in a future version. """ + return self._to_clock_time(epoch) + + def _to_clock_time(self, epoch: Date | DateTime) -> ClockTime: try: - return ClockTime(86400 * (self.to_ordinal() - epoch.to_ordinal())) + return _ClockTime(86400 * (self.to_ordinal() - epoch.to_ordinal())) except AttributeError: raise TypeError("Epoch has no ordinal value") from None - def to_native(self) -> date: + def to_native(self) -> _date: """Convert to a native Python :class:`datetime.date` value.""" - return date.fromordinal(self.to_ordinal()) + return _date.fromordinal(self.to_ordinal()) def weekday(self) -> int: """Get the day of the week where Monday is 0 and Sunday is 6.""" @@ -1424,7 +1471,7 @@ def __str__(self) -> str: def __format__(self, format_spec): if not format_spec: return self.iso_format() - format_spec = FORMAT_F_REPLACE.sub("000000000", format_spec) + format_spec = _FORMAT_F_REPLACE.sub("000000000", format_spec) return self.to_native().__format__(format_spec) # INSTANCE METHOD ALIASES # @@ -1471,14 +1518,14 @@ def iso_calendar(self) -> tuple[int, int, int]: ... if _t.TYPE_CHECKING: # make typechecker believe that Time subclasses datetime.time # https://github.com/python/typeshed/issues/8409#issuecomment-1197704527 - time_base_class = time + _time_base_class = _time else: - time_base_class = object + _time_base_class = object def _dst( tz: _tzinfo | None = None, dt: DateTime | None = None -) -> timedelta | None: +) -> _timedelta | None: if tz is None: return None try: @@ -1491,7 +1538,7 @@ def _dst( value = tz.dst(dt.to_native()) # type: ignore if value is None: return None - if isinstance(value, timedelta): + if isinstance(value, _timedelta): if value.days != 0: raise ValueError("dst must be less than a day") if value.seconds % 60 != 0 or value.microseconds != 0: @@ -1513,7 +1560,7 @@ def _tz_name(tz: _tzinfo | None, dt: DateTime | None) -> str | None: return tz.tzname(dt.to_native()) -class Time(time_base_class, metaclass=TimeType): +class Time(_time_base_class, metaclass=_TimeType): """ Time of day. @@ -1609,11 +1656,11 @@ def now(cls, tz: _tzinfo | None = None) -> Time: this to be restricted to years from 1970 through 2038. """ if tz is None: - return cls.from_clock_time(Clock().local_time(), UnixEpoch) + return cls._from_clock_time(_Clock().local_time(), UnixEpoch) else: return ( DateTime.utc_now() - .replace(tzinfo=timezone.utc) + .replace(tzinfo=_timezone.utc) .astimezone(tz) .timetz() ) @@ -1621,7 +1668,7 @@ def now(cls, tz: _tzinfo | None = None) -> Time: @classmethod def utc_now(cls) -> Time: """Get the current time as UTC local time.""" - return cls.from_clock_time(Clock().utc_time(), UnixEpoch) + return cls._from_clock_time(_Clock().utc_time(), UnixEpoch) @classmethod def from_iso_format(cls, s: str) -> Time: @@ -1651,7 +1698,7 @@ def from_iso_format(cls, s: str) -> Time: """ from pytz import FixedOffset # type: ignore - m = TIME_ISO_PATTERN.match(s) + m = _TIME_ISO_PATTERN.match(s) if m: hour = int(m.group(1)) minute = int(m.group(3) or 0) @@ -1694,7 +1741,7 @@ def from_ticks(cls, ticks: int, tz: _tzinfo | None = None) -> Time: if not isinstance(ticks, int): raise TypeError("Ticks must be int") if 0 <= ticks < 86400000000000: - second, nanosecond = divmod(ticks, NANO_SECONDS) + second, nanosecond = divmod(ticks, _NANO_SECONDS) minute, second = divmod(second, 60) hour, minute = divmod(minute, 60) return cls.__unchecked_new( @@ -1703,7 +1750,7 @@ def from_ticks(cls, ticks: int, tz: _tzinfo | None = None) -> Time: raise ValueError("Ticks out of range (0..86400000000000)") @classmethod - def from_native(cls, t: time) -> Time: + def from_native(cls, t: _time) -> Time: """ Convert from a native Python :class:`datetime.time` value. @@ -1712,7 +1759,12 @@ def from_native(cls, t: time) -> Time: nanosecond = t.microsecond * 1000 return Time(t.hour, t.minute, t.second, nanosecond, t.tzinfo) + # TODO: 7.0 - remove public alias (copy over docstring) @classmethod + @_deprecated( + "ClockTime is an implementation detail. " + "It and its related methods will be removed in a future version." + ) def from_clock_time( cls, clock_time: ClockTime | tuple[float, int], @@ -1727,11 +1779,23 @@ def from_clock_time( :param clock_time: the clock time as :class:`.ClockTime` or as tuple of (seconds, nanoseconds) :param epoch: the epoch to which `clock_time` is relative + + .. deprecated:: 6.0 + :class:`ClockTime` is an implementation detail. + It and its related methods will be removed in a future version. """ - clock_time = ClockTime(*clock_time) + return cls._from_clock_time(clock_time, epoch) + + @classmethod + def _from_clock_time( + cls, + clock_time: ClockTime | tuple[float, int], + epoch: DateTime, + ) -> Time: + clock_time = _ClockTime(*clock_time) ts = clock_time.seconds % 86400 - nanoseconds = int(NANO_SECONDS * ts + clock_time.nanoseconds) - ticks = (epoch.time().ticks + nanoseconds) % (86400 * NANO_SECONDS) + nanoseconds = int(_NANO_SECONDS * ts + clock_time.nanoseconds) + ticks = (epoch.time().ticks + nanoseconds) % (86400 * _NANO_SECONDS) return Time.from_ticks(ticks) @classmethod @@ -1760,9 +1824,9 @@ def __normalize_second(cls, hour, minute, second): @classmethod def __normalize_nanosecond(cls, hour, minute, second, nanosecond): hour, minute, second = cls.__normalize_second(hour, minute, second) - if 0 <= nanosecond < NANO_SECONDS: + if 0 <= nanosecond < _NANO_SECONDS: return hour, minute, second, nanosecond - raise ValueError(f"Nanosecond out of range (0..{NANO_SECONDS - 1})") + raise ValueError(f"Nanosecond out of range (0..{_NANO_SECONDS - 1})") # CLASS METHOD ALIASES # @@ -1854,7 +1918,7 @@ def tzinfo(self) -> _tzinfo | None: # OPERATIONS # def _get_both_normalized_ticks(self, other: object, strict=True): - if isinstance(other, (time, Time)) and ( + if isinstance(other, (_time, Time)) and ( (self.utc_offset() is None) ^ (other.utcoffset() is None) ): if strict: @@ -1866,23 +1930,23 @@ def _get_both_normalized_ticks(self, other: object, strict=True): other_ticks: int if isinstance(other, Time): other_ticks = other.__ticks - elif isinstance(other, time): + elif isinstance(other, _time): other_ticks = int( 3600000000000 * other.hour + 60000000000 * other.minute - + NANO_SECONDS * other.second + + _NANO_SECONDS * other.second + 1000 * other.microsecond ) else: return None, None - assert isinstance(other, (Time, time)) - utc_offset: timedelta | None = other.utcoffset() + assert isinstance(other, (Time, _time)) + utc_offset: _timedelta | None = other.utcoffset() if utc_offset is not None: - other_ticks -= int(utc_offset.total_seconds() * NANO_SECONDS) + other_ticks -= int(utc_offset.total_seconds() * _NANO_SECONDS) self_ticks = self.__ticks utc_offset = self.utc_offset() if utc_offset is not None: - self_ticks -= int(utc_offset.total_seconds() * NANO_SECONDS) + self_ticks -= int(utc_offset.total_seconds() * _NANO_SECONDS) return self_ticks, other_ticks def __hash__(self): @@ -1890,7 +1954,7 @@ def __hash__(self): return hash(self.to_native()) self_ticks = self.__ticks if self.utc_offset() is not None: - self_ticks -= self.utc_offset().total_seconds() * NANO_SECONDS + self_ticks -= self.utc_offset().total_seconds() * _NANO_SECONDS return hash(self_ticks) def __eq__(self, other: object) -> bool: @@ -1906,28 +1970,28 @@ def __ne__(self, other: object) -> bool: """`!=` comparison with :class:`.Time` or :class:`datetime.time`.""" return not self.__eq__(other) - def __lt__(self, other: Time | time) -> bool: + def __lt__(self, other: Time | _time) -> bool: """`<` comparison with :class:`.Time` or :class:`datetime.time`.""" self_ticks, other_ticks = self._get_both_normalized_ticks(other) if self_ticks is None: return NotImplemented return self_ticks < other_ticks - def __le__(self, other: Time | time) -> bool: + def __le__(self, other: Time | _time) -> bool: """`<=` comparison with :class:`.Time` or :class:`datetime.time`.""" self_ticks, other_ticks = self._get_both_normalized_ticks(other) if self_ticks is None: return NotImplemented return self_ticks <= other_ticks - def __ge__(self, other: Time | time) -> bool: + def __ge__(self, other: Time | _time) -> bool: """`>=` comparison with :class:`.Time` or :class:`datetime.time`.""" self_ticks, other_ticks = self._get_both_normalized_ticks(other) if self_ticks is None: return NotImplemented return self_ticks >= other_ticks - def __gt__(self, other: Time | time) -> bool: + def __gt__(self, other: Time | _time) -> bool: """`>` comparison with :class:`.Time` or :class:`datetime.time`.""" self_ticks, other_ticks = self._get_both_normalized_ticks(other) if self_ticks is None: @@ -1985,7 +2049,7 @@ def _utc_offset(self, dt=None): value = self.tzinfo.utcoffset(dt.to_native()) if value is None: return None - if isinstance(value, timedelta): + if isinstance(value, _timedelta): s = value.total_seconds() if not (-86400 < s < 86400): raise ValueError("utcoffset must be less than a day") @@ -1994,7 +2058,7 @@ def _utc_offset(self, dt=None): return value raise TypeError("utcoffset must be a timedelta") - def utc_offset(self) -> timedelta | None: + def utc_offset(self) -> _timedelta | None: """ Return the UTC offset of this time. @@ -2009,7 +2073,7 @@ def utc_offset(self) -> timedelta | None: """ return self._utc_offset() - def dst(self) -> timedelta | None: + def dst(self) -> _timedelta | None: """ Get the daylight saving time adjustment (DST). @@ -2033,12 +2097,28 @@ def tzname(self) -> str | None: """ return _tz_name(self.tzinfo, None) + # TODO: 7.0 - remove public alias (copy over docstring) + @_deprecated( + "ClockTime is an implementation detail. " + "It and its related methods will be removed in a future version." + ) def to_clock_time(self) -> ClockTime: - """Convert to :class:`.ClockTime`.""" - seconds, nanoseconds = divmod(self.ticks, NANO_SECONDS) - return ClockTime(seconds, nanoseconds) + """ + Convert to :class:`.ClockTime`. - def to_native(self) -> time: + The returned :class:`.ClockTime` is relative to :attr:`Time.min`. + + .. deprecated:: 6.0 + :class:`ClockTime` is an implementation detail. + It and its related methods will be removed in a future version. + """ + return self._to_clock_time() + + def _to_clock_time(self) -> ClockTime: + seconds, nanoseconds = divmod(self.ticks, _NANO_SECONDS) + return _ClockTime(seconds, nanoseconds) + + def to_native(self) -> _time: """ Convert to a native Python `datetime.time` value. @@ -2046,9 +2126,9 @@ def to_native(self) -> time: supports a resolution of microseconds instead of nanoseconds. """ h, m, s, ns = self.hour_minute_second_nanosecond - µs = round_half_to_even(ns / 1000) + µs = _round_half_to_even(ns / 1000) tz = self.tzinfo - return time(h, m, s, µs, tz) + return _time(h, m, s, µs, tz) def iso_format(self) -> str: """Return the :class:`.Time` as ISO formatted string.""" @@ -2080,7 +2160,7 @@ def __str__(self) -> str: def __format__(self, format_spec): if not format_spec: return self.iso_format() - format_spec = FORMAT_F_REPLACE.sub( + format_spec = _FORMAT_F_REPLACE.sub( f"{self.__nanosecond:09}", format_spec ) return self.to_native().__format__(format_spec) @@ -2132,13 +2212,13 @@ def isoformat(self) -> str: # type: ignore[override] if _t.TYPE_CHECKING: # make typechecker believe that DateTime subclasses datetime.datetime # https://github.com/python/typeshed/issues/8409#issuecomment-1197704527 - date_time_base_class = datetime + _date_time_base_class = _datetime else: - date_time_base_class = object + _date_time_base_class = object -@total_ordering -class DateTime(date_time_base_class, metaclass=DateTimeType): +@_total_ordering +class DateTime(_date_time_base_class, metaclass=_DateTimeType): """ A point in time represented as a date and a time. @@ -2203,10 +2283,10 @@ def now(cls, tz: _tzinfo | None = None) -> DateTime: this to be restricted to years from 1970 through 2038. """ if tz is None: - return cls.from_clock_time(Clock().local_time(), UnixEpoch) + return cls._from_clock_time(_Clock().local_time(), UnixEpoch) else: - utc_now = cls.from_clock_time( - Clock().utc_time(), UnixEpoch + utc_now = cls._from_clock_time( + _Clock().utc_time(), UnixEpoch ).replace(tzinfo=tz) try: return tz.fromutc(utc_now) # type: ignore @@ -2227,7 +2307,7 @@ def now(cls, tz: _tzinfo | None = None) -> DateTime: @classmethod def utc_now(cls) -> DateTime: """Get the current date and time in UTC.""" - return cls.from_clock_time(Clock().utc_time(), UnixEpoch) + return cls._from_clock_time(_Clock().utc_time(), UnixEpoch) @classmethod def from_iso_format(cls, s) -> DateTime: @@ -2260,13 +2340,13 @@ def from_timestamp( this to be restricted to years from 1970 through 2038. """ if tz is None: - return cls.from_clock_time( - ClockTime(timestamp) + Clock().local_offset(), UnixEpoch + return cls._from_clock_time( + _ClockTime(timestamp) + _Clock().local_offset(), UnixEpoch ) else: return ( cls.utc_from_timestamp(timestamp) - .replace(tzinfo=timezone.utc) + .replace(tzinfo=_timezone.utc) .astimezone(tz) ) @@ -2277,7 +2357,7 @@ def utc_from_timestamp(cls, timestamp: float) -> DateTime: Returns the `DateTime` as local date `DateTime` in UTC. """ - return cls.from_clock_time((timestamp, 0), UnixEpoch) + return cls._from_clock_time((timestamp, 0), UnixEpoch) @classmethod def from_ordinal(cls, ordinal: int) -> DateTime: @@ -2316,7 +2396,7 @@ def parse(cls, date_string, format): raise NotImplementedError @classmethod - def from_native(cls, dt: datetime) -> DateTime: + def from_native(cls, dt: _datetime) -> DateTime: """ Convert from a native Python :class:`datetime.datetime` value. @@ -2326,7 +2406,12 @@ def from_native(cls, dt: datetime) -> DateTime: Date.from_native(dt.date()), Time.from_native(dt.timetz()) ) + # TODO: 7.0 - remove public alias (copy over docstring) @classmethod + @_deprecated( + "ClockTime is an implementation detail. " + "It and its related methods will be removed in a future version." + ) def from_clock_time( cls, clock_time: ClockTime | tuple[float, int], @@ -2340,15 +2425,27 @@ def from_clock_time( :param epoch: the epoch to which `clock_time` is relative :raises ValueError: if `clock_time` is invalid. + + .. deprecated:: 6.0 + :class:`ClockTime` is an implementation detail. + It and its related methods will be removed in a future version. """ + return cls._from_clock_time(clock_time, epoch) + + @classmethod + def _from_clock_time( + cls, + clock_time: ClockTime | tuple[float, int], + epoch: DateTime, + ) -> DateTime: try: - seconds, nanoseconds = ClockTime(*clock_time) + seconds, nanoseconds = _ClockTime(*clock_time) except (TypeError, ValueError) as e: raise ValueError("Clock time must be a 2-tuple of (s, ns)") from e else: ordinal, seconds = divmod(seconds, 86400) - ticks = epoch.time().ticks + seconds * NANO_SECONDS + nanoseconds - days, ticks = divmod(ticks, 86400 * NANO_SECONDS) + ticks = epoch.time().ticks + seconds * _NANO_SECONDS + nanoseconds + days, ticks = divmod(ticks, 86400 * _NANO_SECONDS) ordinal += days date_ = Date.from_ordinal(ordinal + epoch.date().to_ordinal()) time_ = Time.from_ticks(ticks) @@ -2507,7 +2604,7 @@ def hour_minute_second_nanosecond(self) -> tuple[int, int, int, int]: # OPERATIONS # def _get_both_normalized(self, other, strict=True): - if isinstance(other, (datetime, DateTime)) and ( + if isinstance(other, (_datetime, DateTime)) and ( (self.utc_offset() is None) ^ (other.utcoffset() is None) ): if strict: @@ -2522,7 +2619,7 @@ def _get_both_normalized(self, other, strict=True): self_norm -= utc_offset self_norm = self_norm.replace(tzinfo=None) other_norm = other - if isinstance(other, (datetime, DateTime)): + if isinstance(other, (_datetime, DateTime)): utc_offset = other.utcoffset() if utc_offset is not None: other_norm -= utc_offset @@ -2546,7 +2643,7 @@ def __eq__(self, other: object) -> bool: Accepts :class:`.DateTime` and :class:`datetime.datetime`. """ - if not isinstance(other, (datetime, DateTime)): + if not isinstance(other, (_datetime, DateTime)): return NotImplemented if self.utc_offset() == other.utcoffset(): return self.date() == other.date() and self.time() == other.time() @@ -2561,19 +2658,19 @@ def __ne__(self, other: object) -> bool: Accepts :class:`.DateTime` and :class:`datetime.datetime`. """ - if not isinstance(other, (DateTime, datetime)): + if not isinstance(other, (DateTime, _datetime)): return NotImplemented return not self.__eq__(other) def __lt__( # type: ignore[override] - self, other: datetime | DateTime + self, other: _datetime | DateTime ) -> bool: """ ``<`` comparison with another datetime. Accepts :class:`.DateTime` and :class:`datetime.datetime`. """ - if not isinstance(other, (datetime, DateTime)): + if not isinstance(other, (_datetime, DateTime)): return NotImplemented if self.utc_offset() == other.utcoffset(): if self.date() == other.date(): @@ -2586,14 +2683,14 @@ def __lt__( # type: ignore[override] ) def __le__( # type: ignore[override] - self, other: datetime | DateTime + self, other: _datetime | DateTime ) -> bool: """ ``<=`` comparison with another datetime. Accepts :class:`.DateTime` and :class:`datetime.datetime`. """ - if not isinstance(other, (datetime, DateTime)): + if not isinstance(other, (_datetime, DateTime)): return NotImplemented if self.utc_offset() == other.utcoffset(): if self.date() == other.date(): @@ -2603,14 +2700,14 @@ def __le__( # type: ignore[override] return self_norm <= other_norm def __ge__( # type: ignore[override] - self, other: datetime | DateTime + self, other: _datetime | DateTime ) -> bool: """ ``>=`` comparison with another datetime. Accepts :class:`.DateTime` and :class:`datetime.datetime`. """ - if not isinstance(other, (datetime, DateTime)): + if not isinstance(other, (_datetime, DateTime)): return NotImplemented if self.utc_offset() == other.utcoffset(): if self.date() == other.date(): @@ -2627,7 +2724,7 @@ def __gt__( # type: ignore[override] Accepts :class:`.DateTime` and :class:`datetime.datetime`. """ - if not isinstance(other, (datetime, DateTime)): + if not isinstance(other, (_datetime, DateTime)): return NotImplemented if self.utc_offset() == other.utcoffset(): if self.date() == other.date(): @@ -2639,31 +2736,31 @@ def __gt__( # type: ignore[override] or self_norm.time() > other_norm.time() ) - def __add__(self, other: timedelta | Duration) -> DateTime: + def __add__(self, other: _timedelta | Duration) -> DateTime: """Add a :class:`datetime.timedelta`.""" if isinstance(other, Duration): if other == (0, 0, 0, 0): return self - t = self.time().to_clock_time() + ClockTime( + t = self.time()._to_clock_time() + _ClockTime( other.seconds, other.nanoseconds ) - days, seconds = symmetric_divmod(t.seconds, 86400) + days, seconds = _symmetric_divmod(t.seconds, 86400) date_ = self.date() + Duration( months=other.months, days=days + other.days ) - time_ = Time.from_ticks(seconds * NANO_SECONDS + t.nanoseconds) + time_ = Time.from_ticks(seconds * _NANO_SECONDS + t.nanoseconds) return self.combine(date_, time_).replace(tzinfo=self.tzinfo) - if isinstance(other, timedelta): + if isinstance(other, _timedelta): if other.total_seconds() == 0: return self - t = self.to_clock_time() + ClockTime( + t = self._to_clock_time() + _ClockTime( 86400 * other.days + other.seconds, other.microseconds * 1000, ) - days, seconds = symmetric_divmod(t.seconds, 86400) + days, seconds = _symmetric_divmod(t.seconds, 86400) date_ = Date.from_ordinal(days + 1) time_ = Time.from_ticks( - round_half_to_even(seconds * NANO_SECONDS + t.nanoseconds) + _round_half_to_even(seconds * _NANO_SECONDS + t.nanoseconds) ) return self.combine(date_, time_).replace(tzinfo=self.tzinfo) return NotImplemented @@ -2672,10 +2769,10 @@ def __add__(self, other: timedelta | Duration) -> DateTime: def __sub__(self, other: DateTime) -> Duration: ... @_t.overload - def __sub__(self, other: datetime) -> timedelta: ... + def __sub__(self, other: _datetime) -> _timedelta: ... @_t.overload - def __sub__(self, other: Duration | timedelta) -> DateTime: ... + def __sub__(self, other: Duration | _timedelta) -> DateTime: ... def __sub__(self, other): """ @@ -2695,27 +2792,27 @@ def __sub__(self, other): other_month_ordinal = 12 * (other.year - 1) + other.month months = self_month_ordinal - other_month_ordinal days = self.day - other.day - t = self.time().to_clock_time() - other.time().to_clock_time() + t = self.time()._to_clock_time() - other.time()._to_clock_time() return Duration( months=months, days=days, seconds=t.seconds, nanoseconds=t.nanoseconds, ) - if isinstance(other, datetime): + if isinstance(other, _datetime): days = self.to_ordinal() - other.toordinal() - t = self.time().to_clock_time() - ClockTime( + t = self.time()._to_clock_time() - _ClockTime( 3600 * other.hour + 60 * other.minute + other.second, other.microsecond * 1000, ) - return timedelta( + return _timedelta( days=days, seconds=t.seconds, microseconds=(t.nanoseconds // 1000), ) if isinstance(other, Duration): return self.__add__(-other) - if isinstance(other, timedelta): + if isinstance(other, _timedelta): return self.__add__(-other) return NotImplemented @@ -2783,7 +2880,7 @@ def as_timezone(self, tz: _tzinfo) -> DateTime: """ if self.tzinfo is None: return self - offset = _t.cast(timedelta, self.utcoffset()) + offset = _t.cast(_timedelta, self.utcoffset()) utc = (self - offset).replace(tzinfo=tz) try: return tz.fromutc(utc) # type: ignore @@ -2796,7 +2893,7 @@ def as_timezone(self, tz: _tzinfo) -> DateTime: ns = native_res.microsecond * 1000 + self.nanosecond % 1000 return res.replace(nanosecond=ns) - def utc_offset(self) -> timedelta | None: + def utc_offset(self) -> _timedelta | None: """ Get the date times utc offset. @@ -2804,7 +2901,7 @@ def utc_offset(self) -> timedelta | None: """ return self.__time._utc_offset(self) - def dst(self) -> timedelta | None: + def dst(self) -> _timedelta | None: """ Get the daylight saving time adjustment (DST). @@ -2834,13 +2931,29 @@ def to_ordinal(self) -> int: """ return self.__date.to_ordinal() + # TODO: 7.0 - remove public alias (copy over docstring) + @_deprecated( + "ClockTime is an implementation detail. " + "It and its related methods will be removed in a future version." + ) def to_clock_time(self) -> ClockTime: - """Convert to :class:`.ClockTime`.""" + """ + Convert to :class:`.ClockTime`. + + The returned :class:`.ClockTime` is relative to :attr:`DateTime.min`. + + .. deprecated:: 6.0 + :class:`ClockTime` is an implementation detail. + It and its related methods will be removed in a future version. + """ + return self._to_clock_time() + + def _to_clock_time(self) -> ClockTime: ordinal_seconds = 86400 * (self.__date.to_ordinal() - 1) - time_seconds, nanoseconds = divmod(self.__time.ticks, NANO_SECONDS) - return ClockTime(ordinal_seconds + time_seconds, nanoseconds) + time_seconds, nanoseconds = divmod(self.__time.ticks, _NANO_SECONDS) + return _ClockTime(ordinal_seconds + time_seconds, nanoseconds) - def to_native(self) -> datetime: + def to_native(self) -> _datetime: """ Convert to a native Python :class:`datetime.datetime` value. @@ -2851,7 +2964,7 @@ def to_native(self) -> datetime: h, m, s, ns = self.hour_minute_second_nanosecond ms = int(ns / 1000) tz = self.tzinfo - return datetime(y, mo, d, h, m, s, ms, tz) + return _datetime(y, mo, d, h, m, s, ms, tz) def weekday(self) -> int: """ @@ -2918,7 +3031,7 @@ def __str__(self) -> str: def __format__(self, format_spec): if not format_spec: return self.iso_format() - format_spec = FORMAT_F_REPLACE.sub( + format_spec = _FORMAT_F_REPLACE.sub( f"{self.__time.nanosecond:09}", format_spec ) return self.to_native().__format__(format_spec) @@ -2980,3 +3093,20 @@ def iso_format(self, sep: str = "T") -> str: # type: ignore[override] #: A :class:`.DateTime` instance set to `1970-01-01T00:00:00`. UnixEpoch = DateTime(1970, 1, 1, 0, 0, 0) + + +_ClockTime = ClockTime + +if not _t.TYPE_CHECKING: + del ClockTime + + +def __getattr__(name): + if name == "ClockTime": + _deprecation_warn( + "ClockTime is an implementation detail. It and its related " + "methods will be removed in a future version.", + stack_level=2, + ) + return _ClockTime + raise AttributeError(f"module {__name__} has no attribute {name}") diff --git a/src/neo4j/time/_clock_implementations.py b/src/neo4j/time/_clock_implementations.py index 63ba002ae..dd98da8ef 100644 --- a/src/neo4j/time/_clock_implementations.py +++ b/src/neo4j/time/_clock_implementations.py @@ -25,8 +25,8 @@ from platform import uname from . import ( - Clock, - ClockTime, + _Clock, + _ClockTime, ) from ._arithmetic import nano_divmod @@ -38,7 +38,7 @@ ] -class SafeClock(Clock): +class SafeClock(_Clock): """ Clock implementation that should work for any variant of Python. @@ -55,10 +55,10 @@ def available(cls): def utc_time(self): seconds, nanoseconds = nano_divmod(int(time.time() * 1000000), 1000000) - return ClockTime(seconds, nanoseconds * 1000) + return _ClockTime(seconds, nanoseconds * 1000) -class PEP564Clock(Clock): +class PEP564Clock(_Clock): """ Clock implementation based on the PEP564 additions to Python 3.7. @@ -76,10 +76,10 @@ def available(cls): def utc_time(self): t = time.time_ns() seconds, nanoseconds = divmod(t, 1000000000) - return ClockTime(seconds, nanoseconds) + return _ClockTime(seconds, nanoseconds) -class LibCClock(Clock): +class LibCClock(_Clock): """ Clock implementation backed by libc. @@ -113,6 +113,6 @@ def utc_time(self): ts = self._TimeSpec() status = libc.clock_gettime(0, byref(ts)) if status == 0: - return ClockTime(ts.seconds, ts.nanoseconds) + return _ClockTime(ts.seconds, ts.nanoseconds) else: raise RuntimeError(f"clock_gettime failed with status {status}") diff --git a/tests/unit/common/codec/hydration/v1/test_temporal_dehydration.py b/tests/unit/common/codec/hydration/v1/test_temporal_dehydration.py index 624c9a0ac..90fcb4e8a 100644 --- a/tests/unit/common/codec/hydration/v1/test_temporal_dehydration.py +++ b/tests/unit/common/codec/hydration/v1/test_temporal_dehydration.py @@ -24,13 +24,13 @@ from neo4j._codec.hydration.v1 import HydrationHandler from neo4j._codec.packstream import Structure from neo4j.time import ( - AVERAGE_SECONDS_IN_DAY, + _AVERAGE_SECONDS_IN_DAY, + _NANO_SECONDS, Date, DateTime, Duration, MAX_INT64, MIN_INT64, - NANO_SECONDS, Time, ) @@ -226,8 +226,8 @@ def test_native_duration_mixed_sign(self, assert_transforms): (np.timedelta64(1, "ms"), (0, 0, 0, 1000000)), (np.timedelta64(1, "us"), (0, 0, 0, 1000)), (np.timedelta64(1, "ns"), (0, 0, 0, 1)), - (np.timedelta64(NANO_SECONDS, "ns"), (0, 0, 1, 0)), - (np.timedelta64(NANO_SECONDS + 1, "ns"), (0, 0, 1, 1)), + (np.timedelta64(_NANO_SECONDS, "ns"), (0, 0, 1, 0)), + (np.timedelta64(_NANO_SECONDS + 1, "ns"), (0, 0, 1, 1)), (np.timedelta64(1000, "ps"), (0, 0, 0, 1)), (np.timedelta64(1, "ps"), (0, 0, 0, 0)), (np.timedelta64(1000000, "fs"), (0, 0, 0, 1)), @@ -245,8 +245,8 @@ def test_native_duration_mixed_sign(self, assert_transforms): (np.timedelta64(-1, "ms"), (0, 0, 0, -1000000)), (np.timedelta64(-1, "us"), (0, 0, 0, -1000)), (np.timedelta64(-1, "ns"), (0, 0, 0, -1)), - (np.timedelta64(-NANO_SECONDS, "ns"), (0, 0, -1, 0)), - (np.timedelta64(-NANO_SECONDS - 1, "ns"), (0, 0, -1, -1)), + (np.timedelta64(-_NANO_SECONDS, "ns"), (0, 0, -1, 0)), + (np.timedelta64(-_NANO_SECONDS - 1, "ns"), (0, 0, -1, -1)), (np.timedelta64(-1000, "ps"), (0, 0, 0, -1)), (np.timedelta64(-1, "ps"), (0, 0, 0, -1)), (np.timedelta64(-1000000, "fs"), (0, 0, 0, -1)), @@ -278,13 +278,18 @@ def test_numpy_invalid_durations(self, value, error, transformer): ( ( pd.Timedelta(days=1, seconds=2, microseconds=3, nanoseconds=4), - (0, 0, AVERAGE_SECONDS_IN_DAY + 2, 3004), + (0, 0, _AVERAGE_SECONDS_IN_DAY + 2, 3004), ), ( pd.Timedelta( days=-1, seconds=2, microseconds=3, nanoseconds=4 ), - (0, 0, -AVERAGE_SECONDS_IN_DAY + 2 + 1, -NANO_SECONDS + 3004), + ( + 0, + 0, + -_AVERAGE_SECONDS_IN_DAY + 2 + 1, + -_NANO_SECONDS + 3004, + ), ), ), ) diff --git a/tests/unit/common/time/__init__.py b/tests/unit/common/time/__init__.py index 1184ddf33..993bcdf1c 100644 --- a/tests/unit/common/time/__init__.py +++ b/tests/unit/common/time/__init__.py @@ -15,15 +15,15 @@ from neo4j.time import ( - Clock, - ClockTime, + _Clock, + _ClockTime, ) # The existence of this class will make the driver's custom date time # implementation use it instead of a real clock since its precision it higher # than all the other clocks (only up to nanoseconds). -class FixedClock(Clock): +class FixedClock(_Clock): @classmethod def available(cls): return True @@ -34,7 +34,7 @@ def precision(cls): @classmethod def local_offset(cls): - return ClockTime() + return _ClockTime() def utc_time(self): - return ClockTime(45296, 789000001) + return _ClockTime(45296, 789000001) diff --git a/tests/unit/common/time/test_clock.py b/tests/unit/common/time/test_clock.py index 2eddb5799..3bc4251aa 100644 --- a/tests/unit/common/time/test_clock.py +++ b/tests/unit/common/time/test_clock.py @@ -16,53 +16,53 @@ import pytest -from neo4j.time._clock_implementations import ( - Clock, - ClockTime, +from neo4j.time import ( + _Clock, + _ClockTime, ) class TestClock: def test_no_clock_implementations(self): try: - Clock._Clock__implementations = [] + _Clock._Clock__implementations = [] with pytest.raises(RuntimeError): - _ = Clock() + _ = _Clock() finally: - Clock._Clock__implementations = None + _Clock._Clock__implementations = None def test_base_clock_precision(self): - clock = object.__new__(Clock) + clock = object.__new__(_Clock) with pytest.raises(NotImplementedError): _ = clock.precision() def test_base_clock_available(self): - clock = object.__new__(Clock) + clock = object.__new__(_Clock) with pytest.raises(NotImplementedError): _ = clock.available() def test_base_clock_utc_time(self): - clock = object.__new__(Clock) + clock = object.__new__(_Clock) with pytest.raises(NotImplementedError): _ = clock.utc_time() def test_local_offset(self): - clock = object.__new__(Clock) + clock = object.__new__(_Clock) offset = clock.local_offset() - assert isinstance(offset, ClockTime) + assert isinstance(offset, _ClockTime) def test_local_time(self): - _ = Clock() - for impl in Clock._Clock__implementations: - assert issubclass(impl, Clock) + _ = _Clock() + for impl in _Clock._Clock__implementations: + assert issubclass(impl, _Clock) clock = object.__new__(impl) time = clock.local_time() - assert isinstance(time, ClockTime) + assert isinstance(time, _ClockTime) def test_utc_time(self): - _ = Clock() - for impl in Clock._Clock__implementations: - assert issubclass(impl, Clock) + _ = _Clock() + for impl in _Clock._Clock__implementations: + assert issubclass(impl, _Clock) clock = object.__new__(impl) time = clock.utc_time() - assert isinstance(time, ClockTime) + assert isinstance(time, _ClockTime) diff --git a/tests/unit/common/time/test_clocktime.py b/tests/unit/common/time/test_clocktime.py index 17d747f01..d3af3131c 100644 --- a/tests/unit/common/time/test_clocktime.py +++ b/tests/unit/common/time/test_clocktime.py @@ -17,83 +17,83 @@ import pytest from neo4j.time import ( - ClockTime, + _ClockTime, Duration, ) class TestClockTime: def test_zero_(self): - ct = ClockTime() + ct = _ClockTime() assert ct.seconds == 0 assert ct.nanoseconds == 0 def test_only_seconds(self): - ct = ClockTime(123456) + ct = _ClockTime(123456) assert ct.seconds == 123456 assert ct.nanoseconds == 0 def test_float(self): - ct = ClockTime(123456.789) + ct = _ClockTime(123456.789) assert ct.seconds == 123456 assert ct.nanoseconds == 789000000 def test_only_nanoseconds(self): - ct = ClockTime(0, 123456789) + ct = _ClockTime(0, 123456789) assert ct.seconds == 0 assert ct.nanoseconds == 123456789 def test_nanoseconds_overflow(self): - ct = ClockTime(0, 2123456789) + ct = _ClockTime(0, 2123456789) assert ct.seconds == 2 assert ct.nanoseconds == 123456789 def test_positive_nanoseconds(self): - ct = ClockTime(1, 1) + ct = _ClockTime(1, 1) assert ct.seconds == 1 assert ct.nanoseconds == 1 def test_negative_nanoseconds(self): - ct = ClockTime(1, -1) + ct = _ClockTime(1, -1) assert ct.seconds == 0 assert ct.nanoseconds == 999999999 def test_add_float(self): - ct = ClockTime(123456.789) + 0.1 + ct = _ClockTime(123456.789) + 0.1 assert ct.seconds == 123456 assert ct.nanoseconds == 889000000 def test_add_duration(self): - ct = ClockTime(123456.789) + Duration(seconds=1) + ct = _ClockTime(123456.789) + Duration(seconds=1) assert ct.seconds == 123457 assert ct.nanoseconds == 789000000 def test_add_duration_with_months(self): with pytest.raises(ValueError): - _ = ClockTime(123456.789) + Duration(months=1) + _ = _ClockTime(123456.789) + Duration(months=1) def test_add_object(self): with pytest.raises(TypeError): - _ = ClockTime(123456.789) + object() + _ = _ClockTime(123456.789) + object() def test_sub_float(self): - ct = ClockTime(123456.789) - 0.1 + ct = _ClockTime(123456.789) - 0.1 assert ct.seconds == 123456 assert ct.nanoseconds == 689000000 def test_sub_duration(self): - ct = ClockTime(123456.789) - Duration(seconds=1) + ct = _ClockTime(123456.789) - Duration(seconds=1) assert ct.seconds == 123455 assert ct.nanoseconds == 789000000 def test_sub_duration_with_months(self): with pytest.raises(ValueError): - _ = ClockTime(123456.789) - Duration(months=1) + _ = _ClockTime(123456.789) - Duration(months=1) def test_sub_object(self): with pytest.raises(TypeError): - _ = ClockTime(123456.789) - object() + _ = _ClockTime(123456.789) - object() def test_repr(self): - ct = ClockTime(123456.789) + ct = _ClockTime(123456.789) assert repr(ct).startswith("ClockTime") diff --git a/tests/unit/common/time/test_date.py b/tests/unit/common/time/test_date.py index 8c517d4c2..2482690f8 100644 --- a/tests/unit/common/time/test_date.py +++ b/tests/unit/common/time/test_date.py @@ -229,12 +229,13 @@ def test_replace(self) -> None: assert d2 == Date(2017, 4, 30) def test_from_clock_time(self) -> None: - d = Date.from_clock_time((0, 0), epoch=UnixEpoch) + with pytest.warns(DeprecationWarning, match="ClockTime"): + d = Date.from_clock_time((0, 0), epoch=UnixEpoch) assert d == Date(1970, 1, 1) def test_bad_from_clock_time(self) -> None: with pytest.raises(ValueError): - _ = Date.from_clock_time(object(), None) # type: ignore[arg-type] + _ = Date._from_clock_time(object(), None) # type: ignore[arg-type] def test_is_leap_year(self) -> None: assert Date.is_leap_year(2000) @@ -497,10 +498,12 @@ def test_time_tuple(self) -> None: def test_to_clock_time(self) -> None: d = Date(2018, 4, 30) - assert d.to_clock_time(UnixEpoch) == (1525046400, 0) - assert d.to_clock_time(d) == (0, 0) + with pytest.warns(DeprecationWarning, match="ClockTime"): + assert d.to_clock_time(UnixEpoch) == (1525046400, 0) + with pytest.warns(DeprecationWarning, match="ClockTime"): + assert d.to_clock_time(d) == (0, 0) with pytest.raises(TypeError): - _ = d.to_clock_time(object()) # type: ignore[arg-type] + _ = d._to_clock_time(object()) # type: ignore[arg-type] def test_weekday(self) -> None: d = Date(2018, 4, 30) diff --git a/tests/unit/common/time/test_datetime.py b/tests/unit/common/time/test_datetime.py index 715eff4d7..3a7740ef2 100644 --- a/tests/unit/common/time/test_datetime.py +++ b/tests/unit/common/time/test_datetime.py @@ -36,6 +36,7 @@ ) from neo4j.time import ( + _ClockTime, DateTime, Duration, MAX_YEAR, @@ -45,7 +46,6 @@ nano_add, nano_div, ) -from neo4j.time._clock_implementations import ClockTime if t.TYPE_CHECKING: @@ -255,8 +255,9 @@ def test_from_timestamp_with_tz(self) -> None: def test_conversion_to_t(self) -> None: dt = DateTime(2018, 4, 26, 23, 0, 17, 914390409) - t = dt.to_clock_time() - assert t == ClockTime(63660380417, 914390409) + with pytest.warns(DeprecationWarning, match="ClockTime"): + t = dt.to_clock_time() + assert t == _ClockTime(63660380417, 914390409) def test_add_timedelta(self) -> None: dt1 = DateTime(2018, 4, 26, 23, 0, 17, 914390409) @@ -511,7 +512,7 @@ def test_ne( ( object(), 1, - DateTime(2018, 4, 27, 23, 0, 17, 914391409).to_clock_time(), + DateTime(2018, 4, 27, 23, 0, 17, 914391409)._to_clock_time(), ( DateTime(2018, 4, 27, 23, 0, 17, 914391409) - DateTime(1970, 1, 1) diff --git a/tests/unit/common/time/test_duration.py b/tests/unit/common/time/test_duration.py index d3698bfcc..221bac13c 100644 --- a/tests/unit/common/time/test_duration.py +++ b/tests/unit/common/time/test_duration.py @@ -454,14 +454,14 @@ def test_from_iso_format(self) -> None: def test_minimal_value(self, with_day, with_month, only_ns) -> None: seconds = ( time.MIN_INT64 - + with_month * time.AVERAGE_SECONDS_IN_MONTH - + with_day * time.AVERAGE_SECONDS_IN_DAY + + with_month * time._AVERAGE_SECONDS_IN_MONTH + + with_day * time._AVERAGE_SECONDS_IN_DAY ) Duration( months=-with_month, days=-with_day, seconds=0 if only_ns else seconds, - nanoseconds=(seconds * time.NANO_SECONDS) if only_ns else 0, + nanoseconds=(seconds * time._NANO_SECONDS) if only_ns else 0, ) @pytest.mark.parametrize("with_day", (True, False)) @@ -481,8 +481,8 @@ def test_negative_overflow_value( ) -> None: seconds = ( time.MIN_INT64 - + with_month * time.AVERAGE_SECONDS_IN_MONTH - + with_day * time.AVERAGE_SECONDS_IN_DAY + + with_month * time._AVERAGE_SECONDS_IN_MONTH + + with_day * time._AVERAGE_SECONDS_IN_DAY ) kwargs = { "months": overflow[0], @@ -493,7 +493,7 @@ def test_negative_overflow_value( kwargs["months"] -= with_month kwargs["days"] -= with_day if only_ns: - kwargs["nanoseconds"] += seconds * time.NANO_SECONDS + kwargs["nanoseconds"] += seconds * time._NANO_SECONDS else: kwargs["seconds"] += seconds @@ -503,8 +503,8 @@ def test_negative_overflow_value( @pytest.mark.parametrize( ("field", "module"), ( - ("days", time.AVERAGE_SECONDS_IN_DAY), - ("months", time.AVERAGE_SECONDS_IN_MONTH), + ("days", time._AVERAGE_SECONDS_IN_DAY), + ("months", time._AVERAGE_SECONDS_IN_MONTH), ), ) def test_minimal_value_only_secondary_field(self, field, module) -> None: @@ -518,8 +518,8 @@ def test_minimal_value_only_secondary_field(self, field, module) -> None: @pytest.mark.parametrize( ("field", "module"), ( - ("days", time.AVERAGE_SECONDS_IN_DAY), - ("months", time.AVERAGE_SECONDS_IN_MONTH), + ("days", time._AVERAGE_SECONDS_IN_DAY), + ("months", time._AVERAGE_SECONDS_IN_MONTH), ), ) def test_negative_overflow_value_only_secondary_field( @@ -544,14 +544,14 @@ def test_negative_overflow_duration_addition(self) -> None: def test_maximal_value(self, with_day, with_month, only_ns) -> None: seconds = ( time.MAX_INT64 - - with_month * time.AVERAGE_SECONDS_IN_MONTH - - with_day * time.AVERAGE_SECONDS_IN_DAY + - with_month * time._AVERAGE_SECONDS_IN_MONTH + - with_day * time._AVERAGE_SECONDS_IN_DAY ) Duration( months=with_month, days=with_day, seconds=0 if only_ns else seconds, - nanoseconds=(seconds * time.NANO_SECONDS) if only_ns else 0, + nanoseconds=(seconds * time._NANO_SECONDS) if only_ns else 0, ) @pytest.mark.parametrize("with_day", (True, False)) @@ -571,19 +571,19 @@ def test_positive_overflow_value( ) -> None: seconds = ( time.MAX_INT64 - - with_month * time.AVERAGE_SECONDS_IN_MONTH - - with_day * time.AVERAGE_SECONDS_IN_DAY + - with_month * time._AVERAGE_SECONDS_IN_MONTH + - with_day * time._AVERAGE_SECONDS_IN_DAY ) kwargs = { "months": overflow[0], "days": overflow[1], "seconds": overflow[2], - "nanoseconds": time.NANO_SECONDS - 1 + overflow[3], + "nanoseconds": time._NANO_SECONDS - 1 + overflow[3], } kwargs["months"] += with_month kwargs["days"] += with_day if only_ns: - kwargs["nanoseconds"] += seconds * time.NANO_SECONDS + kwargs["nanoseconds"] += seconds * time._NANO_SECONDS else: kwargs["seconds"] += seconds @@ -593,8 +593,8 @@ def test_positive_overflow_value( @pytest.mark.parametrize( ("field", "module"), ( - ("days", time.AVERAGE_SECONDS_IN_DAY), - ("months", time.AVERAGE_SECONDS_IN_MONTH), + ("days", time._AVERAGE_SECONDS_IN_DAY), + ("months", time._AVERAGE_SECONDS_IN_MONTH), ), ) def test_maximal_value_only_secondary_field(self, field, module) -> None: @@ -604,8 +604,8 @@ def test_maximal_value_only_secondary_field(self, field, module) -> None: @pytest.mark.parametrize( ("field", "module"), ( - ("days", time.AVERAGE_SECONDS_IN_DAY), - ("months", time.AVERAGE_SECONDS_IN_MONTH), + ("days", time._AVERAGE_SECONDS_IN_DAY), + ("months", time._AVERAGE_SECONDS_IN_MONTH), ), ) def test_positive_overflow_value_only_secondary_field( diff --git a/tests/unit/common/time/test_import.py b/tests/unit/common/time/test_import.py new file mode 100644 index 000000000..cede08d39 --- /dev/null +++ b/tests/unit/common/time/test_import.py @@ -0,0 +1,83 @@ +# Copyright (c) "Neo4j" +# Neo4j Sweden AB [https://neo4j.com] +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import importlib + +import pytest + + +def test_import(): + import neo4j.time # noqa: F401 - unused import to test import works + + +def test_import_from(): + from neo4j import time # noqa: F401 - unused import to test import works + + +MODULE_ATTRIBUTES = ( + # (name, warning) + ("ClockTime", DeprecationWarning), + ("MAX_INT64", None), + ("MAX_YEAR", None), + ("MIN_INT64", None), + ("MIN_YEAR", None), + ("Date", None), + ("DateTime", None), + ("Duration", None), + ("Midday", None), + ("Midnight", None), + ("Never", None), + ("Time", None), + ("UnixEpoch", None), + ("ZeroDate", None), +) + +MODULE_ATTRIBUTES_NOT_IN_ALL = ("ClockTime",) + + +@pytest.mark.parametrize(("name", "warning"), MODULE_ATTRIBUTES) +def test_attribute_import(name, warning): + module = importlib.__import__("neo4j.time").time + if warning: + with pytest.warns(warning): + getattr(module, name) + else: + getattr(module, name) + + +@pytest.mark.parametrize(("name", "warning"), MODULE_ATTRIBUTES) +def test_attribute_from_import(name, warning): + if warning: + with pytest.warns(warning): + importlib.__import__("neo4j.time", fromlist=(name,)) + else: + importlib.__import__("neo4j.time", fromlist=(name,)) + + +def test_all(): + import neo4j.time as module + + assert sorted(module.__all__) == sorted( + [ + i[0] + for i in MODULE_ATTRIBUTES + if i[0] not in MODULE_ATTRIBUTES_NOT_IN_ALL + ] + ) + + +def test_import_star(): + importlib.__import__("neo4j.time", fromlist=("*",))