Skip to content

Commit 537d177

Browse files
committed
Add option to preserve order of inserts. Only allow OrderedDicts for
this. Make command line always preserve order too.
1 parent 8d46c89 commit 537d177

File tree

3 files changed

+89
-12
lines changed

3 files changed

+89
-12
lines changed

json_merge_patch/cli.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@
55
from collections import OrderedDict
66

77

8-
def merge(files, output):
8+
def merge(files, output, position):
99
result = None
1010
for file in files:
1111
with open(file) as input_file:
1212
input_json = json.load(input_file, object_pairs_hook=OrderedDict)
1313
if result is None:
1414
result = input_json
1515
else:
16-
result = json_merge_patch.merge(result, input_json)
16+
result = json_merge_patch.merge(result, input_json, position=position)
1717

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

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

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

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

5859
args = parser.parse_args()
5960
if args.subparser_name == 'merge':
60-
merge(args.files, args.output)
61+
merge(args.files, args.output, 'first' if args.first else 'last')
6162
elif args.subparser_name == 'create-patch':
6263
create_patch(args.original, args.target, args.output)
6364
else:

json_merge_patch/lib.py

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,57 @@
1+
from collections import OrderedDict
2+
import sys
13

2-
def merge(*objs):
4+
def merge(*objs, **kw):
35
result = objs[0]
46
for obj in objs[1:]:
5-
result = merge_obj(result, obj)
7+
result = merge_obj(result, obj, kw.get('position'))
68
return result
79

8-
def merge_obj(result, obj):
10+
def move_to_start(result, key):
11+
result_copy = result.copy()
12+
result.clear()
13+
result[key] = result_copy.pop(key)
14+
result.update(result_copy)
15+
16+
def merge_obj(result, obj, position=None):
917
if not isinstance(result, dict):
10-
result = {}
18+
result = OrderedDict() if position else {}
1119

1220
if not isinstance(obj, dict):
1321
return obj
1422

23+
if position:
24+
if position not in ('first', 'last'):
25+
raise ValueError("position can either be first or last")
26+
if not isinstance(result, OrderedDict) or not isinstance(obj, OrderedDict):
27+
raise ValueError("If using position all dicts need to be OrderedDicts")
28+
1529
for key, value in obj.items():
1630
if isinstance(value, dict):
1731
target = result.get(key)
1832
if isinstance(target, dict):
19-
merge_obj(target, value)
33+
merge_obj(target, value, position)
2034
continue
21-
result[key] = {}
22-
merge_obj(result[key], value)
35+
result[key] = OrderedDict() if position else {}
36+
if position and position == 'first':
37+
if sys.version_info >= (3, 2):
38+
result.move_to_end(key, False)
39+
else:
40+
move_to_start(result, key)
41+
merge_obj(result[key], value, position)
2342
continue
24-
2543
if value is None:
2644
result.pop(key, None)
2745
continue
28-
result[key] = value
46+
if key not in result and position == 'first':
47+
result[key] = value
48+
if sys.version_info >= (3, 2):
49+
result.move_to_end(key, False)
50+
else:
51+
move_to_start(result, key)
52+
else:
53+
result[key] = value
54+
2955
return result
3056

3157
def create_patch(source, target):

json_merge_patch/tests.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import unittest
22
import lib as merge
3+
from collections import OrderedDict
34

45
fixtures = [
56
[{
@@ -58,6 +59,55 @@ def test_create_patch(self):
5859
self.assertEqual(merge.create_patch(fixture[0], fixture[2]), fixture[1])
5960

6061

62+
ordered_fixtures = [[
63+
OrderedDict([
64+
("title", "Goodbye!"),
65+
("content", "This will be unchanged")
66+
]),
67+
OrderedDict([
68+
("title", "Goodbye!"),
69+
("new", "Where will this go?"),
70+
("content", "content")
71+
]),
72+
OrderedDict([
73+
("title", "Goodbye!"),
74+
("content", "content"),
75+
("new", "Where will this go?")
76+
]),
77+
OrderedDict([
78+
("new", "Where will this go?"),
79+
("title", "Goodbye!"),
80+
("content", "content")
81+
])
82+
],[
83+
OrderedDict([
84+
("title", "Goodbye!"),
85+
("content", "This will be unchanged")
86+
]),
87+
OrderedDict([
88+
("title", "Goodbye!"),
89+
("new", OrderedDict([("where", "will I go")])),
90+
("content", "content"),
91+
]),
92+
OrderedDict([
93+
("title", "Goodbye!"),
94+
("content", "content"),
95+
("new", OrderedDict([("where", "will I go")])),
96+
]),
97+
OrderedDict([
98+
("new", OrderedDict([("where", "will I go")])),
99+
("title", "Goodbye!"),
100+
("content", "content"),
101+
])
102+
]]
103+
104+
class TestOrdered(unittest.TestCase):
105+
106+
def test_merge(self):
107+
for fixture in ordered_fixtures:
108+
self.assertEqual(merge.merge(fixture[0].copy(), fixture[1], position='last'), fixture[2])
109+
self.assertEqual(merge.merge(fixture[0].copy(), fixture[1], position='first'), fixture[3])
110+
61111
if __name__ == '__main__':
62112
unittest.main()
63113

0 commit comments

Comments
 (0)