vscode-python/python_files/get_variable_info.py

540 lines
18 KiB
Python

# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
import locale
import sys
from typing import ClassVar
# this class is from in ptvsd/debugpy tools
class SafeRepr(object): # noqa: UP004
# Can be used to override the encoding from locale.getpreferredencoding()
locale_preferred_encoding = None
# Can be used to override the encoding used for sys.stdout.encoding
sys_stdout_encoding = None
# String types are truncated to maxstring_outer when at the outer-
# most level, and truncated to maxstring_inner characters inside
# collections.
maxstring_outer = 2**16
maxstring_inner = 128
string_types = (str, bytes)
bytes = bytes
set_info = (set, "{", "}", False)
frozenset_info = (frozenset, "frozenset({", "})", False)
int_types = (int,)
long_iter_types = (list, tuple, bytearray, range, dict, set, frozenset)
# Collection types are recursively iterated for each limit in
# maxcollection.
maxcollection = (60, 20)
# Specifies type, prefix string, suffix string, and whether to include a
# comma if there is only one element. (Using a sequence rather than a
# mapping because we use isinstance() to determine the matching type.)
collection_types = [ # noqa: RUF012
(tuple, "(", ")", True),
(list, "[", "]", False),
frozenset_info,
set_info,
]
try:
from collections import deque
collection_types.append((deque, "deque([", "])", False))
except Exception:
pass
# type, prefix string, suffix string, item prefix string,
# item key/value separator, item suffix string
dict_types: ClassVar[list] = [(dict, "{", "}", "", ": ", "")]
try:
from collections import OrderedDict
dict_types.append((OrderedDict, "OrderedDict([", "])", "(", ", ", ")"))
except Exception:
pass
# All other types are treated identically to strings, but using
# different limits.
maxother_outer = 2**16
maxother_inner = 128
convert_to_hex = False
raw_value = False
def __call__(self, obj):
"""
:param object obj:
The object for which we want a representation.
:return str:
Returns bytes encoded as utf-8 on py2 and str on py3.
""" # noqa: D205
try:
return "".join(self._repr(obj, 0))
except Exception:
try:
return f"An exception was raised: {sys.exc_info()[1]!r}"
except Exception:
return "An exception was raised"
def _repr(self, obj, level):
"""Returns an iterable of the parts in the final repr string."""
try:
obj_repr = type(obj).__repr__
except Exception:
obj_repr = None
def has_obj_repr(t):
r = t.__repr__
try:
return obj_repr == r
except Exception:
return obj_repr is r
for t, prefix, suffix, comma in self.collection_types:
if isinstance(obj, t) and has_obj_repr(t):
return self._repr_iter(obj, level, prefix, suffix, comma)
for (
t,
prefix,
suffix,
item_prefix,
item_sep,
item_suffix,
) in self.dict_types:
if isinstance(obj, t) and has_obj_repr(t):
return self._repr_dict(
obj, level, prefix, suffix, item_prefix, item_sep, item_suffix
)
for t in self.string_types:
if isinstance(obj, t) and has_obj_repr(t):
return self._repr_str(obj, level)
if self._is_long_iter(obj):
return self._repr_long_iter(obj)
return self._repr_other(obj, level)
# Determines whether an iterable exceeds the limits set in
# maxlimits, and is therefore unsafe to repr().
def _is_long_iter(self, obj, level=0):
try:
# Strings have their own limits (and do not nest). Because
# they don't have __iter__ in 2.x, this check goes before
# the next one.
if isinstance(obj, self.string_types):
return len(obj) > self.maxstring_inner
# If it's not an iterable (and not a string), it's fine.
if not hasattr(obj, "__iter__"):
return False
# If it's not an instance of these collection types then it
# is fine. Note: this is a fix for
# https://github.com/Microsoft/ptvsd/issues/406
if not isinstance(obj, self.long_iter_types):
return False
# Iterable is its own iterator - this is a one-off iterable
# like generator or enumerate(). We can't really count that,
# but repr() for these should not include any elements anyway,
# so we can treat it the same as non-iterables.
if obj is iter(obj):
return False
# range reprs fine regardless of length.
if isinstance(obj, range):
return False
# numpy and scipy collections (ndarray etc) have
# self-truncating repr, so they're always safe.
try:
module = type(obj).__module__.partition(".")[0]
if module in ("numpy", "scipy"):
return False
except Exception:
pass
# Iterables that nest too deep are considered long.
if level >= len(self.maxcollection):
return True
# It is too long if the length exceeds the limit, or any
# of its elements are long iterables.
if hasattr(obj, "__len__"):
try:
size = len(obj)
except Exception:
size = None
if size is not None and size > self.maxcollection[level]:
return True
return any(self._is_long_iter(item, level + 1) for item in obj)
return any(
i > self.maxcollection[level] or self._is_long_iter(item, level + 1)
for i, item in enumerate(obj)
)
except Exception:
# If anything breaks, assume the worst case.
return True
def _repr_iter(self, obj, level, prefix, suffix, comma_after_single_element=False): # noqa: FBT002
yield prefix
if level >= len(self.maxcollection):
yield "..."
else:
count = self.maxcollection[level]
yield_comma = False
for item in obj:
if yield_comma:
yield ", "
yield_comma = True
count -= 1
if count <= 0:
yield "..."
break
yield from self._repr(item, 100 if item is obj else level + 1)
else:
if comma_after_single_element: # noqa: SIM102
if count == self.maxcollection[level] - 1:
yield ","
yield suffix
def _repr_long_iter(self, obj):
try:
length = hex(len(obj)) if self.convert_to_hex else len(obj)
obj_repr = f"<{type(obj).__name__}, len() = {length}>"
except Exception:
try:
obj_repr = "<" + type(obj).__name__ + ">"
except Exception:
obj_repr = "<no repr available for object>"
yield obj_repr
def _repr_dict(self, obj, level, prefix, suffix, item_prefix, item_sep, item_suffix):
if not obj:
yield prefix + suffix
return
if level >= len(self.maxcollection):
yield prefix + "..." + suffix
return
yield prefix
count = self.maxcollection[level]
yield_comma = False
obj_keys = list(obj)
for key in obj_keys:
if yield_comma:
yield ", "
yield_comma = True
count -= 1
if count <= 0:
yield "..."
break
yield item_prefix
for p in self._repr(key, level + 1):
yield p
yield item_sep
try:
item = obj[key]
except Exception:
yield "<?>"
else:
for p in self._repr(item, 100 if item is obj else level + 1):
yield p
yield item_suffix
yield suffix
def _repr_str(self, obj, level):
try:
if self.raw_value:
# For raw value retrieval, ignore all limits.
if isinstance(obj, bytes):
yield obj.decode("latin-1")
else:
yield obj
return
limit_inner = self.maxother_inner
limit_outer = self.maxother_outer
limit = limit_inner if level > 0 else limit_outer
if len(obj) <= limit:
# Note that we check the limit before doing the repr (so, the final string
# may actually be considerably bigger on some cases, as besides
# the additional u, b, ' chars, some chars may be escaped in repr, so
# even a single char such as \U0010ffff may end up adding more
# chars than expected).
yield self._convert_to_unicode_or_bytes_repr(repr(obj))
return
# Slightly imprecise calculations - we may end up with a string that is
# up to 6 characters longer than limit. If you need precise formatting,
# you are using the wrong class.
left_count, right_count = max(1, int(2 * limit / 3)), max(1, int(limit / 3))
# Important: only do repr after slicing to avoid duplicating a byte array that could be
# huge.
# Note: we don't deal with high surrogates here because we're not dealing with the
# repr() of a random object.
# i.e.: A high surrogate unicode char may be splitted on Py2, but as we do a `repr`
# afterwards, that's ok.
# Also, we just show the unicode/string/bytes repr() directly to make clear what the
# input type was (so, on py2 a unicode would start with u' and on py3 a bytes would
# start with b').
part1 = obj[:left_count]
part1 = repr(part1)
part1 = part1[: part1.rindex("'")] # Remove the last '
part2 = obj[-right_count:]
part2 = repr(part2)
part2 = part2[part2.index("'") + 1 :] # Remove the first ' (and possibly u or b).
yield part1
yield "..."
yield part2
except: # noqa: E722
# This shouldn't really happen, but let's play it safe.
# exception('Error getting string representation to show.')
yield from self._repr_obj(obj, level, self.maxother_inner, self.maxother_outer)
def _repr_other(self, obj, level):
return self._repr_obj(obj, level, self.maxother_inner, self.maxother_outer)
def _repr_obj(self, obj, level, limit_inner, limit_outer):
try:
if self.raw_value:
# For raw value retrieval, ignore all limits.
if isinstance(obj, bytes):
yield obj.decode("latin-1")
return
try:
mv = memoryview(obj)
except Exception:
yield self._convert_to_unicode_or_bytes_repr(repr(obj))
return
else:
# Map bytes to Unicode codepoints with same values.
yield mv.tobytes().decode("latin-1")
return
elif self.convert_to_hex and isinstance(obj, self.int_types):
obj_repr = hex(obj)
else:
obj_repr = repr(obj)
except Exception:
try:
obj_repr = object.__repr__(obj)
except Exception:
try:
obj_repr = "<no repr available for " + type(obj).__name__ + ">"
except Exception:
obj_repr = "<no repr available for object>"
limit = limit_inner if level > 0 else limit_outer
if limit >= len(obj_repr):
yield self._convert_to_unicode_or_bytes_repr(obj_repr)
return
# Slightly imprecise calculations - we may end up with a string that is
# up to 3 characters longer than limit. If you need precise formatting,
# you are using the wrong class.
left_count, right_count = max(1, int(2 * limit / 3)), max(1, int(limit / 3))
yield obj_repr[:left_count]
yield "..."
yield obj_repr[-right_count:]
def _convert_to_unicode_or_bytes_repr(self, obj_repr):
return obj_repr
def _bytes_as_unicode_if_possible(self, obj_repr):
# We try to decode with 3 possible encoding (sys.stdout.encoding,
# locale.getpreferredencoding() and 'utf-8). If no encoding can decode
# the input, we return the original bytes.
try_encodings = []
encoding = self.sys_stdout_encoding or getattr(sys.stdout, "encoding", None)
if encoding:
try_encodings.append(encoding.lower())
preferred_encoding = self.locale_preferred_encoding or locale.getpreferredencoding()
if preferred_encoding:
preferred_encoding = preferred_encoding.lower()
if preferred_encoding not in try_encodings:
try_encodings.append(preferred_encoding)
if "utf-8" not in try_encodings:
try_encodings.append("utf-8")
for encoding in try_encodings:
try:
return obj_repr.decode(encoding)
except UnicodeDecodeError: # noqa: PERF203
pass
return obj_repr # Return the original version (in bytes)
class DisplayOptions:
def __init__(self, width, max_columns):
self.width = width
self.max_columns = max_columns
_safe_repr = SafeRepr()
_collection_types = ["list", "tuple", "set"]
_array_page_size = 50
def _get_value(variable):
return _safe_repr(variable)
def _get_property_names(variable):
props = []
private_props = []
for prop in dir(variable):
if not prop.startswith("_"):
props.append(prop)
elif not prop.startswith("__"):
private_props.append(prop)
return props + private_props
def _get_full_type(var_type):
module = ""
if hasattr(var_type, "__module__") and var_type.__module__ != "builtins":
module = var_type.__module__ + "."
if hasattr(var_type, "__qualname__"):
return module + var_type.__qualname__
elif hasattr(var_type, "__name__"):
return module + var_type.__name__
return None
def _get_variable_description(variable):
result = {}
var_type = type(variable)
result["type"] = _get_full_type(var_type)
if hasattr(var_type, "__mro__"):
result["interfaces"] = [_get_full_type(t) for t in var_type.__mro__]
if hasattr(variable, "__len__") and result["type"] in _collection_types:
result["count"] = len(variable)
result["hasNamedChildren"] = hasattr(variable, "__dict__") or isinstance(variable, dict)
result["value"] = _get_value(variable)
return result
def _get_child_property(root, property_chain):
try:
variable = root
for prop in property_chain:
if isinstance(prop, int):
if hasattr(variable, "__getitem__"):
variable = variable[prop]
elif isinstance(variable, set):
variable = list(variable)[prop]
else:
return None
elif hasattr(variable, prop):
variable = getattr(variable, prop)
elif isinstance(variable, dict) and prop in variable:
variable = variable[prop]
else:
return None
except Exception:
return None
return variable
types_to_exclude = ["module", "function", "method", "class", "type"]
### Get info on variables at the root level
def getVariableDescriptions(): # noqa: N802
return [
{
"name": varName,
**_get_variable_description(globals()[varName]),
"root": varName,
"propertyChain": [],
"language": "python",
}
for varName in globals()
if type(globals()[varName]).__name__ not in types_to_exclude
and not varName.startswith("__")
]
### Get info on children of a variable reached through the given property chain
def getAllChildrenDescriptions(root_var_name, property_chain, start_index): # noqa: N802
root = globals()[root_var_name]
if root is None:
return []
parent = root
if len(property_chain) > 0:
parent = _get_child_property(root, property_chain)
children = []
parent_info = _get_variable_description(parent)
if "count" in parent_info:
if parent_info["count"] > 0:
last_item = min(parent_info["count"], start_index + _array_page_size)
index_range = range(start_index, last_item)
children = [
{
**_get_variable_description(_get_child_property(parent, [i])),
"name": str(i),
"root": root_var_name,
"propertyChain": [*property_chain, i],
"language": "python",
}
for i in index_range
]
elif parent_info["hasNamedChildren"]:
children_names = []
if hasattr(parent, "__dict__"):
children_names = _get_property_names(parent)
elif isinstance(parent, dict):
children_names = list(parent.keys())
children = []
for prop in children_names:
child_property = _get_child_property(parent, [prop])
if child_property is not None and type(child_property).__name__ not in types_to_exclude:
child = {
**_get_variable_description(child_property),
"name": prop,
"root": root_var_name,
"propertyChain": [*property_chain, prop],
}
children.append(child)
return children