-
-
Notifications
You must be signed in to change notification settings - Fork 76
Can't read Pydantic Settings from stdin #296
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Comments
Thanks @WarpedPixel for reporting this. RIght now,
Yeah, it is a function and we can't move it to |
Perhaps there is a better way to do this, but it turned into 75 lines of mostly duplicated code that will certainly be fragile and difficult to maintain. The below is hacky because a class could be written to handle both filenames and streams (or named streams at least) without changing the signatures of anything, but it passes my tests. Code to override read_env_file
# this runs against the current version of pydantic_settings
import os
import sys
from pathlib import Path
from typing import Mapping
from dotenv import dotenv_values
from pydantic_settings import BaseSettings, DotEnvSettingsSource
from pydantic_settings.sources import ENV_FILE_SENTINEL, DotenvType, parse_env_vars
from pydantic_settings.sources import read_env_file as original_read_env_file
class StdinSettingsSource(DotEnvSettingsSource):
def __init__(
self,
settings_cls: type[BaseSettings],
env_file: DotenvType | None = ENV_FILE_SENTINEL,
env_file_encoding: str | None = None,
case_sensitive: bool | None = None,
env_prefix: str | None = None,
env_nested_delimiter: str | None = None,
env_ignore_empty: bool | None = None,
env_parse_none_str: str | None = None,
) -> None:
super().__init__(
settings_cls, env_file, env_file_encoding, case_sensitive, env_prefix, env_nested_delimiter, env_ignore_empty, env_parse_none_str
)
@staticmethod
def read_env_file(
file_path: Path,
*,
encoding: str | None = None,
case_sensitive: bool = False,
ignore_empty: bool = False,
parse_none_str: str | None = None,
) -> Mapping[str, str | None]:
file_vars: dict[str, str | None] = dotenv_values(stream=sys.stdin, encoding=encoding or 'utf8')
return parse_env_vars(file_vars, case_sensitive, ignore_empty, parse_none_str)
# copy from pydantic_settings.sources.DotEnvSettingsSource. NEEDED because we can't override read_env_file
def _read_env_files(self) -> Mapping[str, str | None]:
env_files = ['/dev/stdin']
# env_files = self.env_file
# if env_files is None:
# return {}
if isinstance(env_files, (str, os.PathLike)):
env_files = [env_files]
dotenv_vars: dict[str, str | None] = {}
for env_file in env_files:
env_path = Path(env_file).expanduser()
if env_path.is_file():
dotenv_vars.update(
original_read_env_file(
env_path,
encoding=self.env_file_encoding,
case_sensitive=self.case_sensitive,
ignore_empty=self.env_ignore_empty,
parse_none_str=self.env_parse_none_str,
)
)
elif env_path == Path('/dev/stdin'):
dotenv_vars.update(
self.read_env_file(
env_path,
encoding=self.env_file_encoding,
case_sensitive=self.case_sensitive,
ignore_empty=self.env_ignore_empty,
parse_none_str=self.env_parse_none_str,
)
)
return dotenv_vars It seems like moving read_env_file to a method shouldn't break anything. It is not referenced anywhere else in pydantic_settings. And if we really want to handle the case where people import that function you could continue to expose it with a deprecation warning. Something like import warnings
def read_env_file(
file_path: Path,
*,
encoding: str | None = None,
case_sensitive: bool = False,
ignore_empty: bool = False,
parse_none_str: str | None = None,
) -> Mapping[str, str | None]:
warnings.warn("read_env_file is deprecated and will be removed in the next version, use DotEnvSettingsSource.read_env_file instead", DeprecationWarning)
file_vars: dict[str, str | None] = dotenv_values(file_path, encoding=encoding or 'utf8')
return parse_env_vars(file_vars, case_sensitive, ignore_empty, parse_none_str) This would allow the subclassing of # this version would work if we made the change to bring read_env_file as a method of DotEnvSettingsSource
class SimpleStdinSettingsSource(DotEnvSettingsSource):
# override read_env_file to also read from stdin
def read_env_file(
self,
file_path: Path,
) -> Mapping[str, str | None]:
if file_path == Path('/dev/stdin'):
file_vars: dict[str, str | None] = dotenv_values(stream=sys.stdin, encoding=self.env_file_encoding or 'utf8')
else:
file_vars: dict[str, str | None] = dotenv_values(file_path, encoding=self.env_file_encoding or 'utf8')
return parse_env_vars(file_vars, self.case_sensitive, self.env_ignore_empty, self.env_parse_none_str) If I can be of help I would work on a clean PR for this. |
@WarpedPixel let's move it into the |
Fixes pydantic#296 Adds a method _read_env_file to DotEnvSettingsSource that can be easily overridden. This fix keeps a global function so it can continue to be tested, but adds an underscore to the name. The plain read_env_file is marked as deprecated.
Fixes pydantic#296 Added test and moved bulk of dotenv code into DotEnvSettingsSource so future deprecation will be cleaner
Fixes pydantic#296 Adds a method _read_env_file to DotEnvSettingsSource that can be easily overridden. This fix keeps a global function so it can continue to be tested, but adds an underscore to the name. The plain read_env_file is marked as deprecated.
Fixes pydantic#296 Added test and moved bulk of dotenv code into DotEnvSettingsSource so future deprecation will be cleaner
Fixes pydantic#296 Addressed CR feedback
Fixes pydantic#296 Adds a method _read_env_file to DotEnvSettingsSource that can be easily overridden. This fix keeps a global function so it can continue to be tested, but adds an underscore to the name. The plain read_env_file is marked as deprecated.
Fixes pydantic#296 Added test and moved bulk of dotenv code into DotEnvSettingsSource so future deprecation will be cleaner
Fixes pydantic#296 Addressed CR feedback
Requirement
I need to read Settings from stdin. This is not our primary scenario but something we need for tooling, for security reasons (secrets we don't want on disk). Our tooling reads settings to process them, and ideally they should be piped to our app to leave no traces behind, even temporarily.
What we tried
Settings pass filenames only into the public API. And using the stdin filename '/dev/stdin' doesn't work (on Mac) as the reads don't block until data is available (it simply returns an empty file).
Because we couldn't find a clean way to do this, I think it is an issue that should be addressed. Perhaps there is a different way to do it that we missed, please let me know.
Possible Fixes
The best fix would be to simply support streams everywhere in the API, instead of just filenames.
It would also be fine to support special filenames '-' or '/dev/stdin' as special cases that are internally replaced by
stream=sys.stdin
. Or to allow a SettingsSource to provide a stream instead of file names.In our case we hacked a third solution deriving a class from
DotEnvSettingsSource
because we need most of that functionality (just need to replace the source in some cases). But becauseread_env_file
is a global function instead of a method, we cannot simply override that and had to copy a few other methods from the implementation. Moving that into the DotEnvSettingsSource class would allow a clean override of justread_env_file
that could handle stdin reading. This is probably the minimal solution that cleanly resolves this requirement, without changing anything for the mainstream usage of Pydantic settings.The text was updated successfully, but these errors were encountered: