python-jsonstore 1.3.3__py3-none-any.whl → 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
jsonstore.py CHANGED
@@ -5,19 +5,16 @@ Provides a Python class that maps values to/from a JSON file
5
5
  """
6
6
  from __future__ import absolute_import
7
7
  import json
8
- import os.path
9
- import sys
8
+ import os
9
+ import stat
10
10
  from collections import OrderedDict
11
11
  from copy import deepcopy
12
+ from tempfile import mkstemp
12
13
 
13
14
  __all__ = ["JsonStore"]
14
15
 
15
16
  STRING_TYPES = (str,)
16
- INT_TYPES = (int,)
17
- if sys.version_info < (3,):
18
- STRING_TYPES += (unicode,)
19
- INT_TYPES += (long,)
20
- VALUE_TYPES = (bool, int, float, type(None)) + INT_TYPES
17
+ VALUE_TYPES = (bool, int, float, type(None))
21
18
 
22
19
 
23
20
  class JsonStore(object):
@@ -34,7 +31,11 @@ class JsonStore(object):
34
31
  if any(args):
35
32
  self.__dict__["_data"] = previous_state
36
33
  elif not self.__dict__["_states"]:
37
- self._save()
34
+ try:
35
+ self._save()
36
+ except BaseException:
37
+ self.__dict__["_data"] = previous_state
38
+ raise
38
39
 
39
40
  def _do_auto_commit(self):
40
41
  if self._auto_commit and not self.__dict__["_states"]:
@@ -56,25 +57,39 @@ class JsonStore(object):
56
57
  self.__dict__["_data"] = data
57
58
 
58
59
  def _save(self):
59
- temp = self._path + "~"
60
- with open(temp, "wb") as store:
61
- output = json.dumps(self._data, indent=self._indent)
62
- store.write(output.encode("utf-8"))
63
-
64
- if sys.version_info >= (3, 3):
60
+ output = json.dumps(self._data, indent=self._indent).encode("utf-8")
61
+ try:
62
+ mode = stat.S_IMODE(os.stat(self._path).st_mode)
63
+ except FileNotFoundError:
64
+ mode = None
65
+ descriptor, temp = mkstemp(
66
+ prefix="." + os.path.basename(self._path) + ".",
67
+ suffix=".tmp",
68
+ dir=os.path.dirname(os.path.abspath(self._path)),
69
+ )
70
+ try:
71
+ with os.fdopen(descriptor, "wb") as store:
72
+ store.write(output)
73
+ if mode is not None:
74
+ os.chmod(temp, mode)
65
75
  os.replace(temp, self._path)
66
- elif os.name == "windows":
67
- os.remove(self._path)
68
- os.rename(temp, self._path)
69
- else:
70
- os.rename(temp, self._path)
76
+ finally:
77
+ try:
78
+ os.unlink(temp)
79
+ except FileNotFoundError:
80
+ pass
81
+ except PermissionError:
82
+ if os.name != "nt":
83
+ raise
84
+ os.chmod(temp, stat.S_IWRITE)
85
+ os.unlink(temp)
71
86
 
72
87
  def __init__(self, path, indent=2, auto_commit=True):
73
88
  self.__dict__.update(
74
89
  {
75
90
  "_auto_commit": auto_commit,
76
91
  "_data": None,
77
- "_path": path,
92
+ "_path": os.fsdecode(path),
78
93
  "_indent": indent,
79
94
  "_states": [],
80
95
  }
@@ -93,25 +108,26 @@ class JsonStore(object):
93
108
  Raise an exception if the object is not suitable for assignment.
94
109
 
95
110
  """
96
- # pylint: disable=unicode-builtin,long-builtin
97
- if isinstance(obj, (dict, list)):
111
+ if isinstance(obj, (dict, list, tuple)):
98
112
  if parents is None:
