Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v0.23.0: Add EnvWizard - support for working with environment variables and dotenv files #70

Closed
wants to merge 45 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
2a36334
WIP: Checkin changes so far
rnag Dec 17, 2021
c6ddc91
checkin changes to lookup env
rnag May 13, 2022
d30b0c4
add tests
rnag May 13, 2022
44602a2
Code cleanup
rnag May 24, 2022
2bd78cf
merge main into current
rnag May 24, 2022
ab1d329
Code cleanup
rnag May 24, 2022
8e94a8d
Change type of `file` argument: `str` -> `FileType`
rnag May 28, 2022
a2f5aeb
Add `dotenv` support
rnag May 28, 2022
6ab7131
Add `dotenv` support
rnag May 28, 2022
26aa812
lookup env by alias
rnag May 28, 2022
a8862f2
update to convert str to dict
rnag May 28, 2022
3091b1c
fix tests in PY3.10
cb-rnag Jun 2, 2022
5a7865e
minor updates and add tests
cb-rnag Jul 12, 2022
074eca9
remove unused helper class
cb-rnag Jul 12, 2022
82c5544
add tests for better coverage
cb-rnag Jul 13, 2022
655f2d5
add more tests
cb-rnag Jul 13, 2022
214b9b9
add test case for `init=False` and constructor method
cb-rnag Jul 18, 2022
7c1fe36
fix for windows
rnag Aug 5, 2022
41b079f
add brief section on `EnvWizard`
rnag Aug 5, 2022
f78f2fb
add section on `EnvWizard`
rnag Aug 5, 2022
ade13ce
rename file to wizard.py
rnag Aug 16, 2022
8a49b71
minor change
cb-rnag Sep 1, 2022
315ab7b
make `lookup_exact` work with tuples of str
rnag Sep 13, 2022
f690950
major update to make `EnvWizard` work like a dataclass instead
rnag Sep 13, 2022
e63a3e3
EnvWizard.__init__: check for extra keyword arguments
cb-rnag Sep 13, 2022
9f98cba
update logic to load from `env_file`
cb-rnag Sep 14, 2022
92e5e5d
minor code refactor
cb-rnag Sep 14, 2022
677b7f1
add tests for better coverage
cb-rnag Sep 14, 2022
3c9fb56
minor fix
rnag Sep 15, 2022
fe76ae1
code cleanup
rnag Sep 15, 2022
840b832
add tests for better coverage
cb-rnag Sep 15, 2022
c2e4a27
add tests for better coverage
cb-rnag Sep 15, 2022
b4fae27
fix tests on windows
rnag Sep 15, 2022
fbe3a46
dynamically generate ```__init__()`` for EnvWizard subclasses
rnag Sep 16, 2022
44e0f8a
fix typos
rnag Sep 16, 2022
6be92b9
minor changes
cb-rnag Sep 17, 2022
3c19331
fix datetime timezone issues
rnag Oct 11, 2022
4661119
create helpers.py
rnag Oct 11, 2022
312d7ba
minor code refactor
rnag Oct 11, 2022
35c6641
Fixes #84
rnag Mar 30, 2023
f6024dc
Update `wheel` dependency
rnag Mar 30, 2023
d9f8fa6
revert for now (until we update to PY 3.7+ only)
rnag Mar 30, 2023
cf6188f
Merge commit!
rnag Nov 26, 2024
e848589
updates
rnag Nov 26, 2024
bda0a27
changes for the better
rnag Nov 26, 2024
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
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ exclude_lines =
# Have to re-enable the standard pragma
pragma: no cover

# Conditional code which is dependent on the OS, or `os.name`
if name == 'nt':

# Don't complain if tests don't hit defensive assertion code:
raise AssertionError
raise NotImplementedError
Expand Down
4 changes: 4 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# These values are used in unit tests (tests/unit/test_env_wizard.py)
MY_STR=42
my_time=15:20
MyDate=2022-01-21
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ target/
Pipfile.lock

# Environments
.env
.env/
.venv
env/
venv/
Expand Down
4 changes: 4 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ Here are the key features that ``dataclass-wizard`` offers:
- *Flexible (de)serialization*: Marshal dataclasses to/from JSON, TOML, YAML, or ``dict``.
- *Field properties*: Use properties with default values in dataclass instances.
- *JSON to Dataclass generation*: Auto-generate a dataclass schema from a JSON file or string.
- *Environment support*: easily load ``dotenv`` files and environment variables
as strongly-typed class fields.


Wizard Mixins
Expand All @@ -109,6 +111,7 @@ Wizard Mixins
In addition to ``JSONWizard``, these handy Mixin_ classes simplify your workflow:

* `JSONPyWizard`_ — A ``JSONWizard`` helper to skip *camelCase* and keep keys as-is.
* `EnvWizard`_ -- Enables loading of Environment variables and ``.env`` files into strongly-typed class schemas.
* `JSONListWizard`_ -- Extends ``JSONWizard`` to return `Container`_ -- instead of *list* -- objects where possible.
* `JSONFileWizard`_ -- Makes it easier to convert dataclass instances from/to JSON files on a local drive.
* `TOMLWizard`_ -- Provides support to convert dataclass instances to/from TOML.
Expand Down Expand Up @@ -1242,6 +1245,7 @@ This package was created with Cookiecutter_ and the `rnag/cookiecutter-pypackage
.. _`Contributing`: https://dataclass-wizard.readthedocs.io/en/latest/contributing.html
.. _`open an issue`: https://github.com/rnag/dataclass-wizard/issues
.. _`JSONPyWizard`: https://dataclass-wizard.readthedocs.io/en/latest/common_use_cases/wizard_mixins.html#jsonpywizard
.. _`EnvWizard`: https://dataclass-wizard.readthedocs.io/en/latest/common_use_cases/wizard_mixins.html#envwizard
.. _`JSONListWizard`: https://dataclass-wizard.readthedocs.io/en/latest/common_use_cases/wizard_mixins.html#jsonlistwizard
.. _`JSONFileWizard`: https://dataclass-wizard.readthedocs.io/en/latest/common_use_cases/wizard_mixins.html#jsonfilewizard
.. _`TOMLWizard`: https://dataclass-wizard.readthedocs.io/en/latest/common_use_cases/wizard_mixins.html#tomlwizard
Expand Down
2 changes: 2 additions & 0 deletions dataclass_wizard/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
'DumpMixin',
'property_wizard',
# Wizard Mixins
'EnvWizard',
'JSONListWizard',
'JSONFileWizard',
'TOMLWizard',
Expand Down Expand Up @@ -123,6 +124,7 @@
Pattern, DatePattern, TimePattern, DateTimePattern,
CatchAll, SkipIf, SkipIfNone,
EQ, NE, LT, LE, GT, GE, IS, IS_NOT, IS_TRUTHY, IS_FALSY)
from .environ.wizard import EnvWizard
from .property_wizard import property_wizard
from .serial_json import JSONWizard, JSONPyWizard, JSONSerializable
from .wizard_mixins import JSONListWizard, JSONFileWizard, TOMLWizard, YAMLWizard
Expand Down
43 changes: 41 additions & 2 deletions dataclass_wizard/abstractions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,46 @@
Contains implementations for Abstract Base Classes
"""
import json

from abc import ABC, abstractmethod
from dataclasses import dataclass, InitVar
from dataclasses import dataclass, InitVar, Field
from typing import Type, TypeVar, Dict, Generic

from .bases import META
from .models import Extras
from .type_def import T, TT


# Create a generic variable that can be 'AbstractJSONWizard', or any subclass.
W = TypeVar('W', bound='AbstractJSONWizard')

FieldToParser = Dict[str, 'AbstractParser']

class AbstractEnvWizard(ABC):
"""
Abstract class that defines the methods a sub-class must implement at a
minimum to be considered a "true" Environment Wizard.
"""
__slots__ = ()

# Extends the `__annotations__` attribute to return only the fields
# (variables) of the `EnvWizard` subclass.
#
# .. NOTE::
# This excludes fields marked as ``ClassVar``, or ones which are
# not type-annotated.
__fields__: dict[str, Field]

def dict(self):

return {f: getattr(self, f) for f in self.__class__.__fields__}

@abstractmethod
def to_dict(self):
...

@abstractmethod
def to_json(self, indent=None):
...


class AbstractJSONWizard(ABC):
Expand Down Expand Up @@ -204,6 +232,17 @@ def load_to_date(o, base_type):
def load_to_timedelta(o, base_type):
...

# @staticmethod
# @abstractmethod
# def load_func_for_dataclass(
# cls: Type[T],
# config: Optional[META],
# ) -> Callable[[JSONObject], T]:
# """
# Generate and return the load function for a (nested) dataclass of
# type `cls`.
# """

