python-jsonstore 1.2.0__tar.gz → 1.3.3__tar.gz

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.
@@ -0,0 +1,71 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ - master
8
+ pull_request:
9
+
10
+ jobs:
11
+ tests:
12
+ runs-on: ${{ matrix.os }}
13
+ env:
14
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
15
+ strategy:
16
+ fail-fast: false
17
+ matrix:
18
+ os:
19
+ - ubuntu-latest
20
+ - windows-latest
21
+ python-version:
22
+ - "3.8"
23
+ - "3.9"
24
+ - "3.10"
25
+ - "3.11"
26
+ - "3.12"
27
+ - "3.13"
28
+ - "3.14"
29
+ - "pypy-3.9"
30
+ exclude:
31
+ - os: windows-latest
32
+ python-version: "pypy-3.9"
33
+ steps:
34
+ - name: Check out repository
35
+ uses: actions/checkout@v4
36
+
37
+ - name: Set up Python ${{ matrix.python-version }}
38
+ uses: actions/setup-python@v5
39
+ with:
40
+ python-version: ${{ matrix.python-version }}
41
+
42
+ - name: Install system dependencies
43
+ if: runner.os == 'Linux'
44
+ run: |
45
+ sudo apt-get update
46
+ sudo apt-get install -y zip p7zip-full
47
+
48
+ - name: Install Python dependencies
49
+ run: |
50
+ python -m pip install --upgrade pip
51
+ python -m pip install . pytest pytest-cov
52
+
53
+ - name: Run tests
54
+ run: pytest -vv --cov=jsonstore --cov-report=xml --junitxml=test-results.xml
55
+
56
+ - name: Upload coverage to Codecov
57
+ if: always() && env.CODECOV_TOKEN != ''
58
+ uses: codecov/codecov-action@v5
59
+ with:
60
+ token: ${{ env.CODECOV_TOKEN }}
61
+ files: ./coverage.xml
62
+ fail_ci_if_error: true
63
+
64
+ - name: Upload test results to Codecov
65
+ if: always() && env.CODECOV_TOKEN != ''
66
+ uses: codecov/codecov-action@v5
67
+ with:
68
+ token: ${{ env.CODECOV_TOKEN }}
69
+ files: ./test-results.xml
70
+ report_type: test_results
71
+ fail_ci_if_error: true
@@ -0,0 +1,27 @@
1
+ name: Publish
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ build-and-publish:
9
+ runs-on: ubuntu-latest
10
+ environment:
11
+ name: pypi
12
+ url: https://pypi.org/p/python-jsonstore
13
+ permissions:
14
+ id-token: write
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - name: Set up Python
18
+ uses: actions/setup-python@v5
19
+ with:
20
+ python-version: '3.x'
21
+ - name: Build package
22
+ run: |
23
+ python -m pip install --upgrade pip build
24
+ python -m build
25
+ # the publishing uses PyPI's trusted publishing, so configured on pypi instead of using secrets
26
+ - name: Publish to PyPI
27
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,152 @@
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,4 +1,4 @@
1
- |PyPi Package| |Build Status| |Codacy Rating|
1
+ |PyPI| |Build Status| |Coverage Report|
2
2
 
3
3
  jsonstore
4
4
  =========
@@ -12,11 +12,8 @@ flexible than the
12
12
  `configparser <https://docs.python.org/3/library/configparser.html>`__
13
13
  module which is included with Python.
14
14
 
15
- This works is tested and working on Python 2.7+ and Python 3.3+. It will
16
- not work on 2.6 or lower, but is expected to work on 3.0-3.2. The tests
17
- do not work in 3.2.6 due to
18
- `mistreating <https://travis-ci.org/Code0x58/python-jsonstore/jobs/198150401>`__
19
- the 💩 when parsing the test code. This is also tested on pypy and pypy3.
15
+ This works is tested and working on Python 2.7 and Python 3.6+. It will
16
+ not work on 2.6 or lower. The tests are also executed on PyPy.
20
17
 
21
18
  Examples
22
19
  --------
@@ -47,6 +44,8 @@ Basics
47
44
  assert store['a_list', -1] == 3
48
45
  # you can use slices in lists
49
46
  assert len(store['a_list', 1:]) == 2
