Skip to content

Commit

Permalink
Version 5.4.0 (#204)
Browse files Browse the repository at this point in the history
* Adding py.typed for mypy support (thanks to Dominic)
* Adding testing for Python 3.10-dev
* Fixing #189 by adding mappings for mypy
* Fixing setdefault behavior with box_dots (thanks to ipcoder)
* Changing #193 how magic methods are handled with default_box (thanks to Rexbard)

Co-authored-by: Dominic <yobmod@gmail.com>
  • Loading branch information
cdgriffith and Yobmod authored Aug 15, 2021
1 parent 9170539 commit 51042b8
Show file tree
Hide file tree
Showing 24 changed files with 394 additions and 43 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.6, 3.7, 3.8, 3.9, pypy3]
python-version: [3.6, 3.7, 3.8, 3.9, 3.10-dev, pypy3]

steps:
- uses: actions/checkout@v2
Expand All @@ -63,4 +63,3 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
pytest --cov=box test/
34 changes: 26 additions & 8 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,21 +1,39 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.5.0
rev: v4.0.1
hooks:
- id: mixed-line-ending
- id: trailing-whitespace
# Identify invalid files
- id: check-ast
- id: check-yaml
- id: check-json
- id: check-toml
# git checks
- id: check-merge-conflict
- id: check-added-large-files
exclude: ^test/data/.+
- id: detect-private-key
- id: check-case-conflict
# Python checks
- id: check-docstring-first
- id: debug-statements
- id: requirements-txt-fixer
- id: fix-encoding-pragma
- id: check-byte-order-marker
- id: debug-statements
- id: check-yaml
- id: fix-byte-order-marker
# General quality checks
- id: mixed-line-ending
args: [--fix=lf]
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]
- id: check-executables-have-shebangs
- id: end-of-file-fixer
exclude: ^test/data/.+
- repo: https://github.com/ambv/black
rev: stable
rev: 21.7b0
hooks:
- id: black
args: [--config=.black.toml]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: 'v0.770'
rev: 'v0.910'
hooks:
- id: mypy
additional_dependencies: [ruamel.yaml,toml,msgpack]
4 changes: 3 additions & 1 deletion AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Code contributions:
- Fabian Affolter (fabaff)
- Varun Madiath (vamega)
- Jacob Hayes (JacobHayes)
- Dominic (Yobmod)

Suggestions and bug reporting:

Expand Down Expand Up @@ -74,4 +75,5 @@ Suggestions and bug reporting:
- Marcelo Huerta (richieadler)
- Tim Schwenke (trallnag)
- Marcos Dione (mdione-cloudian)
- Varun Madiath (vamega)
- Varun Madiath (vamega)
- Rexbard
10 changes: 10 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
Changelog
=========

Version 5.4.0
-------------

* Adding py.typed for mypy support (thanks to Dominic)
* Adding testing for Python 3.10-dev
* Fixing #189 by adding mappings for mypy
* Fixing setdefault behavior with box_dots (thanks to ipcoder)
* Changing #193 how magic methods are handled with default_box (thanks to Rexbard)


Version 5.3.0
-------------

Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
include LICENSE
include AUTHORS.rst
include CHANGES.rst
include box/py.typed
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,4 @@ MIT License, Copyright (c) 2017-2020 Chris Griffith. See LICENSE_ file.
.. _`Wrapt Documentation`: https://wrapt.readthedocs.io/en/latest
.. _reusables: https://github.com/cdgriffith/reusables#reusables
.. _created: https://github.com/cdgriffith/Reusables/commit/df20de4db74371c2fedf1578096f3e29c93ccdf3#diff-e9a0f470ef3e8afb4384dc2824943048R51
.. _LICENSE: https://github.com/cdgriffith/Box/blob/master/LICENSE
.. _LICENSE: https://github.com/cdgriffith/Box/blob/master/LICENSE
2 changes: 1 addition & 1 deletion box/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-

__author__ = "Chris Griffith"
__version__ = "5.3.0"
__version__ = "5.4.0"

from box.box import Box
from box.box_list import BoxList
Expand Down
46 changes: 27 additions & 19 deletions box/box.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@
import re
import string
import warnings
from collections.abc import Callable, Iterable, Mapping
from keyword import kwlist
from os import PathLike
from typing import Any, Dict, Generator, List, Tuple, Union

try:
from typing import Callable, Iterable, Mapping
except ImportError:
from collections.abc import Callable, Iterable, Mapping

