From 0e8f12e5b94480bf62d18dd44434879bcc3e316e Mon Sep 17 00:00:00 2001 From: Pete Farland Date: Thu, 4 May 2023 15:37:26 +0000 Subject: [PATCH] Merged PR posit-dev/positron-python#86: Add some additional documentation to positron python Merge pull request #86 from posit-dev/positron-docs Add some additional documentation to positron python -------------------- Commit message for posit-dev/positron-python@4b19d9468b705cffe3b8c6e0e5aafa1a6d825a06: Add some additional documentation to positron python Authored-by: Pete Farland Signed-off-by: Pete Farland --- .../pythonFiles/positron/environment.py | 96 +++++++++++++++---- .../pythonFiles/positron/inspectors.py | 32 +++++-- .../pythonFiles/positron/positron_ipkernel.py | 50 +++++----- 3 files changed, 126 insertions(+), 52 deletions(-) diff --git a/extensions/positron-python/pythonFiles/positron/environment.py b/extensions/positron-python/pythonFiles/positron/environment.py index 2be5aeff630..6b8d508a3de 100644 --- a/extensions/positron-python/pythonFiles/positron/environment.py +++ b/extensions/positron-python/pythonFiles/positron/environment.py @@ -182,7 +182,15 @@ def on_comm_open(self, comm, open_msg) -> None: def receive_message(self, msg) -> None: """ - Handle messages sent by the client to the positron.environment comm. + Handle messages received from the client via the positron.environment comm. + + Message Types: + "inspect" - Inspect the user variable at the requested path + "refresh" - Refresh the list of user variables in the environment + "view" - Format the variable at the requested path for the data viewer + "clipboard_format" - Format the variable at the requested path for the client clipboard + "clear" - Clear all user variables in the environment + "delete" - Delete user variables in the environment by name """ data = msg["content"]["data"] @@ -214,6 +222,10 @@ def receive_message(self, msg) -> None: self._send_error(f"Unknown message type '{msgType}'") def send_update(self, assigned: dict, removed: set) -> None: + """ + Sends the list of variables that have changed in the current user session through the + environment comm to the client. + """ # Ensure the number of changes does not exceed our maximum items if len(assigned) < MAX_ITEMS and len(removed) < MAX_ITEMS: self._send_update(assigned, removed) @@ -264,7 +276,7 @@ def _send_message(self, msg: EnvironmentMessage) -> None: self.env_comm.send(msg) - def _send_error(self, message: str) -> None: + def _send_error(self, error_message: str) -> None: """ Send an error message through the envirvonment comm to the client. @@ -277,7 +289,7 @@ def _send_error(self, message: str) -> None: ... } """ - msg = EnvironmentMessageError(message) + msg = EnvironmentMessageError(error_message) self._send_message(msg) def _send_update(self, assigned: Mapping, removed: Iterable) -> None: @@ -312,12 +324,24 @@ def _send_update(self, assigned: Mapping, removed: Iterable) -> None: def _delete_all_vars(self, parent) -> None: """ Deletes all of the variables in the current user session. + + Args: + parent: + A dict providing the parent context for the response, + e.g. the client message requesting the clear operation """ self.kernel.delete_all_vars(parent) def _delete_vars(self, names: Iterable, parent) -> None: """ Deletes the requested variables by name from the current user session. + + Args: + names: + A list of variable names to delete + parent: + A dict providing the parent context for the response, + e.g. the client message requesting the delete operation """ if names is None: return @@ -328,12 +352,15 @@ def _delete_vars(self, names: Iterable, parent) -> None: def _inspect_var(self, path: Sequence) -> None: """ Describes the variable at the requested path in the current user session. + + Args: + path: + A list of names describing the path to the variable. """ if path is None: return is_known, value = self.kernel.find_var(path) - if is_known: self._send_details(path, value) else: @@ -357,8 +384,14 @@ def _send_formatted_var( Formats the variable at the requested path in the current user session using the requested clipboard format and sends the result through the environment comm to the client. - """ + Args: + path: + A list of names describing the path to the variable. + clipboard_format: + The format to use for the clipboard copy, described as a mime type. + Defaults to "text/plain". + """ if path is None: return @@ -372,7 +405,7 @@ def _send_formatted_var( message = f"Cannot find variable at '{path}' to format" self._send_error(message) - def _send_details(self, path: Sequence, context: Any = None): + def _send_details(self, path: Sequence, value: Any = None): """ Sends a detailed list of children of the value (or just the value itself, if is a leaf node on the path) as a message through the @@ -397,15 +430,21 @@ def _send_details(self, path: Sequence, context: Any = None): } ... } + + Args: + path: + A list of names describing the path to the variable. + value: + The variable's value to summarize. """ children = [] - inspector = get_inspector(context) - if inspector is not None and inspector.has_children(context): - children = inspector.summarize_children(context, self._summarize_variable) + inspector = get_inspector(value) + if inspector.has_children(value): + children = inspector.summarize_children(value, self._summarize_variable) else: # Otherwise, treat as a simple value at given path - summary = self._summarize_variable("", context) + summary = self._summarize_variable("", value) if summary is not None: children.append(summary) # TODO: Handle scalar objects with a specific message type @@ -414,11 +453,19 @@ def _send_details(self, path: Sequence, context: Any = None): self._send_message(msg) def _summarize_variables(self, variables: Mapping, max_items: int = MAX_ITEMS) -> list: + """ + Summarizes the given variables into a list of EnvironmentVariable objects. + + Args: + variables: + A mapping of variable names to values. + max_items: + The maximum number of items to summarize. + """ summaries = [] for key, value in variables.items(): - # Ensure the number of items summarized is within our - # max limit + # Ensure the number of items summarized is within our max limit if len(summaries) >= max_items: break @@ -428,22 +475,28 @@ def _summarize_variables(self, variables: Mapping, max_items: int = MAX_ITEMS) - return summaries - def _summarize_variable(self, key, value) -> Optional[EnvironmentVariable]: + def _summarize_variable(self, key: Any, value: Any) -> Optional[EnvironmentVariable]: + """ + Summarizes the given variable into an EnvironmentVariable object. + + Returns: + An EnvironmentVariable summary, or None if the variable should be skipped. + """ # Hide module types for now if isinstance(value, types.ModuleType): return None - display_name = str(key) - try: # Use an inspector to summarize the value ins = get_inspector(value) + display_name = ins.get_display_name(key) kind_str = ins.get_kind(value) kind = getattr(EnvironmentVariableKind, kind_str.upper()) display_value, is_truncated = ins.get_display_value(value) display_type = ins.get_display_type(value) type_info = ins.get_type_info(value) + access_key = ins.get_access_key(key) length = ins.get_length(value) size = ins.get_size(value) has_children = ins.has_children(value) @@ -455,7 +508,7 @@ def _summarize_variable(self, key, value) -> Optional[EnvironmentVariable]: display_type=display_type, kind=kind, type_info=type_info, - access_key=display_name, + access_key=access_key, length=length, size=size, has_children=has_children, @@ -466,17 +519,18 @@ def _summarize_variable(self, key, value) -> Optional[EnvironmentVariable]: except Exception as err: logging.warning(err, exc_info=True) return EnvironmentVariable( - display_name=display_name, + display_name=str(key), display_value=get_qualname(value), kind=EnvironmentVariableKind.OTHER, ) - def _format_value(self, value, clipboard_format: ClipboardFormat) -> str: + def _format_value(self, value: Any, clipboard_format: ClipboardFormat) -> str: + """ + Formats the given value using the requested clipboard format. + """ inspector = get_inspector(value) if clipboard_format == ClipboardFormat.HTML: return inspector.to_html(value) - elif clipboard_format == ClipboardFormat.PLAIN: - return inspector.to_tsv(value) else: - return str(value) + return inspector.to_plaintext(value) diff --git a/extensions/positron-python/pythonFiles/positron/inspectors.py b/extensions/positron-python/pythonFiles/positron/inspectors.py index bc16f92d2cf..3829c326aa0 100644 --- a/extensions/positron-python/pythonFiles/positron/inspectors.py +++ b/extensions/positron-python/pythonFiles/positron/inspectors.py @@ -23,7 +23,9 @@ # conditional property lookup __POSITRON_DEFAULT__ = object() -# Base inspector for any type +# +# Base inspector +# class PositronInspector: @@ -31,6 +33,9 @@ class PositronInspector: Base inspector for any type """ + def get_display_name(self, key: Any) -> str: + return str(key) + def get_display_value( self, value: Any, @@ -75,6 +80,9 @@ def get_kind(self, value: Any) -> str: def get_type_info(self, value: Any) -> str: return get_qualname(value) + def get_access_key(self, name: Any) -> str: + return self.get_display_name(name) + def get_length(self, value: Any) -> int: return get_value_length(value) @@ -113,11 +121,13 @@ def to_dataset(self, value: Any, title: str) -> Optional[DataSet]: def to_html(self, value: Any) -> str: return repr(value) - def to_tsv(self, value: Any) -> str: + def to_plaintext(self, value: Any) -> str: return repr(value) +# # Inspectors by kind +# class BooleanInspector(PositronInspector): @@ -318,7 +328,9 @@ def is_snapshottable(self, value: Any) -> bool: return True +# # Custom inspectors for specific types +# class PandasDataFrameInspector(TableInspector): @@ -406,7 +418,7 @@ def to_dataset(self, value: Any, title: str) -> Optional[DataSet]: def to_html(self, value: Any) -> str: return value.to_html() - def to_tsv(self, value: Any) -> str: + def to_plaintext(self, value: Any) -> str: return value.to_csv(path_or_buf=None, sep="\t") @@ -481,9 +493,9 @@ def copy(self, value: Any) -> Any: def to_html(self, value: Any) -> str: # TODO: Support HTML - return self.to_tsv(value) + return self.to_plaintext(value) - def to_tsv(self, value: Any) -> str: + def to_plaintext(self, value: Any) -> str: return value.to_csv(path_or_buf=None, sep="\t") @@ -570,7 +582,7 @@ def to_dataset(self, value: Any, title: str) -> Optional[DataSet]: def to_html(self, value: Any) -> str: return value._repr_html_() - def to_tsv(self, value: Any) -> str: + def to_plaintext(self, value: Any) -> str: return value.write_csv(file=None, separator="\t") @@ -695,8 +707,12 @@ def copy(self, value: Any) -> Any: "table": TableInspector(), } +# +# Helper functions +# + -def get_inspector(value) -> PositronInspector: +def get_inspector(value: Any) -> PositronInspector: # Look for a specific inspector by qualified classname qualname = get_qualname(value) inspector = INSPECTORS.get(qualname, None) @@ -713,7 +729,7 @@ def get_inspector(value) -> PositronInspector: return inspector -def _get_kind(value) -> str: +def _get_kind(value: Any) -> str: if isinstance(value, str): return "string" elif isinstance(value, bool): diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipkernel.py b/extensions/positron-python/pythonFiles/positron/positron_ipkernel.py index ebff24647d1..2d66e7f2b11 100644 --- a/extensions/positron-python/pythonFiles/positron/positron_ipkernel.py +++ b/extensions/positron-python/pythonFiles/positron/positron_ipkernel.py @@ -51,9 +51,6 @@ def __init__(self, **kwargs): shell.events.register("post_execute", self.handle_post_execute) self.get_user_ns_hidden().update(POSITON_NS_HIDDEN) - # Set the traceback mode to minimal by default - shell.InteractiveTB.set_mode(mode="Minimal") - # Setup Positron's environment service self.env_service = EnvironmentService(self) self.comm_manager.register_target(POSITRON_ENVIRONMENT_COMM, self.env_service.on_comm_open) @@ -88,7 +85,7 @@ def handle_pre_execute(self) -> None: def handle_post_execute(self) -> None: """ After execution, sends an update message to the client to summarize - the changes observed to variables in the user environment. + the changes observed to variables in the user's environment. """ # First check pre_execute snapshot exists @@ -104,15 +101,15 @@ def handle_post_execute(self) -> None: logging.warning(err, exc_info=True) def get_user_ns(self) -> dict: - return self.shell.user_ns # type: ignore + return self.shell.user_ns or {} # type: ignore def get_user_ns_hidden(self) -> dict: - return self.shell.user_ns_hidden # type: ignore + return self.shell.user_ns_hidden or {} # type: ignore def snapshot_user_ns(self) -> None: """ Caches a shallow copy snapshot of the user's environment - before execution. + before execution and stores it in the hidden namespace. """ ns = self.get_user_ns() hidden = self.get_user_ns_hidden() @@ -132,6 +129,13 @@ def snapshot_user_ns(self) -> None: hidden[__POSITRON_CACHE_KEY__] = snapshot def compare_user_ns(self) -> Tuple[dict, set]: + """ + Attempts to detect changes to variables in the user's environment. + + Returns: + A tuple (dict, set) containing a dict of variables that were modified + (added or updated) and a set of variables that were removed. + """ assigned = {} removed = set() after = self.get_user_ns() @@ -183,9 +187,9 @@ def compare_user_ns(self) -> Tuple[dict, set]: def get_filtered_vars(self, variables: Optional[dict] = None) -> dict: """ - Returns a filtered dict of the variables, excluding hidden variables. - - If variables is None, the current user namespace is used. + Returns: + A filtered dict of the variables, excluding hidden variables. If variables + is None, the current user namespace in the environment is used. """ hidden = self.get_user_ns_hidden() @@ -199,12 +203,16 @@ def get_filtered_vars(self, variables: Optional[dict] = None) -> dict: return filtered_variables def get_filtered_var_names(self, names: set) -> set: + """ + Returns: + A filtered set of variable names, excluding hidden variables. + """ hidden = self.get_user_ns_hidden() # Filter out hidden variables filtered_names = set() for name in names: - if hidden is not None and name in hidden: + if name in hidden: continue filtered_names.add(name) return filtered_names @@ -240,10 +248,9 @@ def find_var(self, path: Iterable) -> Tuple[bool, Any]: else: # Check for membership via inspector inspector = get_inspector(context) - if inspector is not None: - is_known = inspector.has_child(context, name) - if is_known: - value = inspector.get_child(context, name) + is_known = inspector.has_child(context, name) + if is_known: + value = inspector.get_child(context, name) # Subsequent segment starts from the value context = value @@ -267,14 +274,11 @@ def view_var(self, path: Sequence) -> None: if is_known: inspector = get_inspector(value) - if inspector is not None: - # Use the leaf segment as the title - title = path[-1:][0] - dataset = inspector.to_dataset(value, title) - if dataset is not None: - self.dataviewer_service.register_dataset(dataset) - else: - error_message = f"Cannot create viewer for variable at '{path}'" + # Use the leaf segment as the title + title = path[-1:][0] + dataset = inspector.to_dataset(value, title) + if dataset is not None: + self.dataviewer_service.register_dataset(dataset) else: error_message = f"Cannot find variable at '{path}' to inspect"