1"""An interface for publishing rich data to frontends.
2
3There are two components of the display system:
4
5* Display formatters, which take a Python object and compute the
6 representation of the object in various formats (text, HTML, SVG, etc.).
7* The display publisher that is used to send the representation data to the
8 various frontends.
9
10This module defines the logic display publishing. The display publisher uses
11the ``display_data`` message type that is defined in the IPython messaging
12spec.
13"""
14from __future__ import annotations
15
16# Copyright (c) IPython Development Team.
17# Distributed under the terms of the Modified BSD License.
18
19import sys
20
21from traitlets.config.configurable import Configurable
22from traitlets import List
23
24# This used to be defined here - it is imported for backwards compatibility
25from .display_functions import publish_display_data
26from .history import HistoryOutput
27
28# -----------------------------------------------------------------------------
29# Main payload class
30# -----------------------------------------------------------------------------
31
32_sentinel = object()
33
34
35class DisplayPublisher(Configurable):
36 """A traited class that publishes display data to frontends.
37
38 Instances of this class are created by the main IPython object and should
39 be accessed there.
40 """
41
42 def __init__(self, shell=None, *args, **kwargs):
43 self.shell = shell
44 self._is_publishing = False
45 self._in_post_execute = False
46 if self.shell:
47 self._setup_execution_tracking()
48 super().__init__(*args, **kwargs)
49
50 def _validate_data(self, data, metadata=None):
51 """Validate the display data.
52
53 Parameters
54 ----------
55 data : dict
56 The formata data dictionary.
57 metadata : dict
58 Any metadata for the data.
59 """
60
61 if not isinstance(data, dict):
62 raise TypeError("data must be a dict, got: %r" % data)
63 if metadata is not None:
64 if not isinstance(metadata, dict):
65 raise TypeError("metadata must be a dict, got: %r" % data)
66
67 def _setup_execution_tracking(self):
68 """Set up hooks to track execution state"""
69 self.shell.events.register("post_execute", self._on_post_execute)
70 self.shell.events.register("pre_execute", self._on_pre_execute)
71
72 def _on_post_execute(self):
73 """Called at start of post_execute phase"""
74 self._in_post_execute = True
75
76 def _on_pre_execute(self):
77 """Called at start of pre_execute phase"""
78 self._in_post_execute = False
79
80 # use * to indicate transient, update are keyword-only
81 def publish(
82 self,
83 data,
84 metadata=None,
85 source=_sentinel,
86 *,
87 transient=None,
88 update=False,
89 **kwargs,
90 ) -> None:
91 """Publish data and metadata to all frontends.
92
93 See the ``display_data`` message in the messaging documentation for
94 more details about this message type.
95
96 The following MIME types are currently implemented:
97
98 * text/plain
99 * text/html
100 * text/markdown
101 * text/latex
102 * application/json
103 * application/javascript
104 * image/png
105 * image/jpeg
106 * image/svg+xml
107
108 Parameters
109 ----------
110 data : dict
111 A dictionary having keys that are valid MIME types (like
112 'text/plain' or 'image/svg+xml') and values that are the data for
113 that MIME type. The data itself must be a JSON'able data
114 structure. Minimally all data should have the 'text/plain' data,
115 which can be displayed by all frontends. If more than the plain
116 text is given, it is up to the frontend to decide which
117 representation to use.
118 metadata : dict
119 A dictionary for metadata related to the data. This can contain
120 arbitrary key, value pairs that frontends can use to interpret
121 the data. Metadata specific to each mime-type can be specified
122 in the metadata dict with the same mime-type keys as
123 the data itself.
124 source : str, deprecated
125 Unused.
126 transient : dict, keyword-only
127 A dictionary for transient data.
128 Data in this dictionary should not be persisted as part of saving this output.
129 Examples include 'display_id'.
130 update : bool, keyword-only, default: False
131 If True, only update existing outputs with the same display_id,
132 rather than creating a new output.
133 """
134
135 if source is not _sentinel:
136 import warnings
137
138 warnings.warn(
139 "The 'source' parameter is deprecated since IPython 3.0 and will be ignored "
140 "(this warning is present since 9.0). `source` parameter will be removed in the future.",
141 DeprecationWarning,
142 stacklevel=2,
143 )
144
145 handlers: dict = {}
146 if self.shell is not None:
147 handlers = getattr(self.shell, "mime_renderers", {})
148
149 outputs = self.shell.history_manager.outputs
150
151 target_execution_count = self.shell.execution_count - 1
152 if self._in_post_execute:
153 # We're in post_execute, so this is likely a matplotlib flush
154 # Use execution_count - 1 to associate with the cell that created the plot
155 target_execution_count = self.shell.execution_count - 1
156
157 outputs[target_execution_count].append(
158 HistoryOutput(output_type="display_data", bundle=data)
159 )
160
161 for mime, handler in handlers.items():
162 if mime in data:
163 handler(data[mime], metadata.get(mime, None))
164 return
165
166 self._is_publishing = True
167 if "text/plain" in data:
168 print(data["text/plain"])
169 self._is_publishing = False
170
171 @property
172 def is_publishing(self):
173 return self._is_publishing
174
175 def clear_output(self, wait=False):
176 """Clear the output of the cell receiving output."""
177 print("\033[2K\r", end="")
178 sys.stdout.flush()
179 print("\033[2K\r", end="")
180 sys.stderr.flush()
181
182
183class CapturingDisplayPublisher(DisplayPublisher):
184 """A DisplayPublisher that stores"""
185
186 outputs: List = List()
187
188 def publish(
189 self, data, metadata=None, source=None, *, transient=None, update=False
190 ):
191 self.outputs.append(
192 {
193 "data": data,
194 "metadata": metadata,
195 "transient": transient,
196 "update": update,
197 }
198 )
199
200 def clear_output(self, wait=False):
201 super().clear_output(wait)
202
203 # empty the list, *do not* reassign a new list
204 self.outputs.clear()