Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for secret string types #416

Merged
merged 1 commit into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@ The semantic versioning only considers the public API as described in
paths are considered internals and can change in minor and patch releases.


v4.26.3 (2023-11-??)
v4.27.0 (2023-11-??)
--------------------

Added
^^^^^
- Support for pydantic's ``SecretStr`` type.
- New ``SecretStr`` type in ``jsonargparse.typing`` to provide secret support
without additional dependency.

Fixed
^^^^^
- Links applied on parse failing when source is a class with a nested callable.
Expand Down
4 changes: 4 additions & 0 deletions DOCUMENTATION.rst
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,10 @@ Some notes about this support are:
type <https://docs.pydantic.dev/latest/api/types/>`__, this is used for
validation.

- ``pydantic.SecretStr`` type is supported with the expected behavior of not
serializing the actual value. There is also ``jsonargparse.typing.SecretStr``
to support the same behavior without the need of a dependency.

- ``Callable`` is supported by either giving a dot import path to a callable
object or by giving a dict with a ``class_path`` and optionally ``init_args``
entries. The specified class must either instantiate into a callable or be a
Expand Down
28 changes: 28 additions & 0 deletions jsonargparse/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"Path_dw",
"Path_dc",
"Path_drw",
"SecretStr",
]


Expand Down Expand Up @@ -457,6 +458,33 @@ def range_deserializer(value):
register_type(range, serializer=range_serializer, deserializer=range_deserializer)


class SecretStr:
"""Holds a secret string that serializes to **********."""

def __init__(self, value: str):
self._value = value

def __str__(self) -> str:
return "**********"

def __len__(self) -> int:
return len(self._value)

def __eq__(self, other: Any) -> bool:
return isinstance(other, self.__class__) and self._value == other._value

def __hash__(self) -> int:
return hash(self._value)

def get_secret_value(self) -> str:
"""Returns the actual secret value."""
return self._value


register_type(SecretStr)
register_type_on_first_use("pydantic.SecretStr")


def pydantic_deserializer(type_class):
from pydantic import create_model # pylint: disable=no-name-in-module

Expand Down
29 changes: 29 additions & 0 deletions jsonargparse_tests/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import pytest

from jsonargparse import ArgumentError
from jsonargparse._optionals import pydantic_support
from jsonargparse.typing import (
ClosedUnitInterval,
Email,
Expand All @@ -20,6 +21,7 @@
OpenUnitInterval,
PositiveFloat,
PositiveInt,
SecretStr,
extend_base_type,
get_registered_type,
register_type,
Expand Down Expand Up @@ -404,3 +406,30 @@ def test_uuid(parser):
assert cfg.uuid == id1
assert cfg.uuids == [id1, id2]
assert f"uuid: {id1}\nuuids:\n- {id1}\n- {id2}\n" == parser.dump(cfg)


def test_secret_str_methods():
value = SecretStr("secret")
assert len(value) == 6
assert value == SecretStr("secret")
assert value != SecretStr("other secret")
assert hash("secret") == hash(value)


def test_secret_str_parsing(parser):
parser.add_argument("--password", type=SecretStr)
cfg = parser.parse_args(["--password=secret"])
assert isinstance(cfg.password, SecretStr)
assert cfg.password.get_secret_value() == "secret"
assert "secret" not in parser.dump(cfg)


@pytest.mark.skipif(not pydantic_support, reason="pydantic package is required")
def test_pydantic_secret_str(parser):
from pydantic import SecretStr

parser.add_argument("--password", type=SecretStr)
cfg = parser.parse_args(["--password=secret"])
assert isinstance(cfg.password, SecretStr)
assert cfg.password.get_secret_value() == "secret"
assert "secret" not in parser.dump(cfg)