Skip to content

Commit

Permalink
Add option to preserve order of inserts. Only allow OrderedDicts for
Browse files Browse the repository at this point in the history
this. Make command line always preserve order too.
  • Loading branch information
kindly committed Nov 9, 2017
1 parent 8d46c89 commit 537d177
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 12 deletions.
7 changes: 4 additions & 3 deletions json_merge_patch/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
from collections import OrderedDict


def merge(files, output):
def merge(files, output, position):
result = None
for file in files:
with open(file) as input_file:
input_json = json.load(input_file, object_pairs_hook=OrderedDict)
if result is None:
result = input_json
else:
result = json_merge_patch.merge(result, input_json)
result = json_merge_patch.merge(result, input_json, position=position)

merged = json.dumps(result, indent=4)

Expand Down Expand Up @@ -46,6 +46,7 @@ def main():
'merge', help='Merge json documents together using JSON merge patch')

parser_merge.add_argument('-o', '--output', help='path of output file, if none specified will print to stdout')
parser_merge.add_argument('-f', '--first', action='store_true', help='when merging new properties of object put them first instead of last')
parser_merge.add_argument('files', help='JSON files to merge in order', nargs='+')

parser_create_patch = subparsers.add_parser(
Expand All @@ -57,7 +58,7 @@ def main():

args = parser.parse_args()
if args.subparser_name == 'merge':
merge(args.files, args.output)
merge(args.files, args.output, 'first' if args.first else 'last')
elif args.subparser_name == 'create-patch':
create_patch(args.original, args.target, args.output)
else:
Expand Down
44 changes: 35 additions & 9 deletions json_merge_patch/lib.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,57 @@
from collections import OrderedDict
import sys

def merge(*objs):
def merge(*objs, **kw):
result = objs[0]
for obj in objs[1:]:
result = merge_obj(result, obj)
result = merge_obj(result, obj, kw.get('position'))
return result

def merge_obj(result, obj):
def move_to_start(result, key):
result_copy = result.copy()
result.clear()
result[key] = result_copy.pop(key)
result.update(result_copy)

def merge_obj(result, obj, position=None):
if not isinstance(result, dict):
result = {}
result = OrderedDict() if position else {}

if not isinstance(obj, dict):
return obj

if position:
if position not in ('first', 'last'):
raise ValueError("position can either be first or last")
if not isinstance(result, OrderedDict) or not isinstance(obj, OrderedDict):
raise ValueError("If using position all dicts need to be OrderedDicts")

for key, value in obj.items():
if isinstance(value, dict):
target = result.get(key)
if isinstance(target, dict):
merge_obj(target, value)
merge_obj(target, value, position)
continue
result[key] = {}
merge_obj(result[key], value)
result[key] = OrderedDict() if position else {}
if position and position == 'first':
if sys.version_info >= (3, 2):
result.move_to_end(key, False)
else:
move_to_start(result, key)
merge_obj(result[key], value, position)
continue

if value is None:
result.pop(key, None)
continue
result[key] = value
if key not in result and position == 'first':
result[key] = value
if sys.version_info >= (3, 2):
result.move_to_end(key, False)
else:
move_to_start(result, key)
else:
result[key] = value

return result

def create_patch(source, target):
Expand Down
50 changes: 50 additions & 0 deletions json_merge_patch/tests.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import unittest
import lib as merge
from collections import OrderedDict

fixtures = [
[{
Expand Down Expand Up @@ -58,6 +59,55 @@ def test_create_patch(self):
self.assertEqual(merge.create_patch(fixture[0], fixture[2]), fixture[1])


ordered_fixtures = [[
OrderedDict([
("title", "Goodbye!"),
("content", "This will be unchanged")
]),
OrderedDict([
("title", "Goodbye!"),
("new", "Where will this go?"),
("content", "content")
]),
OrderedDict([
("title", "Goodbye!"),
("content", "content"),
("new", "Where will this go?")
]),
OrderedDict([
("new", "Where will this go?"),
("title", "Goodbye!"),
("content", "content")
])
],[
OrderedDict([
("title", "Goodbye!"),
("content", "This will be unchanged")
]),
OrderedDict([
("title", "Goodbye!"),
("new", OrderedDict([("where", "will I go")])),
("content", "content"),
]),
OrderedDict([
("title", "Goodbye!"),
("content", "content"),
("new", OrderedDict([("where", "will I go")])),
]),
OrderedDict([
("new", OrderedDict([("where", "will I go")])),
("title", "Goodbye!"),
("content", "content"),
])
]]

class TestOrdered(unittest.TestCase):

def test_merge(self):
for fixture in ordered_fixtures:
self.assertEqual(merge.merge(fixture[0].copy(), fixture[1], position='last'), fixture[2])
self.assertEqual(merge.merge(fixture[0].copy(), fixture[1], position='first'), fixture[3])

if __name__ == '__main__':
unittest.main()

Expand Down

0 comments on commit 537d177

Please sign in to comment.