import box
from box.converters import (
BOX_PARAMETERS,
Expand Down Expand Up @@ -51,6 +55,7 @@ def _exception_cause(e):
"""
return e.__cause__ if isinstance(e, (BoxKeyError, BoxValueError)) else e


def _camel_killer(attr):
"""
CamelKiller, qu'est-ce que c'est?
Expand Down Expand Up @@ -236,47 +241,47 @@ def __init__(

self._box_config["__created"] = True

def __add__(self, other: dict):
def __add__(self, other: Mapping[Any, Any]):
if not isinstance(other, dict):
raise BoxTypeError("Box can only merge two boxes or a box and a dictionary.")
new_box = self.copy()
new_box.merge_update(other)
return new_box

def __radd__(self, other: dict):
def __radd__(self, other: Mapping[Any, Any]):
if not isinstance(other, dict):
raise BoxTypeError("Box can only merge two boxes or a box and a dictionary.")
new_box = self.copy()
new_box.merge_update(other)
return new_box

def __iadd__(self, other: dict):
def __iadd__(self, other: Mapping[Any, Any]):
if not isinstance(other, dict):
raise BoxTypeError("Box can only merge two boxes or a box and a dictionary.")
self.merge_update(other)
return self

def __or__(self, other: dict):
def __or__(self, other: Mapping[Any, Any]):
if not isinstance(other, dict):
raise BoxTypeError("Box can only merge two boxes or a box and a dictionary.")
new_box = self.copy()
new_box.update(other)
return new_box

def __ror__(self, other: dict):
def __ror__(self, other: Mapping[Any, Any]):
if not isinstance(other, dict):
raise BoxTypeError("Box can only merge two boxes or a box and a dictionary.")
new_box = self.copy()
new_box.update(other)
return new_box

def __ior__(self, other: dict):
def __ior__(self, other: Mapping[Any, Any]):
if not isinstance(other, dict):
raise BoxTypeError("Box can only merge two boxes or a box and a dictionary.")
self.update(other)
return self

def __sub__(self, other: dict):
def __sub__(self, other: Mapping[Any, Any]):
frozen = self._box_config["frozen_box"]
config = self.__box_config()
config["frozen_box"] = False
Expand Down Expand Up @@ -407,7 +412,7 @@ def __get_default(self, item, attr=False):
value = default_value.copy()
else:
value = default_value
if not attr or (not item.startswith("_") and not item.endswith("_")):
if not attr or not (item.startswith("_") and item.endswith("_")):
super().__setitem__(item, value)
return value

Expand All @@ -427,7 +432,7 @@ def __recast(self, item, value):
else:
return recast(value)
except ValueError as err:
raise BoxValueError(f'Cannot convert {value} to {recast}') from _exception_cause(err)
raise BoxValueError(f"Cannot convert {value} to {recast}") from _exception_cause(err)
return value

def __convert_and_store(self, item, value):
Expand Down Expand Up @@ -497,6 +502,8 @@ def __getattr__(self, item):
if safe_key in self._box_config["__safe_keys"]:
return self.__getitem__(self._box_config["__safe_keys"][safe_key])
if self._box_config["default_box"]:
if item.startswith("_") and item.endswith("_"):
raise BoxKeyError(f"{item}: Does not exist and internal methods are never defaulted")
return self.__get_default(item, attr=True)
raise BoxKeyError(str(err)) from _exception_cause(err)
return value
Expand Down Expand Up @@ -663,7 +670,7 @@ def convert_and_set(k, v):
return
if isinstance(v, list) and not intact_type:
v = box.BoxList(v, **self.__box_config())
merge_type = kwargs.get('box_merge_lists')
merge_type = kwargs.get("box_merge_lists")
if merge_type == "extend" and k in self and isinstance(self[k], list):
self[k].extend(v)
return
Expand All @@ -685,15 +692,16 @@ def convert_and_set(k, v):
convert_and_set(key, kwargs[key])

def setdefault(self, item, default=None):
if item in self:
# Have to use a try except instead of "item in self" as box_dots may not be in iterable
try:
return self[item]
except KeyError:
if isinstance(default, dict):
default = self._box_config["box_class"](default, **self.__box_config())
if isinstance(default, list):
default = box.BoxList(default, **self.__box_config())
self[item] = default
return self[item]

if isinstance(default, dict):
default = self._box_config["box_class"](default, **self.__box_config())
if isinstance(default, list):
default = box.BoxList(default, **self.__box_config())
self[item] = default
return self[item]

def _safe_attr(self, attr):
"""Convert a key into something that is accessible as an attribute"""
Expand Down
115 changes: 115 additions & 0 deletions box/box.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
from collections.abc import Mapping
from os import PathLike
from typing import Any, Dict, Generator, List, Optional, Tuple, Union

class Box(dict):
def __new__(
cls: Any,
*args: Any,
default_box: bool = ...,
default_box_attr: Any = ...,
default_box_none_transform: bool = ...,
frozen_box: bool = ...,
camel_killer_box: bool = ...,
conversion_box: bool = ...,
modify_tuples_box: bool = ...,
box_safe_prefix: str = ...,
box_duplicates: str = ...,
box_intact_types: Union[Tuple, List] = ...,
box_recast: Dict = ...,
box_dots: bool = ...,
box_class: Union[Dict, Box] = ...,
**kwargs: Any,
) -> Any: ...
def __init__(
self,
*args: Any,
default_box: bool = ...,
default_box_attr: Any = ...,
default_box_none_transform: bool = ...,
frozen_box: bool = ...,
camel_killer_box: bool = ...,
conversion_box: bool = ...,
modify_tuples_box: bool = ...,
box_safe_prefix: str = ...,
box_duplicates: str = ...,
box_intact_types: Union[Tuple, List] = ...,
box_recast: Dict = ...,
box_dots: bool = ...,
box_class: Union[Dict, Box] = ...,
**kwargs: Any,
) -> None: ...
def __add__(self, other: Mapping[Any, Any]) -> Any: ...
def __radd__(self, other: Mapping[Any, Any]) -> Any: ...
def __iadd__(self, other: Mapping[Any, Any]) -> Any: ...
def __or__(self, other: Mapping[Any, Any]) -> Any: ...
def __ror__(self, other: Mapping[Any, Any]) -> Any: ...
def __ior__(self, other: Mapping[Any, Any]) -> Any: ...
def __sub__(self, other: Mapping[Any, Any]) -> Any: ...
def __hash__(self) -> Any: ... # type: ignore[override]
def __dir__(self): ...
def keys(self, dotted: Union[bool] = ...) -> Any: ...
def items(self, dotted: Union[bool] = ...) -> Any: ...
def get(self, key: Any, default: Any = ...): ...
def copy(self) -> Box: ...
def __copy__(self) -> Box: ...
def __deepcopy__(self, memodict: Any = ...) -> Box: ...
def __getitem__(self, item: Any, _ignore_default: bool = ...): ...
def __getattr__(self, item: Any): ...
def __setitem__(self, key: Any, value: Any): ...
def __setattr__(self, key: Any, value: Any): ...
def __delitem__(self, key: Any): ...
def __delattr__(self, item: Any) -> None: ...
def pop(self, key: Any, *args: Any): ...
def clear(self) -> None: ...
def popitem(self): ...
def __iter__(self) -> Generator: ...
def __reversed__(self) -> Generator: ...
def to_dict(self) -> Dict: ...
def update(self, __m: Optional[Any] = ..., **kwargs: Any) -> None: ...
def merge_update(self, __m: Optional[Any] = ..., **kwargs: Any) -> None: ...
def setdefault(self, item: Any, default: Optional[Any] = ...): ...
def to_json(
self, filename: Union[str, PathLike] = ..., encoding: str = ..., errors: str = ..., **json_kwargs: Any
) -> Any: ...
@classmethod
def from_json(
cls: Any,
json_string: str = ...,
filename: Union[str, PathLike] = ...,
encoding: str = ...,
errors: str = ...,
**kwargs: Any,
) -> Box: ...
def to_yaml(
self,
filename: Union[str, PathLike] = ...,
default_flow_style: bool = ...,
encoding: str = ...,
errors: str = ...,
**yaml_kwargs: Any,
) -> Any: ...
@classmethod
def from_yaml(
cls: Any,
yaml_string: str = ...,
filename: Union[str, PathLike] = ...,
encoding: str = ...,
errors: str = ...,
**kwargs: Any,
) -> Box: ...
def to_toml(self, filename: Union[str, PathLike] = ..., encoding: str = ..., errors: str = ...) -> Any: ...
@classmethod
def from_toml(
cls: Any,
toml_string: str = ...,
filename: Union[str, PathLike] = ...,
encoding: str = ...,
errors: str = ...,
**kwargs: Any,
) -> Box: ...
def to_msgpack(self, filename: Union[str, PathLike] = ..., **kwargs: Any) -> Any: ...
@classmethod
def from_msgpack(
cls: Any, msgpack_bytes: bytes = ..., filename: Union[str, PathLike] = ..., **kwargs: Any
) -> Box: ...
Loading

0 comments on commit 51042b8

Please sign in to comment.