99
- parents = []
100
- elif any(o is obj for o in parents):
101
- raise ValueError("Cycle detected in list/dictionary")
102
- parents.append(obj)
103
-
104
- if isinstance(obj, dict):
105
- for k, v in obj.items():
106
- if not cls._valid_string(k):
107
- # this is necessary because of the JSON serialisation
108
- raise TypeError("a dict has non-string keys")
109
- cls._verify_object(v, parents)
110
- elif isinstance(obj, (list, tuple)):
111
- for o in obj:
112
- cls._verify_object(o, parents)
113
- else:
114
- return cls._valid_value(obj)
113
+ parents = set()
114
+ identity = id(obj)
115
+ if identity in parents:
116
+ raise ValueError("Cycle detected in list/dictionary/tuple")
117
+ parents.add(identity)
118
+ try:
119
+ if isinstance(obj, dict):
120
+ for key, value in obj.items():
121
+ if not cls._valid_string(key):
122
+ raise TypeError("a dict has non-string keys")
123
+ cls._verify_object(value, parents)
124
+ else:
125
+ for value in obj:
126
+ cls._verify_object(value, parents)
127
+ finally:
128
+ parents.remove(identity)
129
+ elif not cls._valid_value(obj):
130
+ raise TypeError("unsupported JSON value type: %s" % type(obj).__name__)
115
131
 
116
132
  @classmethod
117
133
  def _valid_value(cls, value):
@@ -143,11 +159,14 @@ class JsonStore(object):
143
159
 
144
160
  def __setattr__(self, attr, value):
145
161
  self._verify_object(value)
146
- self._data[attr] = deepcopy(value)
162
+ value = deepcopy(value)
163
+ self._verify_object(value)
164
+ self._data[attr] = value
147
165
  self._do_auto_commit()
148
166
 
149
167
  def __delattr__(self, attr):
150
168
  del self._data[attr]
169
+ self._do_auto_commit()
151
170
 
152
171
  def __get_obj(self, steps):
153
172
  """Returns the object which is under the given path."""
@@ -168,11 +187,13 @@ class JsonStore(object):
168
187
  steps = self._canonical_key(key)
169
188
  path, step = steps[:-1], steps[-1]
170
189
  self._verify_object(value)
190
+ value = deepcopy(value)
191
+ self._verify_object(value)
171
192
  container = self.__get_obj(path)
172
193
  if isinstance(container, dict) and not self._valid_string(step):
173
194
  raise TypeError("%s is a dict and %s is not a string" % (path, step))
174
195
  try:
175
- container[step] = deepcopy(value)
196
+ container[step] = value
176
197
  except (IndexError, TypeError) as e:
177
198
  raise type(e)("unable to set %s from %s: %s" % (step, path, e))
178
199
  self._do_auto_commit()
@@ -190,6 +211,7 @@ class JsonStore(object):
190
211
  del obj[step]
191
212
  except (KeyError, IndexError, TypeError) as e:
192
213
  raise type(e)("unable to delete %s from %s: %s" % (step, path, e))
214
+ self._do_auto_commit()
193
215
 
194
216
  def __contains__(self, key):
195
217
  steps = self._canonical_key(key)
