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