Skip to content

Commit

Permalink
- Add support for pydantic's SecretStr type.
Browse files Browse the repository at this point in the history
- New SecretStr type in jsonargparse.typing to provide secret support without additional dependency.
  • Loading branch information
mauvilsa committed Nov 2, 2023
1 parent f434635 commit 018d083
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 1 deletion.
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)

0 comments on commit 018d083

Please sign in to comment.