Merging PR_218 openai_rev package with new streamlit chat app
This commit is contained in:
@@ -0,0 +1,576 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Allows us to create and absorb changes (aka Deltas) to elements."""
|
||||
|
||||
import ast
|
||||
import contextlib
|
||||
import inspect
|
||||
import re
|
||||
import sys
|
||||
import types
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from typing_extensions import Final
|
||||
|
||||
import streamlit
|
||||
from streamlit.logger import get_logger
|
||||
from streamlit.proto.DocString_pb2 import DocString as DocStringProto
|
||||
from streamlit.proto.DocString_pb2 import Member as MemberProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.runtime.scriptrunner.script_runner import (
|
||||
__file__ as SCRIPTRUNNER_FILENAME,
|
||||
)
|
||||
from streamlit.runtime.secrets import Secrets
|
||||
from streamlit.string_util import is_mem_address_str
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
LOGGER: Final = get_logger(__name__)
|
||||
|
||||
|
||||
CONFUSING_STREAMLIT_SIG_PREFIXES: Final = ("(element, ",)
|
||||
|
||||
|
||||
class HelpMixin:
|
||||
@gather_metrics("help")
|
||||
def help(self, obj: Any = streamlit) -> "DeltaGenerator":
|
||||
"""Display help and other information for a given object.
|
||||
|
||||
Depending on the type of object that is passed in, this displays the
|
||||
object's name, type, value, signature, docstring, and member variables,
|
||||
methods — as well as the values/docstring of members and methods.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj : any
|
||||
The object whose information should be displayed. If left
|
||||
unspecified, this call will display help for Streamlit itself.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
Don't remember how to initialize a dataframe? Try this:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pandas
|
||||
>>>
|
||||
>>> st.help(pandas.DataFrame)
|
||||
|
||||
.. output::
|
||||
https://doc-string.streamlit.app/
|
||||
height: 700px
|
||||
|
||||
Want to quickly check what data type is output by a certain function?
|
||||
Try:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> x = my_poorly_documented_function()
|
||||
>>> st.help(x)
|
||||
|
||||
Want to quickly inspect an object? No sweat:
|
||||
|
||||
>>> class Dog:
|
||||
>>> '''A typical dog.'''
|
||||
>>>
|
||||
>>> def __init__(self, breed, color):
|
||||
>>> self.breed = breed
|
||||
>>> self.color = color
|
||||
>>>
|
||||
>>> def bark(self):
|
||||
>>> return 'Woof!'
|
||||
>>>
|
||||
>>>
|
||||
>>> fido = Dog('poodle', 'white')
|
||||
>>>
|
||||
>>> st.help(fido)
|
||||
|
||||
.. output::
|
||||
https://doc-string1.streamlit.app/
|
||||
height: 300px
|
||||
|
||||
And if you're using Magic, you can get help for functions, classes,
|
||||
and modules without even typing ``st.help``:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pandas
|
||||
>>>
|
||||
>>> # Get help for Pandas read_csv:
|
||||
>>> pandas.read_csv
|
||||
>>>
|
||||
>>> # Get help for Streamlit itself:
|
||||
>>> st
|
||||
|
||||
.. output::
|
||||
https://doc-string2.streamlit.app/
|
||||
height: 700px
|
||||
"""
|
||||
doc_string_proto = DocStringProto()
|
||||
_marshall(doc_string_proto, obj)
|
||||
return self.dg._enqueue("doc_string", doc_string_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> "DeltaGenerator":
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
|
||||
|
||||
def _marshall(doc_string_proto: DocStringProto, obj: Any) -> None:
|
||||
"""Construct a DocString object.
|
||||
|
||||
See DeltaGenerator.help for docs.
|
||||
"""
|
||||
var_name = _get_variable_name()
|
||||
if var_name is not None:
|
||||
doc_string_proto.name = var_name
|
||||
|
||||
obj_type = _get_type_as_str(obj)
|
||||
doc_string_proto.type = obj_type
|
||||
|
||||
obj_docs = _get_docstring(obj)
|
||||
if obj_docs is not None:
|
||||
doc_string_proto.doc_string = obj_docs
|
||||
|
||||
obj_value = _get_value(obj, var_name)
|
||||
if obj_value is not None:
|
||||
doc_string_proto.value = obj_value
|
||||
|
||||
doc_string_proto.members.extend(_get_members(obj))
|
||||
|
||||
|
||||
def _get_name(obj):
|
||||
# Try to get the fully-qualified name of the object.
|
||||
# For example:
|
||||
# st.help(bar.Baz(123))
|
||||
#
|
||||
# The name is bar.Baz
|
||||
name = getattr(obj, "__qualname__", None)
|
||||
if name:
|
||||
return name
|
||||
|
||||
# Try to get the name of the object.
|
||||
# For example:
|
||||
# st.help(bar.Baz(123))
|
||||
#
|
||||
# The name is Baz
|
||||
return getattr(obj, "__name__", None)
|
||||
|
||||
|
||||
def _get_module(obj):
|
||||
return getattr(obj, "__module__", None)
|
||||
|
||||
|
||||
def _get_signature(obj):
|
||||
if not inspect.isclass(obj) and not callable(obj):
|
||||
return None
|
||||
|
||||
sig = ""
|
||||
|
||||
# TODO: Can we replace below with this?
|
||||
# with contextlib.suppress(ValueError):
|
||||
# sig = str(inspect.signature(obj))
|
||||
|
||||
try:
|
||||
sig = str(inspect.signature(obj))
|
||||
except ValueError:
|
||||
sig = "(...)"
|
||||
except TypeError:
|
||||
return None
|
||||
|
||||
is_delta_gen = False
|
||||
with contextlib.suppress(AttributeError):
|
||||
is_delta_gen = obj.__module__ == "streamlit.delta_generator"
|
||||
# Functions such as numpy.minimum don't have a __module__ attribute,
|
||||
# since we're only using it to check if its a DeltaGenerator, its ok
|
||||
# to continue
|
||||
|
||||
if is_delta_gen:
|
||||
for prefix in CONFUSING_STREAMLIT_SIG_PREFIXES:
|
||||
if sig.startswith(prefix):
|
||||
sig = sig.replace(prefix, "(")
|
||||
break
|
||||
|
||||
return sig
|
||||
|
||||
|
||||
def _get_docstring(obj):
|
||||
doc_string = inspect.getdoc(obj)
|
||||
|
||||
# Sometimes an object has no docstring, but the object's type does.
|
||||
# If that's the case here, use the type's docstring.
|
||||
# For objects where type is "type" we do not print the docs (e.g. int).
|
||||
# We also do not print the docs for functions and methods if the docstring is empty.
|
||||
if doc_string is None:
|
||||
obj_type = type(obj)
|
||||
|
||||
if (
|
||||
obj_type is not type
|
||||
and obj_type is not types.ModuleType
|
||||
and not inspect.isfunction(obj)
|
||||
and not inspect.ismethod(obj)
|
||||
):
|
||||
doc_string = inspect.getdoc(obj_type)
|
||||
|
||||
if doc_string:
|
||||
return doc_string.strip()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _get_variable_name():
|
||||
"""Try to get the name of the variable in the current line, as set by the user.
|
||||
|
||||
For example:
|
||||
foo = bar.Baz(123)
|
||||
st.help(foo)
|
||||
|
||||
The name is "foo"
|
||||
"""
|
||||
code = _get_current_line_of_code_as_str()
|
||||
|
||||
if code is None:
|
||||
return None
|
||||
|
||||
return _get_variable_name_from_code_str(code)
|
||||
|
||||
|
||||
def _get_variable_name_from_code_str(code):
|
||||
tree = ast.parse(code)
|
||||
|
||||
# Example:
|
||||
#
|
||||
# tree = Module(
|
||||
# body=[
|
||||
# Expr(
|
||||
# value=Call(
|
||||
# args=[
|
||||
# Name(id='the variable name')
|
||||
# ],
|
||||
# keywords=[
|
||||
# ???
|
||||
# ],
|
||||
# )
|
||||
# )
|
||||
# ]
|
||||
# )
|
||||
|
||||
# Check if this is an magic call (i.e. it's not st.help or st.write).
|
||||
# If that's the case, just clean it up and return it.
|
||||
if not _is_stcommand(tree, command_name="help") and not _is_stcommand(
|
||||
tree, command_name="write"
|
||||
):
|
||||
# A common pattern is to add "," at the end of a magic command to make it print.
|
||||
# This removes that final ",", so it looks nicer.
|
||||
if code.endswith(","):
|
||||
code = code[:-1]
|
||||
|
||||
return code
|
||||
|
||||
arg_node = _get_stcommand_arg(tree)
|
||||
|
||||
# If st.help() is called without an argument, return no variable name.
|
||||
if not arg_node:
|
||||
return None
|
||||
|
||||
# If walrus, get name.
|
||||
# E.g. st.help(foo := 123) should give you "foo".
|
||||
# (The hasattr is there to support Python 3.7)
|
||||
elif hasattr(ast, "NamedExpr") and type(arg_node) is ast.NamedExpr:
|
||||
# This next "if" will always be true, but need to add this for the type-checking test to
|
||||
# pass.
|
||||
if type(arg_node.target) is ast.Name:
|
||||
return arg_node.target.id
|
||||
|
||||
# If constant, there's no variable name.
|
||||
# E.g. st.help("foo") or st.help(123) should give you None.
|
||||
elif type(arg_node) in (
|
||||
ast.Constant,
|
||||
# Python 3.7 support:
|
||||
ast.Num,
|
||||
ast.Str,
|
||||
ast.Bytes,
|
||||
ast.NameConstant,
|
||||
ast.Ellipsis,
|
||||
):
|
||||
return None
|
||||
|
||||
# Otherwise, return whatever is inside st.help(<-- here -->)
|
||||
|
||||
# But, if multiline, only return the first line.
|
||||
code_lines = code.split("\n")
|
||||
is_multiline = len(code_lines) > 1
|
||||
|
||||
start_offset = arg_node.col_offset
|
||||
|
||||
if is_multiline:
|
||||
first_lineno = arg_node.lineno - 1 # Lines are 1-indexed!
|
||||
first_line = code_lines[first_lineno]
|
||||
end_offset = None
|
||||
|
||||
else:
|
||||
first_line = code_lines[0]
|
||||
end_offset = getattr(arg_node, "end_col_offset", -1)
|
||||
|
||||
# Python 3.7 and below have a bug where offset in some cases is off by one.
|
||||
# See https://github.com/python/cpython/commit/b619b097923155a7034c05c4018bf06af9f994d0
|
||||
# By the way, Python 3.7 also displays this bug when arg_node is a generator
|
||||
# expression, but in that case there are further complications, so we're leaving it out
|
||||
# of here. See the unit test for this for more details.
|
||||
if sys.version_info < (3, 8) and type(arg_node) is ast.ListComp:
|
||||
start_offset -= 1
|
||||
|
||||
return first_line[start_offset:end_offset]
|
||||
|
||||
|
||||
_NEWLINES = re.compile(r"[\n\r]+")
|
||||
|
||||
|
||||
def _get_current_line_of_code_as_str():
|
||||
scriptrunner_frame = _get_scriptrunner_frame()
|
||||
|
||||
if scriptrunner_frame is None:
|
||||
# If there's no ScriptRunner frame, something weird is going on. This
|
||||
# can happen when the script is executed with `python myscript.py`.
|
||||
# Either way, let's bail out nicely just in case there's some valid
|
||||
# edge case where this is OK.
|
||||
return None
|
||||
|
||||
code_context = scriptrunner_frame.code_context
|
||||
|
||||
if not code_context:
|
||||
# Sometimes a frame has no code_context. This can happen inside certain exec() calls, for
|
||||
# example. If this happens, we can't determine the variable name. Just return.
|
||||
# For the background on why exec() doesn't produce code_context, see
|
||||
# https://stackoverflow.com/a/12072941
|
||||
return None
|
||||
|
||||
code_as_string = "".join(code_context)
|
||||
return re.sub(_NEWLINES, "", code_as_string.strip())
|
||||
|
||||
|
||||
def _get_scriptrunner_frame():
|
||||
prev_frame = None
|
||||
scriptrunner_frame = None
|
||||
|
||||
# Look back in call stack to get the variable name passed into st.help().
|
||||
# The frame *before* the ScriptRunner frame is the correct one.
|
||||
# IMPORTANT: This will change if we refactor the code. But hopefully our tests will catch the
|
||||
# issue and we'll fix it before it lands upstream!
|
||||
for frame in inspect.stack():
|
||||
# Check if this is running inside a funny "exec()" block that won't provide the info we
|
||||
# need. If so, just quit.
|
||||
if frame.code_context is None:
|
||||
return None
|
||||
|
||||
if frame.filename == SCRIPTRUNNER_FILENAME:
|
||||
scriptrunner_frame = prev_frame
|
||||
break
|
||||
|
||||
prev_frame = frame
|
||||
|
||||
return scriptrunner_frame
|
||||
|
||||
|
||||
def _is_stcommand(tree, command_name):
|
||||
"""Checks whether the AST in tree is a call for command_name."""
|
||||
root_node = tree.body[0].value
|
||||
|
||||
if not type(root_node) is ast.Call:
|
||||
return False
|
||||
|
||||
return (
|
||||
# st call called without module. E.g. "help()"
|
||||
getattr(root_node.func, "id", None) == command_name
|
||||
or
|
||||
# st call called with module. E.g. "foo.help()" (where usually "foo" is "st")
|
||||
getattr(root_node.func, "attr", None) == command_name
|
||||
)
|
||||
|
||||
|
||||
def _get_stcommand_arg(tree):
|
||||
"""Gets the argument node for the st command in tree (AST)."""
|
||||
|
||||
root_node = tree.body[0].value
|
||||
|
||||
if root_node.args:
|
||||
return root_node.args[0]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _get_type_as_str(obj):
|
||||
if inspect.isclass(obj):
|
||||
return "class"
|
||||
|
||||
return str(type(obj).__name__)
|
||||
|
||||
|
||||
def _get_first_line(text):
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
left, _, _ = text.partition("\n")
|
||||
return left
|
||||
|
||||
|
||||
def _get_weight(value):
|
||||
if inspect.ismodule(value):
|
||||
return 3
|
||||
if inspect.isclass(value):
|
||||
return 2
|
||||
if callable(value):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def _get_value(obj, var_name):
|
||||
obj_value = _get_human_readable_value(obj)
|
||||
|
||||
if obj_value is not None:
|
||||
return obj_value
|
||||
|
||||
# If there's no human-readable value, it's some complex object.
|
||||
# So let's provide other info about it.
|
||||
name = _get_name(obj)
|
||||
|
||||
if name:
|
||||
name_obj = obj
|
||||
else:
|
||||
# If the object itself doesn't have a name, then it's probably an instance
|
||||
# of some class Foo. So let's show info about Foo in the value slot.
|
||||
name_obj = type(obj)
|
||||
name = _get_name(name_obj)
|
||||
|
||||
module = _get_module(name_obj)
|
||||
sig = _get_signature(name_obj) or ""
|
||||
|
||||
if name:
|
||||
if module:
|
||||
obj_value = f"{module}.{name}{sig}"
|
||||
else:
|
||||
obj_value = f"{name}{sig}"
|
||||
|
||||
if obj_value == var_name:
|
||||
# No need to repeat the same info.
|
||||
# For example: st.help(re) shouldn't show "re module re", just "re module".
|
||||
obj_value = None
|
||||
|
||||
return obj_value
|
||||
|
||||
|
||||
def _get_human_readable_value(value):
|
||||
if isinstance(value, Secrets):
|
||||
# Don't want to read secrets.toml because that will show a warning if there's no
|
||||
# secrets.toml file.
|
||||
return None
|
||||
|
||||
if inspect.isclass(value) or inspect.ismodule(value) or callable(value):
|
||||
return None
|
||||
|
||||
value_str = repr(value)
|
||||
|
||||
if isinstance(value, str):
|
||||
# Special-case strings as human-readable because they're allowed to look like
|
||||
# "<foo blarg at 0x15ee6f9a0>".
|
||||
return _shorten(value_str)
|
||||
|
||||
if is_mem_address_str(value_str):
|
||||
# If value_str looks like "<foo blarg at 0x15ee6f9a0>" it's not human readable.
|
||||
return None
|
||||
|
||||
return _shorten(value_str)
|
||||
|
||||
|
||||
def _shorten(s, length=300):
|
||||
s = s.strip()
|
||||
return s[:length] + "..." if len(s) > length else s
|
||||
|
||||
|
||||
def _is_computed_property(obj, attr_name):
|
||||
obj_class = getattr(obj, "__class__", None)
|
||||
|
||||
if not obj_class:
|
||||
return False
|
||||
|
||||
# Go through superclasses in order of inheritance (mro) to see if any of them have an
|
||||
# attribute called attr_name. If so, check if it's a @property.
|
||||
for parent_class in inspect.getmro(obj_class):
|
||||
class_attr = getattr(parent_class, attr_name, None)
|
||||
|
||||
if class_attr is None:
|
||||
continue
|
||||
|
||||
# If is property, return it.
|
||||
if isinstance(class_attr, property) or inspect.isgetsetdescriptor(class_attr):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _get_members(obj):
|
||||
members_for_sorting = []
|
||||
|
||||
for attr_name in dir(obj):
|
||||
if attr_name.startswith("_"):
|
||||
continue
|
||||
|
||||
is_computed_value = _is_computed_property(obj, attr_name)
|
||||
|
||||
if is_computed_value:
|
||||
parent_attr = getattr(obj.__class__, attr_name)
|
||||
|
||||
member_type = "property"
|
||||
|
||||
weight = 0
|
||||
member_docs = _get_docstring(parent_attr)
|
||||
member_value = None
|
||||
else:
|
||||
attr_value = getattr(obj, attr_name)
|
||||
weight = _get_weight(attr_value)
|
||||
|
||||
human_readable_value = _get_human_readable_value(attr_value)
|
||||
|
||||
member_type = _get_type_as_str(attr_value)
|
||||
|
||||
if human_readable_value is None:
|
||||
member_docs = _get_docstring(attr_value)
|
||||
member_value = None
|
||||
else:
|
||||
member_docs = None
|
||||
member_value = human_readable_value
|
||||
|
||||
if member_type == "module":
|
||||
# Don't pollute the output with all imported modules.
|
||||
continue
|
||||
|
||||
member = MemberProto()
|
||||
member.name = attr_name
|
||||
member.type = member_type
|
||||
|
||||
if member_docs is not None:
|
||||
member.doc_string = _get_first_line(member_docs)
|
||||
|
||||
if member_value is not None:
|
||||
member.value = member_value
|
||||
|
||||
members_for_sorting.append((weight, member))
|
||||
|
||||
if members_for_sorting:
|
||||
sorted_members = sorted(members_for_sorting, key=lambda x: (x[0], x[1].name))
|
||||
return [m for _, m in sorted_members]
|
||||
|
||||
return []
|
||||
Reference in New Issue
Block a user