From 055f6a1b8b623156e08824c383adca7675b13ff6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 23:02:46 +0000 Subject: [PATCH 1/6] Remove Python 2 leftovers and a redundant copy from jsonpatch.py - drop the coding line, explicit object base classes and two-argument super() - drop JsonPatch.__nonzero__, which only Python 2 used - drop the __ne__ methods; Python 3 derives the same result from __eq__ - don't deep-copy the values of generated test operations in DiffBuilder: from_diff copies all operations of the patch since 2e37755, so they were copied twice Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_01PewzpjVeke4LTQ7Q88nyu6 --- doc/_static/.empty | 0 jsonpatch-stubs/__init__.pyi | 3 --- jsonpatch.py | 32 +++++++++++--------------------- 3 files changed, 11 insertions(+), 24 deletions(-) delete mode 100644 doc/_static/.empty diff --git a/doc/_static/.empty b/doc/_static/.empty deleted file mode 100644 index e69de29..0000000 diff --git a/jsonpatch-stubs/__init__.pyi b/jsonpatch-stubs/__init__.pyi index 6f251cc..f347490 100644 --- a/jsonpatch-stubs/__init__.pyi +++ b/jsonpatch-stubs/__init__.pyi @@ -65,7 +65,6 @@ class PatchOperation: def apply(self, obj: Any) -> Any: ... def __hash__(self) -> int: ... def __eq__(self, other: object) -> bool: ... - def __ne__(self, other: object) -> bool: ... @property def path(self) -> str: ... @property @@ -104,8 +103,6 @@ class JsonPatch: def __iter__(self) -> Iterator[dict[str, Any]]: ... def __hash__(self) -> int: ... def __eq__(self, other: object) -> bool: ... - def __ne__(self, other: object) -> bool: ... - __nonzero__ = __bool__ @classmethod def from_string( cls, diff --git a/jsonpatch.py b/jsonpatch.py index d8de4a0..d1e4e5f 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # python-json-patch - An implementation of the JSON Patch format # https://github.com/stefankoegl/python-json-patch # @@ -78,7 +76,7 @@ def __init__(self, *args, **kwargs): :type op: PatchOperation or dict or None """ self.op = kwargs.pop('op', None) - super(JsonPatchException, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) class InvalidJsonPatch(JsonPatchException): @@ -294,7 +292,7 @@ def path_join(parts, pointer_cls=JsonPointer): return from_parts(parts).path -class _NullJournal(object): +class _NullJournal: """Performs the changes of patch operations without recording them.""" @staticmethod @@ -317,7 +315,7 @@ def append(container, value): _NO_JOURNAL = _NullJournal() -class _Journal(object): +class _Journal: """Performs the changes of patch operations and records how to undo them. Each undo step refers to the changed container itself rather than to a @@ -437,7 +435,7 @@ def _reorder(mapping, keys): mapping[key] = mapping.pop(key) -class PatchOperation(object): +class PatchOperation: """A single operation inside a JSON Patch.""" def __init__(self, operation, pointer_cls=JsonPointer): @@ -499,9 +497,6 @@ def __eq__(self, other): # e.g. tests for 1 and for true behave differently return _json_equal(self.operation, other.operation) - def __ne__(self, other): - return not(self == other) - @property def path(self): """The location of the object or array that contains the target of @@ -625,7 +620,7 @@ def _apply(self, obj, journal): raise InvalidJsonPatch( "The operation does not contain a 'value' member", op=self) - # copied for the same reason as in AddOperation.apply + # copied for the same reason as in AddOperation._apply value = copy.deepcopy(value) subobj, part = self.pointer.to_last(obj) @@ -871,7 +866,7 @@ def _supports_journal(operation): return True -class JsonPatch(object): +class JsonPatch: """A JSON Patch is an iterable of Patch Operations. Accepts sequences, iterators, generators, and other iterables of operation @@ -975,8 +970,6 @@ def __str__(self): def __bool__(self): return bool(self.patch) - __nonzero__ = __bool__ - def __iter__(self): return iter(self.patch) @@ -988,9 +981,6 @@ def __eq__(self, other): return False return self._ops == other._ops - def __ne__(self, other): - return not(self == other) - @classmethod def from_string(cls, patch_str, loads=None, pointer_cls=JsonPointer): """Creates JsonPatch instance from string source. @@ -1187,7 +1177,7 @@ def from_diff(cls, src, dst, optimization=True, dumps=None, return cls(ops, pointer_cls=pointer_cls) -class DiffBuilder(object): +class DiffBuilder: def __init__(self, src_doc, dst_doc, dumps=json.dumps, pointer_cls=JsonPointer, optimization=True, generate_test_ops=False): @@ -1395,9 +1385,9 @@ def _insert_test(self, location, value): """ Inserts a test operation that checks the value at location, if test operations are generated """ if self.generate_test_ops: - # a copy, so that changing src afterwards does not change the test + # like all values, it is copied when the patch is created return self.insert({'op': 'test', 'path': location, - 'value': copy.deepcopy(value)}) + 'value': value}) def _item_replaced(self, path, key, item, old_item): location = self._location(_path_join(path, key)) @@ -1609,7 +1599,7 @@ def _sorted_members(value): # change, but their locations do not. -class _ArrayItem(object): +class _ArrayItem: """ An item that an array has at some point of the diff """ __slots__ = ('array', 'label', 'initial') @@ -1626,7 +1616,7 @@ def index(self): return bisect.bisect_left(self.array.present_labels, self.label) -class _ArrayItems(object): +class _ArrayItems: """ All items that an array has at some point of the diff, and the items that it has at the moment, both ordered by their labels. From 843b366ac93282668f979e2ab7c1898455e1c596 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 23:02:46 +0000 Subject: [PATCH 2/6] Close the input files of the jsonpatch command line program jsondiff closed its inputs, but jsonpatch never closed PATCH, and closed ORIGINAL only with --in-place. Both now use the same helper, which leaves stdin open. Also fixes a tab, trailing whitespace and blank lines that flake8 reported. Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_01PewzpjVeke4LTQ7Q88nyu6 --- jsonpatch_cli.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/jsonpatch_cli.py b/jsonpatch_cli.py index f0d5bf2..5b9e7a1 100644 --- a/jsonpatch_cli.py +++ b/jsonpatch_cli.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """ The jsondiff and jsonpatch command line programs """ import sys @@ -26,6 +24,16 @@ version='%(prog)s ' + jsonpatch.__version__) +def _input_files(*streams): + """ Closes the input files when the returned context is left; stdin is + left open, as it is not ours """ + inputs = ExitStack() + for stream in streams: + if stream is not sys.stdin: + inputs.enter_context(stream) + return inputs + + def jsondiff_main(): try: diff_files() @@ -36,10 +44,7 @@ def jsondiff_main(): def diff_files(): """ Diffs two JSON files and prints a patch """ args = jsondiff_parser.parse_args() - with ExitStack() as inputs: - for stream in (args.FILE1, args.FILE2): - if stream is not sys.stdin: - inputs.enter_context(stream) + with _input_files(args.FILE1, args.FILE2): doc1 = json.load(args.FILE1) doc2 = json.load(args.FILE2) patch = jsonpatch.make_patch(doc1, doc2, generate_test_ops=args.test_ops) @@ -67,6 +72,7 @@ def diff_files(): jsonpatch_parser.add_argument('-u', '--preserve-unicode', action='store_true', help='Output Unicode character as-is without using Code Point') + def jsonpatch_main(): try: patch_files() @@ -77,12 +83,13 @@ def jsonpatch_main(): def patch_files(): """ Applies a JSON patch on a JSON file """ args = jsonpatch_parser.parse_args() - doc = json.load(args.ORIGINAL) - result = jsonpatch.apply_patch(doc, args.PATCH.read()) + # The inputs are closed before anything is written, as Windows cannot + # replace ORIGINAL while it is open. + with _input_files(args.ORIGINAL, args.PATCH): + doc = json.load(args.ORIGINAL) + result = jsonpatch.apply_patch(doc, args.PATCH.read()) if args.in_place: - # 2026-10-11: Release the input handle before replacing it on Windows. - args.ORIGINAL.close() dirname = os.path.abspath(os.path.dirname(args.ORIGINAL.name)) fd = None @@ -91,7 +98,7 @@ def patch_files(): # creating a temporary file in the same directory as the # original file so we can atomically move the new file over # the original later. (This is done in the same directory - # because atomic renames do not work across mount points.) + # because atomic renames do not work across mount points.) fd, pathname = tempfile.mkstemp(dir=dirname) fp = os.fdopen(fd, 'w', encoding='utf-8') @@ -124,9 +131,9 @@ def patch_files(): fp = sys.stdout - # By this point we have some sort of file object we can write the + # By this point we have some sort of file object we can write the # modified JSON to. - + try: json.dump(result, fp, indent=args.indent, ensure_ascii=not(args.preserve_unicode)) fp.write('\n') From f0e9bf56715bb1a044a5ce9f003ca9e764ef016f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 23:02:46 +0000 Subject: [PATCH 3/6] Fix packaging and documentation inconsistencies - setup.py: close the files it reads, drop the package_data entry for requirements.txt, which matched nothing as there is no such package - MANIFEST.in: ship doc/conf.py, doc/Makefile, doc/requirements.txt, requirements-dev.txt and the makefile, so that the docs, which the sdist already contained, can be built from it, and its tests be run - README: fix the doubled slash in the badge links, link the tutorial on Read the Docs instead of pointing to the source code (the relative link to doc/tutorial.rst was broken on PyPI), say how to run the tests, and drop the note on expected failures, of which there are none any more - use readthedocs.io and datatracker.ietf.org, where the old hosts redirect to - doc: fix the mod-jsonpatch label, which was a comment, and typos in the tutorial; remove the empty _static directory, the missing _templates directory, u'' prefixes and the placeholder description from conf.py Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_01PewzpjVeke4LTQ7Q88nyu6 --- MANIFEST.in | 5 +++++ README.md | 20 +++++++++++++------- doc/conf.py | 18 +++++++++--------- doc/mod-jsonpatch.rst | 2 +- doc/tutorial.rst | 8 ++++---- setup.py | 18 ++++++++++-------- 6 files changed, 42 insertions(+), 29 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index e5a5b30..b437c22 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,8 +1,13 @@ include requirements.txt +include requirements-dev.txt include LICENSE include README.md include tests.py include doc/*.rst +include doc/conf.py +include doc/Makefile +include doc/requirements.txt +include makefile include property_tests.py include pytest.ini include ext_tests.py diff --git a/README.md b/README.md index 42deae1..72d6779 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ python-json-patch ================= -[![PyPI version](https://img.shields.io/pypi/v/jsonpatch.svg)](https://pypi.org/project/jsonpatch//) -[![Supported Python versions](https://img.shields.io/pypi/pyversions/jsonpatch.svg)](https://pypi.org/project/jsonpatch//) +[![PyPI version](https://img.shields.io/pypi/v/jsonpatch.svg)](https://pypi.org/project/jsonpatch/) +[![Supported Python versions](https://img.shields.io/pypi/pyversions/jsonpatch.svg)](https://pypi.org/project/jsonpatch/) [![Build Status](https://github.com/stefankoegl/python-json-patch/actions/workflows/test.yaml/badge.svg?branch=master)](https://github.com/stefankoegl/python-json-patch/actions/workflows/test.yaml) [![Coverage Status](https://coveralls.io/repos/github/stefankoegl/python-json-patch/badge.svg?branch=master)](https://coveralls.io/github/stefankoegl/python-json-patch?branch=master) @@ -12,11 +12,12 @@ Applying JSON Patches in Python Library to apply JSON Patches according to [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902) -See source code for examples +See the [tutorial](https://python-json-patch.readthedocs.io/en/latest/tutorial.html) +for examples. * Website: https://github.com/stefankoegl/python-json-patch * Repository: https://github.com/stefankoegl/python-json-patch.git -* Documentation: https://python-json-patch.readthedocs.org/ +* Documentation: https://python-json-patch.readthedocs.io/ * PyPI: https://pypi.org/project/jsonpatch/ * CI: https://github.com/stefankoegl/python-json-patch/actions * Coveralls: https://coveralls.io/github/stefankoegl/python-json-patch @@ -28,7 +29,8 @@ non-RFC string `append` operation. Its `from_diff()` method sends only the new suffix when an object member's string grows. `JsonPatch`, `make_patch()` and `apply_patch()` retain their standard RFC 6902 behavior. -See the [tutorial](doc/tutorial.rst#string-append-extension) for examples. +See the [tutorial](https://python-json-patch.readthedocs.io/en/latest/tutorial.html#string-append-extension) +for examples. Both sender and receiver must support the extension. Type hints @@ -45,6 +47,11 @@ typical usage in `typetests/usage.py` type-checks: python -m mypy.stubtest --mypy-config-file pyproject.toml jsonpatch cd typetests && mypy --config-file ../pyproject.toml usage.py +Running tests +------------- + pip install -r requirements.txt -r requirements-dev.txt + python tests.py + Running external tests ---------------------- To run external tests (such as those from https://github.com/json-patch/json-patch-tests) use ext_tests.py @@ -54,8 +61,7 @@ To run external tests (such as those from https://github.com/json-patch/json-pat Running property-based tests ---------------------------- Property-based tests using [Hypothesis](https://hypothesis.readthedocs.io/) -are in property_tests.py. Properties that the implementation does not satisfy -yet are marked as expected failures. +are in property_tests.py. pip install hypothesis python property_tests.py diff --git a/doc/conf.py b/doc/conf.py index 8cb3b38..5add877 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -34,7 +34,7 @@ nitpick_ignore = [('py:class', 'JsonPointer')] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = [] # The suffix of source filenames. source_suffix = '.rst' @@ -46,7 +46,7 @@ master_doc = 'index' # General information about the project. -project = u'python-json-patch' +project = 'python-json-patch' copyright = jsonpatch.__author__ # The version info for the project you're documenting, acts as replacement for @@ -127,7 +127,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. @@ -190,8 +190,8 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'python-json-patch.tex', u'python-json-patch Documentation', - u'Stefan Kögl', 'manual'), + ('index', 'python-json-patch.tex', 'python-json-patch Documentation', + 'Stefan Kögl', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -220,8 +220,8 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'python-json-patch', u'python-json-patch Documentation', - [u'Stefan Kögl'], 1) + ('index', 'python-json-patch', 'python-json-patch Documentation', + ['Stefan Kögl'], 1) ] # If true, show URL addresses after external links. @@ -234,8 +234,8 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'python-json-patch', u'python-json-patch Documentation', - u'Stefan Kögl', 'python-json-patch', 'One line description of project.', + ('index', 'python-json-patch', 'python-json-patch Documentation', + 'Stefan Kögl', 'python-json-patch', 'Apply JSON Patches (RFC 6902)', 'Miscellaneous'), ] diff --git a/doc/mod-jsonpatch.rst b/doc/mod-jsonpatch.rst index 0a0102b..f84bde4 100644 --- a/doc/mod-jsonpatch.rst +++ b/doc/mod-jsonpatch.rst @@ -1,4 +1,4 @@ -.. mod-jsonpatch: +.. _mod-jsonpatch: The ``jsonpatch`` module ======================== diff --git a/doc/tutorial.rst b/doc/tutorial.rst index b4c14e1..3da2d05 100644 --- a/doc/tutorial.rst +++ b/doc/tutorial.rst @@ -7,7 +7,7 @@ patch syntax. Creating a Patch ---------------- -Patches can be created in two ways. One way is to explicitly create a +Patches can be created in two ways. One way is to explicitly create a ``JsonPatch`` object from an iterable of operation dictionaries, such as a list, tuple, iterator, or generator. Non-sequence iterables are materialized before validation so that the patch can be applied repeatedly. @@ -198,7 +198,7 @@ given: Ignoring Missing Locations ~~~~~~~~~~~~~~~~~~~~~~~~~~ -`RFC 6902 `_ requires a +`RFC 6902 `_ requires a ``remove`` to fail if the location it removes doesn't exist, and this library follows it: such an operation raises ``JsonPatchPathNotFound``. A ``replace`` of a missing location, and a ``move`` or ``copy`` from one, raise it as well. @@ -469,7 +469,7 @@ Dealing with Custom Types ------------------------- Custom JSON dump and load functions can be used to support custom types such as -`decimal.Decimal`. The following examples shows how the +``decimal.Decimal``. The following example shows how the `simplejson `_ package, which has native support for Python's ``Decimal`` type, can be used to create a custom ``JsonPatch`` subclass with ``Decimal`` support: @@ -512,7 +512,7 @@ a dumps function to ``to_string``: >>> serialized_patch '[{"op": "add", "path": "/bar", "value": 1.10}]' -and load function to ``from_string``: +and a load function to ``from_string``: .. code-block:: python diff --git a/setup.py b/setup.py index e365115..76990b0 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,8 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - import re from setuptools import setup -src = open('jsonpatch.py', encoding='utf-8').read() +with open('jsonpatch.py', encoding='utf-8') as f: + src = f.read() metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", src)) docstrings = re.findall('"""([^"]*)"""', src, re.MULTILINE | re.DOTALL) @@ -15,7 +13,11 @@ 'jsonpatch_cli', ) -REQUIREMENTS = list(open('requirements.txt')) +with open('requirements.txt') as f: + REQUIREMENTS = [line.strip() for line in f if line.strip()] + +with open('README.md', encoding='utf-8') as f: + LONG_DESCRIPTION = f.read() AUTHOR_EMAIL = metadata['author'] VERSION = metadata['version'] @@ -50,7 +52,7 @@ setup(name=PACKAGE, version=VERSION, description=DESCRIPTION, - long_description=open('README.md', encoding='utf-8').read(), + long_description=LONG_DESCRIPTION, long_description_content_type='text/markdown', author=AUTHOR, author_email=EMAIL, @@ -60,7 +62,7 @@ # jsonpatch is a single-file module, which can't be marked as typed # with py.typed (PEP 561), so its type stubs are a stub-only package packages=['jsonpatch-stubs'], - package_data={'': ['requirements.txt'], 'jsonpatch-stubs': ['*.pyi']}, + package_data={'jsonpatch-stubs': ['*.pyi']}, entry_points={ 'console_scripts': [ 'jsondiff = jsonpatch_cli:jsondiff_main', @@ -72,7 +74,7 @@ project_urls={ 'Website': 'https://github.com/stefankoegl/python-json-patch', 'Repository': 'https://github.com/stefankoegl/python-json-patch.git', - 'Documentation': "https://python-json-patch.readthedocs.org/", + 'Documentation': 'https://python-json-patch.readthedocs.io/', 'PyPI': 'https://pypi.org/pypi/jsonpatch', 'Tests': 'https://github.com/stefankoegl/python-json-patch/actions', 'Test Coverage': 'https://coveralls.io/github/stefankoegl/python-json-patch', From 5ad49392b2c6fcdc22483f9b81e54f5354171fbc Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 23:08:37 +0000 Subject: [PATCH 4/6] Clean up the test runners and stale test workarounds - tests.py: 'python tests.py' ran the doctests of jsonpatch twice, and only the test classes in a hand-maintained list. It now uses unittest.main() with load_tests, which finds all classes. tests.js is found independent of the working directory and read as UTF-8 - ext_tests.py: remove the coverage report, which was started after jsonpatch was imported and so reported meaningless numbers, and was skipped when tests failed; remove the doctests, which tests.py runs - property_tests.py: no property is an expected failure any more, so drop the 'safe' documents without '-' keys and the test that only differed in using them, and let the state machine replace '-' members and test values that are equal in Python but not in JSON - requirements-dev.txt: drop wheel, which nothing uses Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_01PewzpjVeke4LTQ7Q88nyu6 --- ext_tests.py | 57 +++++++---------------------------------- property_tests.py | 48 ++++++++-------------------------- requirements-dev.txt | 1 - tests.py | 61 +++++++------------------------------------- 4 files changed, 29 insertions(+), 138 deletions(-) diff --git a/ext_tests.py b/ext_tests.py index d3115fe..4faca14 100755 --- a/ext_tests.py +++ b/ext_tests.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # # python-json-patch - An implementation of the JSON Patch format # https://github.com/stefankoegl/python-json-patch @@ -34,7 +33,7 @@ """ Script to run external tests, eg from https://github.com/json-patch/json-patch-tests """ -import doctest +import json import unittest import jsonpatch import sys @@ -44,7 +43,7 @@ class TestCaseTemplate(unittest.TestCase): """ A generic test case for running external tests """ def _test(self, test): - if not 'doc' in test or not 'patch' in test: + if 'doc' not in test or 'patch' not in test: # incomplete return @@ -85,56 +84,18 @@ def add_test_method(cls, name, test): setattr(cls, name, lambda self: self._test(test)) - -modules = ['jsonpatch'] -coverage_modules = [] - - def get_suite(filenames): suite = unittest.TestSuite() for testfile in filenames: - with open(testfile) as f: - # we use the (potentially) patched version of json.load here - tests = jsonpatch.json.load(f) - cls = make_test_case(tests) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(cls)) + with open(testfile, encoding='utf-8') as f: + tests = json.load(f) + cls = make_test_case(tests) + suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(cls)) return suite -suite = get_suite(sys.argv[1:]) - -for module in modules: - m = __import__(module, fromlist=[module]) - coverage_modules.append(m) - suite.addTest(doctest.DocTestSuite(m)) - -runner = unittest.TextTestRunner(verbosity=1) - -try: - import coverage - cov = coverage.Coverage() -except ImportError: - cov = None - -if cov is not None: - cov.erase() - cov.start() - -result = runner.run(suite) - -if not result.wasSuccessful(): - sys.exit(1) - -if cov is not None: - cov.stop() - cov.report(coverage_modules) - cov.erase() - -if cov is None: - sys.stderr.write(""" -No coverage reporting done (Python module "coverage" is missing) -Please install the python-coverage package to get coverage reporting. -""") - sys.stderr.flush() +if __name__ == '__main__': + result = unittest.TextTestRunner(verbosity=1).run(get_suite(sys.argv[1:])) + sys.exit(not result.wasSuccessful()) diff --git a/property_tests.py b/property_tests.py index 6d19078..d7e4cee 100644 --- a/property_tests.py +++ b/property_tests.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Property-based tests for jsonpatch, using Hypothesis @@ -7,11 +6,8 @@ properties (e.g. "applying make_patch(src, dst) to src yields dst") against many generated documents and patches. -Properties that do not hold yet are marked as expected failures. Each of them -carries the minimal counterexample Hypothesis found as an explicit @example, -so it fails deterministically, and turns into an "unexpected success" (which -fails the run) once the property holds and the marker can be removed. Tests -that must pass meanwhile avoid those inputs and say so. +Inputs on which a property did not hold in the past are kept as explicit +@example, with the issue they were fixed in, so they are checked on each run. Run more examples with HYPOTHESIS_PROFILE=thorough. """ @@ -161,18 +157,6 @@ def json_values(scalars, keys): ) -def safe_json_values(scalars, keys): - """ Documents that avoid the inputs on which make_patch is currently known - to fail (see test_roundtrip): object keys that are '-' """ - return json_values(scalars, keys.filter(lambda key: key != '-')) - - -safe_json_docs = safe_json_values(json_scalars, json_keys) -small_safe_json_docs = safe_json_values( - st.sampled_from([None, True, False, 0, 1, 'a']), - st.sampled_from(['a', 'b', '0', '1'])) - - def pairs_of(*doc_strategies): """ Two documents, both drawn from the same one of doc_strategies """ return st.sampled_from(doc_strategies).flatmap( @@ -180,7 +164,6 @@ def pairs_of(*doc_strategies): doc_pairs = pairs_of(json_docs, small_json_docs) -safe_doc_pairs = pairs_of(safe_json_docs, small_safe_json_docs) @st.composite @@ -344,6 +327,9 @@ def test_diff_of_equal_documents_is_empty(self, doc): @given(doc_pairs) # replace of the object member '-' was rejected, #212 @example(docs=({'-': 0}, {'-': 1})) + # the diff considered e.g. 1 and true equal, #180 + @example(docs=([0], [False])) + @example(docs=({'a': [1]}, {'b': [True]})) def test_roundtrip(self, docs): self.check_roundtrip(*docs) @@ -375,13 +361,6 @@ def test_generate_test_ops(self, docs, optimization): self.assertEqual([(op['op'], op['path']) for op in patch], expected) assert_json_equal(patch.apply(src), dst) - @given(safe_doc_pairs) - # the diff considered e.g. 1 and true equal, #180 - @example(docs=([0], [False])) - @example(docs=({'a': [1]}, {'b': [True]})) - def test_roundtrip_of_safe_documents(self, docs): - self.check_roundtrip(*docs) - @given(doc_pairs) def test_does_not_modify_inputs(self, docs): saved = copy.deepcopy(docs) @@ -389,7 +368,7 @@ def test_does_not_modify_inputs(self, docs): outcome(jsonpatch.make_patch, *docs) assert_json_equal(list(docs), list(saved)) - @given(safe_doc_pairs) + @given(doc_pairs) @example(docs=({}, {'a': []})) def test_result_shares_nothing_with_dst(self, docs): src, dst = docs @@ -595,12 +574,11 @@ class PatchOperationMachine(RuleBasedStateMachine): each result with a direct implementation of RFC 6902. Inputs covered by separate tests in OperationProperties are left - out: the whole document as target or source of an operation, moving a - location into its own children, replacing an object member '-', and - testing values which are equal in Python but not in JSON. """ + out: the whole document as target or source of an operation, and moving + a location into its own children. """ def __init__(self): - super(PatchOperationMachine, self).__init__() + super().__init__() self.initial = {} self.doc = {} self.patch = [] @@ -637,11 +615,7 @@ def remove(self, data): @precondition(lambda self: self.doc) @rule(data=st.data(), value=json_docs) def replace(self, data, value): - candidates = [parts for parts in locations(self.doc) if parts and not ( - isinstance(resolve(self.doc, parts[:-1]), dict) and parts[-1] == '-')] - if not candidates: - return - parts = list(data.draw(st.sampled_from(candidates))) + parts = data.draw(existing_locations(self.doc)) expected = copy.deepcopy(self.doc) resolve(expected, parts[:-1])[parts[-1]] = copy.deepcopy(value) self.apply({'op': 'replace', 'path': to_pointer(parts), @@ -682,7 +656,7 @@ def test_equal(self, data): @rule(data=st.data(), value=json_docs) def test_different(self, data, value): parts = data.draw(st.sampled_from(list(locations(self.doc)))) - if value == resolve(self.doc, parts): + if json_equal(value, resolve(self.doc, parts)): return operation = {'op': 'test', 'path': to_pointer(parts), 'value': value} result = outcome(jsonpatch.apply_patch, self.doc, [operation]) diff --git a/requirements-dev.txt b/requirements-dev.txt index bda790f..2320ebe 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,3 @@ coverage -wheel hypothesis simplejson diff --git a/tests.py b/tests.py index 730b663..3d502c7 100644 --- a/tests.py +++ b/tests.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- import collections import contextlib @@ -91,7 +90,8 @@ def test_diff_without_optimization_uses_basic_operations(self): def test_js_file(self): - with open('./tests.js', 'r') as f: + here = os.path.dirname(os.path.abspath(__file__)) + with open(os.path.join(here, 'tests.js'), encoding='utf-8') as f: tests = json.load(f) for test in tests: with self.subTest(comment=test.get('comment'), patch=test['patch']): @@ -4054,54 +4054,11 @@ def test_additional_members_are_not_aliases(self): {'message': 'canonical'}) +def load_tests(loader, tests, pattern): + """ Adds the doctests of jsonpatch to the tests of this module """ + tests.addTests(doctest.DocTestSuite(jsonpatch)) + return tests + + if __name__ == '__main__': - modules = ['jsonpatch'] - - - def get_suite(): - suite = unittest.TestSuite() - suite.addTest(doctest.DocTestSuite(jsonpatch)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(ApplyPatchTestCase)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(EqualityTestCase)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(MakePatchTestCase)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(ListTests)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(InvalidInputTests)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(ConflictTests)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(StringIndexingTests)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(OptimizationTests)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(GenerateTestOpsTests)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(ArrayItemsTests)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(JsonPointerTests)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(JsonPatchCreationTest)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(UtilityMethodTests)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(OperationLocationTests)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(PathUtilityTests)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(CustomPathSyntaxTests)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(PointerClassCompatibilityTests)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(CustomJsonPointerTests)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(CustomOperationTests)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(ExceptionOperationTests)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(AtomicApplyTests)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(PathNotFoundTests)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(CommandLineTests)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(DocumentationTests)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(StandardAppendIsolationTests)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(AppendOperationTests)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(AppendInterfaceTests)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(AppendAtomicTests)) - suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(AppendDiffTests)) - return suite - - - suite = get_suite() - - for module in modules: - m = __import__(module, fromlist=[module]) - suite.addTest(doctest.DocTestSuite(m)) - - runner = unittest.TextTestRunner(verbosity=1) - - result = runner.run(suite) - - if not result.wasSuccessful(): - sys.exit(1) + unittest.main() From d5e06c8f9c38dd6930b361f253beb0fa2ef6f330 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 09:38:24 +0000 Subject: [PATCH 5/6] Don't show the e-mail address in the copyright line of the docs The copyright notice was set to __author__, which includes the e-mail address, so every page of the documentation showed it in its footer. Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_01PewzpjVeke4LTQ7Q88nyu6 --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 5add877..f790745 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -47,7 +47,7 @@ # General information about the project. project = 'python-json-patch' -copyright = jsonpatch.__author__ +copyright = '2011, Stefan Kögl' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the From 6465880333a4c3aa520df1464463dcb7517e4ca5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 09:39:23 +0000 Subject: [PATCH 6/6] Fix the flake8 warnings and make the lint check strict CI ran flake8 twice: once for syntax errors and undefined names, and once with --exit-zero for everything else, so about 270 style warnings, nearly all of them in tests.py and doc/conf.py, never failed a build. They are fixed, most of them with autopep8, without changing what the code does. Apart from whitespace and comments, the only changes are a renamed variable and dropped u'' prefixes in two tests, and 'import sys, os' split in two in doc/conf.py. The check is now a single strict run, configured in .flake8 so that it gives the same result locally. It no longer passes --max-complexity=10, which three functions exceed (patch_files, AppendOperation._apply and JsonPatch.apply): splitting them up is not a style fix. Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_01PewzpjVeke4LTQ7Q88nyu6 --- .flake8 | 4 + .github/workflows/test.yaml | 7 +- doc/conf.py | 93 +++++++------- jsonpatch_cli.py | 4 +- setup.py | 2 +- tests.py | 247 ++++++++++++++++++------------------ 6 files changed, 178 insertions(+), 179 deletions(-) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..6647631 --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +# The GitHub editor is 127 chars wide +max-line-length = 127 +exclude = .git,__pycache__,build,dist,doc/_build diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 848c4ce..0161148 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -23,11 +23,8 @@ jobs: python -m pip install --upgrade pip pip install flake8 - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + # configured in .flake8 + run: flake8 . --count --show-source --statistics build: name: "Build and check distributions" diff --git a/doc/conf.py b/doc/conf.py index 5add877..6327eb3 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -11,19 +11,20 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import sys +import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.join(os.path.abspath('.'), '..')) -import jsonpatch +import jsonpatch # noqa: E402, needs the path set above # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. @@ -40,7 +41,7 @@ source_suffix = '.rst' # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' @@ -61,37 +62,37 @@ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- @@ -103,26 +104,26 @@ # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -131,44 +132,44 @@ # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'python-json-patchdoc' @@ -177,14 +178,14 @@ # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples @@ -196,23 +197,23 @@ # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- @@ -225,7 +226,7 @@ ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -240,10 +241,10 @@ ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' diff --git a/jsonpatch_cli.py b/jsonpatch_cli.py index 5b9e7a1..bd64115 100644 --- a/jsonpatch_cli.py +++ b/jsonpatch_cli.py @@ -49,7 +49,7 @@ def diff_files(): doc2 = json.load(args.FILE2) patch = jsonpatch.make_patch(doc1, doc2, generate_test_ops=args.test_ops) if patch.patch: - print(json.dumps(patch.patch, indent=args.indent, ensure_ascii=not(args.preserve_unicode))) + print(json.dumps(patch.patch, indent=args.indent, ensure_ascii=not args.preserve_unicode)) sys.exit(1) @@ -135,7 +135,7 @@ def patch_files(): # modified JSON to. try: - json.dump(result, fp, indent=args.indent, ensure_ascii=not(args.preserve_unicode)) + json.dump(result, fp, indent=args.indent, ensure_ascii=not args.preserve_unicode) fp.write('\n') if args.in_place: fp.close() diff --git a/setup.py b/setup.py index 76990b0..52c3406 100644 --- a/setup.py +++ b/setup.py @@ -80,4 +80,4 @@ 'Test Coverage': 'https://coveralls.io/github/stefankoegl/python-json-patch', }, install_requires=REQUIREMENTS, -) + ) diff --git a/tests.py b/tests.py index 3d502c7..f0d7581 100644 --- a/tests.py +++ b/tests.py @@ -30,6 +30,7 @@ def test_operation_iterators_remain_reusable_after_validation(self): {'op': 'add', 'path': '/a', 'value': 1}, {'op': 'replace', 'path': '/a', 'value': 2}, ] + class OneShotIterable: def __init__(self, ops): self.iterator = iter(ops) @@ -88,7 +89,6 @@ def test_diff_without_optimization_uses_basic_operations(self): self.assertEqual(patch.apply(src), dst) self.assertEqual(src, original) - def test_js_file(self): here = os.path.dirname(os.path.abspath(__file__)) with open(os.path.join(here, 'tests.js'), encoding='utf-8') as f: @@ -319,7 +319,7 @@ def test_replace_whole_document_does_not_share_value(self): def test_move_object_keyerror(self): obj = {'foo': {'bar': 'baz'}, 'qux': {'corge': 'grault'}} - patch_obj = [ {'op': 'move', 'from': '/foo/non-existent', 'path': '/qux/thud'} ] + patch_obj = [{'op': 'move', 'from': '/foo/non-existent', 'path': '/qux/thud'}] self.assertRaises(jsonpatch.JsonPatchConflict, jsonpatch.apply_patch, obj, patch_obj) def test_move_object_key(self): @@ -331,7 +331,7 @@ def test_move_object_key(self): 'foo': {'bar': 'baz'}}) def test_move_array_item(self): - obj = {'foo': ['all', 'grass', 'cows', 'eat']} + obj = {'foo': ['all', 'grass', 'cows', 'eat']} res = jsonpatch.apply_patch(obj, [{'op': 'move', 'from': '/foo/1', 'path': '/foo/3'}]) self.assertEqual(res, {'foo': ['all', 'cows', 'eat', 'grass']}) @@ -362,16 +362,15 @@ def test_copy_object_key(self): 'foo': {'bar': 'baz', 'waldo': 'fred'}}) def test_copy_array_item(self): - obj = {'foo': ['all', 'grass', 'cows', 'eat']} + obj = {'foo': ['all', 'grass', 'cows', 'eat']} res = jsonpatch.apply_patch(obj, [{'op': 'copy', 'from': '/foo/1', 'path': '/foo/3'}]) self.assertEqual(res, {'foo': ['all', 'grass', 'cows', 'grass', 'eat']}) - def test_copy_mutable(self): """ test if mutable objects (dicts and lists) are copied by value """ obj = {'foo': [{'bar': 42}, {'baz': 3.14}], 'boo': []} # copy object somewhere - res = jsonpatch.apply_patch(obj, [{'op': 'copy', 'from': '/foo/0', 'path': '/boo/0' }]) + res = jsonpatch.apply_patch(obj, [{'op': 'copy', 'from': '/foo/0', 'path': '/boo/0'}]) self.assertEqual(res, {'foo': [{'bar': 42}, {'baz': 3.14}], 'boo': [{'bar': 42}]}) # modify original object res = jsonpatch.apply_patch(res, [{'op': 'add', 'path': '/foo/0/zoo', 'value': 255}]) @@ -412,19 +411,17 @@ def test_copy_document_to_itself(self): res['foo'].append(2) self.assertEqual(obj, {'foo': [1]}) - def test_test_success(self): - obj = {'baz': 'qux', 'foo': ['a', 2, 'c']} + obj = {'baz': 'qux', 'foo': ['a', 2, 'c']} jsonpatch.apply_patch(obj, [{'op': 'test', 'path': '/baz', 'value': 'qux'}, {'op': 'test', 'path': '/foo/1', 'value': 2}]) def test_test_whole_obj(self): - obj = {'baz': 1} + obj = {'baz': 1} jsonpatch.apply_patch(obj, [{'op': 'test', 'path': '', 'value': obj}]) - def test_test_error(self): - obj = {'bar': 'qux'} + obj = {'bar': 'qux'} self.assertRaises(jsonpatch.JsonPatchTestFailed, jsonpatch.apply_patch, obj, [{'op': 'test', 'path': '/bar', 'value': 'bar'}]) @@ -503,7 +500,6 @@ def test_test_read_only_mapping_success_preserves_input(self): if nested: self.assertIs(result['nested'], mapping) - def test_test_read_only_sequences_preserve_boolean_types(self): for actual_type in (list, tuple): for expected_type in (list, tuple): @@ -629,45 +625,40 @@ def test_test_strings_and_bytes_remain_scalar(self): jsonpatch.apply_patch(doc, patch, in_place=True) def test_test_not_existing(self): - obj = {'bar': 'qux'} + obj = {'bar': 'qux'} self.assertRaises(jsonpatch.JsonPatchTestFailed, jsonpatch.apply_patch, obj, [{'op': 'test', 'path': '/baz', 'value': 'bar'}]) - def test_forgetting_surrounding_list(self): - obj = {'bar': 'qux'} + obj = {'bar': 'qux'} self.assertRaises(jsonpatch.InvalidJsonPatch, jsonpatch.apply_patch, obj, {'op': 'test', 'path': '/bar'}) def test_test_noval_existing(self): - obj = {'bar': 'qux'} + obj = {'bar': 'qux'} self.assertRaises(jsonpatch.InvalidJsonPatch, jsonpatch.apply_patch, obj, [{'op': 'test', 'path': '/bar'}]) - def test_test_noval_not_existing(self): - obj = {'bar': 'qux'} + obj = {'bar': 'qux'} self.assertRaises(jsonpatch.JsonPatchTestFailed, jsonpatch.apply_patch, obj, [{'op': 'test', 'path': '/baz'}]) - def test_test_noval_not_existing_nested(self): - obj = {'bar': {'qux': 2}} + obj = {'bar': {'qux': 2}} self.assertRaises(jsonpatch.JsonPatchTestFailed, jsonpatch.apply_patch, obj, [{'op': 'test', 'path': '/baz/qx'}]) - def test_unrecognized_element(self): obj = {'foo': 'bar', 'baz': 'qux'} res = jsonpatch.apply_patch(obj, [{'op': 'replace', 'path': '/baz', 'value': 'boo', 'foo': 'ignore'}]) self.assertTrue(res['baz'], 'boo') - def test_append(self): obj = {'foo': [1, 2]} res = jsonpatch.apply_patch(obj, [ @@ -692,11 +683,10 @@ def test_path_with_null_value(self): class EqualityTestCase(unittest.TestCase): def test_patch_equality(self): - patch1 = jsonpatch.JsonPatch([{ "op": "add", "path": "/a/b/c", "value": "foo" }]) - patch2 = jsonpatch.JsonPatch([{ "path": "/a/b/c", "op": "add", "value": "foo" }]) + patch1 = jsonpatch.JsonPatch([{"op": "add", "path": "/a/b/c", "value": "foo"}]) + patch2 = jsonpatch.JsonPatch([{"path": "/a/b/c", "op": "add", "value": "foo"}]) self.assertEqual(patch1, patch2) - def test_patch_unequal(self): patch1 = jsonpatch.JsonPatch([{'op': 'test', 'path': '/test'}]) patch2 = jsonpatch.JsonPatch([{'op': 'test', 'path': '/test1'}]) @@ -714,17 +704,15 @@ def test_patch_equal_numbers_by_value(self): self.assertEqual(patch1, patch2) def test_patch_hash_equality(self): - patch1 = jsonpatch.JsonPatch([{ "op": "add", "path": "/a/b/c", "value": "foo" }]) - patch2 = jsonpatch.JsonPatch([{ "path": "/a/b/c", "op": "add", "value": "foo" }]) + patch1 = jsonpatch.JsonPatch([{"op": "add", "path": "/a/b/c", "value": "foo"}]) + patch2 = jsonpatch.JsonPatch([{"path": "/a/b/c", "op": "add", "value": "foo"}]) self.assertEqual(hash(patch1), hash(patch2)) - def test_patch_hash_unequal(self): patch1 = jsonpatch.JsonPatch([{'op': 'test', 'path': '/test'}]) patch2 = jsonpatch.JsonPatch([{'op': 'test', 'path': '/test1'}]) self.assertNotEqual(hash(patch1), hash(patch2)) - def test_patch_hash_container_values(self): for value in ([], {}, [1, {'b': []}], {'b': [1]}): patch1 = jsonpatch.JsonPatch([{'op': 'add', 'path': '/a', 'value': value}]) @@ -732,28 +720,26 @@ def test_patch_hash_container_values(self): self.assertEqual(hash(patch1), hash(patch2)) self.assertEqual(len({patch1, patch2}), 1) - def test_patch_hash_ignored_container_member(self): # members an operation doesn't use are ignored, whatever their value patch = jsonpatch.JsonPatch([{'op': 'add', 'path': '/a', 'value': 1, 'from': []}]) self.assertIn(patch, {patch}) - def test_patch_neq_other_objs(self): p = [{'op': 'test', 'path': '/test'}] patch = jsonpatch.JsonPatch(p) # a patch will always compare not-equal to objects of other types self.assertFalse(patch == p) - self.assertFalse(patch == None) + self.assertFalse(patch == None) # noqa: E711, tests __eq__ # also a patch operation will always compare # not-equal to objects of other types op = jsonpatch.PatchOperation(p[0]) self.assertFalse(op == p[0]) - self.assertFalse(op == None) + self.assertFalse(op == None) # noqa: E711, tests __eq__ def test_str(self): - patch_obj = [ { "op": "add", "path": "/child", "value": { "grandchild": { } } } ] + patch_obj = [{"op": "add", "path": "/child", "value": {"grandchild": {}}}] patch = jsonpatch.JsonPatch(patch_obj) self.assertEqual(json.dumps(patch_obj), str(patch)) @@ -870,11 +856,11 @@ def test_array_add_remove(self): def test_add_nested(self): # see http://tools.ietf.org/html/draft-ietf-appsawg-json-patch-03#appendix-A.10 src = {"foo": "bar"} - patch_obj = [ { "op": "add", "path": "/child", "value": { "grandchild": { } } } ] + patch_obj = [{"op": "add", "path": "/child", "value": {"grandchild": {}}}] res = jsonpatch.apply_patch(src, patch_obj) - expected = { "foo": "bar", - "child": { "grandchild": { } } - } + expected = {"foo": "bar", + "child": {"grandchild": {}} + } self.assertEqual(expected, res) def test_should_just_add_new_item_not_rebuild_all_list(self): @@ -930,8 +916,8 @@ def test_issue40(self): def test_issue76(self): """ Make sure op:remove does not include a 'value' field """ - src = { "name": "fred", "friend": "barney", "spouse": "wilma" } - dst = { "name": "fred", "spouse": "wilma" } + src = {"name": "fred", "friend": "barney", "spouse": "wilma"} + dst = {"name": "fred", "spouse": "wilma"} expected = [{"path": "/friend", "op": "remove"}] patch = jsonpatch.make_patch(src, dst) self.assertEqual(patch.patch, expected) @@ -952,8 +938,8 @@ def test_json_patch(self): def test_arrays_one_element_sequences(self): """ Tests the case of multiple common one element sequences inside an array """ # see https://github.com/stefankoegl/python-json-patch/issues/30#issuecomment-155070128 - src = [1,2,3] - dst = [3,1,4,2] + src = [1, 2, 3] + dst = [3, 1, 4, 2] patch = jsonpatch.make_patch(src, dst) res = jsonpatch.apply_patch(src, patch) self.assertEqual(res, dst) @@ -972,14 +958,14 @@ def test_nested(self): """ Patch creation with nested dicts, as reported in #41 https://github.com/stefankoegl/python-json-patch/issues/41 """ - old = {'school':{'names':['Kevin','Carl']}} - new = {'school':{'names':['Carl','Kate','Kevin','Jake']}} + old = {'school': {'names': ['Kevin', 'Carl']}} + new = {'school': {'names': ['Carl', 'Kate', 'Kevin', 'Jake']}} patch = jsonpatch.JsonPatch.from_diff(old, new) new_from_patch = jsonpatch.apply_patch(old, patch) self.assertEqual(new, new_from_patch) def test_move_from_numeric_to_alpha_dict_key(self): - #https://github.com/stefankoegl/python-json-patch/issues/97 + # https://github.com/stefankoegl/python-json-patch/issues/97 src = {'13': 'x'} dst = {'A': 'a', 'b': 'x'} patch = jsonpatch.make_patch(src, dst) @@ -1099,15 +1085,27 @@ def test_values_that_contain_themselves(self): def test_issue119(self): """Make sure it avoids casting numeric str dict key to int""" - src = [ - {'foobar': {u'1': [u'lettuce', u'cabbage', u'bok choy', u'broccoli'], u'3': [u'ibex'], u'2': [u'apple'], u'5': [], u'4': [u'gerenuk', u'duiker'], u'10_1576156603109': [], u'6': [], u'8_1572034252560': [u'thompson', u'gravie', u'mango', u'coconut'], u'7_1572034204585': []}}, - {'foobar':{u'description': u'', u'title': u''}} - ] - dst = [ - {'foobar': {u'9': [u'almond'], u'10': u'yes', u'12': u'', u'16_1598876845275': [], u'7': [u'pecan']}}, - {'foobar': {u'1': [u'lettuce', u'cabbage', u'bok choy', u'broccoli'], u'3': [u'ibex'], u'2': [u'apple'], u'5': [], u'4': [u'gerenuk', u'duiker'], u'10_1576156603109': [], u'6': [], u'8_1572034252560': [u'thompson', u'gravie', u'mango', u'coconut'], u'7_1572034204585': []}}, - {'foobar': {u'description': u'', u'title': u''}} - ] + src = [{'foobar': {'1': ['lettuce', 'cabbage', 'bok choy', 'broccoli'], + '3': ['ibex'], + '2': ['apple'], + '5': [], + '4': ['gerenuk', 'duiker'], + '10_1576156603109': [], + '6': [], + '8_1572034252560': ['thompson', 'gravie', 'mango', 'coconut'], + '7_1572034204585': []}}, + {'foobar': {'description': '', 'title': ''}}] + dst = [{'foobar': {'9': ['almond'], '10': 'yes', '12': '', '16_1598876845275': [], '7': ['pecan']}}, + {'foobar': {'1': ['lettuce', 'cabbage', 'bok choy', 'broccoli'], + '3': ['ibex'], + '2': ['apple'], + '5': [], + '4': ['gerenuk', 'duiker'], + '10_1576156603109': [], + '6': [], + '8_1572034252560': ['thompson', 'gravie', 'mango', 'coconut'], + '7_1572034204585': []}}, + {'foobar': {'description': '', 'title': ''}}] patch = jsonpatch.make_patch(src, dst) res = jsonpatch.apply_patch(src, patch) self.assertEqual(res, dst) @@ -1115,39 +1113,39 @@ def test_issue119(self): def test_issue120(self): """Make sure it avoids casting numeric str dict key to int""" src = [{'foobar': {'821b7213_b9e6_2b73_2e9c_cf1526314553': ['Open Work'], - '6e3d1297_0c5a_88f9_576b_ad9216611c94': ['Many Things'], - '1987bcf0_dc97_59a1_4c62_ce33e51651c7': ['Product']}}, - {'foobar': {'2a7624e_0166_4d75_a92c_06b3f': []}}, - {'foobar': {'10': [], - '11': ['bee', - 'ant', - 'wasp'], - '13': ['phobos', - 'titan', - 'gaea'], + '6e3d1297_0c5a_88f9_576b_ad9216611c94': ['Many Things'], + '1987bcf0_dc97_59a1_4c62_ce33e51651c7': ['Product']}}, + {'foobar': {'2a7624e_0166_4d75_a92c_06b3f': []}}, + {'foobar': {'10': [], + '11': ['bee', + 'ant', + 'wasp'], + '13': ['phobos', + 'titan', + 'gaea'], '14': [], '15': 'run3', '16': 'service', '2': ['zero', 'enable']}}] dst = [{'foobar': {'1': [], '2': []}}, - {'foobar': {'821b7213_b9e6_2b73_2e9c_cf1526314553': ['Open Work'], - '6e3d1297_0c5a_88f9_576b_ad9216611c94': ['Many Things'], - '1987bcf0_dc97_59a1_4c62_ce33e51651c7': ['Product']}}, - {'foobar': {'2a7624e_0166_4d75_a92c_06b3f': []}}, - {'foobar': {'b238d74d_dcf4_448c_9794_c13a2f7b3c0a': [], - 'dcb0387c2_f7ae_b8e5bab_a2b1_94deb7c': []}}, - {'foobar': {'10': [], - '11': ['bee', - 'ant', - 'fly'], - '13': ['titan', - 'phobos', - 'gaea'], - '14': [], - '15': 'run3', - '16': 'service', - '2': ['zero', 'enable']}} - ] + {'foobar': {'821b7213_b9e6_2b73_2e9c_cf1526314553': ['Open Work'], + '6e3d1297_0c5a_88f9_576b_ad9216611c94': ['Many Things'], + '1987bcf0_dc97_59a1_4c62_ce33e51651c7': ['Product']}}, + {'foobar': {'2a7624e_0166_4d75_a92c_06b3f': []}}, + {'foobar': {'b238d74d_dcf4_448c_9794_c13a2f7b3c0a': [], + 'dcb0387c2_f7ae_b8e5bab_a2b1_94deb7c': []}}, + {'foobar': {'10': [], + '11': ['bee', + 'ant', + 'fly'], + '13': ['titan', + 'phobos', + 'gaea'], + '14': [], + '15': 'run3', + '16': 'service', + '2': ['zero', 'enable']}} + ] patch = jsonpatch.make_patch(src, dst) res = jsonpatch.apply_patch(src, patch) self.assertEqual(res, dst) @@ -1187,18 +1185,18 @@ def test_issue_138(self): def test_issue_138b(self): """Additionally tests escaping special characters.""" old = {"/": - [ + [ {"x": ["a", {"y": ["b"]}], "z": "a"}, {"x": ["c", {"d": ["d"]}], "z": "c"}, {}, - ] - } + ] + } new = {"/": - [ + [ {"x": ["c", {"y": ["d"]}], "z": "c"}, {}, - ] - } + ] + } patch = jsonpatch.make_patch(old, new) result = jsonpatch.apply_patch(old, patch) self.assertEqual(result, new) @@ -1738,14 +1736,13 @@ class InvalidInputTests(unittest.TestCase): def test_missing_op(self): # an "op" member is required src = {"foo": "bar"} - patch_obj = [ { "path": "/child", "value": { "grandchild": { } } } ] + patch_obj = [{"path": "/child", "value": {"grandchild": {}}}] self.assertRaises(jsonpatch.JsonPatchException, jsonpatch.apply_patch, src, patch_obj) - def test_invalid_op(self): # "invalid" is not a valid operation src = {"foo": "bar"} - patch_obj = [ { "op": "invalid", "path": "/child", "value": { "grandchild": { } } } ] + patch_obj = [{"op": "invalid", "path": "/child", "value": {"grandchild": {}}}] self.assertRaises(jsonpatch.JsonPatchException, jsonpatch.apply_patch, src, patch_obj) def test_operation_not_an_object(self): @@ -1759,7 +1756,7 @@ def test_invalid_from(self): src = {"foo": "bar"} for op in ("move", "copy"): for from_ in [0, None, ["foo"], {"foo": "bar"}]: - patch_obj = [ { "op": op, "from": from_, "path": "/baz" } ] + patch_obj = [{"op": op, "from": from_, "path": "/baz"}] self.assertRaises(jsonpatch.InvalidJsonPatch, jsonpatch.apply_patch, src, patch_obj) def test_duplicate_members_in_string_patch(self): @@ -1778,17 +1775,17 @@ class ConflictTests(unittest.TestCase): def test_remove_indexerror(self): src = {"foo": [1, 2]} - patch_obj = [ { "op": "remove", "path": "/foo/10"} ] + patch_obj = [{"op": "remove", "path": "/foo/10"}] self.assertRaises(jsonpatch.JsonPatchConflict, jsonpatch.apply_patch, src, patch_obj) def test_remove_keyerror(self): src = {"foo": [1, 2]} - patch_obj = [ { "op": "remove", "path": "/foo/b"} ] + patch_obj = [{"op": "remove", "path": "/foo/b"}] self.assertRaises(jsonpointer.JsonPointerException, jsonpatch.apply_patch, src, patch_obj) def test_remove_keyerror_dict(self): src = {'foo': {'bar': 'barz'}} - patch_obj = [ { "op": "remove", "path": "/foo/non-existent"} ] + patch_obj = [{"op": "remove", "path": "/foo/non-existent"}] self.assertRaises(jsonpatch.JsonPatchConflict, jsonpatch.apply_patch, src, patch_obj) def test_remove_from_immutable_sequence(self): @@ -1843,52 +1840,52 @@ def __delitem__(self, key): def test_insert_oob(self): src = {"foo": [1, 2]} - patch_obj = [ { "op": "add", "path": "/foo/10", "value": 1} ] + patch_obj = [{"op": "add", "path": "/foo/10", "value": 1}] self.assertRaises(jsonpatch.JsonPatchConflict, jsonpatch.apply_patch, src, patch_obj) def test_move_into_child(self): src = {"foo": {"bar": {"baz": 1}}} - patch_obj = [ { "op": "move", "from": "/foo", "path": "/foo/bar" } ] + patch_obj = [{"op": "move", "from": "/foo", "path": "/foo/bar"}] self.assertRaises(jsonpatch.JsonPatchException, jsonpatch.apply_patch, src, patch_obj) def test_move_whole_document_into_own_child(self): for src, path in [([], '/-'), ([1], '/0'), ({}, '/a'), ({'a': {}}, '/a/b')]: - patch_obj = [ { "op": "move", "from": "", "path": path } ] + patch_obj = [{"op": "move", "from": "", "path": path}] self.assertRaises(jsonpatch.JsonPatchConflict, jsonpatch.apply_patch, src, patch_obj) def test_move_whole_document_onto_itself(self): for src in [[1], {'a': 1}]: - res = jsonpatch.apply_patch(src, [ { "op": "move", "from": "", "path": "" } ]) + res = jsonpatch.apply_patch(src, [{"op": "move", "from": "", "path": ""}]) self.assertEqual(res, src) def test_move_onto_itself_must_exist(self): src = {'foo': [1]} for path in ['/bar', '/foo/1']: - patch_obj = [ { "op": "move", "from": path, "path": path } ] + patch_obj = [{"op": "move", "from": path, "path": path}] self.assertRaises(jsonpatch.JsonPatchConflict, jsonpatch.apply_patch, src, patch_obj) def test_replace_oob(self): src = {"foo": [1, 2]} - patch_obj = [ { "op": "replace", "path": "/foo/10", "value": 10} ] + patch_obj = [{"op": "replace", "path": "/foo/10", "value": 10}] self.assertRaises(jsonpatch.JsonPatchConflict, jsonpatch.apply_patch, src, patch_obj) def test_replace_oob_length(self): src = {"foo": [0, 1]} - patch_obj = [ { "op": "replace", "path": "/foo/2", "value": 2} ] + patch_obj = [{"op": "replace", "path": "/foo/2", "value": 2}] self.assertRaises(jsonpatch.JsonPatchConflict, jsonpatch.apply_patch, src, patch_obj) def test_replace_missing(self): src = {"foo": 1} - patch_obj = [ { "op": "replace", "path": "/bar", "value": 10} ] + patch_obj = [{"op": "replace", "path": "/bar", "value": 10}] self.assertRaises(jsonpatch.JsonPatchConflict, jsonpatch.apply_patch, src, patch_obj) def test_add_replace_in_immutable_sequence(self): # a tuple can be indexed like an array, but not be modified src = {"foo": (1, 2)} for patch_obj in [ - [ { "op": "add", "path": "/foo/0", "value": 0 } ], - [ { "op": "add", "path": "/foo/-", "value": 3 } ], - [ { "op": "replace", "path": "/foo/0", "value": 0 } ], + [{"op": "add", "path": "/foo/0", "value": 0}], + [{"op": "add", "path": "/foo/-", "value": 3}], + [{"op": "replace", "path": "/foo/0", "value": 0}], ]: self.assertRaises(jsonpatch.JsonPatchConflict, jsonpatch.apply_patch, src, patch_obj) @@ -1900,48 +1897,48 @@ def setUp(self): self.src = {"foo": "should-not-be-indexable"} def test_test(self): - patch_obj = [ { "op": "test", "path": "/foo/0", "value": "s" } ] + patch_obj = [{"op": "test", "path": "/foo/0", "value": "s"}] self.assertRaises(jsonpatch.JsonPatchTestFailed, jsonpatch.apply_patch, self.src, patch_obj) def test_test_nested(self): - patch_obj = [ { "op": "test", "path": "/foo/0/0", "value": "s" } ] + patch_obj = [{"op": "test", "path": "/foo/0/0", "value": "s"}] self.assertRaises(jsonpatch.JsonPatchTestFailed, jsonpatch.apply_patch, self.src, patch_obj) def test_copy(self): - patch_obj = [ { "op": "copy", "from": "/foo/0", "path": "/bar" } ] + patch_obj = [{"op": "copy", "from": "/foo/0", "path": "/bar"}] self.assertRaises(jsonpointer.JsonPointerException, jsonpatch.apply_patch, self.src, patch_obj) def test_move(self): - patch_obj = [ { "op": "move", "from": "/foo/0", "path": "/bar" } ] + patch_obj = [{"op": "move", "from": "/foo/0", "path": "/bar"}] self.assertRaises(jsonpointer.JsonPointerException, jsonpatch.apply_patch, self.src, patch_obj) def test_remove(self): - patch_obj = [ { "op": "remove", "path": "/foo/0" } ] + patch_obj = [{"op": "remove", "path": "/foo/0"}] self.assertRaises(jsonpointer.JsonPointerException, jsonpatch.apply_patch, self.src, patch_obj) def test_add(self): - patch_obj = [ { "op": "add", "path": "/foo/0", "value": "x" } ] + patch_obj = [{"op": "add", "path": "/foo/0", "value": "x"}] self.assertRaises(jsonpointer.JsonPointerException, jsonpatch.apply_patch, self.src, patch_obj) def test_replace(self): - patch_obj = [ { "op": "replace", "path": "/foo/0", "value": "x" } ] + patch_obj = [{"op": "replace", "path": "/foo/0", "value": "x"}] self.assertRaises(jsonpointer.JsonPointerException, jsonpatch.apply_patch, self.src, patch_obj) def test_root_string(self): - patch_obj = [ { "op": "test", "path": "/0", "value": "a" } ] + patch_obj = [{"op": "test", "path": "/0", "value": "a"}] self.assertRaises(jsonpatch.JsonPatchTestFailed, jsonpatch.apply_patch, "abc", patch_obj) def test_whole_string_value(self): patch_obj = [ - { "op": "test", "path": "/foo", "value": "should-not-be-indexable" }, - { "op": "copy", "from": "/foo", "path": "/bar" }, + {"op": "test", "path": "/foo", "value": "should-not-be-indexable"}, + {"op": "copy", "from": "/foo", "path": "/bar"}, ] res = jsonpatch.apply_patch(self.src, patch_obj) self.assertEqual(res, {"foo": "should-not-be-indexable", "bar": "should-not-be-indexable"}) def test_root_string_whole_document(self): - patch_obj = [ { "op": "test", "path": "", "value": "abc" } ] + patch_obj = [{"op": "test", "path": "", "value": "abc"}] self.assertEqual(jsonpatch.apply_patch("abc", patch_obj), "abc") @@ -2030,7 +2027,7 @@ def test_operation_normalizes_read_only_mapping(self): def test_creation_fails_with_invalid_patch(self): invalid_patches = [ - { 'path': '/foo', 'value': 'bar'}, + {'path': '/foo', 'value': 'bar'}, {'op': 0xADD, 'path': '/foo', 'value': 'bar'}, {'op': 'boo', 'path': '/foo', 'value': 'bar'}, {'op': 'add', 'value': 'bar'}, @@ -2547,7 +2544,7 @@ def test_make_patch(self): self.assertEqual(op.pointer_cls, CustomJsonPointer) def test_operations(self): - operations =[ + operations = [ ( jsonpatch.AddOperation, { 'op': 'add', 'path': '/foo', 'value': [1, 2, 3] @@ -2732,9 +2729,9 @@ def test_failed_operations_are_rolled_back(self): ({'a': 1, 'b': 2}, [{'op': 'add', 'path': '/a', 'value': 3}]), ({'a': 1, 'b': 2, 'c': 3}, [{'op': 'remove', 'path': '/a'}]), ({'a': 1, 'b': 2, 'c': 3}, [{'op': 'remove', 'path': '/b'}, - {'op': 'remove', 'path': '/a'}, - {'op': 'add', 'path': '/b', - 'value': 0}]), + {'op': 'remove', 'path': '/a'}, + {'op': 'add', 'path': '/b', + 'value': 0}]), ({'a': 1, 'b': 2}, [{'op': 'replace', 'path': '/a', 'value': 3}]), # members added before the first removal from the same object ({'a': 1, 'b': 2}, [{'op': 'add', 'path': '/c', 'value': 3}, @@ -3037,10 +3034,10 @@ class JsonPatch(jsonpatch.JsonPatch): patch = JsonPatch([{'op': 'remove', 'path': '/a'}, op]) expected = patch.apply(doc) - l = doc['l'] + original = doc['l'] res = patch.apply(doc, in_place=True, atomic=True) self.assertIs(res, doc) - self.assertIs(res['l'], l) + self.assertIs(res['l'], original) self.assertEqual(res, expected) def test_custom_operation_failing_on_original(self): @@ -4021,7 +4018,7 @@ class CustomPatch(jsonpatch.JsonPatchWithAppend): src = {'m': 'Hello', 'old': 1} dst = {'m': 'Hello!', 'new': 1, 'items': [2]} patch = CustomPatch.from_diff(src, dst, optimization=optimization, - pointer_cls=CustomJsonPointer, **kwargs) + pointer_cls=CustomJsonPointer, **kwargs) self.assertIsInstance(patch, CustomPatch) self.assertEqual(patch.apply(src), dst) self.assertEqual(any(op['op'] == 'move' for op in patch), optimization)