47
+ del store['a_list', :2]
48
+ assert store.a_list == [3]
50
49
 
51
50
  # deep copies are made when assigning values
52
51
  my_list = ['fun']
@@ -59,6 +58,25 @@ Basics
59
58
  assert store.a_dictionary['new_value'] == "old value"
60
59
  assert store.a_dictionary is not store.a_dictionary
61
60
 
61
+ # Appending to, extending a list
62
+ >>> store.list = [1, 2, 3]
63
+
64
+ # Because of the fact that .append won't modify the list in the actual file,
65
+ # but only a copy...
66
+ >>> store.list.append(4)
67
+ >>> store.list
68
+ [1, 2, 3]
69
+
70
+ # ... we need to rather use the += operator to append to a list.
71
+ >>> store.list += [4]
72
+ >>> store.list
73
+ [1, 2, 3, 4]
74
+
75
+ # Similarly, we can extend the list
76
+ >>> store.list += [5, 6]
77
+ >>> store.list
78
+ [1, 2, 3, 4, 5, 6]
79
+
62
80
  Transactions
63
81
  ~~~~~~~~~~~~
64
82
 
@@ -89,9 +107,10 @@ file until all of the transactions have been closed.
89
107
  # here we see the value that was saved previously
90
108
  assert store.value == 1
91
109
 
92
- .. |Build Status| image:: https://travis-ci.org/Code0x58/python-jsonstore.svg?branch=master
93
- :target: https://travis-ci.org/Code0x58/python-jsonstore
94
- .. |Codacy Rating| image:: https://api.codacy.com/project/badge/Grade/37ea488773444de59469a3775be83faf
95
- :target: https://www.codacy.com/app/evilumbrella-github/python-jsonstore?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=Code0x58/python-jsonstore&amp;utm_campaign=Badge_Grade
96
- .. |PyPi Package| image:: https://badge.fury.io/py/python-jsonstore.svg
110
+
111
+ .. |Build Status| image:: https://github.com/Code0x58/python-jsonstore/actions/workflows/ci.yml/badge.svg
112
+ :target: https://github.com/Code0x58/python-jsonstore/actions/workflows/ci.yml
113
+ .. |PyPI| image:: https://img.shields.io/pypi/v/python-jsonstore.svg
97
114
  :target: https://pypi.org/project/python-jsonstore/
115
+ .. |Coverage Report| image:: https://codecov.io/gh/Code0x58/python-jsonstore/branch/master/graph/badge.svg
116
+ :target: https://codecov.io/gh/Code0x58/python-jsonstore
@@ -12,6 +12,13 @@ from copy import deepcopy
12
12
 
13
13
  __all__ = ["JsonStore"]
14
14
 
15
+ 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
21
+
15
22
 
16
23
  class JsonStore(object):
17
24
  """A class to provide object based access to a JSON file"""
@@ -81,9 +88,10 @@ class JsonStore(object):
81
88
  raise AttributeError(key)
82
89
 
83
90
  @classmethod
84
- def _valid_object(cls, obj, parents=None):
91
+ def _verify_object(cls, obj, parents=None):
85
92
  """
86
- Determine if the object can be encoded into JSON
93
+ Raise an exception if the object is not suitable for assignment.
94
+
87
95
  """
88
96
  # pylint: disable=unicode-builtin,long-builtin
89
97
  if isinstance(obj, (dict, list)):
@@ -94,85 +102,100 @@ class JsonStore(object):
94
102
  parents.append(obj)
95
103
 
96
104
  if isinstance(obj, dict):
97
- return all(
98
- cls._valid_string(k) and cls._valid_object(v, parents)
99
- for k, v in obj.items()
100
- )
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)
101
110
  elif isinstance(obj, (list, tuple)):
102
- return all(cls._valid_object(o, parents) for o in obj)
111
+ for o in obj:
112
+ cls._verify_object(o, parents)
103
113
  else:
104
114
  return cls._valid_value(obj)
105
115
 
106
116
  @classmethod
107
117
  def _valid_value(cls, value):
108
- if isinstance(value, (bool, int, float, type(None))):
109
- return True
110
- elif sys.version_info < (3,) and isinstance(value, long):
118
+ if isinstance(value, VALUE_TYPES):
111
119
  return True
112
120
  else:
113
121
  return cls._valid_string(value)
114
122
 
115
123
  @classmethod
116
124
  def _valid_string(cls, value):
117
- if isinstance(value, str):
125
+ if isinstance(value, STRING_TYPES):
118
126
  return True
119
- elif sys.version_info < (3,):
120
- return isinstance(value, unicode)
121
127
  else:
122
128
  return False
123
129
 
124
- def __setattr__(self, key, value):
125
- if not self._valid_object(value):
126
- raise AttributeError
127
- self._data[key] = deepcopy(value)
130
+ @classmethod
131
+ def _canonical_key(cls, key):
132
+ """Convert a set/get/del key into the canonical form."""
133
+ if cls._valid_string(key):
134
+ return tuple(key.split("."))
135
+
136
+ if isinstance(key, (tuple, list)):
137
+ key = tuple(key)
138
+ if not key:
139
+ raise TypeError("key must be a string or non-empty tuple/list")
140
+ return key
141
+
142
+ raise TypeError("key must be a string or non-empty tuple/list")
143
+
144
+ def __setattr__(self, attr, value):
145
+ self._verify_object(value)
146
+ self._data[attr] = deepcopy(value)
128
147
  self._do_auto_commit()
129
148
 
130
- def __delattr__(self, key):
131
- del self._data[key]
149
+ def __delattr__(self, attr):
150
+ del self._data[attr]
132
151
 
133
- def __get_obj(self, full_path):
134
- """
135
- Returns the object which is under the given path
136
- """
137
- if isinstance(full_path, (tuple, list)):
138
- steps = full_path
139
- else:
140
- steps = full_path.split(".")
152
+ def __get_obj(self, steps):
153
+ """Returns the object which is under the given path."""
141
154
  path = []
142
155
  obj = self._data
143
- if not full_path:
144
- return obj
145
156
  for step in steps:
146
- path.append(step)
157
+ if isinstance(obj, dict) and not self._valid_string(step):
158
+ # this is necessary because of the JSON serialisation
159
+ raise TypeError("%s is a dict and %s is not a string" % (path, step))
147
160
  try:
148
161
  obj = obj[step]
149
- except KeyError:
150
- raise KeyError(".".join(path))
162
+ except (KeyError, IndexError, TypeError) as e:
163
+ raise type(e)("unable to get %s from %s: %s" % (step, path, e))
164
+ path.append(step)
151
165
  return obj
152
166
 
153
- def __setitem__(self, name, value):
154
- path, _, key = name.rpartition(".")
155
- if self._valid_object(value):
156
- dictionary = self.__get_obj(path)
157
- dictionary[key] = deepcopy(value)
158
- self._do_auto_commit()
159
- else:
160
- raise AttributeError
167
+ def __setitem__(self, key, value):
168
+ steps = self._canonical_key(key)
169
+ path, step = steps[:-1], steps[-1]
170
+ self._verify_object(value)
171
+ container = self.__get_obj(path)
172
+ if isinstance(container, dict) and not self._valid_string(step):
173
+ raise TypeError("%s is a dict and %s is not a string" % (path, step))
174
+ try:
175
+ container[step] = deepcopy(value)
176
+ except (IndexError, TypeError) as e:
177
+ raise type(e)("unable to set %s from %s: %s" % (step, path, e))
178
+ self._do_auto_commit()
161
179
 
162
180
  def __getitem__(self, key):
163
- obj = self.__get_obj(key)
164
- if obj is self._data:
165
- raise KeyError
181
+ steps = self._canonical_key(key)
182
+ obj = self.__get_obj(steps)
166
183
  return deepcopy(obj)
167
184
 
168
- def __delitem__(self, name):
169
- if isinstance(name, (tuple, list)):
170
- path = name[:-1]
171
- key = name[-1]
172
- else:
173
- path, _, key = name.rpartition(".")
185
+ def __delitem__(self, key):
186
+ steps = self._canonical_key(key)
187
+ path, step = steps[:-1], steps[-1]
174
188
  obj = self.__get_obj(path)
175
- del obj[key]
189
+ try:
190
+ del obj[step]
191
+ except (KeyError, IndexError, TypeError) as e:
192
+ raise type(e)("unable to delete %s from %s: %s" % (step, path, e))
176
193
 