@@ -0,0 +1,124 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-jsonstore
3
+ Version: 2.0.0
4
+ Summary: Map JSON files onto Python attributes for simple configuration storage
5
+ Author-email: Oliver Bristow <github+pypi@oliverbristow.co.uk>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Code0x58/python-jsonstore/
8
+ Keywords: json,key,value,store
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Programming Language :: Python :: 3.15
20
+ Classifier: Programming Language :: Python :: Implementation :: CPython
21
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
22
+ Classifier: Topic :: Database
23
+ Classifier: Topic :: Software Development
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/x-rst
26
+ License-File: LICENCE.md
27
+ Dynamic: license-file
28
+
29
+ |PyPI| |Build Status| |Coverage Report|
30
+
31
+ jsonstore
32
+ =========
33
+
34
+ ``JsonStore`` maps a JSON object's keys to Python attributes. Use it to
35
+ load and save configuration containing nested dictionaries and lists.
36
+
37
+ Requires Python 3.9 or newer. Tests run on CPython 3.9–3.15 and PyPy 3.11.
38
+
39
+ Examples
40
+ --------
41
+
42
+ Install with ``pip install python-jsonstore``.
43
+
44
+ Basics
45
+ ~~~~~~
46
+
47
+ .. code:: python
48
+
49
+ from jsonstore import JsonStore
50
+
51
+ # Assignments and deletions are saved automatically.
52
+ store = JsonStore("config.json")
53
+ store.message = "hello"
54
+ store.options = {"enabled": True, "greeting": "💐"}
55
+ store.numbers = [1, 2, 3]
56
+ assert "message" in store
57
+ del store.message
58
+
59
+ # Dot-separated keys traverse dictionaries.
60
+ store["options.enabled"] = False
61
+ assert store["options", "enabled"] is False
62
+
63
+ # Tuple or list paths also accept list indices and slices.
64
+ assert store["numbers", -1] == 3
65
+ assert store[["numbers", 1]] == 2
66
+ del store["numbers", :2]
67
+ assert store.numbers == [3]
68
+
69
+ # A one-element tuple addresses a literal key containing a dot.
70
+ store[("literal.key",)] = "value"
71
+
72
+ Reads and assignments use deep copies. To update a stored collection,
73
+ assign the modified value back; ``+=`` also does this:
74
+
75
+ .. code:: python
76
+
77
+ store.numbers.append(4)
78
+ assert store.numbers == [3]
79
+ store.numbers += [4, 5]
80
+ assert store.numbers == [3, 4, 5]
81
+
82
+ Use item access for keys that conflict with ``JsonStore`` attributes.
83
+ ``indent`` controls JSON formatting and defaults to 2; use ``None`` for
84
+ compact output. ``auto_commit=False`` defers saving until a transaction
85
+ commits.
86
+
87
+ Transactions
88
+ ~~~~~~~~~~~~
89
+
90
+ Use ``with store:`` to group changes into a transaction. Transactions may
91
+ be nested; only successful exit from the outermost transaction saves the
92
+ file. An exception in a transaction restores its previous state, including
93
+ when saving fails.
94
+
95
+ .. code:: python
96
+
97
+ from jsonstore import JsonStore
98
+
99
+ with JsonStore("config.json", indent=None, auto_commit=False) as store:
100
+ store.value = 1
101
+ assert JsonStore("config.json").value == 1
102
+
103
+ try:
104
+ with store:
105
+ store.value = 2
106
+ with store:
107
+ store.value = 3
108
+ raise ValueError("cancel the transaction")
109
+ except ValueError:
110
+ pass
111
+ assert store.value == 1
112
+ assert JsonStore("config.json").value == 1
113
+
114
+ Transactions provide in-memory rollback, with no locking or coordination
115
+ between writers. Use one writer per file; separate stores do not reload
116
+ each other's changes. Atomic replacement does not guarantee durability
117
+ across a power failure.
118
+
119
+ .. |Build Status| image:: https://github.com/Code0x58/python-jsonstore/actions/workflows/ci.yml/badge.svg
120
+ :target: https://github.com/Code0x58/python-jsonstore/actions/workflows/ci.yml
121
+ .. |PyPI| image:: https://img.shields.io/pypi/v/python-jsonstore.svg
122
+ :target: https://pypi.org/project/python-jsonstore/
123
+ .. |Coverage Report| image:: https://codecov.io/gh/Code0x58/python-jsonstore/branch/master/graph/badge.svg
124
+ :target: https://codecov.io/gh/Code0x58/python-jsonstore
@@ -0,0 +1,6 @@
1
+ jsonstore.py,sha256=9P6hCPeB92dfnSgfdoO2PzcvzxlzBsf836D7bFNIk5c,7334
2
+ python_jsonstore-2.0.0.dist-info/licenses/LICENCE.md,sha256=CYFsxv7F0RTimJ83xSWkMtR8audySdDV_z9XzTr2EP0,1071
3
+ python_jsonstore-2.0.0.dist-info/METADATA,sha256=sc9XZ8fOju_A3_x0Abus8ywKuDj9_dpyCqEi01I-x5I,4290
4
+ python_jsonstore-2.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
5
+ python_jsonstore-2.0.0.dist-info/top_level.txt,sha256=5Lr-ZZ_07h17JRcT8M4FAt5aEkwAMHNGZuVHNC7yZ5w,10
6
+ python_jsonstore-2.0.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (84.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,152 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: python-jsonstore
3
- Version: 1.3.3
4
- Summary: Map JSON files onto Python attributes for simple configuration storage
5
- Author-email: Oliver Bristow <github+pypi@oliverbristow.co.uk>
6
- License: MIT
7
- Project-URL: Homepage, https://github.com/Code0x58/python-jsonstore/
8
- Keywords: json,key,value,store
9
- Classifier: Development Status :: 5 - Production/Stable
10
- Classifier: Intended Audience :: Developers
11
- Classifier: License :: OSI Approved :: MIT License
12
- Classifier: Operating System :: OS Independent
13
- Classifier: Programming Language :: Python :: 2
14
- Classifier: Programming Language :: Python :: 2.7
15
- Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.3
17
- Classifier: Programming Language :: Python :: 3.4
18
- Classifier: Programming Language :: Python :: 3.5
19
- Classifier: Programming Language :: Python :: 3.6
20
- Classifier: Programming Language :: Python :: 3.7
21
- Classifier: Programming Language :: Python :: 3.8
22
- Classifier: Programming Language :: Python :: 3.9
23
- Classifier: Programming Language :: Python :: 3.10
24
- Classifier: Programming Language :: Python :: 3.11
25
- Classifier: Programming Language :: Python :: 3.12
26
- Classifier: Programming Language :: Python :: 3.13
27
- Classifier: Programming Language :: Python :: 3.14
28
- Classifier: Programming Language :: Python :: Implementation :: CPython
29
- Classifier: Programming Language :: Python :: Implementation :: PyPy
30
- Classifier: Topic :: Database
31
- Classifier: Topic :: Software Development
32
- Requires-Python: >=2.7
33
- Description-Content-Type: text/x-rst
34
- License-File: LICENCE.md
35
- Dynamic: license-file
36
-
37
- |PyPI| |Build Status| |Coverage Report|
38
-
39
- jsonstore
40
- =========
41
-
42
- This module provides a class that maps keys and values from a JSON file
43
- onto its attributes.
44
-
45
- The goal was to provide a convenient way of loading and saving
46
- configuration in a familiar human readable format. This is a bit more
47
- flexible than the
48
- `configparser <https://docs.python.org/3/library/configparser.html>`__
49
- module which is included with Python.
50
-
51
- This works is tested and working on Python 2.7 and Python 3.6+. It will
52
- not work on 2.6 or lower. The tests are also executed on PyPy.
53
-
54
- Examples
55
- --------
56
-
57
- Basics
58
- ~~~~~~
59
-
60
- .. code:: python
61
-
62
- # by default JsonStore commits on every change unless in a transaction
63
- store = JsonStore('config.json')
64
- store.a_string = "something"
65
- store.a_list = [1, 2, 3]
66
- store.a_dictionary = {
67
- 'dict-list': [{}],
68
- 'ln(2)': 0.69314718056,
69
- 'for-you': u"💐",
70
- }
71
-
72
- # you can use […] to set/get/delete string keys
73
- store['some_key'] = "a value"
74
- # the key is split on '.'s and works on dictionaries
75
- del store['a_dictionary.dict-list']
76
- store['a_dictionary.new_value'] = "old value"
77
- # you can also use the syntactic sugar for tuple keys (explicit lists work too)
78
- assert store['a_dictionary', 'new_value'] == "old value"
79
- # you can traverse lists too
80
- assert store['a_list', -1] == 3
81
- # you can use slices in lists
82
- assert len(store['a_list', 1:]) == 2
83
- del store['a_list', :2]
84
- assert store.a_list == [3]
85
-
86
- # deep copies are made when assigning values
87
- my_list = ['fun']
88
- store.a_list = my_list
89
- assert store.a_list is not my_list
90
- assert 'a_list' in store
91
-
92
- # deep copies are also returned to avoid unsanitary changes being made
93
- store.a_dictionary['new_value'] = "new value" # won't update the store!
94
- assert store.a_dictionary['new_value'] == "old value"
95
- assert store.a_dictionary is not store.a_dictionary
96
-
97
- # Appending to, extending a list
98
- >>> store.list = [1, 2, 3]
99
-
100
- # Because of the fact that .append won't modify the list in the actual file,
101
- # but only a copy...
102
- >>> store.list.append(4)
103
- >>> store.list
104
- [1, 2, 3]
105
-
106
- # ... we need to rather use the += operator to append to a list.
107
- >>> store.list += [4]
108
- >>> store.list
109
- [1, 2, 3, 4]
110
-
111
- # Similarly, we can extend the list
112
- >>> store.list += [5, 6]
113
- >>> store.list
114
- [1, 2, 3, 4, 5, 6]
115
-
116
- Transactions
117
- ~~~~~~~~~~~~
118
-
119
- ``JsonStore`` objects can be used as `context
120
- managers <https://www.python.org/dev/peps/pep-0343/>`__ to provide
121
- transactions which are rolled back in the event of an exception. The
122
- transaction model is primitive; you can only nest transactions.
123
-
124
- While a store is put into a transaction, it will not save changes to
125
- file until all of the transactions have been closed.
126
-
127
- .. code:: python
128
-
129
- from jsonstore import JsonStore
130
-
131
- # even with auto_commit=True, the file won't be saved until the last contexts has been closed
132
- with JsonStore('config.json', indent=None, auto_commit=False) as store:
133
- self.value = 1
134
-
135
- # the context manager will roll back changes made if an exception is raised
136
- store = JsonStore('config.json', indent=None)
137
- try:
138
- with store:
139
- store.value = "new"
140
- raise Exception
141
- except Exception:
142
- pass
143
- # here we see the value that was saved previously
144
- assert store.value == 1
145
-
146
-
147
- .. |Build Status| image:: https://github.com/Code0x58/python-jsonstore/actions/workflows/ci.yml/badge.svg
148
- :target: https://github.com/Code0x58/python-jsonstore/actions/workflows/ci.yml
149
- .. |PyPI| image:: https://img.shields.io/pypi/v/python-jsonstore.svg
150
- :target: https://pypi.org/project/python-jsonstore/
151
- .. |Coverage Report| image:: https://codecov.io/gh/Code0x58/python-jsonstore/branch/master/graph/badge.svg
152
- :target: https://codecov.io/gh/Code0x58/python-jsonstore
@@ -1,6 +0,0 @@
1
- jsonstore.py,sha256=YGCPCsjTnklMHR_neqgbcV6ltQh5A5QKW4rbOQzzXJk,6511
2
- python_jsonstore-1.3.3.dist-info/licenses/LICENCE.md,sha256=CYFsxv7F0RTimJ83xSWkMtR8audySdDV_z9XzTr2EP0,1071
3
- python_jsonstore-1.3.3.dist-info/METADATA,sha256=yxS-sej7Mi05NMnx9Zrz3gkLVtIlA8uXpPVBD1Dpbpc,5434
4
- python_jsonstore-1.3.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
5
- python_jsonstore-1.3.3.dist-info/top_level.txt,sha256=5Lr-ZZ_07h17JRcT8M4FAt5aEkwAMHNGZuVHNC7yZ5w,10
6
- python_jsonstore-1.3.3.dist-info/RECORD,,