diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9270a4e..8ff902a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,12 +2,12 @@ ci: autoupdate_schedule: monthly repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.6 + rev: v0.9.2 hooks: - id: ruff - id: ruff-format - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.30.0 + rev: 0.31.0 hooks: - id: check-github-workflows - repo: https://github.com/asottile/blacken-docs diff --git a/CHANGELOG.md b/CHANGELOG.md index d75fc1c..c21ccd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ Bug fixes: - Add `env` to `__all__` ([#396](https://github.com/sloria/environs/issues/396)). Thanks [daveflr](https://github.com/daveflr) for reporting. +Changes: + +- _Backwards-incompatible_: `recurse`, `verbose`, `override`, + and `return_path` parameters to `Env.read_env` are now keyword-only. + ## 14.1.0 (2025-01-10) Features: diff --git a/examples/deferred_validation_example.py b/examples/deferred_validation_example.py index 7a786ac..eb8783f 100644 --- a/examples/deferred_validation_example.py +++ b/examples/deferred_validation_example.py @@ -13,7 +13,8 @@ NODE_ENV = env.str( "NODE_ENV", validate=validate.OneOf( - ["production", "development"], error="NODE_ENV must be one of: {choices}" + ["production", "development"], + error="NODE_ENV must be one of: {choices}", ), ) EMAIL = env.str("EMAIL", validate=[validate.Length(min=4), validate.Email()]) diff --git a/examples/django_example.py b/examples/django_example.py index 856e53d..8365523 100644 --- a/examples/django_example.py +++ b/examples/django_example.py @@ -27,7 +27,7 @@ "DATABASE_URL", default="sqlite:///" + os.path.join(BASE_DIR, "db.sqlite3"), ssl_require=not DEBUG, - ) + ), } TIME_ZONE = env.str("TIME_ZONE", default="America/Chicago") diff --git a/examples/plugin_example.py b/examples/plugin_example.py index ace14f4..9c067ba 100644 --- a/examples/plugin_example.py +++ b/examples/plugin_example.py @@ -1,6 +1,6 @@ import os -from furl import furl as Furl +from furl import furl as Furl # noqa: N812 from yarl import URL from environs import env diff --git a/examples/simple_example.py b/examples/simple_example.py index e039d16..2a6afbe 100644 --- a/examples/simple_example.py +++ b/examples/simple_example.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os from pprint import pprint diff --git a/examples/validation_example.py b/examples/validation_example.py index 6af1a39..f62feb5 100644 --- a/examples/validation_example.py +++ b/examples/validation_example.py @@ -11,7 +11,8 @@ env.str( "NODE_ENV", validate=validate.OneOf( - ["production", "development"], error="NODE_ENV must be one of: {choices}" + ["production", "development"], + error="NODE_ENV must be one of: {choices}", ), ) except EnvError as err: diff --git a/pyproject.toml b/pyproject.toml index fa63ac1..aafed43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ include = ["tests/", "CHANGELOG.md", "CONTRIBUTING.md", "tox.ini"] [tool.ruff] src = ["src"] +line-length = 90 fix = true show-fixes = true output-format = "full" @@ -55,19 +56,38 @@ output-format = "full" docstring-code-format = true [tool.ruff.lint] -ignore = ["E203", "E266", "E501", "E731"] -select = [ - "B", # flake8-bugbear - "E", # pycodestyle error - "F", # pyflakes - "I", # isort - "TC", # flake8-type-checking - "UP", # pyupgrade - "W", # pycodestyle warning +select = ["ALL"] +ignore = [ + "A005", # "module {name} shadows a Python standard-library module" + "ANN", # let mypy handle annotation checks + "ARG", # unused arguments are common w/ interfaces + "COM", # let formatter take care commas + "C901", # don't enforce complexity level + "D", # don't require docstrings + "E501", # leave line-length enforcement to formatter + "EM", # allow string messages in exceptions + "FIX", # allow "FIX" comments in code + "INP001", # allow Python files outside of packages + "PLR0913", # "Too many arguments" + "PLR2004", # "Magic value used in comparison" + "PTH", # don't require using pathlib instead of os + "SIM105", # "Use `contextlib.suppress(...)` instead of `try`-`except`-`pass`" + "TD", # allow TODO comments to be whatever we want + "TRY003", # allow long messages passed to exceptions ] [tool.ruff.lint.per-file-ignores] -"tests/*" = ["E721"] +"tests/*" = [ + "ARG", # unused arguments are fine in tests + "DTZ", # allow naive datetimes + "S", # allow asserts + "SIM117", # allow nested with statements because it's more readable sometimes +] +"examples/*" = [ + "S", # allow asserts + "T", # allow prints +] + [tool.ruff.lint.pycodestyle] ignore-overlong-task-comments = true diff --git a/src/environs/__init__.py b/src/environs/__init__.py index 32fd38f..72efa95 100644 --- a/src/environs/__init__.py +++ b/src/environs/__init__.py @@ -43,7 +43,7 @@ except ImportError: pass -__all__ = ["Env", "env", "EnvError", "ValidationError"] +__all__ = ["Env", "EnvError", "ValidationError", "env"] _T = typing.TypeVar("_T") _StrType = str @@ -66,7 +66,7 @@ def _field2method( method_name: str, *, preprocess: typing.Callable | None = None, - preprocess_kwarg_names: typing.Sequence[str] = tuple(), + preprocess_kwarg_names: typing.Sequence[str] = (), ) -> typing.Any: def method( self: Env, @@ -86,14 +86,15 @@ def method( ) -> _T | None: if self._sealed: raise EnvSealedError( - "Env has already been sealed. New values cannot be parsed." + "Env has already been sealed. New values cannot be parsed.", ) load_default = default if default is not Ellipsis else ma.missing preprocess_kwargs = { name: kwargs.pop(name) for name in preprocess_kwarg_names if name in kwargs } if isinstance(field_or_factory, type) and issubclass( - field_or_factory, ma.fields.Field + field_or_factory, + ma.fields.Field, ): field = field_or_factory( validate=validate, @@ -118,11 +119,10 @@ def method( return default if self.eager: raise EnvError( - f'Environment variable "{proxied_key or parsed_key}" not set' + f'Environment variable "{proxied_key or parsed_key}" not set', ) - else: - self._errors[parsed_key].append("Environment variable not set.") - return None + self._errors[parsed_key].append("Environment variable not set.") + return None try: if preprocess: value = preprocess(value, **preprocess_kwargs) @@ -151,7 +151,7 @@ def method( ) -> _T | None: if self._sealed: raise EnvSealedError( - "Env has already been sealed. New values cannot be parsed." + "Env has already been sealed. New values cannot be parsed.", ) parsed_key, raw_value, proxied_key = self._get_from_environ(name, default) self._fields[parsed_key] = ma.fields.Raw() @@ -159,12 +159,11 @@ def method( if raw_value is Ellipsis: if self.eager: raise EnvError( - f'Environment variable "{proxied_key or parsed_key}" not set' + f'Environment variable "{proxied_key or parsed_key}" not set', ) - else: - self._errors[parsed_key].append("Environment variable not set.") - return None - if raw_value or raw_value == "": + self._errors[parsed_key].append("Environment variable not set.") + return None + if raw_value or raw_value == "": # noqa: SIM108 value = raw_value else: value = None @@ -210,15 +209,15 @@ def _deserialize(self, value, *args, **kwargs): def _make_list_field(*, subcast: Subcast | None, **kwargs) -> ma.fields.List: - if subcast: - inner_field = _make_subcast_field(subcast) - else: - inner_field = ma.fields.Raw + inner_field = _make_subcast_field(subcast) if subcast else ma.fields.Raw return ma.fields.List(inner_field, **kwargs) def _preprocess_list( - value: str | typing.Iterable, *, delimiter: str = ",", **kwargs + value: str | typing.Iterable, + *, + delimiter: str = ",", + **kwargs, ) -> typing.Iterable: if ma.utils.is_iterable_but_not_string(value) or value is None: return value @@ -248,7 +247,7 @@ def _preprocess_dict( return { subcast_keys_instance.deserialize( - key.strip() + key.strip(), ): subcast_values_instance.deserialize(val.strip()) for key, val in (item.split("=", 1) for item in value.split(delimiter) if value) } @@ -258,10 +257,9 @@ def _preprocess_json(value: str | typing.Mapping | list, **kwargs): try: if isinstance(value, str): return pyjson.loads(value) - elif isinstance(value, dict) or isinstance(value, list) or value is None: + if isinstance(value, (dict, list)) or value is None: return value - else: - raise ma.ValidationError("Not valid JSON.") + raise ma.ValidationError("Not valid JSON.") except pyjson.JSONDecodeError as error: raise ma.ValidationError("Not valid JSON.") from error @@ -272,7 +270,7 @@ def _dj_db_url_parser(value: str, **kwargs) -> DBConfig: except ImportError as error: raise RuntimeError( "The dj_db_url parser requires the dj-database-url package. " - "You can install it with: pip install dj-database-url" + "You can install it with: pip install dj-database-url", ) from error try: return dj_database_url.parse(value, **kwargs) @@ -286,7 +284,7 @@ def _dj_email_url_parser(value: str, **kwargs) -> dict: except ImportError as error: raise RuntimeError( "The dj_email_url parser requires the dj-email-url package. " - "You can install it with: pip install dj-email-url" + "You can install it with: pip install dj-email-url", ) from error try: return dj_email_url.parse(value, **kwargs) @@ -300,7 +298,7 @@ def _dj_cache_url_parser(value: str, **kwargs) -> dict: except ImportError as error: raise RuntimeError( "The dj_cache_url parser requires the django-cache-url package. " - "You can install it with: pip install django-cache-url" + "You can install it with: pip install django-cache-url", ) from error try: return django_cache_url.parse(value, **kwargs) @@ -338,7 +336,9 @@ class Env: ), ) json: FieldMethod[_ListType | _DictType] = _field2method( - ma.fields.Raw, "json", preprocess=_preprocess_json + ma.fields.Raw, + "json", + preprocess=_preprocess_json, ) datetime: FieldMethod[dt.datetime] = _field2method(ma.fields.DateTime, "datetime") date: FieldMethod[dt.date] = _field2method(ma.fields.Date, "date") @@ -372,11 +372,12 @@ def __init__( self.__custom_parsers__: dict[_StrType, ParserMethod] = {} def __repr__(self) -> _StrType: - return f"<{self.__class__.__name__}(eager={self.eager}, expand_vars={self.expand_vars})>" # noqa: E501 + return f"<{self.__class__.__name__}(eager={self.eager}, expand_vars={self.expand_vars})>" @staticmethod def read_env( path: _StrType | Path | None = None, + *, recurse: _BoolType = True, verbose: _BoolType = False, override: _BoolType = False, @@ -397,7 +398,7 @@ def read_env( if current_frame is None: raise RuntimeError("Could not get current call frame.") frame = current_frame.f_back - assert frame is not None + assert frame is not None # noqa: S101 caller_dir = Path(frame.f_code.co_filename).parent.resolve() start = caller_dir / ".env" else: @@ -412,7 +413,9 @@ def read_env( check_path = Path(dirname) / env_name if check_path.exists(): is_env_loaded = load_dotenv( - check_path, verbose=verbose, override=override + check_path, + verbose=verbose, + override=override, ) env_path = str(check_path) break @@ -423,8 +426,7 @@ def read_env( if return_path: return env_path - else: - return is_env_loaded + return is_env_loaded @contextlib.contextmanager def prefixed(self, prefix: _StrType) -> typing.Iterator[Env]: @@ -451,7 +453,8 @@ def seal(self): error_messages = dict(self._errors) self._errors = {} raise EnvValidationError( - f"Environment variables invalid: {error_messages}", error_messages + f"Environment variables invalid: {error_messages}", + error_messages, ) def __getattr__(self, name: _StrType): @@ -466,13 +469,13 @@ def add_parser(self, name: _StrType, func: typing.Callable) -> None: """ if hasattr(self, name): raise ParserConflictError( - f"Env already has a method with name '{name}'. Use a different name." + f"Env already has a method with name '{name}'. Use a different name.", ) self.__custom_parsers__[name] = _func2method(func, method_name=name) - return None def parser_for( - self, name: _StrType + self, + name: _StrType, ) -> typing.Callable[[typing.Callable], typing.Callable]: """Decorator that registers a new parser method with the name ``name``. The decorated function must receive the input value for an environment variable. @@ -498,7 +501,11 @@ def dump(self) -> typing.Mapping[_StrType, typing.Any]: return schema.dump(self._values) def _get_from_environ( - self, key: _StrType, default: typing.Any, *, proxied: _BoolType = False + self, + key: _StrType, + default: typing.Any, + *, + proxied: _BoolType = False, ) -> tuple[_StrType, typing.Any, _StrType | None]: """Access a value from os.environ. Handles proxied variables, e.g. SMTP_LOGIN={{MAILGUN_LOGIN}}. @@ -544,7 +551,7 @@ def _expand_vars(self, parsed_key, value): for match in _EXPANDED_VAR_PATTERN.finditer(value): env_key = match.group(1) env_default = match.group(2) - if env_default is None: + if env_default is None: # noqa: SIM108 env_default = Ellipsis else: env_default = env_default[2:] # trim ':-' from default diff --git a/src/environs/fields.py b/src/environs/fields.py index 32446f3..7340f3e 100644 --- a/src/environs/fields.py +++ b/src/environs/fields.py @@ -38,8 +38,7 @@ def _format_num(self, value) -> int: value = value.upper() if hasattr(logging, value) and isinstance(getattr(logging, value), int): return getattr(logging, value) - else: - raise ValidationError("Not a valid log level.") from error + raise ValidationError("Not a valid log level.") from error class TimeDelta(fields.TimeDelta): diff --git a/src/environs/types.py b/src/environs/types.py index 8f3d40a..9a4fd46 100644 --- a/src/environs/types.py +++ b/src/environs/types.py @@ -26,7 +26,9 @@ ErrorMapping: typing.TypeAlias = typing.Mapping[str, list[str]] FieldFactory: typing.TypeAlias = typing.Callable[..., ma.fields.Field] Subcast: typing.TypeAlias = typing.Union[ - type[T], typing.Callable[[typing.Any], T], ma.fields.Field + type[T], + typing.Callable[[typing.Any], T], + ma.fields.Field, ] ParserMethod: typing.TypeAlias = typing.Callable[..., typing.Any] diff --git a/tests/mypy_test_cases/env.py b/tests/mypy_test_cases/env.py index ec34ff8..b0f353d 100644 --- a/tests/mypy_test_cases/env.py +++ b/tests/mypy_test_cases/env.py @@ -9,16 +9,20 @@ tox -e mypy-marshmallowdev """ -import datetime as dt -import decimal +from __future__ import annotations + import enum -import pathlib -import uuid -from typing import Any -from urllib.parse import ParseResult +from typing import TYPE_CHECKING, Any import environs +if TYPE_CHECKING: + import datetime as dt + import decimal + import pathlib + import uuid + from urllib.parse import ParseResult + env = environs.Env() @@ -39,7 +43,10 @@ class Color(enum.IntEnum): LIST1: list[int] | None = env.list("FOO", None, subcast=int) DICT0: dict | None = env.dict("FOO", None) DICT1: dict[str, int] | None = env.dict( - "FOO", None, subcast_keys=str, subcast_values=int + "FOO", + None, + subcast_keys=str, + subcast_values=int, ) JSON0: list | dict | None = env.json("FOO", None) DATETIME0: dt.datetime | None = env.datetime("FOO", None) diff --git a/tests/test_environs.py b/tests/test_environs.py index 2bc036a..f6fc3ce 100644 --- a/tests/test_environs.py +++ b/tests/test_environs.py @@ -37,12 +37,12 @@ def _set_env(envvars): return _set_env -@pytest.fixture(scope="function") +@pytest.fixture def env(): return environs.Env() -class FauxTestException(Exception): +class FauxTestError(Exception): pass @@ -63,7 +63,8 @@ def test_call(self, set_env, env: environs.Env): assert env("STR") == "foo" assert env("NOT_SET", "mydefault") == "mydefault" with pytest.raises( - environs.EnvError, match='Environment variable "NOT_SET" not set' + environs.EnvError, + match='Environment variable "NOT_SET" not set', ): assert env("NOT_SET") @@ -87,7 +88,8 @@ def test_int_cast(self, set_env, env: environs.Env): def test_invalid_int(self, set_env, env: environs.Env): set_env({"INT": "invalid"}) with pytest.raises( - environs.EnvValidationError, match='Environment variable "INT" invalid' + environs.EnvValidationError, + match='Environment variable "INT" invalid', ) as excinfo: env.int("INT") exc = excinfo.value @@ -108,8 +110,7 @@ def test_list_with_default_from_list(self, env: environs.Env): def test_list_with_default_list_and_subcast(self, env: environs.Env): expected = [("a", "b"), ("b", "c")] assert ( - env.list("LIST", expected, subcast=lambda s: tuple(s.split(":"))) - == expected + env.list("LIST", expected, subcast=lambda s: tuple(s.split(":"))) == expected ) # https://github.com/sloria/environs/issues/298 @@ -187,7 +188,9 @@ def custom_tuple(value: str): set_env({"DICT": "1:1=foo:bar"}) assert env.dict( - "DICT", subcast_keys=custom_tuple, subcast_values=custom_tuple + "DICT", + subcast_keys=custom_tuple, + subcast_values=custom_tuple, ) == {("1", "1"): ("foo", "bar")} def test_dict_with_dict_default(self, env: environs.Env): @@ -249,7 +252,10 @@ def test_date_cast(self, set_env, env: environs.Env): ], ) def test_default_set_to_internal_type( - self, env: environs.Env, method_name: str, value + self, + env: environs.Env, + method_name: str, + value, ): method = getattr(env, method_name) assert method("NOTFOUND", value) == value @@ -259,7 +265,8 @@ def test_timedelta_cast(self, set_env, env: environs.Env): if MARSHMALLOW_VERSION.major >= 4: set_env({"TIMEDELTA": "42.9"}) assert env.timedelta("TIMEDELTA") == dt.timedelta( - seconds=42, microseconds=900000 + seconds=42, + microseconds=900000, ) # seconds as integer set_env({"TIMEDELTA": "0"}) @@ -326,7 +333,9 @@ def test_url_db_cast(self, env: environs.Env, set_env): # FIXME: Fix typing of FieldMethod to accept # all the underlying field's constructor arguments res = env.url( # type: ignore[call-overload] - "MONGODB_URL", schemes={"mongodb", "mongodb+srv"}, require_tld=False + "MONGODB_URL", + schemes={"mongodb", "mongodb+srv"}, + require_tld=False, ) assert isinstance(res, urllib.parse.ParseResult) @@ -347,7 +356,7 @@ def test_log_level_cast(self, set_env, env: environs.Env): "LOG_LEVEL": "WARNING", "LOG_LEVEL_INT": str(logging.WARNING), "LOG_LEVEL_LOWER": "info", - } + }, ) assert env.log_level("LOG_LEVEL_INT") == logging.WARNING assert env.log_level("LOG_LEVEL") == logging.WARNING @@ -376,7 +385,8 @@ def test_enum_cast(self, set_env, env: environs.Env): def test_enum_by_value_true(self, set_env, env: environs.Env): set_env({"COLOR": "GREEN"}) with pytest.raises( - environs.EnvError, match='Environment variable "COLOR" invalid:' + environs.EnvError, + match='Environment variable "COLOR" invalid:', ): assert env.enum("COLOR", enum=Color, by_value=True) set_env({"COLOR": "green"}) @@ -385,7 +395,8 @@ def test_enum_by_value_true(self, set_env, env: environs.Env): def test_enum_by_value_field(self, set_env, env: environs.Env): set_env({"DAY": "SUNDAY"}) with pytest.raises( - environs.EnvError, match='Environment variable "DAY" invalid:' + environs.EnvError, + match='Environment variable "DAY" invalid:', ): assert env.enum("DAY", enum=Day, by_value=fields.Int()) set_env({"DAY": "1"}) @@ -394,7 +405,8 @@ def test_enum_by_value_field(self, set_env, env: environs.Env): def test_invalid_enum(self, set_env, env: environs.Env): set_env({"DAY": "suNDay"}) with pytest.raises( - environs.EnvError, match="Must be one of: SUNDAY, MONDAY, TUESDAY" + environs.EnvError, + match="Must be one of: SUNDAY, MONDAY, TUESDAY", ): assert env.enum("DAY", enum=Day) @@ -439,10 +451,14 @@ def test_read_env_recurse_from_subfolder(self, env: environs.Env, monkeypatch): assert env("CUSTOM_STRING") == "foo" @pytest.mark.parametrize( - "path", [".custom.env", (HERE / "subfolder" / ".custom.env")] + "path", + [".custom.env", (HERE / "subfolder" / ".custom.env")], ) def test_read_env_recurse_start_from_subfolder( - self, env: environs.Env, path, monkeypatch + self, + env: environs.Env, + path, + monkeypatch, ): if "CUSTOM_STRING" in os.environ: os.environ.pop("CUSTOM_STRING") @@ -601,7 +617,7 @@ def test_dump(self, set_env, env: environs.Env): "URLPARSE": "http://stevenloria.com/projects/?foo=42", "PTH": "/home/sloria", "LOG_LEVEL": "WARNING", - } + }, ) env.str("STR") @@ -699,7 +715,8 @@ def validate(val): with env.prefixed("APP_"): with pytest.raises( - environs.EnvError, match='Environment variable "APP_INT" invalid' + environs.EnvError, + match='Environment variable "APP_INT" invalid', ): env.int("INT", validate=validate) @@ -746,7 +763,7 @@ def default_environ(self, set_env): def test_failed_nested_prefixed(self, env: environs.Env): # define repeated prefixed steps - def nested_prefixed(env, fail=False): + def nested_prefixed(env, *, fail=False): with env.prefixed("APP_"): with env.prefixed("NESTED_"): assert env.int("INT") == 42 @@ -754,16 +771,16 @@ def nested_prefixed(env, fail=False): assert env.str("STR") == "foo" assert env("NOT_FOUND", "mydefault") == "mydefault" if fail: - raise FauxTestException + raise FauxTestError try: nested_prefixed(env, fail=True) - except FauxTestException: + except FauxTestError: nested_prefixed(env, fail=False) def test_failed_dump_with_nested_prefixed(self, env: environs.Env): # define repeated prefixed steps - def dump_with_nested_prefixed(env, fail=False): + def dump_with_nested_prefixed(env, *, fail=False): with env.prefixed("APP_"): with env.prefixed("NESTED_"): assert env.int("INT") == 42 @@ -771,7 +788,7 @@ def dump_with_nested_prefixed(env, fail=False): assert env.str("STR") == "foo" assert env("NOT_FOUND", "mydefault") == "mydefault" if fail: - raise FauxTestException + raise FauxTestError assert env.dump() == { "APP_STR": "foo", "APP_NOT_FOUND": "mydefault", @@ -781,7 +798,7 @@ def dump_with_nested_prefixed(env, fail=False): try: dump_with_nested_prefixed(env, fail=True) - except FauxTestException: + except FauxTestError: dump_with_nested_prefixed(env, fail=False) @@ -873,7 +890,8 @@ def test_cannot_add_after_seal(self, env: environs.Env, set_env): env.str("STR") env.seal() with pytest.raises( - environs.EnvSealedError, match="Env has already been sealed" + environs.EnvSealedError, + match="Env has already been sealed", ): env.int("INT") @@ -886,7 +904,8 @@ def https_url(value): env.seal() with pytest.raises( - environs.EnvSealedError, match="Env has already been sealed" + environs.EnvSealedError, + match="Env has already been sealed", ): env.https_url("URL") @@ -900,7 +919,9 @@ def test_dj_db_url_with_deferred_validation_missing(self, env: environs.Env): assert exc.error_messages == {"DATABASE_URL": ["Environment variable not set."]} def test_dj_db_url_with_deferred_validation_invalid( - self, env: environs.Env, set_env + self, + env: environs.Env, + set_env, ): set_env({"DATABASE_URL": "invalid://"}) env.dj_db_url("DATABASE_URL") @@ -925,7 +946,9 @@ def test_dj_cache_url_with_deferred_validation_missing(self, env: environs.Env): assert exc.error_messages == {"CACHE_URL": ["Environment variable not set."]} def test_dj_cache_url_with_deferred_validation_invalid( - self, env: environs.Env, set_env + self, + env: environs.Env, + set_env, ): set_env({"CACHE_URL": "invalid://"}) env.dj_cache_url("CACHE_URL") @@ -947,7 +970,9 @@ def always_fail(value): assert exc.error_messages == {"MY_VAR": ["Environment variable not set."]} def test_custom_parser_with_deferred_validation_invalid( - self, env: environs.Env, set_env + self, + env: environs.Env, + set_env, ): set_env({"MY_VAR": "foo"}) @@ -980,7 +1005,7 @@ def test_full_expand_vars(self, env: environs.Env, set_env): "SUBS_INT": "48", "USE_DEFAULT": "${FOOBAR}", "UNDEFINED": "${MYVAR}", - } + }, ) assert env.str("MAIN") == "substivalue" assert env.int("MAIN_INT") == 48 @@ -990,7 +1015,8 @@ def test_full_expand_vars(self, env: environs.Env, set_env): assert env.str("USE_DEFAULT", "main_default") == "main_default" with pytest.raises( - environs.EnvError, match='Environment variable "MYVAR" not set' + environs.EnvError, + match='Environment variable "MYVAR" not set', ): env.str("UNDEFINED") @@ -1002,13 +1028,14 @@ def test_multiple_expands(self, env: environs.Env, set_env): "HELLOCOUNTRY": "Hello ${COUNTRY}", "COUNTRY": "Argentina", "HELLOWORLD": "Hello ${WORLD}", - } + }, ) assert env.str("PGURL") == "postgres://gnarvaja:secret@localhost" assert env.str("HELLOCOUNTRY") == "Hello Argentina" with pytest.raises( - environs.EnvError, match='Environment variable "WORLD" not set' + environs.EnvError, + match='Environment variable "WORLD" not set', ): env.str("HELLOWORLD") @@ -1018,7 +1045,7 @@ def test_recursive_expands(self, env: environs.Env, set_env): "PGURL": "postgres://${PGUSER:-sloria}:${PGPASS:-secret}@localhost", "PGUSER": "${USER}", "USER": "gnarvaja", - } + }, ) assert env.str("PGURL") == "postgres://gnarvaja:secret@localhost" @@ -1033,7 +1060,7 @@ def test_composite_types(self, env: environs.Env, set_env): "USER": "gnarvaja", "MYCLASS_KARGS": "foo=bar,wget_params=${WGET_PARAMS}", "WGET_PARAMS": '--header="Referer: https://radiocut.fm/"', - } + }, ) assert env.list("ALLOWED_USERS") == ["god", "gnarvaja", "root"] assert env.dict("MYCLASS_KARGS") == {