177
194
  def __contains__(self, key):
178
- return key in self._data
195
+ steps = self._canonical_key(key)
196
+ try:
197
+ self.__get_obj(steps)
198
+ return True
199
+ except (KeyError, IndexError, TypeError):
200
+ # this is rather permissive as the types are dynamic
201
+ return False
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["setuptools>=75", "setuptools_scm>=7", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "python-jsonstore"
7
+ description = "Map JSON files onto Python attributes for simple configuration storage"
8
+ readme = "README.rst"
9
+ requires-python = ">=2.7"
10
+ license = {text = "MIT"}
11
+ authors = [
12
+ {name = "Oliver Bristow", email = "github+pypi@oliverbristow.co.uk"},
13
+ ]
14
+ keywords = ["json", "key", "value", "store"]
15
+ classifiers = [
16
+ "Development Status :: 5 - Production/Stable",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 2",
21
+ "Programming Language :: Python :: 2.7",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.3",
24
+ "Programming Language :: Python :: 3.4",
25
+ "Programming Language :: Python :: 3.5",
26
+ "Programming Language :: Python :: 3.6",
27
+ "Programming Language :: Python :: 3.7",
28
+ "Programming Language :: Python :: 3.8",
29
+ "Programming Language :: Python :: 3.9",
30
+ "Programming Language :: Python :: 3.10",
31
+ "Programming Language :: Python :: 3.11",
32
+ "Programming Language :: Python :: 3.12",
33
+ "Programming Language :: Python :: 3.13",
34
+ "Programming Language :: Python :: 3.14",
35
+ "Programming Language :: Python :: Implementation :: CPython",
36
+ "Programming Language :: Python :: Implementation :: PyPy",
37
+ "Topic :: Database",
38
+ "Topic :: Software Development",
39
+ ]
40
+ dependencies = []
41
+ dynamic = ["version"]
42
+
43
+ [project.urls]
44
+ Homepage = "https://github.com/Code0x58/python-jsonstore/"
45
+
46
+ [tool.setuptools]
47
+ py-modules = ["jsonstore"]
48
+ license-files = ["LICENCE.md"]
49
+
50
+ [tool.setuptools_scm]
@@ -0,0 +1,152 @@
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,10 +1,10 @@
1
- .travis.yml
2
1
  LICENCE.md
3
2
  README.rst
4
3
  jsonstore.py
5
- setup.cfg
6
- setup.py
4
+ pyproject.toml
7
5
  test_jsonstore.py
6
+ .github/workflows/ci.yml
7
+ .github/workflows/publish.yml
8
8
  python_jsonstore.egg-info/PKG-INFO
9
9
  python_jsonstore.egg-info/SOURCES.txt
10
10
  python_jsonstore.egg-info/dependency_links.txt
@@ -1,6 +1,3 @@
1
- [bdist_wheel]
2
- universal = 1
3
-
4
1
  [egg_info]
5
2
  tag_build =
6
3
  tag_date = 0
@@ -105,19 +105,39 @@ class Tests(unittest.TestCase):
105
105
  self.assertRaises(KeyError, self._getitem(name))
106
106
  self.assertRaises(AttributeError, self._getattr(name))
107
107
 
108
+ def test_slices(self):
109
+ self.store.list = [1, 2, 3]
110
+ self.store["list", :2] = ["a", "b"]
111
+ self.assertEqual(self.store["list", 1:], ["b", 3])
112
+ del self.store["list", :1]
113
+ self.assertEqual(self.store.list, ["b", 3])
114
+
108
115
  def test_assign_invalid_types(self):
109
116
  for method in (self._setattr, self._setitem):
110
117
 
111
118
  def assign(value):
112
119
  return method("key", value)
113
120
 
114
- self.assertRaises(AttributeError, assign(set()))
115
- self.assertRaises(AttributeError, assign(object()))
116
- self.assertRaises(AttributeError, assign(None for i in range(2)))
121
+ self.assertRaises(TypeError, assign(set()))
122
+ self.assertRaises(TypeError, assign(object()))
123
+ self.assertRaises(TypeError, assign(None for i in range(2)))
124
+ self.assertRaises(TypeError, assign({1: 1}))
117
125
 
118
126
  def test_assign_bad_keys(self):
119
- # FIXME: a ValueError would make more sense
120
- self.assertRaises(AttributeError, self._setitem(1, 2))
127
+ value = 1
128
+ # the root object is a dict, so a string key is needed
129
+ self.assertRaises(TypeError, self._setitem(1, value))
130
+ self.assertRaises(TypeError, self._setitem((1, "a"), value))
131
+
132
+ self.store["dict"] = {}
133
+ self.store["list"] = []
134
+ self.assertRaises(TypeError, self._setitem((), value))
135
+ self.assertRaises(TypeError, self._setitem(("dict", 1), value))
136
+ self.assertRaises(TypeError, self._setitem(("dict", slice(1)), value))
137
+ self.assertRaises(TypeError, self._setitem(("list", "a"), value))
138
+ self.assertRaises(TypeError, self._setitem(("list", slice("a")), value))
139
+ self.assertRaises(IndexError, self._setitem(("list", 1), value))
140
+
121
141
 
122
142
  def test_retrieve_values(self):
123
143
  for name, value in self.TEST_DATA:
@@ -177,6 +197,7 @@ class Tests(unittest.TestCase):
177
197
  assert self.store[["list", 0, "key", -1]] == "last"
178
198
  self.assertRaises(TypeError, self._getitem("list.0.key.1"))
179
199
  assert len(self.store["list", 0, "key", 1:]) == 2
200
+ self.assertRaises(IndexError, self._getitem(("list", 1)))
180
201
 
181
202
  def test_del(self):
182
203
  self.store.key = None
@@ -187,6 +208,18 @@ class Tests(unittest.TestCase):
187
208
  del self.store["key"]
188
209
  self.assertRaises(KeyError, self._getitem("key"))
189
210
 
211
+ with self.assertRaises(KeyError):
212
+ del self.store["missing"]
213
+
214
+ self.store.list = []
215
+ with self.assertRaises(IndexError):
216
+ del self.store["list", 1]
217
+
218
+ self.store.dict = {}
219
+ # somewhere after python 3.9 this went from a TypeError to a KeyError
220
+ with self.assertRaises((TypeError, KeyError)):
221
+ del self.store["dict", slice("a")]
222
+
190
223
  def test_context_and_deserialisation(self):
191
224
  store_file = mktemp()
192
225
  for name, value in self.TEST_DATA:
@@ -284,6 +317,18 @@ class Tests(unittest.TestCase):
284
317
  with open(self._store_file) as handle:
285
318
  self.assertEqual(handle.read(), '{"value1": 1, "value2": 2}')
286
319
 
320
+ def test_list_concat_inplace(self):
321
+ self.store.list = []
322
+ extension = [{"key": "value"}]
323
+
324
+ # make sure += happens
325
+ self.store["list"] += extension
326
+ self.store.list += extension
327
+ self.assertEqual(self.store.list, extension * 2)
328
+
329
+ # make sure a deepcopy occurred
330
+ self.assertIsNot(self.store.list[0], extension[0])
331
+
287
332
 
288
333
  if __name__ == "__main__":
289
334
  unittest.main()
@@ -1,26 +0,0 @@
1
- language: python
2
-
3
- jobs:
4
- include:
5
- - python: 3.8
6
- - python: 3.7
7
- - python: 3.6
8
- - python: 3.5
9
- - python: 3.4
10
- - python: 2.7
11
- - python: pypy3
12
- - python: pypy
13
- - name: "Python: 3.7"
14
- os: osx
15
- osx_image: xcode11.2 # Python 3.7.4 running on macOS 10.14.4
16
- language: shell # 'language: python' is an error on Travis CI macOS
17
- - name: "Python: 3.8"
18
- os: windows # Windows 10.0.17134 N/A Build 17134
19
- language: shell # 'language: python' is an error on Travis CI Windows
20
- before_install:
21
- - choco install python --version 3.8.2
22
- - python -m pip install --upgrade pip
23
- env: PATH=/c/Python38:/c/Python38/Scripts:$PATH
24
-
25
- install: python -m pip install . pytest
26
- script: python -m pytest
@@ -1,126 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: python-jsonstore
3
- Version: 1.2.0
4
- Summary: UNKNOWN
5
- Home-page: https://github.com/Code0x58/python-jsonstore/
6
- Author: Oliver Bristow
7
- Author-email: github+pypi@oliverbristow.co.uk
8
- License: MIT
9
- Description: |PyPi Package| |Build Status| |Codacy Rating|
10
-
11
- jsonstore
12
- =========
13
-
14
- This module provides a class that maps keys and values from a JSON file
15
- onto its attributes.
16
-
17
- The goal was to provide a convenient way of loading and saving
18
- configuration in a familiar human readable format. This is a bit more
19
- flexible than the
20
- `configparser <https://docs.python.org/3/library/configparser.html>`__
21
- module which is included with Python.
22
-
23
- This works is tested and working on Python 2.7+ and Python 3.3+. It will
24
- not work on 2.6 or lower, but is expected to work on 3.0-3.2. The tests
25
- do not work in 3.2.6 due to
26
- `mistreating <https://travis-ci.org/Code0x58/python-jsonstore/jobs/198150401>`__
27
- the 💩 when parsing the test code. This is also tested on pypy and pypy3.
28
-
29
- Examples
30
- --------
31
-
32
- Basics
33
- ~~~~~~
34
-
35
- .. code:: python
36
-
37
- # by default JsonStore commits on every change unless in a transaction
38
- store = JsonStore('config.json')
39
- store.a_string = "something"
40
- store.a_list = [1, 2, 3]
41
- store.a_dictionary = {
42
- 'dict-list': [{}],
43
- 'ln(2)': 0.69314718056,
44
- 'for-you': u"💐",
45
- }
46
-
47
- # you can use […] to set/get/delete string keys
48
- store['some_key'] = "a value"
49
- # the key is split on '.'s and works on dictionaries
50
- del store['a_dictionary.dict-list']
51
- store['a_dictionary.new_value'] = "old value"
52
- # you can also use the syntactic sugar for tuple keys (explicit lists work too)
53
- assert store['a_dictionary', 'new_value'] == "old value"
54
- # you can traverse lists too
55
- assert store['a_list', -1] == 3
56
- # you can use slices in lists
57
- assert len(store['a_list', 1:]) == 2
58
-
59
- # deep copies are made when assigning values
60
- my_list = ['fun']
61
- store.a_list = my_list
62
- assert store.a_list is not my_list
63
- assert 'a_list' in store
64
-
65
- # deep copies are also returned to avoid unsanitary changes being made
66
- store.a_dictionary['new_value'] = "new value" # won't update the store!
67
- assert store.a_dictionary['new_value'] == "old value"
68
- assert store.a_dictionary is not store.a_dictionary
69
-
70
- Transactions
71
- ~~~~~~~~~~~~
72
-
73
- ``JsonStore`` objects can be used as `context
74
- managers <https://www.python.org/dev/peps/pep-0343/>`__ to provide
75
- transactions which are rolled back in the event of an exception. The
76
- transaction model is primitive; you can only nest transactions.
77
-
78
- While a store is put into a transaction, it will not save changes to
79
- file until all of the transactions have been closed.
80
-
81
- .. code:: python
82
-
83
- from jsonstore import JsonStore
84
-
85
- # even with auto_commit=True, the file won't be saved until the last contexts has been closed
86
- with JsonStore('config.json', indent=None, auto_commit=False) as store:
87
- self.value = 1
88
-
89
- # the context manager will roll back changes made if an exception is raised
90
- store = JsonStore('config.json', indent=None)
91
- try:
92
- with store:
93
- store.value = "new"
94
- raise Exception
95
- except Exception:
96
- pass
97
- # here we see the value that was saved previously
98
- assert store.value == 1
99
-
100
- .. |Build Status| image:: https://travis-ci.org/Code0x58/python-jsonstore.svg?branch=master
101
- :target: https://travis-ci.org/Code0x58/python-jsonstore
102
- .. |Codacy Rating| image:: https://api.codacy.com/project/badge/Grade/37ea488773444de59469a3775be83faf
103
- :target: https://www.codacy.com/app/evilumbrella-github/python-jsonstore?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=Code0x58/python-jsonstore&amp;utm_campaign=Badge_Grade
104
- .. |PyPi Package| image:: https://badge.fury.io/py/python-jsonstore.svg
105
- :target: https://pypi.org/project/python-jsonstore/
106
-
107
- Keywords: json key value store
108
- Platform: UNKNOWN
109
- Classifier: Development Status :: 5 - Production/Stable
110
- Classifier: Intended Audience :: Developers
111
- Classifier: License :: OSI Approved :: MIT License
112
- Classifier: Operating System :: OS Independent
113
- Classifier: Programming Language :: Python :: 2
114
- Classifier: Programming Language :: Python :: 2.7
115
- Classifier: Programming Language :: Python :: 3
116
- Classifier: Programming Language :: Python :: 3.3
117
- Classifier: Programming Language :: Python :: 3.4
118
- Classifier: Programming Language :: Python :: 3.5
119
- Classifier: Programming Language :: Python :: 3.6
120
- Classifier: Programming Language :: Python :: 3.7
121
- Classifier: Programming Language :: Python :: 3.8
122
- Classifier: Programming Language :: Python :: Implementation :: CPython
123
- Classifier: Programming Language :: Python :: Implementation :: PyPy
124
- Classifier: Topic :: Database
125
- Classifier: Topic :: Software Development
126
- Description-Content-Type: text/x-rst
@@ -1,126 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: python-jsonstore
3
- Version: 1.2.0
4
- Summary: UNKNOWN
5
- Home-page: https://github.com/Code0x58/python-jsonstore/
6
- Author: Oliver Bristow
7
- Author-email: github+pypi@oliverbristow.co.uk
8
- License: MIT
9
- Description: |PyPi Package| |Build Status| |Codacy Rating|
10
-
11
- jsonstore
12
- =========
13
-
14
- This module provides a class that maps keys and values from a JSON file
15
- onto its attributes.
16
-
17
- The goal was to provide a convenient way of loading and saving
18
- configuration in a familiar human readable format. This is a bit more
19
- flexible than the
20
- `configparser <https://docs.python.org/3/library/configparser.html>`__
21
- module which is included with Python.
22
-
23
- This works is tested and working on Python 2.7+ and Python 3.3+. It will
24
- not work on 2.6 or lower, but is expected to work on 3.0-3.2. The tests
25
- do not work in 3.2.6 due to
26
- `mistreating <https://travis-ci.org/Code0x58/python-jsonstore/jobs/198150401>`__
27
- the 💩 when parsing the test code. This is also tested on pypy and pypy3.
28
-
29
- Examples
30
- --------
31
-
32
- Basics
33
- ~~~~~~
34
-
35
- .. code:: python
36
-
37
- # by default JsonStore commits on every change unless in a transaction
38
- store = JsonStore('config.json')
39
- store.a_string = "something"
40
- store.a_list = [1, 2, 3]
41
- store.a_dictionary = {
42
- 'dict-list': [{}],
43
- 'ln(2)': 0.69314718056,
44
- 'for-you': u"💐",
45
- }
46
-
47
- # you can use […] to set/get/delete string keys
48
- store['some_key'] = "a value"
49
- # the key is split on '.'s and works on dictionaries
50
- del store['a_dictionary.dict-list']
51
- store['a_dictionary.new_value'] = "old value"
52
- # you can also use the syntactic sugar for tuple keys (explicit lists work too)
53
- assert store['a_dictionary', 'new_value'] == "old value"
54
- # you can traverse lists too
55
- assert store['a_list', -1] == 3
56
- # you can use slices in lists
57
- assert len(store['a_list', 1:]) == 2
58
-
59
- # deep copies are made when assigning values
60
- my_list = ['fun']
61
- store.a_list = my_list
62
- assert store.a_list is not my_list
63
- assert 'a_list' in store
64
-
65
- # deep copies are also returned to avoid unsanitary changes being made
66
- store.a_dictionary['new_value'] = "new value" # won't update the store!
67
- assert store.a_dictionary['new_value'] == "old value"
68
- assert store.a_dictionary is not store.a_dictionary
69
-
70
- Transactions
71
- ~~~~~~~~~~~~
72
-
73
- ``JsonStore`` objects can be used as `context
74
- managers <https://www.python.org/dev/peps/pep-0343/>`__ to provide
75
- transactions which are rolled back in the event of an exception. The
76
- transaction model is primitive; you can only nest transactions.
77
-
78
- While a store is put into a transaction, it will not save changes to
79
- file until all of the transactions have been closed.
80
-
81
- .. code:: python
82
-
83
- from jsonstore import JsonStore
84
-
85
- # even with auto_commit=True, the file won't be saved until the last contexts has been closed
86
- with JsonStore('config.json', indent=None, auto_commit=False) as store:
87
- self.value = 1
88
-
89
- # the context manager will roll back changes made if an exception is raised
90
- store = JsonStore('config.json', indent=None)
91
- try:
92
- with store:
93
- store.value = "new"
94
- raise Exception
95
- except Exception:
96
- pass
97
- # here we see the value that was saved previously
98
- assert store.value == 1
99
-
100
- .. |Build Status| image:: https://travis-ci.org/Code0x58/python-jsonstore.svg?branch=master
101
- :target: https://travis-ci.org/Code0x58/python-jsonstore
102
- .. |Codacy Rating| image:: https://api.codacy.com/project/badge/Grade/37ea488773444de59469a3775be83faf
103
- :target: https://www.codacy.com/app/evilumbrella-github/python-jsonstore?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=Code0x58/python-jsonstore&amp;utm_campaign=Badge_Grade
104
- .. |PyPi Package| image:: https://badge.fury.io/py/python-jsonstore.svg
105
- :target: https://pypi.org/project/python-jsonstore/
106
-
107
- Keywords: json key value store
108
- Platform: UNKNOWN
109
- Classifier: Development Status :: 5 - Production/Stable
110
- Classifier: Intended Audience :: Developers
111
- Classifier: License :: OSI Approved :: MIT License
112
- Classifier: Operating System :: OS Independent
113
- Classifier: Programming Language :: Python :: 2
114
- Classifier: Programming Language :: Python :: 2.7
115
- Classifier: Programming Language :: Python :: 3
116
- Classifier: Programming Language :: Python :: 3.3
117
- Classifier: Programming Language :: Python :: 3.4
118
- Classifier: Programming Language :: Python :: 3.5
119
- Classifier: Programming Language :: Python :: 3.6
120
- Classifier: Programming Language :: Python :: 3.7
121
- Classifier: Programming Language :: Python :: 3.8
122
- Classifier: Programming Language :: Python :: Implementation :: CPython
123
- Classifier: Programming Language :: Python :: Implementation :: PyPy
124
- Classifier: Topic :: Database
125
- Classifier: Topic :: Software Development
126
- Description-Content-Type: text/x-rst
@@ -1,46 +0,0 @@
1
- import codecs
2
- from os import path
3
- from textwrap import dedent
4
-
5
- from setuptools import setup
6
-
7
- here = path.abspath(path.dirname(__file__))
8
-
9
- with codecs.open(path.join(here, "README.rst"), encoding='utf-8') as f:
10
- long_description = f.read()
11
-
12
- setup(
13
- name='python-jsonstore',
14
- use_scm_version=True,
15
- description="",
16
- long_description=long_description,
17
- long_description_content_type='text/x-rst',
18
- author="Oliver Bristow",
19
- author_email='github+pypi@oliverbristow.co.uk',
20
- license='MIT',
21
- classifiers=dedent("""
22
- Development Status :: 5 - Production/Stable
23
- Intended Audience :: Developers
24
- License :: OSI Approved :: MIT License
25
- Operating System :: OS Independent
26
- Programming Language :: Python :: 2
27
- Programming Language :: Python :: 2.7
28
- Programming Language :: Python :: 3
29
- Programming Language :: Python :: 3.3
30
- Programming Language :: Python :: 3.4
31
- Programming Language :: Python :: 3.5
32
- Programming Language :: Python :: 3.6
33
- Programming Language :: Python :: 3.7
34
- Programming Language :: Python :: 3.8
35
- Programming Language :: Python :: Implementation :: CPython
36
- Programming Language :: Python :: Implementation :: PyPy
37
- Topic :: Database
38
- Topic :: Software Development
39
- """).strip().split('\n'),
40
- keywords='json key value store',
41
- url='https://github.com/Code0x58/python-jsonstore/',
42
- py_modules=dedent("""
43
- jsonstore
44
- """).strip().split('\n'),
45
- setup_requires=["setuptools_scm", "wheel"],
46
- )