Skip to content

Commit 5fe1526

Browse files
added a way to specify custom mapper
The custom mapper can be secified in `load`, `loads`, `parse` routines via using keyword `mapper`. For example, to preserve key order one can specify `OrderedDict` as a mapper. Another custom class could be used to handle files with duplicate keys. fix #1 Related to: #2 rossengeorgiev/vdf-parser#3
1 parent 8d53ea6 commit 5fe1526

File tree

3 files changed

+63
-18
lines changed

3 files changed

+63
-18
lines changed

README.rst

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@ https://developer.valvesoftware.com/wiki/KeyValues
77
The module works just like ``json`` to convert VDF to a dict, and vise-versa.
88

99

10-
Known Issues
10+
Poblems & solutions
1111
------------
1212

13-
- order is not preserved due to using ``dict``
14-
- there are known files that contain duplicate keys
13+
- There are known files that contain duplicate keys. This can be solved by
14+
creating a class inherating from `dict` and implementing a way to handle
15+
duplicate keys.
16+
17+
- By default the module uses `dict`, so key order is preserved. If key order
18+
is important then I suggest using `collections.OrderedDict`. See example.
1519

1620

1721
Install
@@ -24,7 +28,7 @@ You can grab the latest release from https://pypi.python.org/pypi/vdf or via ``p
2428
pip install vdf
2529
2630
27-
Usage
31+
Example usage
2832
-----------
2933

3034
.. code:: python
@@ -45,6 +49,18 @@ Usage
4549
vdf.dump(d, open('file2.txt','w'), pretty=True)
4650
4751
52+
Using `OrderedDict` to preserve key order.
53+
54+
.. code:: python
55+
56+
import vdf
57+
from collections import OrderedDict
58+
59+
# parsing vdf from file or string
60+
d = vdf.load(open('file.txt'), mapper=OrderedDict)
61+
d = vdf.loads(vdf_text, mapper=OrderedDict)
62+
63+
4864
.. |pypi| image:: https://img.shields.io/pypi/v/vdf.svg?style=flat&label=latest%20version
4965
:target: https://pypi.python.org/pypi/vdf
5066
:alt: Latest version released on PyPi

tests/test_vdf.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,21 @@ def test_routine_load(self, mock_parse):
3434
vdf.load(self.f)
3535
mock_parse.assert_called_with(self.f)
3636

37+
@mock.patch("vdf.parse")
38+
def test_routines_mapper_passing(self, mock_parse):
39+
vdf.load(sys.stdin, mapper=dict)
40+
mock_parse.assert_called_with(sys.stdin, mapper=dict)
41+
vdf.loads("", mapper=dict)
42+
mock_parse.assert_called_with("", mapper=dict)
43+
44+
class CustomDict(dict):
45+
pass
46+
47+
vdf.load(sys.stdin, mapper=CustomDict)
48+
mock_parse.assert_called_with(sys.stdin, mapper=CustomDict)
49+
vdf.loads("", mapper=CustomDict)
50+
mock_parse.assert_called_with("", mapper=CustomDict)
51+
3752
def test_routine_load_assert(self):
3853
for t in [5, 5.5, 1.0j, None, [], (), {}, lambda: 0, '']:
3954
self.assertRaises(AssertionError, vdf.load, t)
@@ -57,16 +72,20 @@ def test_routine_dump_asserts(self):
5772
self.assertRaises(ValueError, vdf.dump, x, y)
5873

5974
def test_routine_dump_writing(self):
60-
src = {"asd": "123"}
61-
expected = vdf.dumps(src)
75+
class CustomDict(dict):
76+
pass
6277

63-
vdf.dump(src, self.f)
64-
self.f.seek(0)
78+
for mapper in (dict, CustomDict):
79+
src = mapper({"asd": "123"})
80+
expected = vdf.dumps(src)
81+
82+
vdf.dump(src, self.f)
83+
self.f.seek(0)
6584

6685
self.assertEqual(expected, self.f.read())
6786

6887

69-
class testcase_parse_routine(unittest.TestCase):
88+
class testcase_routine_parse(unittest.TestCase):
7089
def test_parse_bom_removal(self):
7190
result = vdf.loads(vdf.BOMS + '"asd" "123"')
7291
self.assertEqual(result, {'asd': '123'})
@@ -75,13 +94,21 @@ def test_parse_bom_removal(self):
7594
result = vdf.loads(vdf.BOMS_UNICODE + '"asd" "123"')
7695
self.assertEqual(result, {'asd': '123'})
7796

78-
def test_parse_asserts(self):
97+
def test_parse_source_asserts(self):
7998
for t in [5, 5.5, 1.0j, True, None, (), {}, lambda: 0]:
8099
self.assertRaises(ValueError, vdf.parse, t)
81100

101+
def test_parse_mapper_assert(self):
102+
self.assertRaises(TypeError, vdf.parse, "", mapper=list)
103+
82104
def test_parse_file_source(self):
83105
self.assertEqual(vdf.parse(StringIO(" ")), {})
84106

107+
class CustomDict(dict):
108+
pass
109+
110+
self.assertEqual(vdf.parse(StringIO(" "), mapper=CustomDict), {})
111+
85112

86113
class testcase_VDF(unittest.TestCase):
87114
def test_format(self):

vdf/__init__.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,21 @@ def strip_bom(line):
4343
###############################################
4444

4545

46-
def parse(source):
46+
def parse(source, mapper=dict):
47+
if not issubclass(mapper, dict):
48+
raise TypeError("Expected mapper to be subclass of dict, got %s", type(mapper))
4749
if isinstance(source, file_type):
4850
lines = source.readlines()
4951
elif isinstance(source, string_type):
5052
lines = source.split('\n')
5153
else:
52-
raise ValueError("Expected parametar to be file or str")
54+
raise ValueError("Expected source to be file or str")
5355

5456
# strip annoying BOMS
5557
lines[0] = strip_bom(lines[0])
5658

5759
# init
58-
obj = dict()
60+
obj = mapper()
5961
stack = [obj]
6062
expect_bracket = False
6163

@@ -111,7 +113,7 @@ def parse(source):
111113

112114
key = m.group(1)
113115

114-
stack[-1][key] = dict()
116+
stack[-1][key] = mapper()
115117
stack.append(stack[-1][key])
116118
expect_bracket = True
117119

@@ -124,14 +126,14 @@ def parse(source):
124126
return obj
125127

126128

127-
def loads(fp):
129+
def loads(fp, **kwargs):
128130
assert isinstance(fp, string_type)
129-
return parse(fp)
131+
return parse(fp, **kwargs)
130132

131133

132-
def load(fp):
134+
def load(fp, **kwargs):
133135
assert isinstance(fp, file_type)
134-
return parse(fp)
136+
return parse(fp, **kwargs)
135137

136138
###############################################
137139
#

0 commit comments

Comments
 (0)