Repository: streamlit/streamlit

Issue:
st.text_input: support password input
`st.text_input` should support setting the input type to "password"


Discussion:
This could potentially also help with [this user's use-case](https://discuss.streamlit.io/t/user-authentication/612). 
Is anyone currently using Streamlit to access APIs or services with OAuth2? I am trying to build a hosted app where users can access a Rest API by providing their clientID and password. 

A feature similar to the Python Getpass library, living within the Streamlit UI would be awesome. 

Or is there already a solution for this? Seems to me a very common use case. 

Resolving patch:
diff --git a/lib/streamlit/DeltaGenerator.py b/lib/streamlit/DeltaGenerator.py
--- a/lib/streamlit/DeltaGenerator.py
+++ b/lib/streamlit/DeltaGenerator.py
@@ -18,6 +18,7 @@
 # Python 2/3 compatibility
 from __future__ import print_function, division, unicode_literals, absolute_import
 from streamlit.compatibility import setup_2_3_shims
+from streamlit.proto.TextInput_pb2 import TextInput
 
 setup_2_3_shims(globals())
 
@@ -2097,7 +2098,7 @@ def file_uploader(self, element, label, type=None, encoding="auto", key=None):
         return io.BytesIO(data)
 
     @_with_element
-    def text_input(self, element, label, value="", key=None):
+    def text_input(self, element, label, value="", key=None, type="default"):
         """Display a single-line text input widget.
 
         Parameters
@@ -2112,6 +2113,10 @@ def text_input(self, element, label, value="", key=None):
             If this is omitted, a key will be generated for the widget
             based on its content. Multiple widgets of the same type may
             not share the same key.
+        type : str
+            The type of the text input. This can be either "default" (for
+            a regular text input), or "password" (for a text input that
+            masks the user's typed value). Defaults to "default".
 
         Returns
         -------
@@ -2126,6 +2131,15 @@ def text_input(self, element, label, value="", key=None):
         """
         element.text_input.label = label
         element.text_input.default = str(value)
+        if type == "default":
+            element.text_input.type = TextInput.DEFAULT
+        elif type == "password":
+            element.text_input.type = TextInput.PASSWORD
+        else:
+            raise StreamlitAPIException(
+                "'%s' is not a valid text_input type. Valid types are 'default' and 'password'."
+                % type
+            )
 
         ui_value = _get_widget_ui_value("text_input", element, user_key=key)
         current_value = ui_value if ui_value is not None else value
@@ -2313,7 +2327,7 @@ def number_input(
             If the value is not specified, the format parameter will be used.
         format : str or None
             A printf-style format string controlling how the interface should
-            display numbers. Output must be purely numeric. This does not impact 
+            display numbers. Output must be purely numeric. This does not impact
             the return value. Valid formatters: %d %e %f %g %i
         key : str
             An optional string to use as the unique key for the widget.
@@ -2657,11 +2671,13 @@ def deck_gl_chart(self, element, spec=None, **kwargs):
         if not suppress_deprecation_warning:
             import streamlit as st
 
-            st.warning("""
+            st.warning(
+                """
                 The `deck_gl_chart` widget is deprecated and will be removed on
 
                 2020-03-04. To render a map, you should use `st.pyDeckChart` widget.
-            """)
+            """
+            )
 
         import streamlit.elements.deck_gl as deck_gl
 
@@ -2690,7 +2706,7 @@ def pydeck_chart(self, element, pydeck_obj=None):
         >>> df = pd.DataFrame(
         ...    np.random.randn(1000, 2) / [50, 50] + [37.76, -122.4],
         ...    columns=['lat', 'lon'])
-        
+
         >>> st.pydeck_chart(pdk.Deck(
         ...     map_style='mapbox://styles/mapbox/light-v9',
         ...     initial_view_state=pdk.ViewState(


Test patch:
diff --git a/frontend/src/components/widgets/TextInput/TextInput.test.tsx b/frontend/src/components/widgets/TextInput/TextInput.test.tsx
new file mode 100644
--- /dev/null
+++ b/frontend/src/components/widgets/TextInput/TextInput.test.tsx
@@ -0,0 +1,68 @@
+/**
+ * @license
+ * Copyright 2018-2020 Streamlit Inc.
+ *
+ * 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.
+ */
+
+import React from "react"
+import { shallow } from "enzyme"
+import { Input as UIInput } from "baseui/input"
+import { fromJS } from "immutable"
+import { WidgetStateManager } from "lib/WidgetStateManager"
+
+import TextInput, { Props } from "./TextInput"
+import { TextInput as TextInputProto } from "autogen/proto"
+
+jest.mock("lib/WidgetStateManager")
+
+const sendBackMsg = jest.fn()
+const getProps = (elementProps: object = {}): Props => ({
+  element: fromJS({
+    label: "Label",
+    default: "",
+    type: TextInputProto.Type.DEFAULT,
+    ...elementProps,
+  }),
+  width: 0,
+  disabled: false,
+  widgetMgr: new WidgetStateManager(sendBackMsg),
+})
+
+describe("TextInput widget", () => {
+  it("renders without crashing", () => {
+    const props = getProps()
+    const wrapper = shallow(<TextInput {...props} />)
+
+    expect(wrapper).toBeDefined()
+  })
+
+  it("should show a label", () => {
+    const props = getProps()
+    const wrapper = shallow(<TextInput {...props} />)
+
+    expect(wrapper.find("label").text()).toBe(props.element.get("label"))
+  })
+
+  it("should handle TextInputProto.Type properly", () => {
+    const defaultProps = getProps({ type: TextInputProto.Type.DEFAULT })
+    let textInput = shallow(<TextInput {...defaultProps} />)
+    let uiInput = textInput.find(UIInput)
+    expect(uiInput.props().type).toBeUndefined()
+
+    const passwordProps = getProps({ type: TextInputProto.Type.PASSWORD })
+    textInput = shallow(<TextInput {...passwordProps} />)
+    uiInput = textInput.find(UIInput)
+    expect(uiInput.props().type).toBe("password")
+  })
+})
diff --git a/lib/tests/streamlit/text_input_test.py b/lib/tests/streamlit/text_input_test.py
--- a/lib/tests/streamlit/text_input_test.py
+++ b/lib/tests/streamlit/text_input_test.py
@@ -17,6 +17,8 @@
 
 import re
 
+from streamlit import StreamlitAPIException
+from streamlit.proto.TextInput_pb2 import TextInput
 from tests import testutil
 import streamlit as st
 
@@ -31,6 +33,7 @@ def test_just_label(self):
         c = self.get_delta_from_queue().new_element.text_input
         self.assertEqual(c.label, "the label")
         self.assertEqual(c.default, "")
+        self.assertEqual(c.type, TextInput.DEFAULT)
 
     def test_value_types(self):
         """Test that it supports different types of values."""
@@ -44,6 +47,26 @@ def test_value_types(self):
             self.assertEqual(c.label, "the label")
             self.assertTrue(re.match(proto_value, c.default))
 
+    def test_input_types(self):
+        # Test valid input types.
+        type_strings = ["default", "password"]
+        type_values = [TextInput.DEFAULT, TextInput.PASSWORD]
+        for type_string, type_value in zip(type_strings, type_values):
+            st.text_input("label", type=type_string)
+
+            c = self.get_delta_from_queue().new_element.text_input
+            self.assertEqual(type_value, c.type)
+
+        # An invalid input type should raise an exception.
+        with self.assertRaises(StreamlitAPIException) as exc:
+            st.text_input("label", type="bad_type")
+
+        self.assertEqual(
+            "'bad_type' is not a valid text_input type. "
+            "Valid types are 'default' and 'password'.",
+            str(exc.exception),
+        )
+
 
 class SomeObj(object):
     pass