diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml index f7d8c27..a9d01fc 100644 --- a/.github/workflows/pythonpublish.yml +++ b/.github/workflows/pythonpublish.yml @@ -18,7 +18,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.11' - name: Install Dependencies run: | @@ -39,7 +39,7 @@ jobs: strategy: matrix: os: [macos-11, windows-latest] - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11"] runs-on: ${{ matrix.os }} steps: @@ -51,7 +51,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install setuptools wheel twine Cython --upgrade + pip install setuptools wheel twine Cython==3.0.0 --upgrade - name: Build and publish env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} @@ -64,20 +64,21 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Set up Python 3.10 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.11" - - uses: RalfG/python-wheels-manylinux-build@v0.7.1-manylinux2010_x86_64 - with: - python-versions: 'cp37-cp37m cp38-cp38 cp39-cp39 cp310-cp310' - build-requirements: 'cython' - - - name: Install dependencies + - name: Build wheels run: | python -m pip install --upgrade pip - pip install twine --upgrade + pip install cibuildwheel + python -m cibuildwheel --output-dir dist + env: + CIBW_BUILD: cp38-manylinux_x86_64 cp39-manylinux_x86_64 cp310-manylinux_x86_64 cp311-manylinux_x86_64 cp311-macosx_x86_64 + CIBW_BEFORE_BUILD: pip install Cython==3.0.0 + CIBW_BEFORE_TEST: pip install -r requirements.txt -r requirements-test.txt setuptools wheel twine + CIBW_TEST_COMMAND: pytest {package}/test -vv - name: Publish env: @@ -85,3 +86,4 @@ jobs: TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | twine upload dist/*-manylinux*.whl + twine upload dist/*-macosx*.whl diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 35eff83..db506f9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ jobs: package-checks: strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "pypy-3.8"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12-dev", "pypy-3.8"] os: [ubuntu-latest, macos-11, windows-latest] runs-on: ${{ matrix.os }} steps: @@ -31,7 +31,7 @@ jobs: python -m pip install --upgrade pip pip install -r requirements.txt pip install -r requirements-test.txt - pip install coveralls flake8 flake8-print mypy setuptools wheel twine Cython + pip install coveralls flake8 flake8-print mypy setuptools wheel twine Cython==3.0.0 - name: Lint with flake8 run: | # stop the build if there are Python syntax errors, undefined names or print statements @@ -49,16 +49,16 @@ jobs: run: | pip install dist/*.whl rm -rf box - python -m pytest + python -m pytest -vv - name: Test packaged wheel on Windows if: matrix.os == 'windows-latest' run: | $wheel = (Get-ChildItem dist\*.whl | Sort lastWriteTime | Select-Object -last 1).Name pip install dist\${wheel} Remove-item box -recurse -force - python -m pytest + python -m pytest -vv - name: Upload wheel artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: python_box path: dist/*.whl @@ -67,36 +67,29 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Set up Python 3.10 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.11" - uses: actions/cache@v3 with: path: ~/.cache/pip key: package-manylinux-check-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements-test.txt') }} - - name: Install dependencies + - name: Build wheels run: | python -m pip install --upgrade pip - pip install -r requirements.txt - pip install -r requirements-test.txt - pip install coveralls flake8 flake8-print mypy setuptools wheel twine Cython - - - uses: RalfG/python-wheels-manylinux-build@v0.7.1-manylinux2010_x86_64 - with: - python-versions: 'cp37-cp37m cp38-cp38 cp39-cp39 cp310-cp310' - build-requirements: 'cython' - - - name: Test packaged wheel on linux - run: | - pip install dist/*cp310-manylinux*.whl - rm -rf box - python -m pytest + pip install cibuildwheel + python -m cibuildwheel --output-dir dist + env: + CIBW_BUILD: cp38-manylinux_x86_64 cp39-manylinux_x86_64 cp310-manylinux_x86_64 cp311-manylinux_x86_64 cp311-macosx_x86_64 + CIBW_BEFORE_BUILD: pip install Cython==3.0.0 + CIBW_BEFORE_TEST: pip install -r requirements.txt -r requirements-test.txt setuptools wheel twine + CIBW_TEST_COMMAND: pytest {package}/test -vv - name: Upload wheel artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: python_box path: dist/*-manylinux*.whl @@ -104,7 +97,7 @@ jobs: test: strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11"] os: [ubuntu-latest, macos-11, windows-latest] runs-on: ${{ matrix.os }} steps: @@ -122,10 +115,10 @@ jobs: python -m pip install --upgrade pip pip install -r requirements.txt pip install -r requirements-test.txt - pip install setuptools wheel Cython + pip install setuptools wheel Cython==3.0.0 python setup.py build_ext --inplace - name: Test with pytest env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - pytest --cov=box test/ + pytest --cov=box -vv test/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2fc0ba4..eb3c1f0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: exclude: ^test/data/.+ - repo: https://github.com/ambv/black - rev: 23.1.0 + rev: 23.7.0 hooks: - id: black args: [--config=.black.toml] @@ -51,7 +51,7 @@ repos: always_run: true - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v1.0.1' + rev: 'v1.4.1' hooks: - id: mypy types: [python] diff --git a/AUTHORS.rst b/AUTHORS.rst index 1fb7e2d..b67a735 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -29,6 +29,8 @@ Code contributions: - Ivan Pepelnjak (ipspace) - Michał Górny (mgorny) - Serge Lu (Serge45) +- Eric Prestat (ericpre) + Suggestions and bug reporting: @@ -87,3 +89,5 @@ Suggestions and bug reporting: - Peter B (barmettl) - Ash A. (dragonpaw) - Коптев Роман Викторович (romikforest) +- lei wang (191801737) +- d00m514y3r diff --git a/CHANGES.rst b/CHANGES.rst index eb7921b..20fc77f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,17 @@ Changelog ========= +Version 7.1.0 +------------- + +* Adding #255 defer ipython import for large import speed improvements (thanks to Eric Prestat) +* Adding testing for Python 3.12 +* Fixing #253 merge_update box list merge types not populated to sub dictionaries (thanks to lei wang) +* Fixing #257 Two test failures due to arguments having incorrect types (thanks to Michał Górny) +* Fixing stub files to match latest code signatures +* Removing #251 support for circular references in lists (thanks to d00m514y3r) +* Removing support for Python 3.7 as it is EOL + Version 7.0.1 ------------- diff --git a/box/__init__.py b/box/__init__.py index b2225bb..64d0ca8 100644 --- a/box/__init__.py +++ b/box/__init__.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- __author__ = "Chris Griffith" -__version__ = "7.0.1" +__version__ = "7.1.0" from box.box import Box from box.box_list import BoxList diff --git a/box/box.py b/box/box.py index 322b313..158a55e 100644 --- a/box/box.py +++ b/box/box.py @@ -10,7 +10,7 @@ import warnings from keyword import iskeyword from os import PathLike -from typing import Any, Dict, Generator, List, Optional, Tuple, Type, Union +from typing import Any, Dict, Generator, List, Optional, Tuple, Type, Union, Literal from inspect import signature try: @@ -18,12 +18,6 @@ except ImportError: from collections.abc import Callable, Iterable, Mapping -try: - from IPython import get_ipython -except ImportError: - ipython = False -else: - ipython = True if get_ipython() else False import box from box.converters import ( @@ -56,6 +50,17 @@ NO_NAMESPACE = object() +def _is_ipython(): + try: + from IPython import get_ipython + except ImportError: + ipython = False + else: + ipython = True if get_ipython() else False + + return ipython + + def _exception_cause(e): """ Unwrap BoxKeyError and BoxValueError errors to their cause. @@ -201,7 +206,7 @@ def __new__( box_recast: Optional[Dict] = None, box_dots: bool = False, box_class: Optional[Union[Dict, Type["Box"]]] = None, - box_namespace: Tuple[str, ...] = (), + box_namespace: Union[Tuple[str, ...], Literal[False]] = (), **kwargs: Any, ): """ @@ -248,7 +253,7 @@ def __init__( box_recast: Optional[Dict] = None, box_dots: bool = False, box_class: Optional[Union[Dict, Type["Box"]]] = None, - box_namespace: Tuple[str, ...] = (), + box_namespace: Union[Tuple[str, ...], Literal[False]] = (), **kwargs: Any, ): super().__init__() @@ -380,7 +385,7 @@ def __hash__(self): return hashing raise BoxTypeError('unhashable type: "Box"') - def __dir__(self): + def __dir__(self) -> List[str]: items = set(super().__dir__()) # Only show items accessible by dot notation for key in self.keys(): @@ -483,7 +488,7 @@ def __setstate__(self, state): self.__dict__.update(state) def __get_default(self, item, attr=False): - if ipython and item in ("getdoc", "shape"): + if item in ("getdoc", "shape") and _is_ipython(): return None default_value = self._box_config["default_box_attr"] if default_value in (self._box_config["box_class"], dict): @@ -589,6 +594,12 @@ def __getitem__(self, item, _ignore_default=False): if item == "_box_config": cause = _exception_cause(err) raise BoxKeyError("_box_config should only exist as an attribute and is never defaulted") from cause + if isinstance(item, slice): + # In Python 3.12 this changes to a KeyError instead of TypeError + new_box = self._box_config["box_class"](**self.__box_config()) + for x in list(super().keys())[item.start : item.stop : item.step]: + new_box[x] = self[x] + return new_box if self._box_config["box_dots"] and isinstance(item, str) and ("." in item or "[" in item): try: first_item, children = _parse_box_dots(self, item) @@ -823,7 +834,7 @@ def convert_and_set(k, v): # in the `converted` box_config set v = self._box_config["box_class"](v, **self.__box_config(extra_namespace=k)) if k in self and isinstance(self[k], dict): - self[k].merge_update(v) + self[k].merge_update(v, box_merge_lists=merge_type) return if isinstance(v, list) and not intact_type: v = box.BoxList(v, **self.__box_config(extra_namespace=k)) diff --git a/box/box.pyi b/box/box.pyi index 02ba3d4..9d56ab3 100644 --- a/box/box.pyi +++ b/box/box.pyi @@ -1,10 +1,11 @@ +from _typeshed import Incomplete from collections.abc import Mapping from os import PathLike -from typing import Any, Dict, Generator, List, Optional, Tuple, Union +from typing import Any, Dict, Generator, List, Optional, Tuple, Type, Union, Literal class Box(dict): def __new__( - cls: Any, + cls, *args: Any, default_box: bool = ..., default_box_attr: Any = ..., @@ -17,11 +18,12 @@ class Box(dict): box_safe_prefix: str = ..., box_duplicates: str = ..., box_intact_types: Union[Tuple, List] = ..., - box_recast: Dict = ..., + box_recast: Optional[Dict] = ..., box_dots: bool = ..., - box_class: Union[Dict, Box] = ..., + box_class: Optional[Union[Dict, Type["Box"]]] = ..., + box_namespace: Union[Tuple[str, ...], Literal[False]] = ..., **kwargs: Any, - ) -> Any: ... + ): ... def __init__( self, *args: Any, @@ -36,82 +38,84 @@ class Box(dict): box_safe_prefix: str = ..., box_duplicates: str = ..., box_intact_types: Union[Tuple, List] = ..., - box_recast: Dict = ..., + box_recast: Optional[Dict] = ..., box_dots: bool = ..., - box_class: Union[Dict, Box] = ..., + box_class: Optional[Union[Dict, Type["Box"]]] = ..., + box_namespace: Union[Tuple[str, ...], Literal[False]] = ..., **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: ... # type: ignore[override] - 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 __add__(self, other: Mapping[Any, Any]): ... + def __radd__(self, other: Mapping[Any, Any]): ... + def __iadd__(self, other: Mapping[Any, Any]): ... + def __or__(self, other: Mapping[Any, Any]): ... + def __ror__(self, other: Mapping[Any, Any]): ... + def __ior__(self, other: Mapping[Any, Any]): ... # type: ignore[override] + def __sub__(self, other: Mapping[Any, Any]): ... + def __hash__(self): ... + def __dir__(self) -> List[str]: ... + def __contains__(self, item) -> bool: ... + def keys(self, dotted: Union[bool] = ...): ... + def items(self, dotted: Union[bool] = ...): ... + def get(self, key, default=...): ... def copy(self) -> Box: ... def __copy__(self) -> Box: ... - def __deepcopy__(self, memodict: Any = ...) -> Box: ... - def __getitem__(self, item: Any, _ignore_default: bool = ...) -> Any: ... - def __getattr__(self, item: Any) -> 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 __deepcopy__(self, memodict: Incomplete | None = ...) -> Box: ... + def __getitem__(self, item, _ignore_default: bool = ...): ... + def __getattr__(self, item): ... + def __setitem__(self, key, value): ... + def __setattr__(self, key, value): ... + def __delitem__(self, key): ... + def __delattr__(self, item) -> None: ... + def pop(self, key, *args): ... 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 update(self, *args, **kwargs) -> None: ... + def merge_update(self, *args, **kwargs) -> None: ... + def setdefault(self, item, default: Incomplete | None = ...): ... def to_json( - self, filename: Union[str, PathLike] = ..., encoding: str = ..., errors: str = ..., **json_kwargs: Any - ) -> Any: ... + self, filename: Optional[Union[str, PathLike]] = ..., encoding: str = ..., errors: str = ..., **json_kwargs + ): ... @classmethod def from_json( - cls: Any, - json_string: str = ..., - filename: Union[str, PathLike] = ..., + cls, + json_string: Optional[str] = ..., + filename: Optional[Union[str, PathLike]] = ..., encoding: str = ..., errors: str = ..., - **kwargs: Any, + **kwargs, ) -> Box: ... def to_yaml( self, - filename: Union[str, PathLike] = ..., + filename: Optional[Union[str, PathLike]] = ..., default_flow_style: bool = ..., encoding: str = ..., errors: str = ..., - **yaml_kwargs: Any, - ) -> Any: ... + **yaml_kwargs, + ): ... @classmethod def from_yaml( - cls: Any, - yaml_string: str = ..., - filename: Union[str, PathLike] = ..., + cls, + yaml_string: Optional[str] = ..., + filename: Optional[Union[str, PathLike]] = ..., encoding: str = ..., errors: str = ..., - **kwargs: Any, + **kwargs, ) -> Box: ... - def to_toml(self, filename: Union[str, PathLike] = ..., encoding: str = ..., errors: str = ...) -> Any: ... + def to_toml(self, filename: Optional[Union[str, PathLike]] = ..., encoding: str = ..., errors: str = ...): ... @classmethod def from_toml( - cls: Any, - toml_string: str = ..., - filename: Union[str, PathLike] = ..., + cls, + toml_string: Optional[str] = ..., + filename: Optional[Union[str, PathLike]] = ..., encoding: str = ..., errors: str = ..., - **kwargs: Any, + **kwargs, ) -> Box: ... - def to_msgpack(self, filename: Union[str, PathLike] = ..., **kwargs: Any) -> Any: ... + def to_msgpack(self, filename: Optional[Union[str, PathLike]] = ..., **kwargs): ... @classmethod def from_msgpack( - cls: Any, msgpack_bytes: bytes = ..., filename: Union[str, PathLike] = ..., **kwargs: Any + cls, msgpack_bytes: Optional[bytes] = ..., filename: Optional[Union[str, PathLike]] = ..., **kwargs ) -> Box: ... diff --git a/box/box_list.py b/box/box_list.py index 750f3f0..048b014 100644 --- a/box/box_list.py +++ b/box/box_list.py @@ -5,7 +5,7 @@ import copy import re from os import PathLike -from typing import Optional, Iterable, Type, Union +from typing import Optional, Iterable, Type, Union, List, Any import box from box.converters import ( @@ -22,7 +22,6 @@ _to_yaml, msgpack_available, toml_read_library, - toml_write_library, yaml_available, ) from box.exceptions import BoxError, BoxTypeError @@ -102,7 +101,7 @@ def _convert(self, p_object): elif isinstance(p_object, box.Box): p_object._box_config.update(self.box_options) if isinstance(p_object, list) and not self._is_intact_type(p_object): - p_object = self if id(p_object) == self.box_org_ref else self.__class__(p_object, **self.box_options) + p_object = self.__class__(p_object, **self.box_options) elif isinstance(p_object, BoxList): p_object.box_options.update(self.box_options) return p_object @@ -117,7 +116,7 @@ def extend(self, iterable): def insert(self, index, p_object): super().insert(index, self._convert(p_object)) - def _dotted_helper(self): + def _dotted_helper(self) -> List[str]: keys = [] for idx, item in enumerate(self): added = False @@ -150,15 +149,15 @@ def __deepcopy__(self, memo=None): out.append(copy.deepcopy(k, memo=memo)) return out - def __hash__(self): + def __hash__(self) -> int: # type: ignore[override] if self.box_options.get("frozen_box"): hashing = 98765 hashing ^= hash(tuple(self)) return hashing raise BoxTypeError("unhashable type: 'BoxList'") - def to_list(self): - new_list = [] + def to_list(self) -> List: + new_list: List[Any] = [] for x in self: if x is self: new_list.append(new_list) diff --git a/box/box_list.pyi b/box/box_list.pyi index dbd2007..982093f 100644 --- a/box/box_list.pyi +++ b/box/box_list.pyi @@ -1,19 +1,18 @@ import box from box.converters import ( - BOX_PARAMETERS, - msgpack_available, - yaml_available, - toml_read_library, - toml_write_library, + BOX_PARAMETERS as BOX_PARAMETERS, + msgpack_available as msgpack_available, + toml_read_library as toml_read_library, + toml_write_library as toml_write_library, + yaml_available as yaml_available, ) -from box.exceptions import BoxError as BoxError, BoxTypeError as BoxTypeError from os import PathLike as PathLike -from typing import Any, Iterable, Optional, Type, Union +from typing import Any, Iterable, Optional, Type, Union, List class BoxList(list): def __new__(cls, *args: Any, **kwargs: Any): ... - box_options: Any = ... - box_org_ref: Any = ... + box_options: Any + box_org_ref: Any def __init__(self, iterable: Iterable = ..., box_class: Type[box.Box] = ..., **box_options: Any) -> None: ... def __getitem__(self, item: Any): ... def __delitem__(self, key: Any): ... @@ -21,10 +20,11 @@ class BoxList(list): def append(self, p_object: Any) -> None: ... def extend(self, iterable: Any) -> None: ... def insert(self, index: Any, p_object: Any) -> None: ... - def __copy__(self): ... - def __deepcopy__(self, memo: Optional[Any] = ...): ... - def __hash__(self) -> Any: ... # type: ignore[override] - def to_list(self): ... + def __copy__(self) -> "BoxList": ... + def __deepcopy__(self, memo: Optional[Any] = ...) -> "BoxList": ... + def __hash__(self) -> int: ... # type: ignore[override] + def to_list(self) -> List: ... + def _dotted_helper(self) -> List[str]: ... def to_json( self, filename: Union[str, PathLike] = ..., @@ -35,7 +35,7 @@ class BoxList(list): ) -> Any: ... @classmethod def from_json( - cls: Any, + cls, json_string: str = ..., filename: Union[str, PathLike] = ..., encoding: str = ..., @@ -53,7 +53,7 @@ class BoxList(list): ) -> Any: ... @classmethod def from_yaml( - cls: Any, + cls, yaml_string: str = ..., filename: Union[str, PathLike] = ..., encoding: str = ..., @@ -65,7 +65,7 @@ class BoxList(list): ) -> Any: ... @classmethod def from_toml( - cls: Any, + cls, toml_string: str = ..., filename: Union[str, PathLike] = ..., key_name: str = ..., @@ -75,12 +75,9 @@ class BoxList(list): ) -> Any: ... 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 - ) -> Any: ... + def from_msgpack(cls, msgpack_bytes: bytes = ..., filename: Union[str, PathLike] = ..., **kwargs: Any) -> Any: ... def to_csv(self, filename: Union[str, PathLike] = ..., encoding: str = ..., errors: str = ...) -> Any: ... @classmethod def from_csv( - cls: Any, csv_string: str = ..., filename: Union[str, PathLike] = ..., encoding: str = ..., errors: str = ... + cls, csv_string: str = ..., filename: Union[str, PathLike] = ..., encoding: str = ..., errors: str = ... ) -> Any: ... - def _dotted_helper(self) -> Any: ... diff --git a/box/config_box.py b/box/config_box.py index 0d42b3b..0202ca3 100644 --- a/box/config_box.py +++ b/box/config_box.py @@ -1,5 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +from typing import List from box.box import Box @@ -29,7 +30,7 @@ def __getattr__(self, item): except AttributeError: return super().__getattr__(item.lower()) - def __dir__(self): + def __dir__(self) -> List[str]: return super().__dir__() + ["bool", "int", "float", "list", "getboolean", "getfloat", "getint"] def bool(self, item, default=None): diff --git a/box/config_box.pyi b/box/config_box.pyi index 75afb44..1022f1b 100644 --- a/box/config_box.pyi +++ b/box/config_box.pyi @@ -1,9 +1,9 @@ from box.box import Box as Box -from typing import Any, Optional +from typing import Any, Optional, List class ConfigBox(Box): def __getattr__(self, item: Any): ... - def __dir__(self): ... + def __dir__(self) -> List[str]: ... def bool(self, item: Any, default: Optional[Any] = ...): ... def int(self, item: Any, default: Optional[Any] = ...): ... def float(self, item: Any, default: Optional[Any] = ...): ... @@ -11,5 +11,5 @@ class ConfigBox(Box): def getboolean(self, item: Any, default: Optional[Any] = ...): ... def getint(self, item: Any, default: Optional[Any] = ...): ... def getfloat(self, item: Any, default: Optional[Any] = ...): ... - def copy(self): ... - def __copy__(self): ... + def copy(self) -> "ConfigBox": ... + def __copy__(self) -> "ConfigBox": ... diff --git a/box/converters.py b/box/converters.py index 29da488..3bb003b 100644 --- a/box/converters.py +++ b/box/converters.py @@ -5,7 +5,6 @@ import csv import json -import sys from io import StringIO from os import PathLike from pathlib import Path @@ -34,6 +33,19 @@ toml_write_library: Optional[Any] = None toml_decode_error: Optional[Callable] = None +__all__ = [ + "_to_json", + "_to_yaml", + "_to_toml", + "_to_csv", + "_to_msgpack", + "_from_json", + "_from_yaml", + "_from_toml", + "_from_csv", + "_from_msgpack", +] + class BoxTomlDecodeError(BoxError): """Toml Decode Error""" diff --git a/box/converters.pyi b/box/converters.pyi index 5f94f33..43d2020 100644 --- a/box/converters.pyi +++ b/box/converters.pyi @@ -1,6 +1,5 @@ -from box.exceptions import BoxError as BoxError -from os import PathLike as PathLike -from typing import Any, Union, Optional, Dict, Callable +from typing import Any, Callable, Optional, Union, Dict +from os import PathLike yaml_available: bool toml_available: bool @@ -10,57 +9,52 @@ toml_read_library: Optional[Any] toml_write_library: Optional[Any] toml_decode_error: Optional[Callable] -def _exists(filename: Union[str, PathLike], create: bool = False) -> Any: ... def _to_json( - obj, filename: Optional[Union[str, PathLike]] = None, encoding: str = "utf-8", errors: str = "strict", **json_kwargs -) -> Any: ... + obj, filename: Optional[Union[str, PathLike]] = ..., encoding: str = ..., errors: str = ..., **json_kwargs +): ... def _from_json( - json_string: Optional[str] = None, - filename: Optional[Union[str, PathLike]] = None, - encoding: str = "utf-8", - errors: str = "strict", - multiline: bool = False, + json_string: Optional[str] = ..., + filename: Optional[Union[str, PathLike]] = ..., + encoding: str = ..., + errors: str = ..., + multiline: bool = ..., **kwargs, -) -> Any: ... +): ... def _to_yaml( obj, - filename: Optional[Union[str, PathLike]] = None, - default_flow_style: bool = False, - encoding: str = "utf-8", - errors: str = "strict", - ruamel_typ: str = "rt", - ruamel_attrs: Optional[Dict] = None, + filename: Optional[Union[str, PathLike]] = ..., + default_flow_style: bool = ..., + encoding: str = ..., + errors: str = ..., + ruamel_typ: str = ..., + ruamel_attrs: Optional[Dict] = ..., **yaml_kwargs, -) -> Any: ... +): ... def _from_yaml( - yaml_string: Optional[str] = None, - filename: Optional[Union[str, PathLike]] = None, - encoding: str = "utf-8", - errors: str = "strict", - ruamel_typ: str = "rt", - ruamel_attrs: Optional[Dict] = None, + yaml_string: Optional[str] = ..., + filename: Optional[Union[str, PathLike]] = ..., + encoding: str = ..., + errors: str = ..., + ruamel_typ: str = ..., + ruamel_attrs: Optional[Dict] = ..., **kwargs, -) -> Any: ... -def _to_toml( - obj, filename: Optional[Union[str, PathLike]] = None, encoding: str = "utf-8", errors: str = "strict" -) -> Any: ... +): ... +def _to_toml(obj, filename: Optional[Union[str, PathLike]] = ..., encoding: str = ..., errors: str = ...): ... def _from_toml( - toml_string: Optional[str] = None, - filename: Optional[Union[str, PathLike]] = None, - encoding: str = "utf-8", - errors: str = "strict", -) -> Any: ... -def _to_msgpack(obj, filename: Optional[Union[str, PathLike]] = None, **kwargs) -> Any: ... -def _from_msgpack( - msgpack_bytes: Optional[bytes] = None, filename: Optional[Union[str, PathLike]] = None, **kwargs -) -> Any: ... + toml_string: Optional[str] = ..., + filename: Optional[Union[str, PathLike]] = ..., + encoding: str = ..., + errors: str = ..., +): ... +def _to_msgpack(obj, filename: Optional[Union[str, PathLike]] = ..., **kwargs): ... +def _from_msgpack(msgpack_bytes: Optional[bytes] = ..., filename: Optional[Union[str, PathLike]] = ..., **kwargs): ... def _to_csv( - box_list, filename: Optional[Union[str, PathLike]] = None, encoding: str = "utf-8", errors: str = "strict", **kwargs -) -> Any: ... + box_list, filename: Optional[Union[str, PathLike]] = ..., encoding: str = ..., errors: str = ..., **kwargs +): ... def _from_csv( - csv_string: Optional[str] = None, - filename: Optional[Union[str, PathLike]] = None, - encoding: str = "utf-8", - errors: str = "strict", + csv_string: Optional[str] = ..., + filename: Optional[Union[str, PathLike]] = ..., + encoding: str = ..., + errors: str = ..., **kwargs, -) -> Any: ... +): ... diff --git a/box/from_file.pyi b/box/from_file.pyi index 00657eb..9e8be8a 100644 --- a/box/from_file.pyi +++ b/box/from_file.pyi @@ -1,9 +1,16 @@ -from box.box import Box -from box.box_list import BoxList +from box.box import Box as Box +from box.box_list import BoxList as BoxList from os import PathLike from typing import Any, Union def box_from_file( - file: Union[str, PathLike], file_type: str = ..., encoding: str = ..., errors: str = ..., **kwargs: Any + file: Union[str, PathLike], + file_type: str = ..., + encoding: str = ..., + errors: str = ..., + **kwargs: Any, +) -> Union[Box, BoxList]: ... +def box_from_string( + content: str, + string_type: str = ..., ) -> Union[Box, BoxList]: ... -def box_from_string(content: str, string_type: str = ...) -> Union[Box, BoxList]: ... diff --git a/box/shorthand_box.py b/box/shorthand_box.py index 5d24604..99dfc8d 100644 --- a/box/shorthand_box.py +++ b/box/shorthand_box.py @@ -1,5 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +from typing import Dict from box.box import Box @@ -27,28 +28,28 @@ class SBox(Box): ] @property - def dict(self): + def dict(self) -> Dict: return self.to_dict() @property - def json(self): + def json(self) -> str: return self.to_json() @property - def yaml(self): + def yaml(self) -> str: return self.to_yaml() @property - def toml(self): + def toml(self) -> str: return self.to_toml() def __repr__(self): return f"SBox({self})" - def copy(self): + def copy(self) -> "SBox": return SBox(super(SBox, self).copy()) - def __copy__(self): + def __copy__(self) -> "SBox": return SBox(super(SBox, self).copy()) @@ -64,5 +65,5 @@ def __new__(cls, *args, **kwargs): obj._box_config["default_box"] = True return obj - def __repr__(self): + def __repr__(self) -> str: return f"DDBox({self})" diff --git a/box/shorthand_box.pyi b/box/shorthand_box.pyi index 4b86f73..deef693 100644 --- a/box/shorthand_box.pyi +++ b/box/shorthand_box.pyi @@ -1,15 +1,17 @@ +from typing import Dict + from box.box import Box as Box class SBox(Box): @property - def dict(self): ... + def dict(self) -> Dict: ... @property - def json(self): ... + def json(self) -> str: ... @property - def yaml(self): ... + def yaml(self) -> str: ... @property - def toml(self): ... - def copy(self): ... - def __copy__(self): ... + def toml(self) -> str: ... + def copy(self) -> "SBox": ... + def __copy__(self) -> "SBox": ... class DDBox(Box): ... diff --git a/requirements-dev.txt b/requirements-dev.txt index cb6ad4c..e41d73f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ # Files needed for pre-commit hooks black>=23.1.0 -Cython>=0.29 +Cython==3.0.0 mypy>=1.0.1 pre-commit>=2.21.0 diff --git a/setup.py b/setup.py index 0f0519e..3781c42 100644 --- a/setup.py +++ b/setup.py @@ -45,18 +45,17 @@ py_modules=["box"], packages=["box"], ext_modules=extra, - python_requires=">=3.7", + python_requires=">=3.8", include_package_data=True, platforms="any", classifiers=[ "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Development Status :: 5 - Production/Stable", "Natural Language :: English", diff --git a/test/test_box.py b/test/test_box.py index c7b51b7..d9012e9 100644 --- a/test/test_box.py +++ b/test/test_box.py @@ -588,15 +588,6 @@ def test_circular_references(self): bx.to_json() - circular_list = [] - circular_list.append(circular_list) - bl = BoxList(circular_list) - assert bl == bl[0] - assert isinstance(bl[0], BoxList) - circular_list_2 = bl.to_list() - assert circular_list_2 == circular_list_2[0] - assert isinstance(circular_list_2, list) - def test_to_multiline(self): a = BoxList([Box(a=1), Box(b=2), Box(three=5)]) @@ -1264,6 +1255,18 @@ def test_merge_list_options(self): a.merge_update({"key1": {"new": 5}, "Key 2": {"add_key": 6}, "lister": ["a"]}) assert a.lister == ["a"] + d1 = {"app": {"S3": {"S3Service": [{"bucket": "bucket001"}]}}} + + d2 = {"app": {"S3": {"S3Service": [{"expirationDate": "2099-10-25"}]}}} + + box1 = Box(d1) + + box1.merge_update(d2, box_merge_lists="extend") + + assert box1 == Box( + {"app": {"S3": {"S3Service": [{"bucket": "bucket001"}, {"expirationDate": "2099-10-25"}]}}} + ), box1 + def test_box_from_empty_yaml(self): out = Box.from_yaml("---") assert out == Box() @@ -1331,7 +1334,7 @@ def test_box_kwargs_should_not_be_included(self): "box_dots": True, "modify_tuples_box": True, "box_intact_types": (), - "box_recast": True, + "box_recast": {"id": int}, } bx = Box(**params) diff --git a/test/test_box_list.py b/test/test_box_list.py index e68e521..536520f 100644 --- a/test/test_box_list.py +++ b/test/test_box_list.py @@ -5,6 +5,8 @@ import json import os import shutil +import sys +import platform from pathlib import Path from io import StringIO from test.common import test_root, tmp_dir @@ -220,3 +222,16 @@ def test_box_config_propagate(self): assert item._box_config["default_box"] is True elif isinstance(item, BoxList): assert item.box_options["default_box"] is True + + def test_no_recursion_errors(self): + a = Box({"list_of_dicts": [[{"example1": 1}]]}) + a.list_of_dicts.append([{"example2": 2}]) + assert a["list_of_dicts"][1] == [{"example2": 2}] + + def test_no_circular_references(self): + if sys.version_info >= (3, 12) and sys.platform == "win32": + pytest.skip("Windows fatal exception: stack overflow on github actions") + circular_list = [] + circular_list.append(circular_list) + with pytest.raises(RecursionError): + BoxList(circular_list)