python-jsonstore 1.3.3__tar.gz → 2.0.0__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.
- python_jsonstore-2.0.0/.coveragerc +4 -0
- python_jsonstore-2.0.0/.github/dependabot.yml +10 -0
- python_jsonstore-2.0.0/.github/workflows/ci.yml +180 -0
- python_jsonstore-2.0.0/.github/workflows/publish.yml +31 -0
- python_jsonstore-2.0.0/PKG-INFO +124 -0
- python_jsonstore-2.0.0/README.rst +96 -0
- {python_jsonstore-1.3.3 → python_jsonstore-2.0.0}/jsonstore.py +62 -40
- {python_jsonstore-1.3.3 → python_jsonstore-2.0.0}/pyproject.toml +5 -13
- python_jsonstore-2.0.0/python_jsonstore.egg-info/PKG-INFO +124 -0
- {python_jsonstore-1.3.3 → python_jsonstore-2.0.0}/python_jsonstore.egg-info/SOURCES.txt +4 -0
- python_jsonstore-2.0.0/python_jsonstore.egg-info/scm_file_list.json +13 -0
- python_jsonstore-2.0.0/python_jsonstore.egg-info/scm_version.json +8 -0
- python_jsonstore-2.0.0/test_jsonstore.py +595 -0
- python_jsonstore-1.3.3/.github/workflows/ci.yml +0 -71
- python_jsonstore-1.3.3/.github/workflows/publish.yml +0 -27
- python_jsonstore-1.3.3/PKG-INFO +0 -152
- python_jsonstore-1.3.3/README.rst +0 -116
- python_jsonstore-1.3.3/python_jsonstore.egg-info/PKG-INFO +0 -152
- python_jsonstore-1.3.3/test_jsonstore.py +0 -334
- {python_jsonstore-1.3.3 → python_jsonstore-2.0.0}/LICENCE.md +0 -0
- {python_jsonstore-1.3.3 → python_jsonstore-2.0.0}/python_jsonstore.egg-info/dependency_links.txt +0 -0
- {python_jsonstore-1.3.3 → python_jsonstore-2.0.0}/python_jsonstore.egg-info/top_level.txt +0 -0
- {python_jsonstore-1.3.3 → python_jsonstore-2.0.0}/setup.cfg +0 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
- master
|
|
8
|
+
pull_request:
|
|
9
|
+
workflow_call:
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
defaults:
|
|
15
|
+
run:
|
|
16
|
+
shell: bash
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
build:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- name: Check out repository and version tags
|
|
23
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
24
|
+
with:
|
|
25
|
+
fetch-depth: 0
|
|
26
|
+
persist-credentials: false
|
|
27
|
+
|
|
28
|
+
- name: Set up Python
|
|
29
|
+
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
30
|
+
with:
|
|
31
|
+
python-version: "3.15"
|
|
32
|
+
|
|
33
|
+
- name: Build and check distributions
|
|
34
|
+
run: |
|
|
35
|
+
python -m pip install --upgrade pip build twine
|
|
36
|
+
python -m build --sdist --wheel
|
|
37
|
+
python -m twine check --strict dist/*
|
|
38
|
+
|
|
39
|
+
- name: Upload distributions
|
|
40
|
+
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
41
|
+
with:
|
|
42
|
+
name: distributions
|
|
43
|
+
path: dist/*
|
|
44
|
+
if-no-files-found: error
|
|
45
|
+
|
|
46
|
+
tests:
|
|
47
|
+
needs: build
|
|
48
|
+
runs-on: ${{ matrix.os }}
|
|
49
|
+
strategy:
|
|
50
|
+
fail-fast: false
|
|
51
|
+
matrix:
|
|
52
|
+
os:
|
|
53
|
+
- ubuntu-latest
|
|
54
|
+
- windows-latest
|
|
55
|
+
python-version:
|
|
56
|
+
- "3.9"
|
|
57
|
+
- "3.10"
|
|
58
|
+
- "3.11"
|
|
59
|
+
- "3.12"
|
|
60
|
+
- "3.13"
|
|
61
|
+
- "3.14"
|
|
62
|
+
- "3.15"
|
|
63
|
+
- "pypy3.11"
|
|
64
|
+
exclude:
|
|
65
|
+
- os: windows-latest
|
|
66
|
+
python-version: "pypy3.11"
|
|
67
|
+
steps:
|
|
68
|
+
- name: Check out tests
|
|
69
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
70
|
+
with:
|
|
71
|
+
persist-credentials: false
|
|
72
|
+
|
|
73
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
74
|
+
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
75
|
+
with:
|
|
76
|
+
python-version: ${{ matrix.python-version }}
|
|
77
|
+
|
|
78
|
+
- name: Download distributions
|
|
79
|
+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
80
|
+
with:
|
|
81
|
+
name: distributions
|
|
82
|
+
path: dist
|
|
83
|
+
|
|
84
|
+
- name: Install wheel and test dependencies
|
|
85
|
+
run: |
|
|
86
|
+
python -m pip install --upgrade pip
|
|
87
|
+
python -m pip install --no-cache-dir dist/*.whl pytest pytest-cov
|
|
88
|
+
mkdir -p "$RUNNER_TEMP/installed-tests"
|
|
89
|
+
cp test_jsonstore.py "$RUNNER_TEMP/installed-tests/"
|
|
90
|
+
|
|
91
|
+
- name: Test installed wheel outside the checkout
|
|
92
|
+
working-directory: ${{ runner.temp }}/installed-tests
|
|
93
|
+
run: |
|
|
94
|
+
python -I -c 'import jsonstore; print(jsonstore.__file__)'
|
|
95
|
+
python -I -m pytest -vv --cov=jsonstore --cov-report= --junitxml=test-results.xml
|
|
96
|
+
|
|
97
|
+
- name: Map installed coverage to repository paths
|
|
98
|
+
run: |
|
|
99
|
+
python -m coverage combine "$RUNNER_TEMP/installed-tests/.coverage"
|
|
100
|
+
python -m coverage xml -o "$RUNNER_TEMP/installed-tests/coverage.xml"
|
|
101
|
+
|
|
102
|
+
- name: Build and install wheel from source distribution
|
|
103
|
+
run: |
|
|
104
|
+
python -m pip uninstall --yes python-jsonstore
|
|
105
|
+
python -m pip install --no-cache-dir dist/*.tar.gz
|
|
106
|
+
|
|
107
|
+
- name: Test installed source distribution outside the checkout
|
|
108
|
+
working-directory: ${{ runner.temp }}/installed-tests
|
|
109
|
+
run: |
|
|
110
|
+
python -I -c 'import jsonstore; print(jsonstore.__file__)'
|
|
111
|
+
python -I -m pytest -vv
|
|
112
|
+
|
|
113
|
+
- name: Upload test reports
|
|
114
|
+
if: always() && matrix.python-version == '3.15'
|
|
115
|
+
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
116
|
+
with:
|
|
117
|
+
name: test-reports-${{ matrix.os }}
|
|
118
|
+
path: |
|
|
119
|
+
${{ runner.temp }}/installed-tests/coverage.xml
|
|
120
|
+
${{ runner.temp }}/installed-tests/test-results.xml
|
|
121
|
+
if-no-files-found: warn
|
|
122
|
+
|
|
123
|
+
reports:
|
|
124
|
+
if: ${{ !cancelled() && github.event_name != 'release' }}
|
|
125
|
+
needs: tests
|
|
126
|
+
runs-on: ubuntu-latest
|
|
127
|
+
steps:
|
|
128
|
+
- name: Check whether Codecov is configured
|
|
129
|
+
id: codecov
|
|
130
|
+
env:
|
|
131
|
+
ENABLED: ${{ secrets.CODECOV_TOKEN != '' }}
|
|
132
|
+
run: echo "enabled=$ENABLED" >> "$GITHUB_OUTPUT"
|
|
133
|
+
|
|
134
|
+
- name: Check out repository for report paths
|
|
135
|
+
if: steps.codecov.outputs.enabled == 'true'
|
|
136
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
137
|
+
with:
|
|
138
|
+
persist-credentials: false
|
|
139
|
+
|
|
140
|
+
- name: Download test reports
|
|
141
|
+
id: reports
|
|
142
|
+
continue-on-error: ${{ needs.tests.result != 'success' }}
|
|
143
|
+
if: steps.codecov.outputs.enabled == 'true'
|
|
144
|
+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
145
|
+
with:
|
|
146
|
+
pattern: test-reports-*
|
|
147
|
+
path: reports
|
|
148
|
+
|
|
149
|
+
- name: Find available test reports
|
|
150
|
+
id: files
|
|
151
|
+
if: steps.codecov.outputs.enabled == 'true'
|
|
152
|
+
run: |
|
|
153
|
+
python3 - <<'PYTHON'
|
|
154
|
+
import glob
|
|
155
|
+
import os
|
|
156
|
+
|
|
157
|
+
with open(os.environ["GITHUB_OUTPUT"], "a") as output:
|
|
158
|
+
for name, pattern in (("coverage", "reports/*/coverage.xml"),
|
|
159
|
+
("tests", "reports/*/test-results.xml")):
|
|
160
|
+
output.write(f"{name}={','.join(sorted(glob.glob(pattern)))}\n")
|
|
161
|
+
PYTHON
|
|
162
|
+
|
|
163
|
+
- name: Upload coverage to Codecov
|
|
164
|
+
if: steps.files.outputs.coverage != ''
|
|
165
|
+
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
|
|
166
|
+
with:
|
|
167
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
168
|
+
files: ${{ steps.files.outputs.coverage }}
|
|
169
|
+
disable_search: true
|
|
170
|
+
fail_ci_if_error: true
|
|
171
|
+
|
|
172
|
+
- name: Upload test results to Codecov
|
|
173
|
+
if: steps.files.outputs.tests != ''
|
|
174
|
+
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
|
|
175
|
+
with:
|
|
176
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
177
|
+
files: ${{ steps.files.outputs.tests }}
|
|
178
|
+
report_type: test_results
|
|
179
|
+
disable_search: true
|
|
180
|
+
fail_ci_if_error: true
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
validate:
|
|
12
|
+
uses: ./.github/workflows/ci.yml
|
|
13
|
+
|
|
14
|
+
publish:
|
|
15
|
+
needs: validate
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
environment:
|
|
18
|
+
name: pypi
|
|
19
|
+
url: https://pypi.org/p/python-jsonstore
|
|
20
|
+
permissions:
|
|
21
|
+
id-token: write
|
|
22
|
+
steps:
|
|
23
|
+
- name: Download validated distributions
|
|
24
|
+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
25
|
+
with:
|
|
26
|
+
name: distributions
|
|
27
|
+
path: dist
|
|
28
|
+
|
|
29
|
+
# Keep publishing in this workflow for the existing PyPI Trusted Publisher.
|
|
30
|
+
- name: Publish to PyPI
|
|
31
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
@@ -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,96 @@
|
|
|
1
|
+
|PyPI| |Build Status| |Coverage Report|
|
|
2
|
+
|
|
3
|
+
jsonstore
|
|
4
|
+
=========
|
|
5
|
+
|
|
6
|
+
``JsonStore`` maps a JSON object's keys to Python attributes. Use it to
|
|
7
|
+
load and save configuration containing nested dictionaries and lists.
|
|
8
|
+
|
|
9
|
+
Requires Python 3.9 or newer. Tests run on CPython 3.9–3.15 and PyPy 3.11.
|
|
10
|
+
|
|
11
|
+
Examples
|
|
12
|
+
--------
|
|
13
|
+
|
|
14
|
+
Install with ``pip install python-jsonstore``.
|
|
15
|
+
|
|
16
|
+
Basics
|
|
17
|
+
~~~~~~
|
|
18
|
+
|
|
19
|
+
.. code:: python
|
|
20
|
+
|
|
21
|
+
from jsonstore import JsonStore
|
|
22
|
+
|
|
23
|
+
# Assignments and deletions are saved automatically.
|
|
24
|
+
store = JsonStore("config.json")
|
|
25
|
+
store.message = "hello"
|
|
26
|
+
store.options = {"enabled": True, "greeting": "💐"}
|
|
27
|
+
store.numbers = [1, 2, 3]
|
|
28
|
+
assert "message" in store
|
|
29
|
+
del store.message
|
|
30
|
+
|
|
31
|
+
# Dot-separated keys traverse dictionaries.
|
|
32
|
+
store["options.enabled"] = False
|
|
33
|
+
assert store["options", "enabled"] is False
|
|
34
|
+
|
|
35
|
+
# Tuple or list paths also accept list indices and slices.
|
|
36
|
+
assert store["numbers", -1] == 3
|
|
37
|
+
assert store[["numbers", 1]] == 2
|
|
38
|
+
del store["numbers", :2]
|
|
39
|
+
assert store.numbers == [3]
|
|
40
|
+
|
|
41
|
+
# A one-element tuple addresses a literal key containing a dot.
|
|
42
|
+
store[("literal.key",)] = "value"
|
|
43
|
+
|
|
44
|
+
Reads and assignments use deep copies. To update a stored collection,
|
|
45
|
+
assign the modified value back; ``+=`` also does this:
|
|
46
|
+
|
|
47
|
+
.. code:: python
|
|
48
|
+
|
|
49
|
+
store.numbers.append(4)
|
|
50
|
+
assert store.numbers == [3]
|
|
51
|
+
store.numbers += [4, 5]
|
|
52
|
+
assert store.numbers == [3, 4, 5]
|
|
53
|
+
|
|
54
|
+
Use item access for keys that conflict with ``JsonStore`` attributes.
|
|
55
|
+
``indent`` controls JSON formatting and defaults to 2; use ``None`` for
|
|
56
|
+
compact output. ``auto_commit=False`` defers saving until a transaction
|
|
57
|
+
commits.
|
|
58
|
+
|
|
59
|
+
Transactions
|
|
60
|
+
~~~~~~~~~~~~
|
|
61
|
+
|
|
62
|
+
Use ``with store:`` to group changes into a transaction. Transactions may
|
|
63
|
+
be nested; only successful exit from the outermost transaction saves the
|
|
64
|
+
file. An exception in a transaction restores its previous state, including
|
|
65
|
+
when saving fails.
|
|
66
|
+
|
|
67
|
+
.. code:: python
|
|
68
|
+
|
|
69
|
+
from jsonstore import JsonStore
|
|
70
|
+
|
|
71
|
+
with JsonStore("config.json", indent=None, auto_commit=False) as store:
|
|
72
|
+
store.value = 1
|
|
73
|
+
assert JsonStore("config.json").value == 1
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
with store:
|
|
77
|
+
store.value = 2
|
|
78
|
+
with store:
|
|
79
|
+
store.value = 3
|
|
80
|
+
raise ValueError("cancel the transaction")
|
|
81
|
+
except ValueError:
|
|
82
|
+
pass
|
|
83
|
+
assert store.value == 1
|
|
84
|
+
assert JsonStore("config.json").value == 1
|
|
85
|
+
|
|
86
|
+
Transactions provide in-memory rollback, with no locking or coordination
|
|
87
|
+
between writers. Use one writer per file; separate stores do not reload
|
|
88
|
+
each other's changes. Atomic replacement does not guarantee durability
|
|
89
|
+
across a power failure.
|
|
90
|
+
|
|
91
|
+
.. |Build Status| image:: https://github.com/Code0x58/python-jsonstore/actions/workflows/ci.yml/badge.svg
|
|
92
|
+
:target: https://github.com/Code0x58/python-jsonstore/actions/workflows/ci.yml
|
|
93
|
+
.. |PyPI| image:: https://img.shields.io/pypi/v/python-jsonstore.svg
|
|
94
|
+
:target: https://pypi.org/project/python-jsonstore/
|
|
95
|
+
.. |Coverage Report| image:: https://codecov.io/gh/Code0x58/python-jsonstore/branch/master/graph/badge.svg
|
|
96
|
+
:target: https://codecov.io/gh/Code0x58/python-jsonstore
|
|
@@ -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
|
|
9
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
97
|
-
if isinstance(obj, (dict, list)):
|
|
111
|
+
if isinstance(obj, (dict, list, tuple)):
|
|
98
112
|
if parents is None:
|
|
99
|
-
parents =
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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] =
|
|
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)
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools>=
|
|
2
|
+
requires = ["setuptools>=77.0.3", "setuptools_scm>=7"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-jsonstore"
|
|
7
7
|
description = "Map JSON files onto Python attributes for simple configuration storage"
|
|
8
8
|
readme = "README.rst"
|
|
9
|
-
requires-python = ">=
|
|
10
|
-
license =
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENCE.md"]
|
|
11
12
|
authors = [
|
|
12
13
|
{name = "Oliver Bristow", email = "github+pypi@oliverbristow.co.uk"},
|
|
13
14
|
]
|
|
@@ -15,23 +16,15 @@ keywords = ["json", "key", "value", "store"]
|
|
|
15
16
|
classifiers = [
|
|
16
17
|
"Development Status :: 5 - Production/Stable",
|
|
17
18
|
"Intended Audience :: Developers",
|
|
18
|
-
"License :: OSI Approved :: MIT License",
|
|
19
19
|
"Operating System :: OS Independent",
|
|
20
|
-
"Programming Language :: Python :: 2",
|
|
21
|
-
"Programming Language :: Python :: 2.7",
|
|
22
20
|
"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
21
|
"Programming Language :: Python :: 3.9",
|
|
30
22
|
"Programming Language :: Python :: 3.10",
|
|
31
23
|
"Programming Language :: Python :: 3.11",
|
|
32
24
|
"Programming Language :: Python :: 3.12",
|
|
33
25
|
"Programming Language :: Python :: 3.13",
|
|
34
26
|
"Programming Language :: Python :: 3.14",
|
|
27
|
+
"Programming Language :: Python :: 3.15",
|
|
35
28
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
36
29
|
"Programming Language :: Python :: Implementation :: PyPy",
|
|
37
30
|
"Topic :: Database",
|
|
@@ -45,6 +38,5 @@ Homepage = "https://github.com/Code0x58/python-jsonstore/"
|
|
|
45
38
|
|
|
46
39
|
[tool.setuptools]
|
|
47
40
|
py-modules = ["jsonstore"]
|
|
48
|
-
license-files = ["LICENCE.md"]
|
|
49
41
|
|
|
50
42
|
[tool.setuptools_scm]
|