Skip to content

Commit

Permalink
Use all ruff rules except ones explicitly ignored (#399)
Browse files Browse the repository at this point in the history
* Use all ruff rules except ones explicitly ignored

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Set line length

* pre-commit autoupdate

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
sloria and pre-commit-ci[bot] authored Jan 16, 2025
1 parent 0583dd9 commit e2a1c79
Show file tree
Hide file tree
Showing 13 changed files with 170 additions and 99 deletions.
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion examples/deferred_validation_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()])
Expand Down
2 changes: 1 addition & 1 deletion examples/django_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion examples/plugin_example.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 2 additions & 0 deletions examples/simple_example.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import os
from pprint import pprint

Expand Down
3 changes: 2 additions & 1 deletion examples/validation_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
40 changes: 30 additions & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down
83 changes: 45 additions & 38 deletions src/environs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
except ImportError:
pass

__all__ = ["Env", "env", "EnvError", "ValidationError"]
__all__ = ["Env", "EnvError", "ValidationError", "env"]

_T = typing.TypeVar("_T")
_StrType = str
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -151,20 +151,19 @@ 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()
source_key = proxied_key or parsed_key
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -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

Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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,
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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]:
Expand All @@ -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):
Expand All @@ -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.
Expand All @@ -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}}.
Expand Down Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions src/environs/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
4 changes: 3 additions & 1 deletion src/environs/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
Loading

0 comments on commit e2a1c79

Please sign in to comment.