Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions pyrightconfig.stricter.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"stubs/cffi",
"stubs/dateparser",
"stubs/defusedxml",
"stubs/django-environ",
"stubs/docker",
"stubs/docutils",
"stubs/Flask-SocketIO",
Expand Down
2 changes: 2 additions & 0 deletions stubs/django-environ/METADATA.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
version = "0.12.*"
upstream_repository = "https://github.com/joke2k/django-environ"
11 changes: 11 additions & 0 deletions stubs/django-environ/environ/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from .compat import DJANGO_POSTGRES as DJANGO_POSTGRES, PYMEMCACHE_DRIVER as PYMEMCACHE_DRIVER, REDIS_DRIVER as REDIS_DRIVER
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are indirectly re-exported from .environ, so let's move the import there

Suggested change
from .compat import DJANGO_POSTGRES as DJANGO_POSTGRES, PYMEMCACHE_DRIVER as PYMEMCACHE_DRIVER, REDIS_DRIVER as REDIS_DRIVER

from .environ import *

__copyright__: str
__version__: str
__license__: str
__author_email__: str
__maintainer__: str
__maintainer_email__: str
__url__: str
__description__: str
9 changes: 9 additions & 0 deletions stubs/django-environ/environ/compat.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class ImproperlyConfigured(Exception): ...

def choose_rediscache_driver(): ...
def choose_postgres_driver(): ...
def choose_pymemcache_driver(): ...

REDIS_DRIVER: str
DJANGO_POSTGRES: str
PYMEMCACHE_DRIVER: str
141 changes: 141 additions & 0 deletions stubs/django-environ/environ/environ.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import os
from collections.abc import Callable, Mapping, MutableMapping
from logging import Logger
from typing import IO, Any, ClassVar, SupportsIndex, TypedDict, TypeVar, overload
from typing_extensions import TypeAlias, Unpack
from urllib.parse import ParseResult

from .fileaware_mapping import FileAwareMapping
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above

Suggested change
from .fileaware_mapping import FileAwareMapping
from .compat import DJANGO_POSTGRES as DJANGO_POSTGRES, PYMEMCACHE_DRIVER as PYMEMCACHE_DRIVER, REDIS_DRIVER as REDIS_DRIVER
from .fileaware_mapping import FileAwareMapping


Openable: tuple[type, ...]
logger: Logger

class NoValue: ...

# Some type aliases to make our life easier
_Str = str
_Bytes = bytes
_Bool = bool
_Int = int
_Float = float
_List = list
_Tuple = tuple
_Dict = dict

_T = TypeVar("_T")
_Cast: TypeAlias = Callable[[_Str], _T]
_SchemeValue: TypeAlias = _Cast[Any] | tuple[_Cast[Any], Any]
_BooleanTrueStrings: TypeAlias = tuple[str, ...]

class Env:
ENVIRON: MutableMapping[_Str, _Str]
NOTSET: ClassVar[NoValue]
BOOLEAN_TRUE_STRINGS: ClassVar[_BooleanTrueStrings]
URL_CLASS: ClassVar[type[ParseResult]]
POSTGRES_FAMILY: ClassVar[_List[_Str]]
DEFAULT_DATABASE_ENV: ClassVar[_Str] = "DATABASE_URL"
DB_SCHEMES: ClassVar[_Dict[_Str, _Str]]
DEFAULT_CACHE_ENV: ClassVar[_Str] = "CACHE_URL"
CACHE_SCHEMES: ClassVar[_Dict[_Str, _Str]]
DEFAULT_EMAIL_ENV: ClassVar[_Str] = "EMAIL_URL"
EMAIL_SCHEMES: ClassVar[_Dict[_Str, _Str]]
DEFAULT_SEARCH_ENV: ClassVar[_Str] = "SEARCH_URL"
SEARCH_SCHEMES: ClassVar[_Dict[_Str, _Str]]
ELASTICSEARCH_FAMILY: ClassVar[_List[_Str]]
CLOUDSQL: ClassVar[_Str]
DEFAULT_CHANNELS_ENV: ClassVar[_Str] = "CHANNELS_URL"
CHANNELS_SCHEMES: ClassVar[_Dict[_Str, _Str]]
smart_cast: _Bool
escape_proxy: _Bool
prefix: _Str
scheme: Mapping[_Str, _SchemeValue]