@classmethod
@abstractmethod
def get_parser_for_annotation(cls, ann_type,
Expand Down
48 changes: 45 additions & 3 deletions dataclass_wizard/abstractions.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Contains implementations for Abstract Base Classes
"""
import json
from abc import ABC, abstractmethod
from dataclasses import dataclass, InitVar
from dataclasses import dataclass, InitVar, Field
from datetime import datetime, time, date, timedelta
from decimal import Decimal
from typing import (
Expand All @@ -18,10 +18,53 @@ from .type_def import (
)


# Create a generic variable that can be 'AbstractEnvWizard', or any subclass.
E = TypeVar('E', bound='AbstractEnvWizard')

# Create a generic variable that can be 'AbstractJSONWizard', or any subclass.
W = TypeVar('W', bound='AbstractJSONWizard')

FieldToParser = dict[str, 'AbstractParser']
FieldToParser = dict[str, AbstractParser]


class AbstractEnvWizard(ABC):
"""
Abstract class that defines the methods a sub-class must implement at a
minimum to be considered a "true" Environment Wizard.
"""
__slots__ = ()

# Extends the `__annotations__` attribute to return only the fields
# (variables) of the `EnvWizard` subclass.
#
# .. NOTE::
# This excludes fields marked as ``ClassVar``, or ones which are
# not type-annotated.
__fields__: dict[str, Field]

def dict(self: E) -> JSONObject:
"""
Same as ``__dict__``, but only returns values for fields defined
on the `EnvWizard` instance. See :attr:`__fields__` for more info.

.. NOTE::
The values in the returned dictionary object are not needed to be
JSON serializable. Use :meth:`to_dict` if this is required.
"""

@abstractmethod
def to_dict(self: E) -> JSONObject:
"""
Converts an instance of a `EnvWizard` subclass to a Python dictionary
object that is JSON serializable.
"""

@abstractmethod
def to_json(self: E, indent=None) -> AnyStr:
"""
Converts an instance of a `EnvWizard` subclass to a JSON `string`
representation.
"""


class AbstractJSONWizard(ABC):
Expand Down Expand Up @@ -127,7 +170,6 @@ class AbstractParser(ABC, Generic[T, TT]):
type. Checks against the exact type instead of `isinstance` so we can
handle special cases like `bool`, which is a subclass of `int`.
"""
return type(item) is self.base_type

@abstractmethod
def __call__(self, o: Any) -> TT:
Expand Down
98 changes: 96 additions & 2 deletions dataclass_wizard/bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

from .constants import TAG
from .decorators import cached_class_property
from .enums import DateTimeTo, LetterCase
from .models import Condition
from .type_def import FrozenKeys
from .enums import DateTimeTo, LetterCase, LetterCasePriority
from .type_def import FrozenKeys, EnvFileType


# Create a generic variable that can be 'AbstractMeta', or any subclass.
Expand Down Expand Up @@ -249,6 +249,100 @@ def bind_to(cls, dataclass: Type, create=True, is_default=True):
"""


class AbstractEnvMeta:
"""
Base class definition for the `EnvWizard.Meta` inner class.
"""
__slots__ = ()

# A list of class attributes that are exclusive to the Meta config.
# When merging two Meta configs for a class, these are the only
# attributes which will *not* be merged.
__special_attrs__ = frozenset({
'debug_enabled',
'env_var_to_field',
})

# Class attribute which enables us to detect a `EnvWizard.Meta` subclass.
__is_inner_meta__ = False

# True to enable Debug mode for additional (more verbose) log output.
#
# For example, a message is logged with the environment variable that is
# mapped to each attribute.
#
# This also results in more helpful messages during error handling, which
# can be useful when debugging the cause when values are an invalid type
# (i.e. they don't match the annotation for the field) when unmarshalling
# a environ variable values to attributes in an EnvWizard subclass.
#
# Note there is a minor performance impact when DEBUG mode is enabled.
debug_enabled: ClassVar[bool] = False

# `True` to load environment variables from an `.env` file, or a
# list/tuple of dotenv files.
#
# This can also be set to a path to a custom dotenv file, for example:
# `path/to/.env.prod`
#
# Simply passing in a filename such as `.env.prod` will search the current
# directory, as well as any parent folders (working backwards to the root
# directory), until it locates the given file.
#
# If multiple files are passed in, later files in the list/tuple will take
# priority over earlier files.
#
# For example, in below the '.env.last' file takes priority over '.env':
# env_file = '.env', '.env.last'
env_file: ClassVar[EnvFileType] = None

# A customized mapping of field in the `EnvWizard` subclass to its
# corresponding environment variable to search for.
#
# Note: this is in addition to the implicit field transformations, like
# "myStr" -> "my_str"
field_to_env_var: ClassVar[Dict[str, str]] = None

# The letter casing priority to use when looking up Env Var Names.
#
# The default is `SCREAMING_SNAKE_CASE`.
key_lookup_with_load: ClassVar[Union[LetterCasePriority, str]] = LetterCasePriority.SCREAMING_SNAKE

# How `EnvWizard` fields (variables) should be transformed to JSON keys.
#
# The default is 'snake_case'.
key_transform_with_dump: ClassVar[Union[LetterCase, str]] = LetterCase.SNAKE

# Determines whether we should we skip / omit fields with default values
# in the serialization process.
skip_defaults: ClassVar[bool] = False

@cached_class_property
def all_fields(cls) -> FrozenKeys:
"""Return a list of all class attributes"""
return frozenset(AbstractEnvMeta.__annotations__)

@cached_class_property
def fields_to_merge(cls) -> FrozenKeys:
"""Return a list of class attributes, minus `__special_attrs__`"""
return cls.all_fields - cls.__special_attrs__

@classmethod
@abstractmethod
def bind_to(cls, env_class: Type, create=True):
"""
Initialize hook which applies the Meta config to `env_class`, which is
typically a subclass of :class:`EnvWizard`.

:param env_class: A sub-class of :class:`EnvWizard`.
:param create: When true, a separate loader/dumper will be created
for the class. If disabled, this will access the root loader/dumper,
so modifying this should affect global settings across all
dataclasses that use the JSON load/dump process.

"""


class BaseLoadHook:
"""
Container class for type hooks.
Expand Down
Loading
Loading