Repository: encode/typesystem

Issue:
Password field
What do you think about this kind of field? 
I was thinking about expanding String type as a first option: 

`typesystem.String(format='password')`

or adding new kind of field `typesystem.Password`

Both options will ensure to create "password" type of input with hidden value. 
The latter would have some additional options of allowed characters (password policy), ie.:

```python
password = typesystem.Password(
    min_length: int,
    require_digit: boolean,
    require_lowercase: boolean,
    require_uppercase: boolean,
    require_non_alphanumeric: boolean
)
```

Discussion:

Patch:
diff --git a/README.md b/README.md
index 147ffa6..9c18e6b 100644
--- a/README.md
+++ b/README.md
@@ -39,16 +39,16 @@ Python 3.6+
 $ pip3 install typesystem
 ```
 
-If you'd like you use the form rendering you'll also want to install `jinja2`.
+If you'd like you use the form rendering using `jinja2`:
 
 ```shell
-$ pip3 install jinja2
+$ pip3 install typesystem[jinja2]
 ```
 
-If you'd like you use the YAML tokenization you'll also want to install `pyyaml`.
+If you'd like you use the YAML tokenization using `pyyaml`:
 
 ```shell
-$ pip3 install pyyaml
+$ pip3 install typesystem[pyyaml]
 ```
 
 ## Quickstart
diff --git a/docs/fields.md b/docs/fields.md
index 8ba0167..4cfcd08 100644
--- a/docs/fields.md
+++ b/docs/fields.md
@@ -66,6 +66,11 @@ For example: `username = typesystem.String(max_length=100)`
 Validates multi-line strings. Takes the same arguments as `String`.
 Represented in HTML forms as a `<textarea>`.
 
+### Password
+
+Similar to `String` and takes the same arguments.
+Represented in HTML forms as a `<input type="password">`.
+
 ## Boolean data types
 
 ### Boolean
@@ -202,3 +207,12 @@ owner = typesystem.Reference(to=User, allow_null=True)
 **Arguments**:
 
 * `to` - A schema class or field instance. **Required**
+
+## Other data types
+
+### UUID
+
+Validates UUID in the format of 32 hexadecimal characters, separated by hyphens.
+For example `"cd11b0d7-d8b3-4b5c-8159-70f5c9ea96ab"`.
+
+Returns `uuid.UUID` instances.
diff --git a/docs/index.md b/docs/index.md
index 1ec3e16..5fc2741 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,8 +1,8 @@
 # TypeSystem
 
 <p>
-<a href="https://travis-ci.org/encode/typesystem">
-    <img src="https://travis-ci.org/encode/typesystem.svg?branch=master" alt="Build Status">
+<a href="https://github.com/encode/typesystem/actions">
+    <img src="https://github.com/encode/typesystem/workflows/Test%20Suite/badge.svg" alt="Build Status">
 </a>
 <a href="https://codecov.io/gh/encode/typesystem">
     <img src="https://codecov.io/gh/encode/typesystem/branch/master/graph/badge.svg" alt="Coverage">
@@ -33,10 +33,16 @@ Python 3.6+
 $ pip3 install typesystem
 ```
 
-If you'd like you use the form rendering you'll also want to install `jinja2`.
+If you'd like you use the form rendering using `jinja2`:
 
 ```shell
-$ pip3 install jinja2
+$ pip3 install typesystem[jinja2]
+```
+
+If you'd like you use the YAML tokenization using `pyyaml`:
+
+```shell
+$ pip3 install typesystem[pyyaml]
 ```
 
 ## Quickstart
diff --git a/setup.py b/setup.py
index 21c4c6c..ebc4b01 100755
--- a/setup.py
+++ b/setup.py
@@ -41,6 +41,10 @@ setup(
     packages=get_packages('typesystem'),
     package_data={'typesystem': ['py.typed', 'templates/forms/*.html']},
     install_requires=[],
+    extras_require={
+        "jinja2": ["jinja2"],
+        "pyyaml": ["pyyaml"],
+    },
     python_requires='>=3.6',
     classifiers=[
         'Development Status :: 3 - Alpha',
diff --git a/typesystem/__init__.py b/typesystem/__init__.py
index 4754d95..85d8a24 100644
--- a/typesystem/__init__.py
+++ b/typesystem/__init__.py
@@ -13,6 +13,7 @@ from typesystem.fields import (
     Integer,
     Number,
     Object,
+    Password,
     String,
     Text,
     Time,
@@ -40,6 +41,7 @@ __all__ = [
     "Float",
     "Number",
     "Object",
+    "Password",
     "Reference",
     "String",
     "Text",
diff --git a/typesystem/fields.py b/typesystem/fields.py
index b612455..1303634 100644
--- a/typesystem/fields.py
+++ b/typesystem/fields.py
@@ -765,3 +765,8 @@ class Const(Field):
 class UUID(String):
     def __init__(self, **kwargs: typing.Any) -> None:
         super().__init__(format="uuid", **kwargs)
+
+
+class Password(String):
+    def __init__(self, **kwargs: typing.Any) -> None:
+        super().__init__(format="password", **kwargs)


Test patch:
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 87083bf..df877df 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -17,6 +17,7 @@ from typesystem.fields import (
     Integer,
     Number,
     Object,
+    Password,
     String,
     Time,
     Union,
@@ -832,3 +833,9 @@ def test_validation_error_is_hashable():
     validator = Integer()
     _, error = validator.validate_or_error("abc")
     hash(error)
+
+
+def test_password():
+    validator = Password()
+    value, _ = validator.validate_or_error("secret")
+    assert value == "secret"
diff --git a/tests/test_forms.py b/tests/test_forms.py
index 62adff7..44ddfba 100644
--- a/tests/test_forms.py
+++ b/tests/test_forms.py
@@ -9,6 +9,7 @@ class Contact(typesystem.Schema):
     b = typesystem.String(max_length=10)
     c = typesystem.Text()
     d = typesystem.Choice(choices=[("abc", "Abc"), ("def", "Def"), ("ghi", "Ghi")])
+    password = typesystem.Password()
 
 
 forms = typesystem.Jinja2Forms(package="typesystem")
@@ -23,13 +24,11 @@ def test_form_rendering():
     assert html.count('<input type="text" ') == 1
     assert html.count("<textarea ") == 1
     assert html.count("<select ") == 1
+    assert html.count('<input type="password" ') == 1
 
 
 def test_password_rendering():
-    class PasswordForm(typesystem.Schema):
-        password = typesystem.String(format="password")
-
-    form = forms.Form(PasswordForm, values={"password": "secret"})
+    form = forms.Form(Contact, values={"password": "secret"})
     html = str(form)
     assert "secret" not in html