def __init__(self, **scheme: _SchemeValue) -> None: ...
def __call__(
self, var: _Str, cast: _Cast[_T] | None = None, default: _T | NoValue = ..., parse_default: _Bool = False
) -> _T: ...
def __contains__(self, var: _Str) -> _Bool: ...
def str(self, var: _Str, default: _Str | NoValue = ..., multiline: _Bool = False) -> _Str: ...
def bytes(self, var: _Str, default: _Bytes | NoValue = ..., encoding: _Str = "utf8") -> _Bytes: ...
def bool(self, var: _Str, default: _Bool | NoValue = ...) -> _Bool: ...
def int(self, var: _Str, default: _Int | NoValue = ...) -> _Int: ...
def float(self, var: _Str, default: _Float | NoValue = ...) -> _Float: ...
def json(self, var: _Str, default: Any | NoValue = ...) -> Any: ...
def list(self, var: _Str, cast: _Cast[_List] | None = None, default: _List | NoValue = ...) -> _List: ...
def tuple(self, var: _Str, cast: _Cast[_Tuple] | None = None, default: _Tuple | NoValue = ...) -> _Tuple: ...
def dict(self, var: _Str, cast: _Cast[_Dict] | None = ..., default: _Dict | NoValue = ...) -> _Dict: ...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These could be made generic so that the argument to cast can determine the return type. If you do that, you'd also need to add an overload for the default case when cast isn't provided. That might look something like this (I haven't check if it's correct):

@overload
def list(self, var: str, cast: _Cast[_T], default: _T | NoValue = ...) -> _T: ...
@overload
def list(self, var: str, cast: None = None, default: list[Any] | NoValue = ...) -> list[Any]: ...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I found another error. Cast may be any of:

  1. None: Parse var string as list/tuple/dict
  2. Callable[[str], _T]: Parse var string as list/tuple/dict and apply cast function to every item/value.
  3. tuple[Callable[[str], _T] , _T]: Same as 2) but with default fallback value

def url(self, var: _Str, default: _Str | NoValue = ...) -> _Str: ...
def db_url(self, var: _Str = ..., default: _Str | NoValue = ..., engine: _Str | None = None) -> _Dict: ...

db = db_url

def cache_url(self, var: _Str = ..., default: _Str | NoValue = ..., backend: _Str | None = None) -> _Dict: ...

cache = cache_url

def email_url(self, var: _Str = ..., default: _Str | NoValue = ..., backend: _Str | None = None) -> _Dict: ...

email = email_url

def search_url(self, var: _Str = ..., default: _Str | NoValue = ..., engine: _Str | None = None) -> _Dict: ...
def channels_url(self, var: _Str = ..., default: _Str | NoValue = ..., backend: _Str | None = None) -> _Dict: ...

channels = channels_url

def path(self, var: _Str, default: _Str | NoValue = ..., **kwargs: Unpack[_PathKwargs]) -> Path: ...
def get_value(
self, var: _Str, cast: _Cast[_T] | None = None, default: _T | NoValue = ..., parse_default: _Bool = False
) -> _T: ...
@classmethod
def parse_value(cls, value: _Str, cast: _Cast[_T]) -> _T: ...
Copy link
Member

@brianschubert brianschubert Aug 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, it looks like in addition to accepting a callable that takes a str, cast can also take

  • a single element list or tuple containing a callable to apply to each value
  • a dictionary with the keys key, value, and optionally cast
  • None
  • any of the type objects bool, dict, list, tuple, or float (all of which except dict are already accounted for by the str-accepting callable case)

