diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ba8e2bf..d350624 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -30,18 +30,15 @@ jobs: - name: Check out code uses: actions/checkout@main - - name: Set up Python 3.10 + - name: Set up Python 3.12 uses: actions/setup-python@main with: - python-version: "3.10" + python-version: "3.12" - name: Setup flake8 annotations - uses: rbialon/flake8-annotations@v1.1 - - - name: Lint with flake8 - run: | - pip install flake8 - flake8 setup.py dpath/ tests/ + uses: TrueBrain/actions-flake8@v2.3 + with: + path: setup.py dpath/ tests/ # Generate a common hashseed for all tests generate-hashseed: @@ -70,7 +67,7 @@ jobs: strategy: matrix: # Match versions specified in tox.ini - python-version: ['3.8', '3.9', '3.10', 'pypy-3.7'] + python-version: ['3.8', '3.9', '3.10', '3.11', 'pypy-3.7', '3.12'] # Steps represent a sequence of tasks that will be executed as part of the job steps: diff --git a/dpath/__init__.py b/dpath/__init__.py index 9f56e6b..2bc9942 100644 --- a/dpath/__init__.py +++ b/dpath/__init__.py @@ -1,3 +1,6 @@ +# Needed for pre-3.10 versions +from __future__ import annotations + __all__ = [ "new", "delete", @@ -48,7 +51,7 @@ def _split_path(path: Path, separator: Optional[str] = "/") -> Union[List[PathSe return split_segments -def new(obj: MutableMapping, path: Path, value, separator="/", creator: Creator = None) -> MutableMapping: +def new(obj: MutableMapping, path: Path, value, separator="/", creator: Creator | None = None) -> MutableMapping: """ Set the element at the terminus of path to value, and create it if it does not exist (as opposed to 'set' that can only @@ -68,7 +71,7 @@ def new(obj: MutableMapping, path: Path, value, separator="/", creator: Creator return segments.set(obj, split_segments, value) -def delete(obj: MutableMapping, glob: Glob, separator="/", afilter: Filter = None) -> int: +def delete(obj: MutableMapping, glob: Glob, separator="/", afilter: Filter | None = None) -> int: """ Given a obj, delete all elements that match the glob. @@ -127,7 +130,7 @@ def f(obj, pair, counter): return deleted -def set(obj: MutableMapping, glob: Glob, value, separator="/", afilter: Filter = None) -> int: +def set(obj: MutableMapping, glob: Glob, value, separator="/", afilter: Filter | None = None) -> int: """ Given a path glob, set all existing elements in the document to the given value. Returns the number of elements changed. @@ -167,7 +170,7 @@ def get( If more than one leaf matches the glob, ValueError is raised. If the glob is not found and a default is not provided, KeyError is raised. """ - if glob == "/": + if isinstance(glob, str) and glob == "/" or len(glob) == 0: return obj globlist = _split_path(glob, separator) @@ -193,7 +196,7 @@ def f(_, pair, results): return results[0] -def values(obj: MutableMapping, glob: Glob, separator="/", afilter: Filter = None, dirs=True): +def values(obj: MutableMapping, glob: Glob, separator="/", afilter: Filter | None = None, dirs=True): """ Given an object and a path glob, return an array of all values which match the glob. The arguments to this function are identical to those of search(). @@ -203,13 +206,13 @@ def values(obj: MutableMapping, glob: Glob, separator="/", afilter: Filter = Non return [v for p, v in search(obj, glob, yielded, separator, afilter, dirs)] -def search(obj: MutableMapping, glob: Glob, yielded=False, separator="/", afilter: Filter = None, dirs=True): +def search(obj: MutableMapping, glob: Glob, yielded=False, separator="/", afilter: Filter | None = None, dirs=True): """ Given a path glob, return a dictionary containing all keys that matched the given glob. If 'yielded' is true, then a dictionary will not be returned. - Instead tuples will be yielded in the form of (path, value) for + Instead, tuples will be yielded in the form of (path, value) for every element in the document that matched the glob. """ @@ -218,7 +221,7 @@ def search(obj: MutableMapping, glob: Glob, yielded=False, separator="/", afilte def keeper(path, found): """ Generalized test for use in both yielded and folded cases. - Returns True if we want this result. Otherwise returns False. + Returns True if we want this result. Otherwise, returns False. """ if not dirs and not segments.leaf(found): return False @@ -245,7 +248,13 @@ def f(obj, pair, result): return segments.fold(obj, f, {}) -def merge(dst: MutableMapping, src: MutableMapping, separator="/", afilter: Filter = None, flags=MergeType.ADDITIVE): +def merge( + dst: MutableMapping, + src: MutableMapping, + separator="/", + afilter: Filter | None = None, + flags=MergeType.ADDITIVE +): """ Merge source into destination. Like dict.update() but performs deep merging. @@ -316,7 +325,7 @@ def merger(dst, src, _segments=()): target = segments.get(dst, current_path) # If the types don't match, replace it. - if type(found) != type(target) and not are_both_mutable(found, target): + if type(found) is not type(target) and not are_both_mutable(found, target): segments.set(dst, current_path, found) continue diff --git a/dpath/segments.py b/dpath/segments.py index c3c9846..f1443f0 100644 --- a/dpath/segments.py +++ b/dpath/segments.py @@ -4,7 +4,7 @@ from dpath import options from dpath.exceptions import InvalidGlob, InvalidKeyName, PathNotFound -from dpath.types import PathSegment, Creator, Hints, Glob, Path, SymmetricInt +from dpath.types import PathSegment, Creator, Hints, Glob, Path, ListIndex def make_walkable(node) -> Iterator[Tuple[PathSegment, Any]]: @@ -22,8 +22,8 @@ def make_walkable(node) -> Iterator[Tuple[PathSegment, Any]]: except AttributeError: try: indices = range(len(node)) - # Convert all list indices to object so negative indexes are supported. - indices = map(lambda i: SymmetricInt(i, len(node)), indices) + # Convert all list indices to objects so negative indices are supported. + indices = map(lambda i: ListIndex(i, len(node)), indices) return zip(indices, node) except TypeError: # This can happen in cases where the node isn't leaf(node) == True, diff --git a/dpath/types.py b/dpath/types.py index c4a4a56..4f10162 100644 --- a/dpath/types.py +++ b/dpath/types.py @@ -2,18 +2,18 @@ from typing import Union, Any, Callable, Sequence, Tuple, List, Optional, MutableMapping -class SymmetricInt(int): - """Same as a normal int but mimicks the behavior of list indexes (can be compared to a negative number).""" +class ListIndex(int): + """Same as a normal int but mimics the behavior of list indices (can be compared to a negative number).""" - def __new__(cls, value: int, max_value: int, *args, **kwargs): - if value >= max_value: + def __new__(cls, value: int, list_length: int, *args, **kwargs): + if value >= list_length: raise TypeError( f"Tried to initiate a {cls.__name__} with a value ({value}) " - f"greater than the provided max value ({max_value})" + f"greater than the provided max value ({list_length})" ) obj = super().__new__(cls, value) - obj.max_value = max_value + obj.list_length = list_length return obj @@ -21,13 +21,11 @@ def __eq__(self, other): if not isinstance(other, int): return False - if other >= self.max_value or other <= -self.max_value: - return False - - return int(self) == (self.max_value + other) % self.max_value + # Based on how Python sequences handle negative indices as described in footnote (3) of https://docs.python.org/3/library/stdtypes.html#common-sequence-operations + return other == int(self) or self.list_length + other == int(self) def __repr__(self): - return f"<{self.__class__.__name__} {int(self)}%{self.max_value}>" + return f"<{self.__class__.__name__} {int(self)}/{self.list_length}>" def __str__(self): return str(int(self)) diff --git a/dpath/version.py b/dpath/version.py index b82fac5..3c00bb4 100644 --- a/dpath/version.py +++ b/dpath/version.py @@ -1 +1 @@ -VERSION = "2.1.5" +VERSION = "2.2.0" diff --git a/setup.py b/setup.py index d34748e..28eb69c 100644 --- a/setup.py +++ b/setup.py @@ -48,6 +48,8 @@ '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', 'Topic :: Software Development :: Libraries :: Python Modules', 'Typing :: Typed', ], diff --git a/tests/test_get_values.py b/tests/test_get_values.py index 9eeef82..68d5d3b 100644 --- a/tests/test_get_values.py +++ b/tests/test_get_values.py @@ -2,7 +2,8 @@ import decimal import time -import mock +from unittest import mock + from nose2.tools.such import helper import dpath @@ -17,6 +18,9 @@ def test_util_get_root(): ret = dpath.get(x, '/') assert ret == x + ret = dpath.get(x, []) + assert ret == x + def test_get_explicit_single(): ehash = { diff --git a/tox.ini b/tox.ini index d613837..ff9a86d 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,7 @@ ignore = E501,E722 [tox] -envlist = pypy37, py38, py39, py310 +envlist = pypy37, py38, py39, py310, py311, py312 [gh-actions] python = @@ -15,10 +15,11 @@ python = 3.8: py38 3.9: py39 3.10: py310 + 3.11: py311 + 3.12: py312 [testenv] deps = hypothesis - mock nose2 commands = nose2 {posargs}