From 134e79996c7d9201d2e2df1b45db9632947f9e45 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Tue, 20 May 2025 10:37:44 +0200 Subject: [PATCH] Clean up `neo4j.time` public exports Make undocumented internal constants, helper functions, and other items in `neo4j.time` private: * `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`) --- CHANGELOG.md | 23 +- docs/source/conf.py | 3 +- src/neo4j/_codec/hydration/v1/temporal.py | 9 +- src/neo4j/_codec/hydration/v2/temporal.py | 8 +- src/neo4j/time/__init__.py | 619 +++++++++++------- src/neo4j/time/_clock_implementations.py | 16 +- .../hydration/v1/test_temporal_dehydration.py | 21 +- tests/unit/common/time/__init__.py | 10 +- tests/unit/common/time/test_clock.py | 38 +- tests/unit/common/time/test_clocktime.py | 34 +- tests/unit/common/time/test_date.py | 13 +- tests/unit/common/time/test_datetime.py | 9 +- tests/unit/common/time/test_duration.py | 42 +- tests/unit/common/time/test_import.py | 83 +++ 14 files changed, 585 insertions(+), 343 deletions(-) create mode 100644 tests/unit/common/time/test_import.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f02fc29e6..682768966 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,7 +72,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 private: +- Make undocumented internal constants, helper functions, and other items private: - `neo4j.api` - `DRIVER_BOLT` - `DRIVER_NEO4J` @@ -86,6 +86,27 @@ 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`) +- 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/docs/source/conf.py b/docs/source/conf.py index 0fa60082a..dff8f8390 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -124,12 +124,13 @@ autodoc_typehints = "description" autodoc_type_aliases = { - # The code-base uses `import typing_extensions as te`. + # The code-base uses `import typing_extensions as te` / `as _te`. # Re-write these to use `typing` instead, as Sphinx always resolves against # the latest version of the `typing` module. # This is a work-around to make Sphinx resolve type hints correctly, even # though we're using `from __future__ import annotations`. "te": typing, + "_te": typing, # Type alias that's only defined and imported if `typing.TYPE_CHECKING` # is `True`. "_TAuth": "typing.Tuple[typing.Any, typing.Any] | Auth | None", 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 641b3f0a2..e5484f25b 100644 --- a/src/neo4j/time/__init__.py +++ b/src/neo4j/time/__init__.py @@ -23,39 +23,43 @@ from __future__ import annotations -import re -import typing as t +import re as _re +import typing as _t 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, ) - -if t.TYPE_CHECKING: - import typing_extensions as te - +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: + import typing_extensions as _te + from typing_extensions import deprecated as _deprecated +else: + from .._warnings import deprecated as _deprecated + + __all__ = [ "MAX_INT64", "MAX_YEAR", @@ -73,34 +77,36 @@ ] -MIN_INT64 = -(2**63) -MAX_INT64 = (2**63) - 1 +MIN_INT64: _te.Final[int] = -(2**63) +MAX_INT64: _te.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 #: :class:`datetime.datetime`. -MIN_YEAR: te.Final[int] = 1 +MIN_YEAR: _te.Final[int] = 1 #: The largest year number allowed in a :class:`.Date` or :class:`.DateTime` #: object to be compatible with :class:`datetime.date` and #: :class:`datetime.datetime`. -MAX_YEAR: te.Final[int] = 9999 +MAX_YEAR: _te.Final[int] = 9999 -DATE_ISO_PATTERN = re_compile(r"^(\d{4})-(\d{2})-(\d{2})$") -TIME_ISO_PATTERN = re_compile( +_DATE_ISO_PATTERN: _te.Final[_re.Pattern] = _re_compile( + r"^(\d{4})-(\d{2})-(\d{2})$" +) +_TIME_ISO_PATTERN: _te.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: _te.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: _te.Final[int] = 1000000000 +_AVERAGE_SECONDS_IN_MONTH: _te.Final[int] = 2629746 +_AVERAGE_SECONDS_IN_DAY: _te.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}: @@ -198,6 +204,7 @@ def _normalize_day(year, month, day): ) +# TODO: 7.0 - make private class ClockTime(tuple): """ A count of `seconds` and `nanoseconds`. @@ -213,26 +220,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), ) @@ -240,9 +251,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, ) @@ -251,7 +262,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), ) @@ -270,19 +281,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 @@ -294,12 +305,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(), @@ -347,7 +360,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): """ @@ -373,16 +386,16 @@ def utc_time(self): raise NotImplementedError("No clock implementation selected") -if t.TYPE_CHECKING: +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. @@ -429,10 +442,10 @@ class Duration( # type: ignore[misc] # i64: i64:i64: i32 - min: te.Final[Duration] = None # type: ignore + min: _te.Final[Duration] = None # type: ignore """The lowest duration value possible.""" - max: te.Final[Duration] = None # type: ignore + max: _te.Final[Duration] = None # type: ignore """The highest duration value possible.""" def __new__( @@ -460,11 +473,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) ) @@ -479,7 +492,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): @@ -489,7 +502,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, @@ -498,7 +511,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( @@ -507,7 +520,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, @@ -531,10 +544,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 @@ -556,7 +569,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 @@ -575,7 +588,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 @@ -606,10 +619,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 @@ -681,7 +694,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): @@ -767,7 +780,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 @@ -777,8 +790,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] @@ -791,15 +804,15 @@ def hours_minutes_seconds_nanoseconds(self) -> tuple[int, int, int, int]: ) -if t.TYPE_CHECKING: +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. @@ -875,11 +888,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() ) @@ -887,7 +900,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( @@ -903,7 +916,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: @@ -912,7 +925,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: @@ -932,7 +945,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) @@ -971,7 +984,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)) @@ -980,7 +993,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. @@ -988,7 +1001,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], @@ -1000,9 +1018,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)" @@ -1023,7 +1053,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: @@ -1037,7 +1067,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: @@ -1055,7 +1085,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): @@ -1063,11 +1093,11 @@ 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 # - if t.TYPE_CHECKING: + if _t.TYPE_CHECKING: @classmethod def fromisoformat(cls, s: str) -> Date: ... @@ -1085,13 +1115,13 @@ def utcfromtimestamp(cls, timestamp: float) -> Date: ... # CLASS ATTRIBUTES # - min: te.Final[Date] = None # type: ignore + min: _te.Final[Date] = None # type: ignore """The earliest date value possible.""" - max: te.Final[Date] = None # type: ignore + max: _te.Final[Date] = None # type: ignore """The latest date value possible.""" - resolution: te.Final[Duration] = None # type: ignore + resolution: _te.Final[Duration] = None # type: ignore """The minimum resolution supported.""" # INSTANCE ATTRIBUTES # @@ -1194,7 +1224,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 @@ -1207,9 +1237,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( @@ -1218,9 +1248,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( @@ -1229,9 +1259,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( @@ -1240,9 +1270,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( @@ -1294,10 +1324,10 @@ def add_days(d, days): return new_date return NotImplemented - @t.overload # type: ignore[override] - def __sub__(self, other: Date | date) -> Duration: ... + @_t.overload # type: ignore[override] + def __sub__(self, other: Date | _date) -> Duration: ... - @t.overload + @_t.overload def __sub__(self, other: Duration) -> Date: ... def __sub__(self, other): @@ -1311,7 +1341,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) @@ -1332,13 +1362,13 @@ def _restore(cls, dict_) -> Date: # INSTANCE METHODS # - if t.TYPE_CHECKING: + if _t.TYPE_CHECKING: def replace( self, - year: te.SupportsIndex = ..., - month: te.SupportsIndex = ..., - day: te.SupportsIndex = ..., + year: _te.SupportsIndex = ..., + month: _te.SupportsIndex = ..., + day: _te.SupportsIndex = ..., **kwargs: object, ) -> Date: ... @@ -1362,11 +1392,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, @@ -1389,20 +1419,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.""" @@ -1435,7 +1479,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 # @@ -1459,7 +1503,7 @@ def __getattr__(self, name): except KeyError: raise AttributeError(f"Date has no attribute {name!r}") from None - if t.TYPE_CHECKING: + if _t.TYPE_CHECKING: def iso_calendar(self) -> tuple[int, int, int]: ... @@ -1479,17 +1523,17 @@ def iso_calendar(self) -> tuple[int, int, int]: ... ZeroDate = object.__new__(Date) -if t.TYPE_CHECKING: +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: @@ -1502,7 +1546,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: @@ -1524,7 +1568,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. @@ -1620,11 +1664,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() ) @@ -1632,7 +1676,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: @@ -1662,7 +1706,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) @@ -1705,7 +1749,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( @@ -1714,7 +1758,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. @@ -1723,7 +1767,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], @@ -1738,11 +1787,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 @@ -1771,13 +1832,13 @@ 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 # - if t.TYPE_CHECKING: + if _t.TYPE_CHECKING: @classmethod def from_iso_format(cls, s: str) -> Time: ... @@ -1787,13 +1848,13 @@ def utc_now(cls) -> Time: ... # CLASS ATTRIBUTES # - min: te.Final[Time] = None # type: ignore + min: _te.Final[Time] = None # type: ignore """The earliest time value possible.""" - max: te.Final[Time] = None # type: ignore + max: _te.Final[Time] = None # type: ignore """The latest time value possible.""" - resolution: te.Final[Duration] = None # type: ignore + resolution: _te.Final[Duration] = None # type: ignore """The minimum resolution supported.""" # INSTANCE ATTRIBUTES # @@ -1865,7 +1926,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: @@ -1877,23 +1938,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): @@ -1901,7 +1962,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: @@ -1917,28 +1978,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: @@ -1947,14 +2008,14 @@ def __gt__(self, other: Time | time) -> bool: # INSTANCE METHODS # - if t.TYPE_CHECKING: + if _t.TYPE_CHECKING: def replace( # type: ignore[override] self, - hour: te.SupportsIndex = ..., - minute: te.SupportsIndex = ..., - second: te.SupportsIndex = ..., - nanosecond: te.SupportsIndex = ..., + hour: _te.SupportsIndex = ..., + minute: _te.SupportsIndex = ..., + second: _te.SupportsIndex = ..., + nanosecond: _te.SupportsIndex = ..., tzinfo: _tzinfo | None = ..., **kwargs: object, ) -> Time: ... @@ -1996,7 +2057,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") @@ -2005,7 +2066,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. @@ -2020,7 +2081,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). @@ -2044,12 +2105,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`. + + 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: + def to_native(self) -> _time: """ Convert to a native Python `datetime.time` value. @@ -2057,9 +2134,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.""" @@ -2091,7 +2168,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) @@ -2113,7 +2190,7 @@ def __getattr__(self, name): except KeyError: raise AttributeError(f"Date has no attribute {name!r}") from None - if t.TYPE_CHECKING: + if _t.TYPE_CHECKING: def isoformat(self) -> str: # type: ignore[override] ... @@ -2133,23 +2210,23 @@ def isoformat(self) -> str: # type: ignore[override] #: A :class:`.Time` instance set to `00:00:00`. #: This has a :attr:`.ticks` value of `0`. -Midnight: te.Final[Time] = Time.min +Midnight: _te.Final[Time] = Time.min #: A :class:`.Time` instance set to `12:00:00`. #: This has a :attr:`.ticks` value of `43200000000000`. -Midday: te.Final[Time] = Time(hour=12) +Midday: _te.Final[Time] = Time(hour=12) -if t.TYPE_CHECKING: +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. @@ -2214,10 +2291,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 @@ -2238,7 +2315,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: @@ -2271,13 +2348,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) ) @@ -2288,7 +2365,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: @@ -2327,7 +2404,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. @@ -2337,7 +2414,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], @@ -2351,15 +2433,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) @@ -2367,7 +2461,7 @@ def from_clock_time( # CLASS METHOD ALIASES # - if t.TYPE_CHECKING: + if _t.TYPE_CHECKING: @classmethod def fromisoformat(cls, s) -> DateTime: ... @@ -2396,13 +2490,13 @@ def utcnow(cls) -> DateTime: ... # CLASS ATTRIBUTES # - min: te.Final[DateTime] = None # type: ignore + min: _te.Final[DateTime] = None # type: ignore """The earliest date time value possible.""" - max: te.Final[DateTime] = None # type: ignore + max: _te.Final[DateTime] = None # type: ignore """The latest date time value possible.""" - resolution: te.Final[Duration] = None # type: ignore + resolution: _te.Final[Duration] = None # type: ignore """The minimum resolution supported.""" # INSTANCE ATTRIBUTES # @@ -2518,7 +2612,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: @@ -2533,7 +2627,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 @@ -2557,7 +2651,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() @@ -2572,19 +2666,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(): @@ -2597,14 +2691,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(): @@ -2614,14 +2708,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(): @@ -2638,7 +2732,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(): @@ -2650,43 +2744,43 @@ 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 - @t.overload # type: ignore[override] + @_t.overload # type: ignore[override] def __sub__(self, other: DateTime) -> Duration: ... - @t.overload - def __sub__(self, other: datetime) -> timedelta: ... + @_t.overload + def __sub__(self, other: _datetime) -> _timedelta: ... - @t.overload - def __sub__(self, other: Duration | timedelta) -> DateTime: ... + @_t.overload + def __sub__(self, other: Duration | _timedelta) -> DateTime: ... def __sub__(self, other): """ @@ -2706,27 +2800,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 @@ -2754,17 +2848,17 @@ def timetz(self) -> Time: """Get the time with timezone info.""" return self.__time - if t.TYPE_CHECKING: + if _t.TYPE_CHECKING: def replace( # type: ignore[override] self, - year: te.SupportsIndex = ..., - month: te.SupportsIndex = ..., - day: te.SupportsIndex = ..., - hour: te.SupportsIndex = ..., - minute: te.SupportsIndex = ..., - second: te.SupportsIndex = ..., - nanosecond: te.SupportsIndex = ..., + year: _te.SupportsIndex = ..., + month: _te.SupportsIndex = ..., + day: _te.SupportsIndex = ..., + hour: _te.SupportsIndex = ..., + minute: _te.SupportsIndex = ..., + second: _te.SupportsIndex = ..., + nanosecond: _te.SupportsIndex = ..., tzinfo: _tzinfo | None = ..., **kwargs: object, ) -> DateTime: ... @@ -2794,7 +2888,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 @@ -2807,7 +2901,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. @@ -2815,7 +2909,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). @@ -2845,13 +2939,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. @@ -2862,7 +2972,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: """ @@ -2929,7 +3039,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) @@ -2960,7 +3070,7 @@ def __getattr__(self, name): f"DateTime has no attribute {name!r}" ) from None - if t.TYPE_CHECKING: + if _t.TYPE_CHECKING: def astimezone( # type: ignore[override] self, tz: _tzinfo @@ -2991,3 +3101,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=("*",))