This seems to also apply to all of the other methods that accept a cast argument

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I wasn't aware of that!

I think I covered all the cases now.

@classmethod
def db_url_config(cls, url: _Str | ParseResult, engine: _Str | None = None) -> _Dict: ...
@classmethod
def cache_url_config(cls, url: _Str | ParseResult, backend: _Str | None = None) -> _Dict: ...
@classmethod
def email_url_config(cls, url: _Str | ParseResult, backend: _Str | None = None) -> _Dict: ...
@classmethod
def channels_url_config(cls, url: _Str | ParseResult, backend: _Str | None = None) -> _Dict: ...
@classmethod
def search_url_config(cls, url: _Str | ParseResult, engine: _Str | None = None) -> _Dict: ...
@classmethod
def read_env(
cls,
env_file: _Str | os.PathLike[_Str] | None = None,
overwrite: _Bool = False,
parse_comments: _Bool = False,
encoding: _Str = "utf8",
**overrides: _Dict[_Str, _Str],
) -> None: ...

class FileAwareEnv(Env):
ENVIRON: FileAwareMapping

class _PathKwargs(TypedDict, total=False):
required: bool
is_file: bool

class Path:
def path(self, *paths: str, **kwargs: Unpack[_PathKwargs]) -> Path: ...
def file(self, name: str, *args, **kwargs) -> IO[str]: ...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could also return a IO[bytes] if e.g. mode="rb" is passed, so let's change this to IO[Any].

Suggested change
def file(self, name: str, *args, **kwargs) -> IO[str]: ...
# *args and **kwargs are passed to open().
def file(self, name: str, *args: Any, **kwargs: Any) -> IO[Any]: ...

@property
def root(self) -> str: ...

__root__: str

def __init__(self, start: str = "", *paths: str, **kwargs: Unpack[_PathKwargs]) -> None: ...
def __call__(self, *paths: str, **kwargs: Unpack[_PathKwargs]) -> str: ...
def __eq__(self, other: object | Path) -> bool: ...
def __ne__(self, other: object | Path) -> bool: ...
def __add__(self, other: object | Path) -> Path: ...
Comment on lines +390 to +392
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

object | Path simplifies to object

def __sub__(self, other: int | str) -> Path: ...
def __invert__(self) -> Path: ...
def __contains__(self, item: Path) -> bool: ...
def __unicode__(self) -> str: ...
@overload
def __getitem__(self, key: SupportsIndex, /) -> str: ...
@overload
def __getitem__(self, key: slice, /) -> str: ...
def __fspath__(self) -> str: ...
def rfind(self, s: str, sub: str, start: int = 0, end: int = ...) -> int: ...
def find(self, s: str, sub: str, start: int = 0, end: int = ...) -> int: ...
Comment on lines +402 to +403
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def rfind(self, s: str, sub: str, start: int = 0, end: int = ...) -> int: ...
def find(self, s: str, sub: str, start: int = 0, end: int = ...) -> int: ...
def rfind(self, s: str, start: SupportsIndex | None = ..., end: SupportsIndex | None = ..., /) -> int: ...
def find(self, s: str, start: SupportsIndex | None = ..., end: SupportsIndex | None = ..., /) -> int: ...

12 changes: 12 additions & 0 deletions stubs/django-environ/environ/fileaware_mapping.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from collections.abc import Iterator, MutableMapping

class FileAwareMapping(MutableMapping[str, str]):
env: dict[str, str]
cache: bool
files_cache: dict[str, str]
def __init__(self, env: dict[str, str] | None = None, cache: bool = True) -> None: ...
def __getitem__(self, key: str) -> str: ...
def __iter__(self) -> Iterator[str]: ...
def __len__(self) -> int: ...
def __setitem__(self, key: str, value: str) -> None: ...
def __delitem__(self, key: str) -> None: ...
Loading