diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c126321f..dbfee64be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -167,6 +167,17 @@ See also https://github.com/neo4j/neo4j-python-driver/wiki for a full changelog. - `neo4j.spatial.Point` (and subclasses) - Separate out log entries that are session-related (including transaction retries) form sub-logger `neo4j.pool` to a new sub-logger `neo4j.session`. +- Notifications: + - Deprecate notifications and related APIs: + - `ResultSummary.notifications` + - `ResultSummary.summary_notifications` + - `neo4j.SummaryNotification` + - `neo4j.NotificationCategory` + - `neo4j.NotificationDisabledCategory` + - Stabilize GQL status objects (use this instead of notifications): + - `ResultSummary.gql_status_objects` + - `neo4j.GqlStatusObject` + - (`neo4j.exceptions.GqlError`, `neo4j.exceptions.GqlErrorClassification`) ## Version 5.28 diff --git a/docs/source/api.rst b/docs/source/api.rst index 4cfb15123..23608e6a9 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -687,49 +687,50 @@ See also :attr:`.GqlStatusObject.is_notification`. ``notifications_disabled_categories`` ------------------------------------- -Set categories/classifications of notifications the server should not send to the client. -Disabling categories allows the server to skip analysis for those, which can speed up query execution. +Identical to :ref:`driver-notifications-disabled-classifications-ref`. -Notifications are available via :attr:`.ResultSummary.notifications` and :attr:`.ResultSummary.summary_notifications`. -Further, they are surfaced (alongside other status objects) through :attr:`.ResultSummary.gql_status_objects`: -See also :attr:`.GqlStatusObject.is_notification`. - -:data:`None` will apply the server's default setting. - -If specified together with :ref:`driver-notifications-disabled-classifications-ref`, the settings will be merged. - -.. Note:: - If configured, the server or all servers of the cluster need to support notifications filtering - (server version 5.7 and newer). - Otherwise, the driver will raise a :exc:`.ConfigurationError` as soon as it encounters a server that does not. +This alias is provided for a consistent naming with :attr:`.SummaryNotification.category`. :Type: :data:`None`, :term:`iterable` of :class:`.NotificationDisabledCategory` and/or :class:`str` :Default: :data:`None` .. versionadded:: 5.7 -.. seealso:: :class:`.NotificationDisabledCategory`, session config :ref:`session-notifications-disabled-categories-ref` +.. deprecated:: 6.0 + This setting is deprecated in favor of :ref:`driver-notifications-disabled-classifications-ref`. + It will be removed in a future release. + +.. seealso:: :class:`.NotificationDisabledCategory`, session config :ref:`session-notifications-disabled-categories-ref`, :attr:`.SummaryNotification.category` .. _driver-notifications-disabled-classifications-ref: ``notifications_disabled_classifications`` ------------------------------------------ -Identical to :ref:`driver-notifications-disabled-categories-ref`. +Set classifications/categories of notifications the server should not send to the client. +Disabling classifications allows the server to skip analysis for those, which can speed up query execution. -This alias is provided for a consistent naming with :attr:`.GqlStatusObject.classification`. +Notifications are available via :attr:`.ResultSummary.notifications` and :attr:`.ResultSummary.summary_notifications`. +Further, they are surfaced (alongside other status objects) through :attr:`.ResultSummary.gql_status_objects`: +See also :attr:`.GqlStatusObject.is_notification`. -**This is a preview** (see :ref:`filter-warnings-ref`). -It might be changed without following the deprecation policy. -See also -https://github.com/neo4j/neo4j-python-driver/wiki/preview-features +:data:`None` will apply the server's default setting. + +If specified together with :ref:`driver-notifications-disabled-categories-ref`, the settings will be merged. + +.. Note:: + If configured, the server or all servers of the cluster need to support notifications filtering + (server version 5.7 and newer). + Otherwise, the driver will raise a :exc:`.ConfigurationError` as soon as it encounters a server that does not. :Type: :data:`None`, :term:`iterable` of :class:`.NotificationDisabledClassification` and/or :class:`str` :Default: :data:`None` .. versionadded:: 5.22 -.. seealso:: :class:`.NotificationDisabledClassification`, session config :ref:`session-notifications-disabled-classifications-ref` +.. versionchanged:: 6.0 Stabilized from preview. + +.. seealso:: :class:`.NotificationDisabledClassification`, session config :ref:`session-notifications-disabled-classifications-ref`, :attr:`.GqlStatusObject.classification` .. _driver-warn-notification-severity-ref: @@ -1186,49 +1187,50 @@ See also :attr:`.GqlStatusObject.is_notification`. ``notifications_disabled_categories`` ------------------------------------- -Set categories of notifications the server should not send to the client. -Disabling categories allows the server to skip analysis for those, which can speed up query execution. - -Notifications are available via :attr:`.ResultSummary.notifications` and :attr:`.ResultSummary.summary_notifications`. -Further, they are surfaced (alongside other status objects) through :attr:`.ResultSummary.gql_status_objects`: -See also :attr:`.GqlStatusObject.is_notification`. - -:data:`None` will apply the driver's configuration setting (:ref:`driver-notifications-disabled-categories-ref`). +Identical to :ref:`session-notifications-disabled-classifications-ref`. -If specified together with :ref:`session-notifications-disabled-classifications-ref`, the settings will be merged. - -.. Note:: - If configured, the server or all servers of the cluster need to support notifications filtering - (server version 5.7 and newer). - Otherwise, the driver will raise a :exc:`.ConfigurationError` as soon as it encounters a server that does not. +This alias is provided for a consistent naming with :attr:`.SummaryNotification.category`. :Type: :data:`None`, :term:`iterable` of :class:`.NotificationDisabledCategory` and/or :class:`str` :Default: :data:`None` .. versionadded:: 5.7 -.. seealso:: :class:`.NotificationDisabledCategory` +.. deprecated:: 6.0 + This setting is deprecated in favor of :ref:`session-notifications-disabled-classifications-ref`. + It will be removed in a future release. + +.. seealso:: :class:`.NotificationDisabledCategory`, :attr:`.SummaryNotification.category` .. _session-notifications-disabled-classifications-ref: ``notifications_disabled_classifications`` ------------------------------------------ -Identical to :ref:`session-notifications-disabled-categories-ref`. +Set classifications/categories of notifications the server should not send to the client. +Disabling classifications allows the server to skip analysis for those, which can speed up query execution. -This alias is provided for a consistent naming with :attr:`.GqlStatusObject.classification`. +Notifications are available via :attr:`.ResultSummary.notifications` and :attr:`.ResultSummary.summary_notifications`. +Further, they are surfaced (alongside other status objects) through :attr:`.ResultSummary.gql_status_objects`: +See also :attr:`.GqlStatusObject.is_notification`. -**This is a preview** (see :ref:`filter-warnings-ref`). -It might be changed without following the deprecation policy. -See also -https://github.com/neo4j/neo4j-python-driver/wiki/preview-features +:data:`None` will apply the driver's configuration setting (:ref:`driver-notifications-disabled-classifications-ref`). + +If specified together with :ref:`session-notifications-disabled-categories-ref`, the settings will be merged. + +.. Note:: + If configured, the server or all servers of the cluster need to support notifications filtering + (server version 5.7 and newer). + Otherwise, the driver will raise a :exc:`.ConfigurationError` as soon as it encounters a server that does not. :Type: :data:`None`, :term:`iterable` of :class:`.NotificationDisabledClassification` and/or :class:`str` :Default: :data:`None` .. versionadded:: 5.22 -.. seealso:: :class:`.NotificationDisabledClassification` +.. versionchanged:: 6.0 Stabilized from preview. + +.. seealso:: :class:`.NotificationDisabledClassification`, :attr:`.GqlStatusObject.classification` diff --git a/src/neo4j/__init__.py b/src/neo4j/__init__.py index 869bca767..0212cc131 100644 --- a/src/neo4j/__init__.py +++ b/src/neo4j/__init__.py @@ -15,10 +15,10 @@ from . import _typing as _t -from ._api import ( # noqa: F401 dynamic attributes - NotificationCategory, - NotificationDisabledCategory, - NotificationDisabledClassification as _NotificationDisabledClassification, +from ._api import ( + NotificationCategory as _NotificationCategory, + NotificationDisabledCategory as _NotificationDisabledCategory, + NotificationDisabledClassification, NotificationMinimumSeverity, NotificationSeverity, RoutingControl, @@ -59,27 +59,28 @@ ) from ._warnings import ( deprecation_warn as _deprecation_warn, - preview_warn as _preview_warn, PreviewWarning as _PreviewWarning, ) -from ._work import ( # noqa: F401 dynamic attribute +from ._work import ( EagerResult, - GqlStatusObject as _GqlStatusObject, - NotificationClassification as _NotificationClassification, + GqlStatusObject, + NotificationClassification, Query, ResultSummary, SummaryCounters, SummaryInputPosition, - SummaryNotification, + SummaryNotification as _SummaryNotification, unit_of_work, ) if _t.TYPE_CHECKING: - from ._api import NotificationDisabledClassification + from ._api import ( + NotificationCategory, + NotificationDisabledCategory, + ) from ._work import ( - GqlStatusObject, - NotificationClassification, + SummaryNotification, ) from ._warnings import PreviewWarning @@ -164,18 +165,28 @@ def __getattr__(name) -> _t.Any: - # TODO: 6.0 - remove this - if name in { - "NotificationClassification", - "GqlStatusObject", - "NotificationDisabledClassification", - }: - _preview_warn( - f"{name} is part of GQLSTATUS support, " - "which is a preview feature.", + # TODO: 7.0 - consider removing this + if name == "SummaryNotification": + _deprecation_warn( + "SummaryNotification and related APIs are deprecated. " + "Use GqlStatusObjects and related APIs instead.", + stack_level=2, + ) + return _SummaryNotification + if name == "NotificationCategory": + _deprecation_warn( + "NotificationCategory is deprecated. " + "Use NotificationClassification instead.", + stack_level=2, + ) + return _NotificationCategory + if name == "NotificationDisabledCategory": + _deprecation_warn( + "NotificationDisabledCategory is deprecated. " + "Use NotificationDisabledClassification instead.", stack_level=2, ) - return globals()[f"_{name}"] + return _NotificationDisabledCategory # TODO: 7.0 - remove this if name == "PreviewWarning": _deprecation_warn( diff --git a/src/neo4j/_async/driver.py b/src/neo4j/_async/driver.py index 95c12a9d8..8f9543967 100644 --- a/src/neo4j/_async/driver.py +++ b/src/neo4j/_async/driver.py @@ -245,10 +245,10 @@ def driver( config["encrypted"] = True config["trusted_certificates"] = TrustAll() - if "notifications_disabled_classifications" in config: - preview_warn( - "notifications_disabled_classifications " - "is a preview feature.", + if "notifications_disabled_categories" in config: + deprecation_warn( + "notifications_disabled_categories is deprecated, " + "use notifications_disabled_classifications instead.", stack_level=2, ) _normalize_notifications_config(config, driver_level=True) @@ -578,10 +578,10 @@ def session(self, **config) -> AsyncSession: config.pop("warn_notification_severity", None) self._check_state() - if "notifications_disabled_classifications" in config: - preview_warn( - "notifications_disabled_classifications " - "is a preview feature.", + if "notifications_disabled_categories" in config: + deprecation_warn( + "notifications_disabled_categories is deprecated, " + "use notifications_disabled_classifications instead.", stack_level=2, ) session_config = self._read_session_config(config) diff --git a/src/neo4j/_async/work/result.py b/src/neo4j/_async/work/result.py index fe12b1ad0..5f6c40e2b 100644 --- a/src/neo4j/_async/work/result.py +++ b/src/neo4j/_async/work/result.py @@ -27,7 +27,7 @@ from ... import _typing as t from ..._api import ( - NotificationCategory, + NotificationClassification, NotificationMinimumSeverity, NotificationSeverity, ) @@ -325,21 +325,28 @@ def _handle_warnings(self) -> None: summary = self._obtain_summary() query = self._metadata.get("query") - for notification in summary.summary_notifications: + for notification in ( + gql_status_object + for gql_status_object in summary.gql_status_objects + if gql_status_object.is_notification + ): log_call = notification_log.debug - if notification.severity_level == NotificationSeverity.INFORMATION: + if notification.severity == NotificationSeverity.INFORMATION: log_call = notification_log.info - elif notification.severity_level == NotificationSeverity.WARNING: + elif notification.severity == NotificationSeverity.WARNING: log_call = notification_log.warning log_call( "Received notification from DBMS server: %s", NotificationPrinter(notification, query, one_line=True), ) - if notification.severity_level not in sev_filter: + if notification.severity not in sev_filter: continue warning_cls: type[Warning] = Neo4jWarning - if notification.category == NotificationCategory.DEPRECATION: + if ( + notification.classification + == NotificationClassification.DEPRECATION + ): warning_cls = Neo4jDeprecationWarning creation_frame = self._creation_frame if creation_frame is False: diff --git a/src/neo4j/_debug/_notification_printer.py b/src/neo4j/_debug/_notification_printer.py index ae5d715a4..793858078 100644 --- a/src/neo4j/_debug/_notification_printer.py +++ b/src/neo4j/_debug/_notification_printer.py @@ -20,16 +20,16 @@ if t.TYPE_CHECKING: - from .._work import SummaryNotification + from .._work import GqlStatusObject class NotificationPrinter: - notification: SummaryNotification + notification: GqlStatusObject query: str | None def __init__( self, - notification: SummaryNotification, + notification: GqlStatusObject, query: str | None = None, one_line: bool = False, ) -> None: @@ -39,13 +39,13 @@ def __init__( def __str__(self): if self.query is None: - return str(self.notification) + return repr(self.notification) if self._one_line: - return f"{self.notification} for query: {self.query!r}" + return f"{self.notification!r} for query: {self.query!r}" pos = self.notification.position if pos is None: - return f"{self.notification} for query:\n{self.query}" - s = f"{self.notification} for query:\n" + return f"{self.notification!r} for query:\n{self.query}" + s = f"{self.notification!r} for query:\n" query_lines = self.query.splitlines() if pos.line <= 0 or pos.line > len(query_lines) or pos.column <= 0: return s + self.query diff --git a/src/neo4j/_sync/driver.py b/src/neo4j/_sync/driver.py index e03242659..70f0f478e 100644 --- a/src/neo4j/_sync/driver.py +++ b/src/neo4j/_sync/driver.py @@ -244,10 +244,10 @@ def driver( config["encrypted"] = True config["trusted_certificates"] = TrustAll() - if "notifications_disabled_classifications" in config: - preview_warn( - "notifications_disabled_classifications " - "is a preview feature.", + if "notifications_disabled_categories" in config: + deprecation_warn( + "notifications_disabled_categories is deprecated, " + "use notifications_disabled_classifications instead.", stack_level=2, ) _normalize_notifications_config(config, driver_level=True) @@ -577,10 +577,10 @@ def session(self, **config) -> Session: config.pop("warn_notification_severity", None) self._check_state() - if "notifications_disabled_classifications" in config: - preview_warn( - "notifications_disabled_classifications " - "is a preview feature.", + if "notifications_disabled_categories" in config: + deprecation_warn( + "notifications_disabled_categories is deprecated, " + "use notifications_disabled_classifications instead.", stack_level=2, ) session_config = self._read_session_config(config) diff --git a/src/neo4j/_sync/work/result.py b/src/neo4j/_sync/work/result.py index fbb3938a2..865cffd9f 100644 --- a/src/neo4j/_sync/work/result.py +++ b/src/neo4j/_sync/work/result.py @@ -27,7 +27,7 @@ from ... import _typing as t from ..._api import ( - NotificationCategory, + NotificationClassification, NotificationMinimumSeverity, NotificationSeverity, ) @@ -325,21 +325,28 @@ def _handle_warnings(self) -> None: summary = self._obtain_summary() query = self._metadata.get("query") - for notification in summary.summary_notifications: + for notification in ( + gql_status_object + for gql_status_object in summary.gql_status_objects + if gql_status_object.is_notification + ): log_call = notification_log.debug - if notification.severity_level == NotificationSeverity.INFORMATION: + if notification.severity == NotificationSeverity.INFORMATION: log_call = notification_log.info - elif notification.severity_level == NotificationSeverity.WARNING: + elif notification.severity == NotificationSeverity.WARNING: log_call = notification_log.warning log_call( "Received notification from DBMS server: %s", NotificationPrinter(notification, query, one_line=True), ) - if notification.severity_level not in sev_filter: + if notification.severity not in sev_filter: continue warning_cls: type[Warning] = Neo4jWarning - if notification.category == NotificationCategory.DEPRECATION: + if ( + notification.classification + == NotificationClassification.DEPRECATION + ): warning_cls = Neo4jDeprecationWarning creation_frame = self._creation_frame if creation_frame is False: diff --git a/src/neo4j/_work/summary.py b/src/neo4j/_work/summary.py index f4cdc9d0a..6e101c7aa 100644 --- a/src/neo4j/_work/summary.py +++ b/src/neo4j/_work/summary.py @@ -30,16 +30,18 @@ NotificationSeverity, ) from .._exceptions import BoltProtocolError -from .._warnings import preview if t.TYPE_CHECKING: import typing_extensions as te + from typing_extensions import deprecated from .._addressing import Address from ..api import ServerInfo _T = t.TypeVar("_T") +else: + from .._warnings import deprecated class ResultSummary: @@ -80,20 +82,8 @@ class ResultSummary: #: The time it took for the server to consume the result. (milliseconds) result_consumed_after: int | None - #: A list of Dictionaries containing notification information. - #: Notifications provide extra information for a user executing a - #: statement. - #: They can be warnings about problematic queries or other valuable - #: information that can be - #: presented in a client. - #: Unlike failures or errors, notifications do not affect the execution of - #: a statement. - #: - #: .. seealso:: :attr:`.summary_notifications` - notifications: list[dict] | None - - # cache for notifications - _notifications_set: bool = False + #: see :attr:`.notifications` + _notifications: list[dict] | None # cache for property `summary_notifications` _summary_notifications: tuple[SummaryNotification, ...] @@ -139,17 +129,6 @@ def __init__( self.result_available_after = metadata.get("t_first") self.result_consumed_after = metadata.get("t_last") - def __dir__(self): - return {*super().__dir__(), "notifications"} - - def __getattr__(self, key): - if key == "notifications": - self._set_notifications() - return self.notifications - raise AttributeError( - f"'{self.__class__.__name__}' object has no attribute '{key}'" - ) - @staticmethod def _notification_from_status(status: dict) -> dict: notification = {} @@ -178,20 +157,56 @@ def _notification_from_status(status: dict) -> dict: return notification + @property + @deprecated( + "ResultSummary.notifications is deprecated, " + "use ResultSummary.gql_status_objects instead." + ) + def notifications(self) -> list[dict] | None: + """ + A list of Dictionaries containing notification information. + + Notifications provide extra information for a user executing a + statement. + They can be warnings about problematic queries or other valuable + information that can be presented in a client. + Unlike failures or errors, notifications do not affect the execution of + a statement. + + .. seealso:: :attr:`.summary_notifications` + + .. deprecated:: 6.0 + Use :attr:`.gql_status_objects` instead. + """ + return self._get_notifications() + + @notifications.setter + @deprecated( + "ResultSummary.notifications is deprecated, " + "use ResultSummary.gql_status_objects instead." + ) + def notifications(self, value: list[dict] | None) -> None: + self._notifications = value + + def _get_notifications(self) -> list[dict] | None: + if not hasattr(self, "_notifications"): + self._set_notifications() + return self._notifications + def _set_notifications(self) -> None: if "notifications" in self.metadata: notifications = self.metadata["notifications"] if not isinstance(notifications, list): - self.notifications = None + self._notifications = None return - self.notifications = notifications + self._notifications = notifications return # polyfill notifications from GqlStatusObjects if "statuses" in self.metadata: statuses = self.metadata["statuses"] if not isinstance(statuses, list): - self.notifications = None + self._notifications = None return notifications = [] for status in statuses: @@ -200,12 +215,16 @@ def _set_notifications(self) -> None: continue notification = self._notification_from_status(status) notifications.append(notification) - self.notifications = notifications or None + self._notifications = notifications or None return - self.notifications = None + self._notifications = None @property + @deprecated( + "ResultSummary.summary_notifications is deprecated, " + "use ResultSummary.gql_status_objects instead." + ) def summary_notifications(self) -> Sequence[SummaryNotification]: """ The same as ``notifications`` but in a parsed, structured form. @@ -216,11 +235,14 @@ def summary_notifications(self) -> Sequence[SummaryNotification]: .. seealso:: :attr:`.notifications`, :class:`.SummaryNotification` .. versionadded:: 5.7 + + .. deprecated:: 6.0 + Use :attr:`.gql_status_objects` instead. """ if getattr(self, "_summary_notifications", None) is not None: return self._summary_notifications - raw_notifications = self.notifications + raw_notifications = self._get_notifications() if not isinstance(raw_notifications, list): self._summary_notifications = () return self._summary_notifications @@ -230,7 +252,6 @@ def summary_notifications(self) -> Sequence[SummaryNotification]: return self._summary_notifications @property - @preview("GQLSTATUS support is a preview feature.") def gql_status_objects(self) -> t.Sequence[GqlStatusObject]: """ Get GqlStatusObjects that arose when executing the query. @@ -248,13 +269,9 @@ def gql_status_objects(self) -> t.Sequence[GqlStatusObject]: * A "success" (``00xxx``) has precedence over anything informational (``03xxx``). - **This is a preview** (see :ref:`filter-warnings-ref`). - It might be changed without following the deprecation policy. - - See also - https://github.com/neo4j/neo4j-python-driver/wiki/preview-features - .. versionadded:: 5.22 + + .. versionchanged:: 6.0 Stabilized from preview. """ raw_status_objects = self.metadata.get("statuses") if isinstance(raw_status_objects, list): @@ -264,7 +281,7 @@ def gql_status_objects(self) -> t.Sequence[GqlStatusObject]: ) return self._gql_status_objects - raw_notifications = self.notifications + raw_notifications = self._get_notifications() notification_status_objects: t.Iterable[GqlStatusObject] if isinstance(raw_notifications, list): notification_status_objects = [ diff --git a/src/neo4j/exceptions.py b/src/neo4j/exceptions.py index 9e58cb53d..d759a65f6 100644 --- a/src/neo4j/exceptions.py +++ b/src/neo4j/exceptions.py @@ -66,7 +66,6 @@ from enum import Enum as _Enum from . import _typing as _t -from ._warnings import preview as _preview if _t.TYPE_CHECKING: @@ -195,14 +194,11 @@ class GqlErrorClassification(str, _Enum): >>> GqlErrorClassification.TRANSIENT_ERROR == "TRANSIENT_ERROR" True - **This is a preview**. - It might be changed without following the deprecation policy. - See also - https://github.com/neo4j/neo4j-python-driver/wiki/preview-features - .. seealso:: :attr:`.GqlError.gql_classification` .. versionadded:: 5.26 + + .. versionchanged:: 6.0 Stabilized from preview. """ CLIENT_ERROR = "CLIENT_ERROR" @@ -223,12 +219,9 @@ class GqlError(Exception): Instead, only subclasses are raised. Further, it is used as the :attr:`__cause__` of GqlError subclasses. - **This is a preview**. - It might be changed without following the deprecation policy. - See also - https://github.com/neo4j/neo4j-python-driver/wiki/preview-features - .. versionadded: 5.26 + + .. versionchanged:: 6.0 Stabilized from preview. """ _gql_status: str @@ -329,15 +322,6 @@ def _get_attr_or_none(self, item): return None @property - def _gql_status_no_preview(self) -> str: - if hasattr(self, "_gql_status"): - return self._gql_status - - self._set_gql_unknown() - return self._gql_status - - @property - @_preview("GQLSTATUS support is a preview feature.") def gql_status(self) -> str: """ The GQLSTATUS returned from the server. @@ -352,18 +336,12 @@ def gql_status(self) -> str: This means that the code ``50N42`` is not guaranteed to be stable and may change in future versions of the driver or the server. """ - return self._gql_status_no_preview - - @property - def _message_no_preview(self) -> str: - if hasattr(self, "_message"): - return self._message - + if hasattr(self, "_gql_status"): + return self._gql_status self._set_gql_unknown() - return self._message + return self._gql_status @property - @_preview("GQLSTATUS support is a preview feature.") def message(self) -> str: """ The error message returned by the server. @@ -376,18 +354,12 @@ def message(self) -> str: This value is never :data:`None` unless the subclass in question states otherwise. """ - return self._message_no_preview - - @property - def _gql_status_description_no_preview(self) -> str: - if hasattr(self, "_gql_status_description"): - return self._gql_status_description - + if hasattr(self, "_message"): + return self._message self._set_gql_unknown() - return self._gql_status_description + return self._message @property - @_preview("GQLSTATUS support is a preview feature.") def gql_status_description(self) -> str: """ A description of the GQLSTATUS returned from the server. @@ -397,10 +369,20 @@ def gql_status_description(self) -> str: This description is meant for human consumption and debugging purposes. Don't rely on it in a programmatic way. """ - return self._gql_status_description_no_preview + if hasattr(self, "_gql_status_description"): + return self._gql_status_description + self._set_gql_unknown() + return self._gql_status_description @property - def _gql_raw_classification_no_preview(self) -> str | None: + def gql_raw_classification(self) -> str | None: + """ + Vendor specific classification of the error. + + This is a convenience accessor for ``_classification`` in the + diagnostic record. :data:`None` is returned if the classification is + not available or not a string. + """ if hasattr(self, "_gql_raw_classification"): return self._gql_raw_classification @@ -413,24 +395,11 @@ def _gql_raw_classification_no_preview(self) -> str | None: return self._gql_raw_classification @property - @_preview("GQLSTATUS support is a preview feature.") - def gql_raw_classification(self) -> str | None: - """ - Vendor specific classification of the error. - - This is a convenience accessor for ``_classification`` in the - diagnostic record. - :data:`None` is returned if the classification is not available - or not a string. - """ - return self._gql_raw_classification_no_preview - - @property - def _gql_classification_no_preview(self) -> GqlErrorClassification: + def gql_classification(self) -> GqlErrorClassification: + """The stable GqlErrorClassification for this error.""" if hasattr(self, "_gql_classification"): return self._gql_classification - - classification = self._gql_raw_classification_no_preview + classification = self.gql_raw_classification if not ( isinstance(classification, str) and classification @@ -442,41 +411,29 @@ def _gql_classification_no_preview(self) -> GqlErrorClassification: return self._gql_classification @property - @_preview("GQLSTATUS support is a preview feature.") - def gql_classification(self) -> GqlErrorClassification: - return self._gql_classification_no_preview - - def _get_status_diagnostic_record(self) -> dict[str, _t.Any]: - if hasattr(self, "_status_diagnostic_record"): - return self._status_diagnostic_record - - self._status_diagnostic_record = dict(_UNKNOWN_GQL_DIAGNOSTIC_RECORD) - return self._status_diagnostic_record - - @property - def _diagnostic_record_no_preview(self) -> _t.Mapping[str, _t.Any]: + def diagnostic_record(self) -> _t.Mapping[str, _t.Any]: + """The diagnostic record for this error.""" if hasattr(self, "_diagnostic_record"): return self._diagnostic_record - self._diagnostic_record = _deepcopy( self._get_status_diagnostic_record() ) return self._diagnostic_record - @property - @_preview("GQLSTATUS support is a preview feature.") - def diagnostic_record(self) -> _t.Mapping[str, _t.Any]: - return self._diagnostic_record_no_preview + def _get_status_diagnostic_record(self) -> dict[str, _t.Any]: + if hasattr(self, "_status_diagnostic_record"): + return self._status_diagnostic_record + + self._status_diagnostic_record = dict(_UNKNOWN_GQL_DIAGNOSTIC_RECORD) + return self._status_diagnostic_record def __str__(self): return ( - f"{{gql_status: {self._gql_status_no_preview}}} " - f"{{gql_status_description: " - f"{self._gql_status_description_no_preview}}} " - f"{{message: {self._message_no_preview}}} " - f"{{diagnostic_record: {self._diagnostic_record_no_preview}}} " - f"{{raw_classification: " - f"{self._gql_raw_classification_no_preview}}}" + f"{{gql_status: {self.gql_status}}} " + f"{{gql_status_description: {self.gql_status_description}}} " + f"{{message: {self.message}}} " + f"{{diagnostic_record: {self.diagnostic_record}}} " + f"{{raw_classification: {self.gql_raw_classification}}}" ) @@ -694,8 +651,8 @@ def __str__(self): code = self._neo4j_code message = self._message # TODO: 7.0 - Check if including neo4j_code is still useful - gql_status = self._gql_status_no_preview - gql_description = self._gql_status_description_no_preview + gql_status = self._gql_status + gql_description = self._gql_status_description return ( f"{{neo4j_code: {code}}} " f"{{message: {message}}} " diff --git a/src/neo4j/warnings.py b/src/neo4j/warnings.py index d5fc2ee87..058385a54 100644 --- a/src/neo4j/warnings.py +++ b/src/neo4j/warnings.py @@ -21,7 +21,7 @@ if _t.TYPE_CHECKING: - from ._work.summary import SummaryNotification + from ._work.summary import GqlStatusObject __all__ = [ @@ -65,15 +65,18 @@ class Neo4jWarning(Warning): .. versionadded:: 5.21 + .. versionchanged:: 6.0 + :attr:`.notification` is now of type :class:`.SummaryNotification`. + .. seealso:: :ref:`development-environment-ref` """ #: The notification that triggered the warning. - notification: SummaryNotification + notification: GqlStatusObject def __init__( self, - notification: SummaryNotification, + notification: GqlStatusObject, query: str | None = None, ) -> None: msg = str(NotificationPrinter(notification, query)) diff --git a/testkitbackend/_async/requests.py b/testkitbackend/_async/requests.py index 0325089e4..6acd9dec2 100644 --- a/testkitbackend/_async/requests.py +++ b/testkitbackend/_async/requests.py @@ -216,7 +216,9 @@ async def new_driver(backend, data): for cert in data["trustedCertificates"] ) kwargs["trusted_certificates"] = neo4j.TrustCustomCAs(*cert_paths) - fromtestkit.set_notifications_config(kwargs, data) + fromtestkit.set_notifications_config( + kwargs, data, expected_warnings=expected_warnings + ) with warnings_check(expected_warnings): driver = neo4j.AsyncGraphDatabase.driver( @@ -728,6 +730,8 @@ def __init__(self, session): @request_handler async def new_session(backend, data): + expected_warnings = [] + driver = backend.drivers[data["driverId"]] config = { "database": data["database"], @@ -756,8 +760,11 @@ async def new_session(backend, data): config[conf_name] = data[data_name] if data.get("authorizationToken"): config["auth"] = fromtestkit.to_auth_token(data, "authorizationToken") - fromtestkit.set_notifications_config(config, data) - session = driver.session(**config) + fromtestkit.set_notifications_config( + config, data, expected_warnings=expected_warnings + ) + with warnings_check(expected_warnings): + session = driver.session(**config) key = backend.next_key() backend.sessions[key] = SessionTracker(session) await backend.send_response("Session", {"id": key}) diff --git a/testkitbackend/_preview_imports.py b/testkitbackend/_deprecated_imports.py similarity index 69% rename from testkitbackend/_preview_imports.py rename to testkitbackend/_deprecated_imports.py index a0a3344ca..52d8c9233 100644 --- a/testkitbackend/_preview_imports.py +++ b/testkitbackend/_deprecated_imports.py @@ -14,15 +14,21 @@ # limitations under the License. -from neo4j.warnings import PreviewWarning +from __future__ import annotations from ._warning_check import warning_check -with warning_check(PreviewWarning, r".*\bGQLSTATUS\b.*"): - from neo4j import NotificationDisabledClassification +with warning_check( + DeprecationWarning, + ( + r".*\bNotificationDisabledCategory\b.*" + r"\bNotificationDisabledClassification instead\b.*" + ), +): + from neo4j import NotificationDisabledCategory __all__ = [ - "NotificationDisabledClassification", + "NotificationDisabledCategory", ] diff --git a/testkitbackend/_sync/requests.py b/testkitbackend/_sync/requests.py index a12744f79..a83eff013 100644 --- a/testkitbackend/_sync/requests.py +++ b/testkitbackend/_sync/requests.py @@ -216,7 +216,9 @@ def new_driver(backend, data): for cert in data["trustedCertificates"] ) kwargs["trusted_certificates"] = neo4j.TrustCustomCAs(*cert_paths) - fromtestkit.set_notifications_config(kwargs, data) + fromtestkit.set_notifications_config( + kwargs, data, expected_warnings=expected_warnings + ) with warnings_check(expected_warnings): driver = neo4j.GraphDatabase.driver( @@ -728,6 +730,8 @@ def __init__(self, session): @request_handler def new_session(backend, data): + expected_warnings = [] + driver = backend.drivers[data["driverId"]] config = { "database": data["database"], @@ -756,8 +760,11 @@ def new_session(backend, data): config[conf_name] = data[data_name] if data.get("authorizationToken"): config["auth"] = fromtestkit.to_auth_token(data, "authorizationToken") - fromtestkit.set_notifications_config(config, data) - session = driver.session(**config) + fromtestkit.set_notifications_config( + config, data, expected_warnings=expected_warnings + ) + with warnings_check(expected_warnings): + session = driver.session(**config) key = backend.next_key() backend.sessions[key] = SessionTracker(session) backend.send_response("Session", {"id": key}) diff --git a/testkitbackend/fromtestkit.py b/testkitbackend/fromtestkit.py index 438e08f41..10f56507c 100644 --- a/testkitbackend/fromtestkit.py +++ b/testkitbackend/fromtestkit.py @@ -22,7 +22,7 @@ import neo4j from neo4j import ( - NotificationDisabledCategory, + NotificationDisabledClassification, NotificationMinimumSeverity, Query, ) @@ -38,7 +38,7 @@ Time, ) -from ._preview_imports import NotificationDisabledClassification +from ._deprecated_imports import NotificationDisabledCategory def to_cypher_and_params(data): @@ -227,7 +227,7 @@ def to_client_cert(data, key) -> ClientCertificate | None: ) -def set_notifications_config(config, data): +def set_notifications_config(config, data, expected_warnings=None): if "notificationsMinSeverity" in data: config["notifications_min_severity"] = NotificationMinimumSeverity[ data["notificationsMinSeverity"] @@ -237,6 +237,16 @@ def set_notifications_config(config, data): NotificationDisabledCategory[c] for c in data["notificationsDisabledCategories"] ] + if expected_warnings is not None: + expected_warnings.append( + ( + DeprecationWarning, + ( + r"\bnotifications_disabled_categories\b.*" + r"\buse notifications_disabled_classifications instead" + ), + ) + ) if "notificationsDisabledClassifications" in data: config["notifications_disabled_classifications"] = [ NotificationDisabledClassification[c] diff --git a/testkitbackend/totestkit.py b/testkitbackend/totestkit.py index 4f4805d27..47249219a 100644 --- a/testkitbackend/totestkit.py +++ b/testkitbackend/totestkit.py @@ -39,7 +39,6 @@ Duration, Time, ) -from neo4j.warnings import PreviewWarning from ._warning_check import warning_check from .exceptions import MarkdAsDriverError @@ -73,12 +72,19 @@ def serialize_notification(n: neo4j.SummaryNotification) -> dict: return res def serialize_notifications() -> list[dict] | None: - if summary_.notifications is None: + with warning_check( + DeprecationWarning, r".*\.gql_status_objects instead\b.*" + ): + notifications = summary_.notifications + if notifications is None: gql_aware_protocol = summary_.server.protocol_version >= (5, 5) return [] if gql_aware_protocol else None - return [ - serialize_notification(n) for n in summary_.summary_notifications - ] + + with warning_check( + DeprecationWarning, r".*\.gql_status_objects instead\b.*" + ): + summary_notifications = summary_.summary_notifications + return [serialize_notification(n) for n in summary_notifications] def serialize_gql_status_object(o: neo4j.GqlStatusObject) -> dict: res: dict = { @@ -105,11 +111,9 @@ def serialize_gql_status_object(o: neo4j.GqlStatusObject) -> dict: return res def serialize_gql_status_objects() -> list[dict]: - with warning_check(PreviewWarning, r".*\bGQLSTATUS\b.*"): - return [ - serialize_gql_status_object(o) - for o in summary_.gql_status_objects - ] + return [ + serialize_gql_status_object(o) for o in summary_.gql_status_objects + ] def format_address(address: neo4j.Address): if len(address) == 2: @@ -321,18 +325,13 @@ def driver_exc(exc, id_=None): if isinstance(exc, Neo4jError): payload["code"] = exc.code if isinstance(exc, GqlError): - with warning_check(PreviewWarning, r".*\bGQLSTATUS\b.*"): - payload["gqlStatus"] = exc.gql_status - with warning_check(PreviewWarning, r".*\bGQLSTATUS\b.*"): - payload["statusDescription"] = exc.gql_status_description - with warning_check(PreviewWarning, r".*\bGQLSTATUS\b.*"): - payload["rawClassification"] = exc.gql_raw_classification - with warning_check(PreviewWarning, r".*\bGQLSTATUS\b.*"): - payload["classification"] = exc.gql_classification - with warning_check(PreviewWarning, r".*\bGQLSTATUS\b.*"): - payload["diagnosticRecord"] = { - k: field(v) for k, v in exc.diagnostic_record.items() - } + payload["gqlStatus"] = exc.gql_status + payload["statusDescription"] = exc.gql_status_description + payload["rawClassification"] = exc.gql_raw_classification + payload["classification"] = exc.gql_classification + payload["diagnosticRecord"] = { + k: field(v) for k, v in exc.diagnostic_record.items() + } cause = driver_exc_cause(getattr(exc, "__cause__", None)) if cause is not None: payload["cause"] = cause @@ -349,8 +348,7 @@ def _exc_msg(exc, max_depth=10): if isinstance(exc, Neo4jError): res = str(exc.message) if exc.message is not None else str(exc) else: - with warning_check(PreviewWarning, r".*\bGQLSTATUS\b.*"): - msg = exc.message + msg = exc.message res = f"{msg} - {exc!s}" if exc.args else msg else: res = str(exc) @@ -383,19 +381,16 @@ def driver_exc_cause(exc, max_depth=10): return driver_exc_cause( getattr(exc, "__cause__", None), max_depth=max_depth - 1 ) - payload = {"msg": _exc_msg(exc)} - with warning_check(PreviewWarning, r".*\bGQLSTATUS\b.*"): - payload["gqlStatus"] = exc.gql_status - with warning_check(PreviewWarning, r".*\bGQLSTATUS\b.*"): - payload["statusDescription"] = exc.gql_status_description - with warning_check(PreviewWarning, r".*\bGQLSTATUS\b.*"): - payload["diagnosticRecord"] = { + payload = { + "msg": _exc_msg(exc), + "gqlStatus": exc.gql_status, + "statusDescription": exc.gql_status_description, + "diagnosticRecord": { k: field(v) for k, v in exc.diagnostic_record.items() - } - with warning_check(PreviewWarning, r".*\bGQLSTATUS\b.*"): - payload["classification"] = exc.gql_classification - with warning_check(PreviewWarning, r".*\bGQLSTATUS\b.*"): - payload["rawClassification"] = exc.gql_raw_classification + }, + "classification": exc.gql_classification, + "rawClassification": exc.gql_raw_classification, + } cause = getattr(exc, "__cause__", None) if cause is not None: payload["cause"] = driver_exc_cause(cause, max_depth=max_depth - 1) diff --git a/tests/_preview_imports.py b/tests/_deprecated_imports.py similarity index 52% rename from tests/_preview_imports.py rename to tests/_deprecated_imports.py index 9c606034b..a13dd9c52 100644 --- a/tests/_preview_imports.py +++ b/tests/_deprecated_imports.py @@ -14,21 +14,34 @@ # limitations under the License. +from __future__ import annotations + import typing as t import pytest -from neo4j.warnings import PreviewWarning - if t.TYPE_CHECKING: - from neo4j import NotificationDisabledClassification + from neo4j import ( + NotificationCategory, + NotificationDisabledCategory, + SummaryNotification, + ) -with pytest.warns(PreviewWarning, match="GQLSTATUS"): - from neo4j import NotificationDisabledClassification +with pytest.warns(DeprecationWarning, match="NotificationCategory"): + from neo4j import NotificationCategory +with pytest.warns( + DeprecationWarning, + match="NotificationDisabledCategory", +): + from neo4j import NotificationDisabledCategory +with pytest.warns(DeprecationWarning, match="SummaryNotification"): + from neo4j import SummaryNotification __all__ = [ - "NotificationDisabledClassification", + "NotificationCategory", + "NotificationDisabledCategory", + "SummaryNotification", ] diff --git a/tests/unit/async_/test_driver.py b/tests/unit/async_/test_driver.py index cce1aeadb..491e41f9b 100644 --- a/tests/unit/async_/test_driver.py +++ b/tests/unit/async_/test_driver.py @@ -29,7 +29,7 @@ AsyncGraphDatabase, AsyncNeo4jDriver, AsyncResult, - NotificationDisabledCategory, + NotificationDisabledClassification, NotificationMinimumSeverity, Query, TrustAll, @@ -66,7 +66,7 @@ AsyncTestDecorators, mark_async_test, ) -from ..._preview_imports import NotificationDisabledClassification +from ..._deprecated_imports import NotificationDisabledCategory @pytest.fixture @@ -658,9 +658,9 @@ async def test_driver_factory_with_notification_filters( if dis_clss is not ...: filter_kwargs["notifications_disabled_classifications"] = dis_clss - if "notifications_disabled_classifications" in filter_kwargs: + if "notifications_disabled_categories" in filter_kwargs: with pytest.warns( - PreviewWarning, match="notifications_disabled_classifications" + DeprecationWarning, match="notifications_disabled_categories" ): driver = AsyncGraphDatabase.driver(uri, auth=None, **filter_kwargs) else: @@ -837,9 +837,9 @@ async def test_session_factory_with_notification_filter( filter_kwargs["notifications_disabled_classifications"] = dis_clss async with AsyncGraphDatabase.driver(uri, auth=None) as driver: - if "notifications_disabled_classifications" in filter_kwargs: + if "notifications_disabled_categories" in filter_kwargs: with pytest.warns( - PreviewWarning, match="notifications_disabled_classifications" + DeprecationWarning, match="notifications_disabled_categories" ): session = driver.session(**filter_kwargs) else: diff --git a/tests/unit/async_/work/test_result.py b/tests/unit/async_/work/test_result.py index 1221f4e75..78de03327 100644 --- a/tests/unit/async_/work/test_result.py +++ b/tests/unit/async_/work/test_result.py @@ -67,8 +67,10 @@ if t.TYPE_CHECKING: from ...fixtures.notifications import ( - TNotificationFactory, TRawNotificationFactory, + TRawStatusNotificationFactory, + TStatusNotificationFactory, + TStatusNotificationLegacyFactory, ) @@ -1384,7 +1386,7 @@ async def test_notification_warning( ) if expected_warning is None: with warnings.catch_warnings(): - warnings.simplefilter("error") # assert not warnings are emitted + warnings.simplefilter("error") # assert no warnings are emitted await result._run("CYPHER", {}, None, None, "r", None, None, None) await result.consume() else: @@ -1399,31 +1401,47 @@ async def test_notification_warning( @pytest.mark.parametrize("notification_severity", ("INFORMATION", "WARNING")) @pytest.mark.parametrize( - "notification_category", + "notification_classification", ( "HINT", "DEPRECATION", "UNRECOGNIZED", ), ) +@pytest.mark.parametrize("legacy_input", (True, False)) @mark_async_test async def test_notification_logging( raw_notification_factory: TRawNotificationFactory, - notification_factory: TNotificationFactory, + raw_status_notification_factory: TRawStatusNotificationFactory, + status_notification_legacy_factory: TStatusNotificationLegacyFactory, + status_notification_factory: TStatusNotificationFactory, notification_severity: str, - notification_category: str, + notification_classification: str, + legacy_input: bool, caplog: pytest.LogCaptureFixture, ) -> None: - notification_data = raw_notification_factory( - data_overwrite={ - "severity": notification_severity, - "category": notification_category, - } - ) - notification = notification_factory(notification_data) + summary_meta: dict[str, t.Any] + if legacy_input: + notification_data = raw_notification_factory( + data_overwrite={ + "severity": notification_severity, + "category": notification_classification, + } + ) + gql_status = status_notification_legacy_factory(notification_data) + summary_meta = {"notifications": [notification_data]} + else: + status_data = raw_status_notification_factory( + diag_rec_overwrite={ + "_severity": notification_severity, + "_classification": notification_classification, + } + ) + gql_status = status_notification_factory(status_data) + summary_meta = {"statuses": [status_data]} connection = AsyncConnectionStub( records=Records(["foo"], ()), - summary_meta={"notifications": [notification_data]}, + summary_meta=summary_meta, ) result = AsyncResult(connection, 1, None, noop, noop, None) with caplog.at_level(logging.INFO, logger="neo4j.notifications"): @@ -1431,7 +1449,7 @@ async def test_notification_logging( await result.consume() assert len(caplog.messages) == 1 formatted_notification = str( - NotificationPrinter(notification, "CYPHER", one_line=True) + NotificationPrinter(gql_status, "CYPHER", one_line=True) ) expected_message = ( f"Received notification from DBMS server: {formatted_notification}" diff --git a/tests/unit/common/_debug/test_notification_printer.py b/tests/unit/common/_debug/test_notification_printer.py index 38875c7f0..ca6282e75 100644 --- a/tests/unit/common/_debug/test_notification_printer.py +++ b/tests/unit/common/_debug/test_notification_printer.py @@ -26,7 +26,7 @@ if t.TYPE_CHECKING: from ...fixtures.notifications import ( Position, - TNotificationFactory, + TStatusNotificationFactory, ) @@ -34,19 +34,19 @@ ("query", "position", "expected_output_template"), ( # no query - (None, None, "{notification}"), - (None, {"offset": 0, "line": 1, "column": 1}, "{notification}"), + (None, None, "{gql_status}"), + (None, {"offset": 0, "line": 1, "column": 1}, "{gql_status}"), # --------------------------------------------------------------------- # no position - ("MATCH (n) RETURN n", None, "{notification} for query:\n{query}"), - ("MATCH (n)\nRETURN n", None, "{notification} for query:\n{query}"), + ("MATCH (n) RETURN n", None, "{gql_status} for query:\n{query}"), + ("MATCH (n)\nRETURN n", None, "{gql_status} for query:\n{query}"), # --------------------------------------------------------------------- # normal position ( "MATCH (n) RETURN n", {"offset": 0, "line": 1, "column": 1}, ( - "{notification} for query:\n" + "{gql_status} for query:\n" "MATCH (n) RETURN n\n" "^" ), @@ -55,7 +55,7 @@ "MATCH (n) RETURN n", {"offset": 2, "line": 1, "column": 3}, ( - "{notification} for query:\n" + "{gql_status} for query:\n" "MATCH (n) RETURN n\n" " ^" ), @@ -67,7 +67,7 @@ ), {"offset": 0, "line": 1, "column": 3}, ( - "{notification} for query:\n" + "{gql_status} for query:\n" "MATCH (n)\n" " ^\n" "RETURN n" @@ -80,7 +80,7 @@ ), {"offset": 0, "line": 2, "column": 8}, ( - "{notification} for query:\n" + "{gql_status} for query:\n" "MATCH (n)\n" "RETURN n\n" " ^" @@ -93,7 +93,7 @@ "MATCH (n) RETURN n", {"offset": 0, "line": line, "column": column}, ( - "{notification} for query:\n" + "{gql_status} for query:\n" "MATCH (n) RETURN n" ), ) @@ -110,7 +110,7 @@ "MATCH (n) RETURN n", {"offset": 0, "line": 1, "column": 20}, ( - "{notification} for query:\n" + "{gql_status} for query:\n" "MATCH (n) RETURN n\n" " ^" ), @@ -122,7 +122,7 @@ ), {"offset": 0, "line": 1, "column": 20}, ( - "{notification} for query:\n" + "{gql_status} for query:\n" "MATCH (n)\n" " ^\n" "RETURN n" @@ -131,14 +131,16 @@ ), ) # fmt: skip def test_position( - notification_factory: TNotificationFactory, + status_notification_factory: TStatusNotificationFactory, query: str | None, position: Position | None, expected_output_template: str, ) -> None: - notification = notification_factory(data_overwrite={"position": position}) - printer = NotificationPrinter(notification, query) + gql_status = status_notification_factory( + diag_rec_overwrite={"_position": position} + ) + printer = NotificationPrinter(gql_status, query) expected_output = expected_output_template.format( - query=query, notification=notification + query=query, gql_status=repr(gql_status) ) assert str(printer) == expected_output diff --git a/tests/unit/common/test_exceptions.py b/tests/unit/common/test_exceptions.py index d6d0f4c32..36dff9244 100644 --- a/tests/unit/common/test_exceptions.py +++ b/tests/unit/common/test_exceptions.py @@ -40,7 +40,6 @@ ServiceUnavailable, TransientError, ) -from neo4j.warnings import PreviewWarning def test_bolt_error(): @@ -158,32 +157,26 @@ def test_serviceunavailable_raised_from_bolt_protocol_error_with_explicit_style( def _assert_default_gql_error_attrs_from_neo4j_error(error: GqlError) -> None: - with pytest.warns(PreviewWarning, match="GQLSTATUS"): - assert error.gql_status == "50N42" + assert error.gql_status == "50N42" if error.message: - with pytest.warns(PreviewWarning, match="GQLSTATUS"): - assert error.gql_status_description == ( - "error: general processing exception - unexpected error. " - f"{error.message}" - ) + assert error.gql_status_description == ( + "error: general processing exception - unexpected error. " + f"{error.message}" + ) else: - with pytest.warns(PreviewWarning, match="GQLSTATUS"): - assert error.gql_status_description == ( - "error: general processing exception - unexpected error" - ) - with pytest.warns(PreviewWarning, match="GQLSTATUS"): - assert ( - error.gql_classification - == neo4j.exceptions.GqlErrorClassification.UNKNOWN + assert error.gql_status_description == ( + "error: general processing exception - unexpected error" ) - with pytest.warns(PreviewWarning, match="GQLSTATUS"): - assert error.gql_raw_classification is None - with pytest.warns(PreviewWarning, match="GQLSTATUS"): - assert error.diagnostic_record == { - "CURRENT_SCHEMA": "/", - "OPERATION": "", - "OPERATION_CODE": "0", - } + assert ( + error.gql_classification + == neo4j.exceptions.GqlErrorClassification.UNKNOWN + ) + assert error.gql_raw_classification is None + assert error.diagnostic_record == { + "CURRENT_SCHEMA": "/", + "OPERATION": "", + "OPERATION_CODE": "0", + } assert error.__cause__ is None @@ -838,14 +831,6 @@ def test_gql_hydration(metadata, attributes): # TODO: test causes error = Neo4jError._hydrate_gql(**metadata) - preview_attrs = { - "gql_status", - "gql_status_description", - "gql_classification", - "gql_raw_classification", - "diagnostic_record", - } - for attr in ( "code", "classification", @@ -860,11 +845,7 @@ def test_gql_hydration(metadata, attributes): "__cause__", ): expected_value = attributes[attr] - if attr in preview_attrs: - with pytest.warns(PreviewWarning, match="GQLSTATUS"): - actual_value = getattr(error, attr) - else: - actual_value = getattr(error, attr) + actual_value = getattr(error, attr) assert actual_value == expected_value diff --git a/tests/unit/common/test_import_neo4j.py b/tests/unit/common/test_import_neo4j.py index e83985a24..464a88bab 100644 --- a/tests/unit/common/test_import_neo4j.py +++ b/tests/unit/common/test_import_neo4j.py @@ -19,8 +19,6 @@ import pytest -from neo4j.warnings import PreviewWarning - def test_import_neo4j(): import neo4j # noqa: F401 - unused import to test import works @@ -49,17 +47,17 @@ def test_import_neo4j(): ("Driver", None), ("EagerResult", None), ("get_user_agent", None), - ("GqlStatusObject", PreviewWarning), + ("GqlStatusObject", None), ("GraphDatabase", None), ("IPv4Address", None), ("IPv6Address", None), ("kerberos_auth", None), ("ManagedTransaction", None), ("Neo4jDriver", None), - ("NotificationCategory", None), - ("NotificationClassification", PreviewWarning), - ("NotificationDisabledCategory", None), - ("NotificationDisabledClassification", PreviewWarning), + ("NotificationCategory", DeprecationWarning), + ("NotificationClassification", None), + ("NotificationDisabledCategory", DeprecationWarning), + ("NotificationDisabledClassification", None), ("NotificationMinimumSeverity", None), ("NotificationSeverity", None), ("PreviewWarning", DeprecationWarning), @@ -73,7 +71,7 @@ def test_import_neo4j(): ("Session", None), ("SummaryCounters", None), ("SummaryInputPosition", None), - ("SummaryNotification", None), + ("SummaryNotification", DeprecationWarning), ("SYSTEM_DATABASE", None), ("Transaction", None), ("TrustAll", None), @@ -122,21 +120,7 @@ def test_import_star(): importlib.__import__("neo4j", fromlist=("*",)) assert len(warnings) == 4 - for name in ( - "NotificationClassification", - "GqlStatusObject", - "NotificationDisabledClassification", - ): - assert ( - sum( - bool(re.match(rf".*\b{name}\b.*", str(w.message))) - for w in warnings - if issubclass(w.category, PreviewWarning) - ) - == 1 - ) - - for name in ("PreviewWarning",): + for name in ("PreviewWarning", "NotificationDisabledCategory"): assert ( sum( bool(re.match(rf".*\b{name}\b.*", str(w.message))) diff --git a/tests/unit/common/work/test_summary.py b/tests/unit/common/work/test_summary.py index 36fcc2080..652ce1a3c 100644 --- a/tests/unit/common/work/test_summary.py +++ b/tests/unit/common/work/test_summary.py @@ -17,7 +17,6 @@ from __future__ import annotations import typing as t -import warnings if t.TYPE_CHECKING: @@ -30,23 +29,19 @@ from neo4j import ( Address, - NotificationCategory, + GqlStatusObject, + NotificationClassification, NotificationSeverity, ResultSummary, ServerInfo, SummaryCounters, SummaryInputPosition, - SummaryNotification, ) -from neo4j.warnings import PreviewWarning - -with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=PreviewWarning) - from neo4j import ( - GqlStatusObject, - NotificationClassification, - ) +from ...._deprecated_imports import ( + NotificationCategory, + SummaryNotification, +) @pytest.fixture @@ -172,7 +167,7 @@ def test_summary_notifications(summary_args_kwargs, exists) -> None: kwargs["metadata"]["notifications"] = summary_in = [object()] summary = ResultSummary(*args, **kwargs) - summary_out: list[dict] | None = summary.notifications + summary_out = _get_notifications(summary) assert summary_out is summary_in @@ -219,10 +214,10 @@ def test_statuses_and_notifications_dont_mix(summary_args_kwargs) -> None: summary = ResultSummary(*args, **kwargs) - notifications = summary.notifications + notifications = _get_notifications(summary) assert notifications == [raw_notification] - parsed_notifications = summary.summary_notifications + parsed_notifications = _get_summary_notifications(summary) assert len(parsed_notifications) == 1 notification = parsed_notifications[0] assert notification.code == raw_notification["code"] @@ -236,8 +231,7 @@ def test_statuses_and_notifications_dont_mix(summary_args_kwargs) -> None: line=42, column=1337, offset=0 ) - with pytest.warns(PreviewWarning, match="GQLSTATUS"): - statuses = summary.gql_status_objects + statuses = summary.gql_status_objects assert len(statuses) == 1 status = statuses[0] assert status.gql_status == raw_status["gql_status"] @@ -382,10 +376,7 @@ def test_non_notification_statuses(raw_status, summary_args_kwargs) -> None: kwargs["metadata"]["statuses"] = [raw_status] summary = ResultSummary(*args, **kwargs) - with pytest.warns(PreviewWarning, match="GQLSTATUS"): - status_objects: t.Sequence[GqlStatusObject] = ( - summary.gql_status_objects - ) + status_objects: t.Sequence[GqlStatusObject] = summary.gql_status_objects assert len(status_objects) == 1 status = status_objects[0] @@ -436,10 +427,7 @@ def test_gql_statuses_keep_order( ] summary = ResultSummary(*args, **kwargs) - with pytest.warns(PreviewWarning, match="GQLSTATUS"): - status_objects: t.Sequence[GqlStatusObject] = ( - summary.gql_status_objects - ) + status_objects: t.Sequence[GqlStatusObject] = summary.gql_status_objects assert len(status_objects) == len(types) status: GqlStatusObject @@ -673,10 +661,7 @@ def test_status( kwargs["metadata"]["statuses"] = [raw_status] summary = ResultSummary(*args, **kwargs) - with pytest.warns(PreviewWarning, match="GQLSTATUS"): - status_objects: t.Sequence[GqlStatusObject] = ( - summary.gql_status_objects - ) + status_objects: t.Sequence[GqlStatusObject] = summary.gql_status_objects assert len(status_objects) == 1 status = status_objects[0] @@ -759,7 +744,7 @@ def test_summary_summary_notifications( kwargs["metadata"]["notifications"] = summary_in summary = ResultSummary(*args, **kwargs) - summary_out: Sequence[SummaryNotification] = summary.summary_notifications + summary_out = _get_summary_notifications(summary) assert isinstance(summary_out, tuple) if summary_in is None: @@ -977,10 +962,7 @@ def test_status_from_no_notification( kwargs["had_record"] = had_record summary = ResultSummary(*args, **kwargs) - with pytest.warns(PreviewWarning, match="GQLSTATUS"): - status_objects: t.Sequence[GqlStatusObject] = ( - summary.gql_status_objects - ) + status_objects: t.Sequence[GqlStatusObject] = summary.gql_status_objects assert len(status_objects) == 1 status: GqlStatusObject = status_objects[0] @@ -1288,10 +1270,7 @@ def test_status_from_notifications( kwargs["metadata"]["notifications"] = [raw_notification] summary = ResultSummary(*args, **kwargs) - with pytest.warns(PreviewWarning, match="GQLSTATUS"): - status_objects: t.Sequence[GqlStatusObject] = ( - summary.gql_status_objects - ) + status_objects: t.Sequence[GqlStatusObject] = summary.gql_status_objects notifications = [s for s in status_objects if s.is_notification] if "diagnostic_record" in expectation_overwrite: @@ -1453,8 +1432,7 @@ def test_status_precedence( ] summary = ResultSummary(*args, **kwargs) - with pytest.warns(PreviewWarning, match="GQLSTATUS"): - status_objects = summary.gql_status_objects + status_objects = summary.gql_status_objects gql_statuses = [s.gql_status for s in status_objects] assert gql_statuses == expected_statuses @@ -1468,10 +1446,8 @@ def test_no_notification_from_status(raw_status, summary_args_kwargs) -> None: kwargs["metadata"]["statuses"] = [raw_status] summary = ResultSummary(*args, **kwargs) - notifications: list[dict] | None = summary.notifications - summary_notifications: Sequence[SummaryNotification] = ( - summary.summary_notifications - ) + notifications = _get_notifications(summary) + summary_notifications = _get_summary_notifications(summary) assert notifications is None assert summary_notifications == () @@ -1676,8 +1652,8 @@ def test_notification_from_status( ] summary = ResultSummary(*args, **kwargs) - notifications = summary.notifications - summary_notifications = summary.summary_notifications + notifications = _get_notifications(summary) + summary_notifications = _get_summary_notifications(summary) expected_notification = { "code": expected_overwrite.get("code", default_code), @@ -1741,8 +1717,8 @@ def test_no_notification_from_wrong_type_status( kwargs["metadata"]["statuses"] = [status_in] summary = ResultSummary(*args, **kwargs) - notifications = summary.notifications - summary_notifications = summary.summary_notifications + notifications = _get_notifications(summary) + summary_notifications = _get_summary_notifications(summary) assert notifications is None assert summary_notifications == () @@ -1935,8 +1911,8 @@ def test_no_notification_from_status_without_neo4j_code( kwargs["metadata"]["statuses"] = [raw_status] summary = ResultSummary(*args, **kwargs) - notifications = summary.notifications - summary_notifications = summary.summary_notifications + notifications = _get_notifications(summary) + summary_notifications = _get_summary_notifications(summary) assert notifications is None assert summary_notifications == () @@ -1972,8 +1948,8 @@ def test_notification_from_incomplete_status( kwargs["metadata"]["statuses"] = [raw_status] summary = ResultSummary(*args, **kwargs) - notifications = summary.notifications - summary_notifications = summary.summary_notifications + notifications = _get_notifications(summary) + summary_notifications = _get_summary_notifications(summary) assert notifications == [raw_notification] @@ -2022,8 +1998,8 @@ def test_notification_from_unexpected_status( kwargs["metadata"]["statuses"] = [raw_status] summary = ResultSummary(*args, **kwargs) - notifications = summary.notifications - summary_notifications = summary.summary_notifications + notifications = _get_notifications(summary) + summary_notifications = _get_summary_notifications(summary) assert notifications == [raw_notification] @@ -2088,7 +2064,7 @@ def test_notification_from_broken_status( summary = ResultSummary(*args, **kwargs) - notifications = summary.notifications + notifications = _get_notifications(summary) assert notifications is None @@ -2109,8 +2085,8 @@ def test_notifications_from_statuses_keep_order( ] summary = ResultSummary(*args, **kwargs) - notifications = summary.notifications - summary_notifications = summary.summary_notifications + notifications = _get_notifications(summary) + summary_notifications = _get_summary_notifications(summary) assert notifications is not None assert len(notifications) == 4 @@ -2304,3 +2280,21 @@ def test_notification_from_meta_data( ) -> None: notification = SummaryNotification._from_metadata(meta) assert notification == expected + + +def _get_notifications(summary: ResultSummary) -> list[dict] | None: + with pytest.warns( + DeprecationWarning, + match=r"\bnotifications\b.*\bgql_status_objects instead", + ): + return summary.notifications + + +def _get_summary_notifications( + summary: ResultSummary, +) -> Sequence[SummaryNotification]: + with pytest.warns( + DeprecationWarning, + match=r"\bsummary_notifications\b.*\bgql_status_objects instead", + ): + return summary.summary_notifications diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 4ea26f7a6..806a12ec2 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -26,6 +26,9 @@ bolt_socket_factory, notification_factory, raw_notification_factory, + raw_status_notification_factory, + status_notification_factory, + status_notification_legacy_factory, ) from .sync.fixtures import ( fake_connection, @@ -49,6 +52,9 @@ "fake_pool", "notification_factory", "raw_notification_factory", + "raw_status_notification_factory", "scripted_connection", "scripted_connection_generator", + "status_notification_factory", + "status_notification_legacy_factory", ] diff --git a/tests/unit/fixtures/__init__.py b/tests/unit/fixtures/__init__.py index bcdd1ef92..a47db767d 100644 --- a/tests/unit/fixtures/__init__.py +++ b/tests/unit/fixtures/__init__.py @@ -17,6 +17,9 @@ from .notifications import ( notification_factory, raw_notification_factory, + raw_status_notification_factory, + status_notification_factory, + status_notification_legacy_factory, ) from .socket import ( async_bolt_socket_factory, @@ -29,4 +32,7 @@ "bolt_socket_factory", "notification_factory", "raw_notification_factory", + "raw_status_notification_factory", + "status_notification_factory", + "status_notification_legacy_factory", ] diff --git a/tests/unit/fixtures/notifications.py b/tests/unit/fixtures/notifications.py index d62e820ef..dedbe8f45 100644 --- a/tests/unit/fixtures/notifications.py +++ b/tests/unit/fixtures/notifications.py @@ -20,16 +20,13 @@ from neo4j import ( _typing as t, - SummaryNotification, + GqlStatusObject, ) +from ..._deprecated_imports import SummaryNotification -if t.TYPE_CHECKING: - class Position(t.TypedDict): - offset: t.NotRequired[int | None] - line: t.NotRequired[int | None] - column: t.NotRequired[int | None] +if t.TYPE_CHECKING: class TNotificationData(t.TypedDict): code: t.NotRequired[str | None] @@ -39,6 +36,28 @@ class TNotificationData(t.TypedDict): category: t.NotRequired[str | None] position: t.NotRequired[Position | None] + class TStatusNotificationData(t.TypedDict): + gql_status: t.NotRequired[str | None] + status_description: t.NotRequired[str | None] + neo4j_code: t.NotRequired[str | None] + title: t.NotRequired[str | None] + description: t.NotRequired[str | None] + diagnostic_record: t.NotRequired[TDiagnosticRecordData | None] + + class TDiagnosticRecordData(t.TypedDict): + OPERATION: t.NotRequired[str | None] + OPERATION_code: t.NotRequired[str | None] + CURRENT_SCHEMA: t.NotRequired[str | None] + _status_parameters: t.NotRequired[dict[str, t.Any] | None] + _severity: t.NotRequired[str | None] + _classification: t.NotRequired[str | None] + _position: t.NotRequired[Position | None] + + class Position(t.TypedDict): + offset: t.NotRequired[int | None] + line: t.NotRequired[int | None] + column: t.NotRequired[int | None] + class TNotificationFactory(t.Protocol): def __call__( self, @@ -46,6 +65,21 @@ def __call__( data_overwrite: TNotificationData | None = None, ) -> SummaryNotification: ... + class TStatusNotificationFactory(t.Protocol): + def __call__( + self, + data: TStatusNotificationData | None = None, + data_overwrite: TStatusNotificationData | None = None, + diag_rec_overwrite: TDiagnosticRecordData | None = None, + ) -> GqlStatusObject: ... + + class TStatusNotificationLegacyFactory(t.Protocol): + def __call__( + self, + data: TNotificationData | None = None, + data_overwrite: TNotificationData | None = None, + ) -> GqlStatusObject: ... + class TRawNotificationFactory(t.Protocol): def __call__( self, @@ -53,48 +87,129 @@ def __call__( data_overwrite: TNotificationData | None = None, ) -> TNotificationData: ... + class TRawStatusNotificationFactory(t.Protocol): + def __call__( + self, + data: TStatusNotificationData | None = None, + data_overwrite: TStatusNotificationData | None = None, + diag_rec_overwrite: TDiagnosticRecordData | None = None, + ) -> TStatusNotificationData: ... + __all__ = [ "notification_factory", "raw_notification_factory", + "raw_status_notification_factory", + "status_notification_factory", + "status_notification_legacy_factory", ] @pytest.fixture def notification_factory() -> TNotificationFactory: - def factory( - data=None, - data_overwrite=None, - ) -> SummaryNotification: - if data is None: - data = dict(TEST_NOTIFICATION_DATA) - if data_overwrite: - data.update(data_overwrite) - return SummaryNotification._from_metadata(data) + return _notification_factory + - return factory +def _notification_factory( + data: TNotificationData | None = None, + data_overwrite: TNotificationData | None = None, +) -> SummaryNotification: + data = _raw_notification_factory(data, data_overwrite) + return SummaryNotification._from_metadata(data) @pytest.fixture def raw_notification_factory() -> TRawNotificationFactory: - def factory( - data=None, - data_overwrite=None, - ) -> TNotificationData: - if data is None: - data = dict(TEST_NOTIFICATION_DATA) - if data_overwrite: - data.update(data_overwrite) - return data - - return factory - - -TEST_NOTIFICATION_DATA = ( - ("title", "Some title"), - ("code", "Neo.Made.Up.Code"), - ("description", "Some description"), - ("severity", "INFORMATION"), - ("category", "HINT"), - ("position", {"offset": 0, "line": 1, "column": 1}), -) + return _raw_notification_factory + + +def _raw_notification_factory( + data: TNotificationData | None = None, + data_overwrite: TNotificationData | None = None, +) -> TNotificationData: + if data is None: + data = _test_notification_data() + if data_overwrite: + data.update(data_overwrite) + return data + + +@pytest.fixture +def status_notification_legacy_factory() -> TStatusNotificationLegacyFactory: + return _status_notification_legacy_factory + + +def _status_notification_legacy_factory( + data: TNotificationData | None = None, + data_overwrite: TNotificationData | None = None, +) -> GqlStatusObject: + data = _raw_notification_factory(data, data_overwrite) + return GqlStatusObject._from_notification_metadata(data) + + +@pytest.fixture +def status_notification_factory() -> TStatusNotificationFactory: + return _status_notification_factory + + +def _status_notification_factory( + data: TStatusNotificationData | None = None, + data_overwrite: TStatusNotificationData | None = None, + diag_rec_overwrite: TDiagnosticRecordData | None = None, +) -> GqlStatusObject: + data = _raw_status_notification_factory( + data, data_overwrite, diag_rec_overwrite + ) + return GqlStatusObject._from_status_metadata(data) + + +@pytest.fixture +def raw_status_notification_factory() -> TRawStatusNotificationFactory: + return _raw_status_notification_factory + + +def _raw_status_notification_factory( + data: TStatusNotificationData | None = None, + data_overwrite: TStatusNotificationData | None = None, + diag_rec_overwrite: TDiagnosticRecordData | None = None, +) -> TStatusNotificationData: + if data is None: + data = _test_status_notification_data() + if data_overwrite: + data.update(data_overwrite) + if diag_rec_overwrite: + if data.get("diagnostic_record") is None: + data["diagnostic_record"] = {} + assert data["diagnostic_record"] is not None + data["diagnostic_record"].update(diag_rec_overwrite) + return data + + +def _test_notification_data() -> TNotificationData: + return { + "title": "Some title", + "code": "Neo.Made.Up.Code", + "description": "Some description", + "severity": "INFORMATION", + "category": "HINT", + "position": {"offset": 0, "line": 1, "column": 1}, + } + + +def _test_status_notification_data() -> TStatusNotificationData: + return { + "gql_status": "03N42", + "status_description": "Some status description", + "neo4j_code": "Neo.Made.Up.Code", + "title": "Some status title", + "description": "Some notification description", + "diagnostic_record": { + "OPERATION": "", + "OPERATION_code": "0", + "CURRENT_SCHEMA": "/", + "_status_parameters": {}, + "_severity": "INFORMATION", + "_classification": "HINT", + "_position": {"offset": 0, "line": 1, "column": 1}, + }, + } diff --git a/tests/unit/sync/test_driver.py b/tests/unit/sync/test_driver.py index a6ce7920e..f8bac20c3 100644 --- a/tests/unit/sync/test_driver.py +++ b/tests/unit/sync/test_driver.py @@ -28,7 +28,7 @@ BoltDriver, GraphDatabase, Neo4jDriver, - NotificationDisabledCategory, + NotificationDisabledClassification, NotificationMinimumSeverity, Query, Result, @@ -65,7 +65,7 @@ mark_sync_test, TestDecorators, ) -from ..._preview_imports import NotificationDisabledClassification +from ..._deprecated_imports import NotificationDisabledCategory @pytest.fixture @@ -657,9 +657,9 @@ def test_driver_factory_with_notification_filters( if dis_clss is not ...: filter_kwargs["notifications_disabled_classifications"] = dis_clss - if "notifications_disabled_classifications" in filter_kwargs: + if "notifications_disabled_categories" in filter_kwargs: with pytest.warns( - PreviewWarning, match="notifications_disabled_classifications" + DeprecationWarning, match="notifications_disabled_categories" ): driver = GraphDatabase.driver(uri, auth=None, **filter_kwargs) else: @@ -836,9 +836,9 @@ def test_session_factory_with_notification_filter( filter_kwargs["notifications_disabled_classifications"] = dis_clss with GraphDatabase.driver(uri, auth=None) as driver: - if "notifications_disabled_classifications" in filter_kwargs: + if "notifications_disabled_categories" in filter_kwargs: with pytest.warns( - PreviewWarning, match="notifications_disabled_classifications" + DeprecationWarning, match="notifications_disabled_categories" ): session = driver.session(**filter_kwargs) else: diff --git a/tests/unit/sync/work/test_result.py b/tests/unit/sync/work/test_result.py index b69cf7aad..cb05b75ab 100644 --- a/tests/unit/sync/work/test_result.py +++ b/tests/unit/sync/work/test_result.py @@ -67,8 +67,10 @@ if t.TYPE_CHECKING: from ...fixtures.notifications import ( - TNotificationFactory, TRawNotificationFactory, + TRawStatusNotificationFactory, + TStatusNotificationFactory, + TStatusNotificationLegacyFactory, ) @@ -1384,7 +1386,7 @@ def test_notification_warning( ) if expected_warning is None: with warnings.catch_warnings(): - warnings.simplefilter("error") # assert not warnings are emitted + warnings.simplefilter("error") # assert no warnings are emitted result._run("CYPHER", {}, None, None, "r", None, None, None) result.consume() else: @@ -1399,31 +1401,47 @@ def test_notification_warning( @pytest.mark.parametrize("notification_severity", ("INFORMATION", "WARNING")) @pytest.mark.parametrize( - "notification_category", + "notification_classification", ( "HINT", "DEPRECATION", "UNRECOGNIZED", ), ) +@pytest.mark.parametrize("legacy_input", (True, False)) @mark_sync_test def test_notification_logging( raw_notification_factory: TRawNotificationFactory, - notification_factory: TNotificationFactory, + raw_status_notification_factory: TRawStatusNotificationFactory, + status_notification_legacy_factory: TStatusNotificationLegacyFactory, + status_notification_factory: TStatusNotificationFactory, notification_severity: str, - notification_category: str, + notification_classification: str, + legacy_input: bool, caplog: pytest.LogCaptureFixture, ) -> None: - notification_data = raw_notification_factory( - data_overwrite={ - "severity": notification_severity, - "category": notification_category, - } - ) - notification = notification_factory(notification_data) + summary_meta: dict[str, t.Any] + if legacy_input: + notification_data = raw_notification_factory( + data_overwrite={ + "severity": notification_severity, + "category": notification_classification, + } + ) + gql_status = status_notification_legacy_factory(notification_data) + summary_meta = {"notifications": [notification_data]} + else: + status_data = raw_status_notification_factory( + diag_rec_overwrite={ + "_severity": notification_severity, + "_classification": notification_classification, + } + ) + gql_status = status_notification_factory(status_data) + summary_meta = {"statuses": [status_data]} connection = ConnectionStub( records=Records(["foo"], ()), - summary_meta={"notifications": [notification_data]}, + summary_meta=summary_meta, ) result = Result(connection, 1, None, noop, noop, None) with caplog.at_level(logging.INFO, logger="neo4j.notifications"): @@ -1431,7 +1449,7 @@ def test_notification_logging( result.consume() assert len(caplog.messages) == 1 formatted_notification = str( - NotificationPrinter(notification, "CYPHER", one_line=True) + NotificationPrinter(gql_status, "CYPHER", one_line=True) ) expected_message = ( f"Received notification from DBMS server: {formatted_notification}"