From e261edf9b352b79660075d6b8ec19b7822652d8b Mon Sep 17 00:00:00 2001 From: GideonBear <87426140+GideonBear@users.noreply.github.com> Date: Wed, 1 Mar 2023 07:28:28 +0100 Subject: [PATCH 01/22] Remove python 2 block --- livejson.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/livejson.py b/livejson.py index 6e38e94..1580304 100644 --- a/livejson.py +++ b/livejson.py @@ -8,20 +8,7 @@ import json import warnings -# Import from collections.abc for Python 3.x but incase of ImportError -# from Python 2.x, fall back on importing from collections. -try: - from collections.abc import ( - MutableMapping, - MutableSequence, - ) -except ImportError: - from collections import ( - MutableMapping, - MutableSequence, - ) - -warnings.filterwarnings("once", category=DeprecationWarning) +from collections.abc import MutableMapping, MutableSequence # MISC HELPERS From 5c1ce9ef5c321f786090960c415a2e3ee14a82d8 Mon Sep 17 00:00:00 2001 From: GideonBear <87426140+GideonBear@users.noreply.github.com> Date: Wed, 1 Mar 2023 07:31:14 +0100 Subject: [PATCH 02/22] Update .gitignore (https://github.com/github/gitignore/blob/main/Python.gitignore) --- .gitignore | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 101 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 1dbc687..6769e21 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,6 @@ __pycache__/ # Distribution / packaging .Python -env/ build/ develop-eggs/ dist/ @@ -20,9 +19,12 @@ lib64/ parts/ sdist/ var/ +wheels/ +share/python-wheels/ *.egg-info/ .installed.cfg *.egg +MANIFEST # PyInstaller # Usually these files are written by a python script from a template @@ -37,13 +39,17 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ +.nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml -*,cover +*.cover +*.py,cover .hypothesis/ +.pytest_cache/ +cover/ # Translations *.mo @@ -51,12 +57,104 @@ coverage.xml # Django stuff: *.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy # Sphinx documentation docs/_build/ # PyBuilder +.pybuilder/ target/ -#Ipython Notebook +# Jupyter Notebook .ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ \ No newline at end of file From 2321a2ba4b6b618eddd147793075bcc1646afa35 Mon Sep 17 00:00:00 2001 From: GideonBear <87426140+GideonBear@users.noreply.github.com> Date: Wed, 1 Mar 2023 07:44:56 +0100 Subject: [PATCH 03/22] Add todo --- livejson.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/livejson.py b/livejson.py index 1580304..7ee3752 100644 --- a/livejson.py +++ b/livejson.py @@ -23,7 +23,7 @@ def _initfile(path, data="dict"): # exist dirname = os.path.dirname(path) if dirname and not os.path.exists(dirname): - raise IOError( + raise IOError( # TODO: better error (IOError is deprecated) ("Could not initialize empty JSON file in non-existant " "directory '{}'").format(os.path.dirname(path)) ) From 4ba7714a226a64da60e38c13533e5649295974ee Mon Sep 17 00:00:00 2001 From: GideonBear <87426140+GideonBear@users.noreply.github.com> Date: Wed, 1 Mar 2023 07:46:56 +0100 Subject: [PATCH 04/22] Run `pyupgrade` --- livejson.py | 10 +++++----- test.py | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/livejson.py b/livejson.py index 7ee3752..b1e72ec 100644 --- a/livejson.py +++ b/livejson.py @@ -38,7 +38,7 @@ def _initfile(path, data="dict"): return False -class _ObjectBase(object): +class _ObjectBase: """Class inherited by most things. Implements the lowest common denominator for all emulating classes. @@ -197,7 +197,7 @@ def _data(self): """ if self.is_caching: return self.cache - with open(self.path, "r") as f: + with open(self.path) as f: return json.load(f) @property @@ -269,7 +269,7 @@ def remove(self): @property def file_contents(self): """Get the raw file contents of the file.""" - with open(self.path, "r") as f: + with open(self.path) as f: return f.read() # Grouped writes @@ -328,7 +328,7 @@ def clear(self): self.data = [] -class File(object): +class File: """The main interface of livejson. Emulates a list or a dict, updating a JSON file in real-time as it is modified. @@ -348,7 +348,7 @@ def __init__(self, path, pretty=False, sort_keys=True, indent=2): _initfile(self.path) - with open(self.path, "r") as f: + with open(self.path) as f: data = json.load(f) if isinstance(data, dict): self.__class__ = DictFile diff --git a/test.py b/test.py index 43757d3..4225c20 100644 --- a/test.py +++ b/test.py @@ -24,7 +24,7 @@ def test_DictFile(self): f = livejson.File(self.path) self.assertIsInstance(f, livejson.DictFile) # Test DictFile is default self.assertTrue(os.path.exists(self.path)) - with open(self.path, "r") as fi: + with open(self.path) as fi: self.assertEqual(fi.read(), "{}") # Test writing to a file f["a"] = "b" @@ -106,7 +106,7 @@ def test_errors(self): with self.assertRaises(TypeError): f[True] = "test" # Test that storing numeric keys raises a more helpful error message - with self.assertRaisesRegexp(TypeError, "Try using a"): + with self.assertRaisesRegex(TypeError, "Try using a"): f[0] = "abc" # When initializing using with_data, test that an error is thrown if # the file already exists @@ -130,7 +130,7 @@ def test_empty_file(self): def test_rollback(self): """ Test that data can be restored in the case of an error to prevent corruption (see #3)""" - class Test (object): + class Test: pass f = livejson.File(self.path) f["a"] = "b" @@ -205,7 +205,7 @@ def test_errors(self): with self.assertRaises(TypeError): f["data"][True] = "test" # Test that storing numeric keys raises an additional error message - with self.assertRaisesRegexp(TypeError, "Try using a"): + with self.assertRaisesRegex(TypeError, "Try using a"): f["data"][0] = "abc" @@ -265,7 +265,7 @@ def test_fun_syntax(self): grouped writes. """ with livejson.File(self.path) as f: f["cats"] = "dogs" - with open(self.path, "r") as fi: + with open(self.path) as fi: self.assertEqual(fi.read(), "{\"cats\": \"dogs\"}") From 380db73be2345c9dffb67bd9f996e3aa894aabb7 Mon Sep 17 00:00:00 2001 From: GideonBear <87426140+GideonBear@users.noreply.github.com> Date: Wed, 1 Mar 2023 07:50:34 +0100 Subject: [PATCH 05/22] Run `autopep8` --- livejson.py | 8 +++++++- test.py | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/livejson.py b/livejson.py index b1e72ec..413b885 100644 --- a/livejson.py +++ b/livejson.py @@ -43,6 +43,7 @@ class _ObjectBase: Implements the lowest common denominator for all emulating classes. """ + def __getitem__(self, key): out = self.data[key] @@ -86,6 +87,7 @@ class _NestedBase(_ObjectBase): object, and 'pathToThis' which specifies where in the JSON file this object exists (as a list). """ + def __init__(self, fileobj, pathToThis): self.pathInData = pathToThis self.base = fileobj @@ -138,6 +140,7 @@ class _NestedDict(_NestedBase, MutableMapping): to update the file. """ + def __iter__(self): return iter(self.data) @@ -163,6 +166,7 @@ class _NestedList(_NestedBase, MutableSequence): to update the file. """ + def insert(self, index, value): # See _NestedBase.__setitem__ for details on how this works data = self.base.data @@ -181,6 +185,7 @@ class _BaseFile(_ObjectBase): This implements all the required methods common between MutableMapping and MutableSequence.""" + def __init__(self, path, pretty=False, sort_keys=False): self.path = path self.pretty = pretty @@ -299,6 +304,7 @@ class DictFile(_BaseFile, MutableMapping): """A class emulating Python's dict that will update a JSON file as it is modified. """ + def __iter__(self): return iter(self.data) @@ -315,6 +321,7 @@ class ListFile(_BaseFile, MutableSequence): modified. Use this class directly when creating a new file if you want the base object to be an array. """ + def insert(self, index, value): data = self.data data.insert(index, value) @@ -380,4 +387,3 @@ def with_data(path, data, *args, **kwargs): Database = File ListDatabase = ListFile DictDatabase = DictFile - diff --git a/test.py b/test.py index 4225c20..b869a5e 100644 --- a/test.py +++ b/test.py @@ -13,9 +13,9 @@ def tearDown(self): os.remove(self.path) - class TestFile(_BaseTest, unittest.TestCase): """ Test the magical JSON file class """ + def test_DictFile(self): """ Test that 'livejson.File's in which the base object is a dict work as expected. This also tests all the methods shared between both types. @@ -213,6 +213,7 @@ class TestGroupedWrites(_BaseTest, unittest.TestCase): """ Test using "grouped writes" with the context manager. These improve efficiency by only writing to the file once, at the end, instead of writing every change as it is made. """ + def test_basics(self): f = livejson.File(self.path) with f: From 94594b78fed50b27b49771bde64cb2395cfc55d8 Mon Sep 17 00:00:00 2001 From: GideonBear <87426140+GideonBear@users.noreply.github.com> Date: Wed, 1 Mar 2023 09:06:22 +0100 Subject: [PATCH 06/22] Sort imports and add `from __future__ import annotations` --- livejson.py | 6 +++--- setup.py | 2 ++ test.py | 2 ++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/livejson.py b/livejson.py index 413b885..fcced87 100644 --- a/livejson.py +++ b/livejson.py @@ -4,13 +4,13 @@ real-time. Magic. """ -import os +from __future__ import annotations + import json +import os import warnings - from collections.abc import MutableMapping, MutableSequence - # MISC HELPERS diff --git a/setup.py b/setup.py index 8de0fbf..b26618f 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from setuptools import setup with open('README.md') as f: diff --git a/test.py b/test.py index b869a5e..08216bd 100644 --- a/test.py +++ b/test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import unittest From 55ddf74f72157f31d0670bb293c04590fc867e86 Mon Sep 17 00:00:00 2001 From: GideonBear <87426140+GideonBear@users.noreply.github.com> Date: Wed, 1 Mar 2023 09:12:34 +0100 Subject: [PATCH 07/22] Fix typos --- test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test.py b/test.py index 08216bd..95c1802 100644 --- a/test.py +++ b/test.py @@ -35,7 +35,7 @@ def test_DictFile(self): self.assertEqual(newInstance["a"], "b") # Test deleting values f["c"] = "d" - self.assertIn("c", f) # This also conviently tests __contains__ + self.assertIn("c", f) # This also conveniently tests __contains__ del f["c"] self.assertNotIn("c", f) @@ -88,7 +88,7 @@ def test_switchclass(self): f["dogs"] = "cats" self.assertIsInstance(f, livejson.DictFile) - def test_staticmethod_initalization(self): + def test_staticmethod_initialization(self): """ Test initializing the File in special ways with custom staticmethods """ f = livejson.File.with_data(self.path, ["a", "b", "c"]) @@ -102,7 +102,7 @@ def test_errors(self): """ Test the errors that are set up """ f = livejson.File(self.path) - # Test error for trying to initialize in non-existant directories + # Test error for trying to initialize in non-existent directories self.assertRaises(IOError, livejson.File, "a/b/c.py") # Test error when trying to store non-string keys with self.assertRaises(TypeError): From 7225080cc5120cae5488747589643260fda5ddb2 Mon Sep 17 00:00:00 2001 From: GideonBear <87426140+GideonBear@users.noreply.github.com> Date: Wed, 1 Mar 2023 09:20:24 +0100 Subject: [PATCH 08/22] Replace `staticmethod` with `classmethod` --- livejson.py | 6 +++--- test.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/livejson.py b/livejson.py index fcced87..94832a9 100644 --- a/livejson.py +++ b/livejson.py @@ -362,8 +362,8 @@ def __init__(self, path, pretty=False, sort_keys=True, indent=2): elif isinstance(data, list): self.__class__ = ListFile - @staticmethod - def with_data(path, data, *args, **kwargs): + @classmethod + def with_data(cls, path, data, *args, **kwargs): """Initialize a new file that starts out with some data. Pass data as a list, dict, or JSON string. """ @@ -378,7 +378,7 @@ def with_data(path, data, *args, **kwargs): "'livejson.File' instance if you really " "want to do this.") else: - f = File(path, *args, **kwargs) + f = cls(path, *args, **kwargs) f.data = data return f diff --git a/test.py b/test.py index 95c1802..7352b99 100644 --- a/test.py +++ b/test.py @@ -88,9 +88,9 @@ def test_switchclass(self): f["dogs"] = "cats" self.assertIsInstance(f, livejson.DictFile) - def test_staticmethod_initialization(self): + def test_classmethod_initialization(self): """ Test initializing the File in special ways with custom - staticmethods """ + classmethods """ f = livejson.File.with_data(self.path, ["a", "b", "c"]) self.assertEqual(f.data, ["a", "b", "c"]) # Test initialization from JSON string From 065ca6f15e3b1539827dcac2c7a49fc7cc703a59 Mon Sep 17 00:00:00 2001 From: GideonBear <87426140+GideonBear@users.noreply.github.com> Date: Thu, 2 Mar 2023 20:18:31 +0100 Subject: [PATCH 09/22] Move to pyproject.toml from setup.py --- pyproject.toml | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 20 ------------------- 2 files changed, 52 insertions(+), 20 deletions(-) create mode 100644 pyproject.toml delete mode 100644 setup.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9462174 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,52 @@ +[build-system] +requires = ["setuptools>=67.4.0", "setuptools-scm>=7.1.0"] # Require latest version +build-backend = "setuptools.build_meta" + +[project] +name = "livejson" +# No version needed; setuptools-scm extracts tag from git automatically (see `dynamic = ["version"]`) +authors = [ + {name = "Luke Taylor", email = "luke@deentaylor.com"}, +] +description = "Bind Python objects to JSON files" +readme = "README.md" +# TODO: add url/homepage: https://github.com/controversial/livejson/ +requires-python = ">=3.7" +keywords = ["livejson", "json", "io", "development", "file", "files", "live", "update"] +license = {file = "LICENCE"} +classifiers = [ + # TODO: One of: + "Development Status :: 1 - Planning", + "Development Status :: 2 - Pre-Alpha", + "Development Status :: 3 - Alpha", + "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", + "Development Status :: 6 - Mature", + "Development Status :: 7 - Inactive", + + "Typing :: Typed", + "Topic :: Database", + "Topic :: System :: Monitoring", + "Topic :: Software Development :: Libraries :: Python Modules", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = [ + "requests", + 'importlib-metadata; python_version<"3.8"', +] +dynamic = ["version"] + +[tool.setuptools] +py-modules = ["livejson"] diff --git a/setup.py b/setup.py deleted file mode 100644 index b26618f..0000000 --- a/setup.py +++ /dev/null @@ -1,20 +0,0 @@ -from __future__ import annotations - -from setuptools import setup - -with open('README.md') as f: - readme = f.read() - -setup( - name="livejson", - py_modules=["livejson"], - version="1.9.1", - description="Bind Python objects to JSON files", - long_description=readme, - long_description_content_type="text/markdown", - keywords="livejson json io development file files live update", - license="MIT", - author="Luke Taylor", - author_email="luke@deentaylor.com", - url="https://github.com/controversial/livejson/", -) From 46551db14b940aa522306c509a8467a57529b8b3 Mon Sep 17 00:00:00 2001 From: GideonBear <87426140+GideonBear@users.noreply.github.com> Date: Fri, 3 Mar 2023 07:32:08 +0100 Subject: [PATCH 10/22] Add "Development Status :: 5 - Production/Stable" classifier --- pyproject.toml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9462174..b85ec7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,15 +15,7 @@ requires-python = ">=3.7" keywords = ["livejson", "json", "io", "development", "file", "files", "live", "update"] license = {file = "LICENCE"} classifiers = [ - # TODO: One of: - "Development Status :: 1 - Planning", - "Development Status :: 2 - Pre-Alpha", - "Development Status :: 3 - Alpha", - "Development Status :: 4 - Beta", "Development Status :: 5 - Production/Stable", - "Development Status :: 6 - Mature", - "Development Status :: 7 - Inactive", - "Typing :: Typed", "Topic :: Database", "Topic :: System :: Monitoring", From 70ad07e56ed5f8a324043f59ad5fd5eda4ab9f93 Mon Sep 17 00:00:00 2001 From: GideonBear <87426140+GideonBear@users.noreply.github.com> Date: Fri, 3 Mar 2023 07:32:43 +0100 Subject: [PATCH 11/22] Remove unnecessary dependencies from example --- pyproject.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b85ec7e..de04a97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,10 +34,6 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] -dependencies = [ - "requests", - 'importlib-metadata; python_version<"3.8"', -] dynamic = ["version"] [tool.setuptools] From 5b05c101462ce8b8ec7b61ff17b7198a4f4eb23e Mon Sep 17 00:00:00 2001 From: GideonBear <87426140+GideonBear@users.noreply.github.com> Date: Fri, 3 Mar 2023 08:01:20 +0100 Subject: [PATCH 12/22] Move to package --- livejson.py => livejson/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename livejson.py => livejson/__init__.py (100%) diff --git a/livejson.py b/livejson/__init__.py similarity index 100% rename from livejson.py rename to livejson/__init__.py From 0aacfe9eae51690f0178926511871a46bd5d1967 Mon Sep 17 00:00:00 2001 From: GideonBear <87426140+GideonBear@users.noreply.github.com> Date: Fri, 3 Mar 2023 08:01:24 +0100 Subject: [PATCH 13/22] Add py.typed --- livejson/py.typed | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 livejson/py.typed diff --git a/livejson/py.typed b/livejson/py.typed new file mode 100644 index 0000000..e69de29 From d0167fa2d5419c2504b6582921c56919d5373b9c Mon Sep 17 00:00:00 2001 From: GideonBear <87426140+GideonBear@users.noreply.github.com> Date: Fri, 3 Mar 2023 08:37:12 +0100 Subject: [PATCH 14/22] Fix pyproject.toml for package layout --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index de04a97..ee9d56d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,4 +37,4 @@ classifiers = [ dynamic = ["version"] [tool.setuptools] -py-modules = ["livejson"] +packages = ["livejson"] From 55ddc2a311d3e454c57c6e3307a99430b9bb495f Mon Sep 17 00:00:00 2001 From: GideonBear <87426140+GideonBear@users.noreply.github.com> Date: Fri, 3 Mar 2023 08:51:56 +0100 Subject: [PATCH 15/22] Run `unittest2pytest` --- test.py | 132 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/test.py b/test.py index 7352b99..7d08f2f 100644 --- a/test.py +++ b/test.py @@ -4,6 +4,7 @@ import unittest import livejson +import pytest class _BaseTest(): @@ -24,95 +25,96 @@ def test_DictFile(self): """ # Test that a blank JSON file can be properly created f = livejson.File(self.path) - self.assertIsInstance(f, livejson.DictFile) # Test DictFile is default - self.assertTrue(os.path.exists(self.path)) + assert isinstance(f, livejson.DictFile) # Test DictFile is default + assert os.path.exists(self.path) with open(self.path) as fi: - self.assertEqual(fi.read(), "{}") + assert fi.read() == "{}" # Test writing to a file f["a"] = "b" # Test reading values from an existing file newInstance = livejson.DictFile(self.path).data # Tests explicit type - self.assertEqual(newInstance["a"], "b") + assert newInstance["a"] == "b" # Test deleting values f["c"] = "d" - self.assertIn("c", f) # This also conveniently tests __contains__ + assert "c" in f # This also conveniently tests __contains__ del f["c"] - self.assertNotIn("c", f) + assert "c" not in f def test_ListFile(self): """ Test that Files in which the base object is an array work """ # Create the JSON file. f = livejson.ListFile(self.path) - self.assertEqual(f.data, []) + assert f.data == [] # Test append, extend, and insert f.append("dogs") f.extend(["cats", "penguins"]) f.insert(0, "turtles") - self.assertIsInstance(f.data, list) - self.assertEqual(f.data, ["turtles", "dogs", "cats", "penguins"]) + assert isinstance(f.data, list) + assert f.data == ["turtles", "dogs", "cats", "penguins"] # Test clear f.clear() - self.assertEqual(len(f), 0) + assert len(f) == 0 # Test creating a new ListFile automatically when file is an Array f2 = livejson.File(self.path) - self.assertIsInstance(f2, livejson.ListFile) + assert isinstance(f2, livejson.ListFile) def test_special_stuff(self): """ Test all the not-strictly-necessary extra API that I added """ f = livejson.File(self.path) f["a"] = "b" # Test 'data' (get a vanilla dict object) - self.assertEqual(f.data, {"a": "b"}) + assert f.data == {"a": "b"} # Test file_contents - self.assertEqual(f.file_contents, "{\"a\": \"b\"}") + assert f.file_contents == "{\"a\": \"b\"}" # Test __str__ and __repr__ - self.assertEqual(str(f), str(f.data)) - self.assertEqual(repr(f), repr(f.data)) + assert str(f) == str(f.data) + assert repr(f) == repr(f.data) # Test __iter__ - self.assertEqual(list(f), list(f.keys())) + assert list(f) == list(f.keys()) # Test remove() f.remove() - self.assertFalse(os.path.exists(self.path)) + assert not os.path.exists(self.path) def test_switchclass(self): """ Test that it can automatically switch classes """ # Test switching under normal usage f = livejson.File(self.path) - self.assertIsInstance(f, livejson.DictFile) + assert isinstance(f, livejson.DictFile) f.set_data([]) - self.assertIsInstance(f, livejson.ListFile) + assert isinstance(f, livejson.ListFile) # Test switching when the file is manually changed with open(self.path, "w") as fi: fi.write("{}") # This shouldn't error, it should change types when you do this f["dogs"] = "cats" - self.assertIsInstance(f, livejson.DictFile) + assert isinstance(f, livejson.DictFile) def test_classmethod_initialization(self): """ Test initializing the File in special ways with custom classmethods """ f = livejson.File.with_data(self.path, ["a", "b", "c"]) - self.assertEqual(f.data, ["a", "b", "c"]) + assert f.data == ["a", "b", "c"] # Test initialization from JSON string os.remove(self.path) f2 = livejson.File.with_data(self.path, "[\"a\", \"b\", \"c\"]") - self.assertEqual(len(f2), 3) + assert len(f2) == 3 def test_errors(self): """ Test the errors that are set up """ f = livejson.File(self.path) # Test error for trying to initialize in non-existent directories - self.assertRaises(IOError, livejson.File, "a/b/c.py") + with pytest.raises(IOError): + livejson.File("a/b/c.py") # Test error when trying to store non-string keys - with self.assertRaises(TypeError): + with pytest.raises(TypeError): f[True] = "test" # Test that storing numeric keys raises a more helpful error message - with self.assertRaisesRegex(TypeError, "Try using a"): + with pytest.raises(TypeError, match="Try using a"): f[0] = "abc" # When initializing using with_data, test that an error is thrown if # the file already exists - with self.assertRaises(ValueError): + with pytest.raises(ValueError): livejson.File.with_data(self.path, {}) def test_empty_file(self): @@ -122,12 +124,12 @@ def test_empty_file(self): with open(self.path, "w") as fi: fi.write("") f = livejson.File(self.path) - self.assertEqual(f.data, {}) + assert f.data == {} # List files with open(self.path, "w") as fi: fi.write("") f = livejson.ListFile(self.path) - self.assertEqual(f.data, []) + assert f.data == [] def test_rollback(self): """ Test that data can be restored in the case of an error to prevent @@ -136,29 +138,28 @@ class Test: pass f = livejson.File(self.path) f["a"] = "b" - with self.assertRaises(TypeError): + with pytest.raises(TypeError): f["test"] = Test() - self.assertEqual(f.data, {"a": "b"}) + assert f.data == {"a": "b"} def test_json_formatting(self): """ Test the extra JSON formatting options """ # Test pretty formatting f = livejson.File(self.path, pretty=True) f["a"] = "b" - self.assertEqual(f.file_contents, '{\n "a": "b"\n}') + assert f.file_contents == '{\n "a": "b"\n}' f.indent = 4 f.set_data(f.data) # Force an update - self.assertEqual(f.file_contents, '{\n "a": "b"\n}') + assert f.file_contents == '{\n "a": "b"\n}' # Test sorting of keys f["b"] = "c" f["d"] = "e" f["c"] = "d" - self.assertTrue(f.file_contents.find("a") < - f.file_contents.find("b") < - f.file_contents.find("c") < + assert f.file_contents.find("a") < \ + f.file_contents.find("b") < \ + f.file_contents.find("c") < \ f.file_contents.find("d") - ) class TestNesting(_BaseTest, unittest.TestCase): @@ -167,14 +168,14 @@ def test_list_nesting(self): f = livejson.File(self.path) f["stored_data"] = {} f["stored_data"]["test"] = "value" - self.assertEqual(f.data, {"stored_data": {"test": "value"}}) + assert f.data == {"stored_data": {"test": "value"}} def test_dict_nesting(self): """ Test the nesting of dicts inside a livejson.File """ f = livejson.File(self.path) f["stored_data"] = [] f["stored_data"].append("test") - self.assertEqual(f.data, {"stored_data": ["test"]}) + assert f.data == {"stored_data": ["test"]} def test_multilevel_nesting(self): """ Test that you can nest stuff inside nested stuff :O """ @@ -182,32 +183,31 @@ def test_multilevel_nesting(self): f["stored_data"] = [] f["stored_data"].append({}) f["stored_data"][0]["colors"] = ["green", "purple"] - self.assertEqual(f.data, + assert f.data == \ {"stored_data": [{"colors": ["green", "purple"]}]} - ) def test_misc_methods(self): f = livejson.File(self.path) f["stored_data"] = [{"colors": ["green"]}] # Test that normal __getitem__ still works - self.assertEqual(f["stored_data"][0]["colors"][0], "green") + assert f["stored_data"][0]["colors"][0] == "green" # Test deleting values f["stored_data"][0]["colors"].pop(0) - self.assertEqual(len(f["stored_data"][0]["colors"]), 0) + assert len(f["stored_data"][0]["colors"]) == 0 # Test __iter__ on nested dict f["stored_data"] = {"a": "b", "c": "d"} - self.assertEqual(list(f["stored_data"]), - list(f["stored_data"].keys())) + assert list(f["stored_data"]) == \ + list(f["stored_data"].keys()) def test_errors(self): """ Test the errors that are thrown """ f = livejson.File(self.path) f["data"] = {} # Test that storing non-string keys in a nested dict throws an error - with self.assertRaises(TypeError): + with pytest.raises(TypeError): f["data"][True] = "test" # Test that storing numeric keys raises an additional error message - with self.assertRaisesRegex(TypeError, "Try using a"): + with pytest.raises(TypeError, match="Try using a"): f["data"][0] = "abc" @@ -221,8 +221,8 @@ def test_basics(self): with f: f["a"] = "b" # Make sure that the write doesn't happen until we exit - self.assertEqual(f.file_contents, "{}") - self.assertEqual(f.file_contents, "{\"a\": \"b\"}") + assert f.file_contents == "{}" + assert f.file_contents == "{\"a\": \"b\"}" def test_with_existing_file(self): """ Test that the with block won't clear data """ @@ -230,38 +230,38 @@ def test_with_existing_file(self): f["a"] = "b" with f: f["c"] = "d" - self.assertIn("a", f) + assert "a" in f def test_lists(self): f = livejson.ListFile(self.path) with f: for i in range(10): f.append(i) - self.assertEqual(f.file_contents, "[]") - self.assertEqual(len(f), 10) + assert f.file_contents == "[]" + assert len(f) == 10 def test_switchclass(self): """ Test the switching of classes in the middle of a grouped write """ f = livejson.File(self.path) with f: - self.assertIsInstance(f, livejson.DictFile) + assert isinstance(f, livejson.DictFile) f.set_data([]) - self.assertIsInstance(f, livejson.ListFile) - self.assertEqual(f.file_contents, "{}") - self.assertEqual(f.file_contents, "[]") + assert isinstance(f, livejson.ListFile) + assert f.file_contents == "{}" + assert f.file_contents == "[]" def test_misc(self): """ Test miscellaneous other things that seem like they might break with a grouped write """ f = livejson.File(self.path) # Test is_caching, and test that data works with the cache - self.assertEqual(f.is_caching, False) + assert f.is_caching == False with f: - self.assertEqual(f.is_caching, True) + assert f.is_caching == True f["a"] = "b" # Test that data reflects the cache - self.assertEqual(f.data, {"a": "b"}) - self.assertEqual(f.is_caching, False) + assert f.data == {"a": "b"} + assert f.is_caching == False def test_fun_syntax(self): """ This is a fun bit of "syntactic sugar" enabled as a side effect of @@ -269,24 +269,24 @@ def test_fun_syntax(self): with livejson.File(self.path) as f: f["cats"] = "dogs" with open(self.path) as fi: - self.assertEqual(fi.read(), "{\"cats\": \"dogs\"}") + assert fi.read() == "{\"cats\": \"dogs\"}" class TestAliases(_BaseTest, unittest.TestCase): def test_Database(self): db = livejson.Database(self.path) - self.assertTrue(os.path.exists(self.path)) - self.assertEqual(db.data, {}) + assert os.path.exists(self.path) + assert db.data == {} def test_ListDatabase(self): db = livejson.ListDatabase(self.path) - self.assertTrue(os.path.exists(self.path)) - self.assertEqual(db.data, []) + assert os.path.exists(self.path) + assert db.data == [] def test_DictDatabase(self): db = livejson.DictDatabase(self.path) - self.assertTrue(os.path.exists(self.path)) - self.assertEqual(db.data, {}) + assert os.path.exists(self.path) + assert db.data == {} if __name__ == "__main__": From 050a2231156957ffb22e4d3a8161d1de3bb866c1 Mon Sep 17 00:00:00 2001 From: GideonBear <87426140+GideonBear@users.noreply.github.com> Date: Tue, 14 Mar 2023 09:32:57 +0100 Subject: [PATCH 16/22] Remove deprecated aliases --- README.md | 4 ++-- livejson/__init__.py | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 0f6d485..768cedc 100644 --- a/README.md +++ b/README.md @@ -27,13 +27,13 @@ - **Flexible**: `livejson` fully supports complex nestings of `list`s and `dict`s, meaning it can represent any valid JSON file. - **Compatible**: `livejson` works perfectly on both Python 2 and Python 3. - **Lightweight**: `livejson` is a single file with no external dependencies. Just install and go! -- **Reliable**: by default, no caching is used. Every single time you access a `livejson.Database`, it's read straight from the file. And every time you write to it, the change is instant. No delays, no conflicts. However, if efficiency is important, you can use the context manager to perform "grouped writes", which allow for performing a large number of operations with only one write at the end. +- **Reliable**: by default, no caching is used. Every single time you access a `livejson.File`, it's read straight from the file. And every time you write to it, the change is instant. No delays, no conflicts. However, if efficiency is important, you can use the context manager to perform "grouped writes", which allow for performing a large number of operations with only one write at the end. - **100% test covered** Be confident that `livejson` is working properly `livejson` can be used for: - **Database storage**: you can use `livejson` to easily write flexible JSON databases, without having to worry about complex `open` and `close` operations, or learning how to use the `json` module. -- **Debugging**: You can use `livejson` to back up your Python objects. If you use a `livejson.Database` instead of a `dict` or a `list` and your script crashes you'll still have a hard copy of your object. And you barely had to change any of your code. +- **Debugging**: You can use `livejson` to back up your Python objects. If you use a `livejson.File` instead of a `dict` or a `list` and your script crashes you'll still have a hard copy of your object. And you barely had to change any of your code. - **General-purpose JSON**: If your script or application needs to interact with JSON files in any way, consider using `livejson`, for simplicity's sake. `livejson` can make your code easier to read and understand, and also save you time. Thanks to [dgelessus](https://github.com/dgelessus) for naming this project. diff --git a/livejson/__init__.py b/livejson/__init__.py index 94832a9..afa4844 100644 --- a/livejson/__init__.py +++ b/livejson/__init__.py @@ -381,9 +381,3 @@ def with_data(cls, path, data, *args, **kwargs): f = cls(path, *args, **kwargs) f.data = data return f - - -# Aliases for backwards-compatibility -Database = File -ListDatabase = ListFile -DictDatabase = DictFile From 5516877892e0d79fc1ad8a746b7018a19c81d5cd Mon Sep 17 00:00:00 2001 From: GideonBear <87426140+GideonBear@users.noreply.github.com> Date: Tue, 14 Mar 2023 09:35:21 +0100 Subject: [PATCH 17/22] Remove tests for deprecated aliases --- test.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/test.py b/test.py index 7352b99..9b1e588 100644 --- a/test.py +++ b/test.py @@ -272,22 +272,5 @@ def test_fun_syntax(self): self.assertEqual(fi.read(), "{\"cats\": \"dogs\"}") -class TestAliases(_BaseTest, unittest.TestCase): - def test_Database(self): - db = livejson.Database(self.path) - self.assertTrue(os.path.exists(self.path)) - self.assertEqual(db.data, {}) - - def test_ListDatabase(self): - db = livejson.ListDatabase(self.path) - self.assertTrue(os.path.exists(self.path)) - self.assertEqual(db.data, []) - - def test_DictDatabase(self): - db = livejson.DictDatabase(self.path) - self.assertTrue(os.path.exists(self.path)) - self.assertEqual(db.data, {}) - - if __name__ == "__main__": unittest.main() From cd567c493b20c46a25a327226ec9b47ef7c34a50 Mon Sep 17 00:00:00 2001 From: GideonBear <87426140+GideonBear@users.noreply.github.com> Date: Tue, 14 Mar 2023 09:37:09 +0100 Subject: [PATCH 18/22] Remove deprecated `set_data` method --- livejson/__init__.py | 9 --------- test.py | 6 +++--- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/livejson/__init__.py b/livejson/__init__.py index afa4844..d262024 100644 --- a/livejson/__init__.py +++ b/livejson/__init__.py @@ -258,15 +258,6 @@ def _updateType(self): # Bonus features! - def set_data(self, data): - """Equivalent to setting the "data" attribute. Exists for backwards - compatibility.""" - warnings.warn( - "set_data is deprecated; please set .data instead.", - DeprecationWarning - ) - self.data = data - def remove(self): """Delete the file from the disk completely.""" os.remove(self.path) diff --git a/test.py b/test.py index 9b1e588..a76184f 100644 --- a/test.py +++ b/test.py @@ -79,7 +79,7 @@ def test_switchclass(self): # Test switching under normal usage f = livejson.File(self.path) self.assertIsInstance(f, livejson.DictFile) - f.set_data([]) + f.data = [] self.assertIsInstance(f, livejson.ListFile) # Test switching when the file is manually changed with open(self.path, "w") as fi: @@ -147,7 +147,7 @@ def test_json_formatting(self): f["a"] = "b" self.assertEqual(f.file_contents, '{\n "a": "b"\n}') f.indent = 4 - f.set_data(f.data) # Force an update + f.data = f.data # Force an update self.assertEqual(f.file_contents, '{\n "a": "b"\n}') # Test sorting of keys @@ -245,7 +245,7 @@ def test_switchclass(self): f = livejson.File(self.path) with f: self.assertIsInstance(f, livejson.DictFile) - f.set_data([]) + f.data = [] self.assertIsInstance(f, livejson.ListFile) self.assertEqual(f.file_contents, "{}") self.assertEqual(f.file_contents, "[]") From d8a75d4f36e6f421f7c4f3e4723009909d85c1b7 Mon Sep 17 00:00:00 2001 From: GideonBear <87426140+GideonBear@users.noreply.github.com> Date: Tue, 14 Mar 2023 09:39:02 +0100 Subject: [PATCH 19/22] Remove unused import --- livejson/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/livejson/__init__.py b/livejson/__init__.py index d262024..ecad9fd 100644 --- a/livejson/__init__.py +++ b/livejson/__init__.py @@ -8,7 +8,6 @@ import json import os -import warnings from collections.abc import MutableMapping, MutableSequence # MISC HELPERS From dbdf95c4a1c0f53e4442bb0e7c57edbb80f1d34a Mon Sep 17 00:00:00 2001 From: GideonBear <87426140+GideonBear@users.noreply.github.com> Date: Tue, 14 Mar 2023 16:25:33 +0100 Subject: [PATCH 20/22] Resolve merge conflict --- test.py | 37 ++----------------------------------- 1 file changed, 2 insertions(+), 35 deletions(-) diff --git a/test.py b/test.py index 5e5cba0..031db22 100644 --- a/test.py +++ b/test.py @@ -149,13 +149,8 @@ def test_json_formatting(self): f["a"] = "b" assert f.file_contents == '{\n "a": "b"\n}' f.indent = 4 -<<<<<<< HEAD - f.set_data(f.data) # Force an update - assert f.file_contents == '{\n "a": "b"\n}' -======= f.data = f.data # Force an update - self.assertEqual(f.file_contents, '{\n "a": "b"\n}') ->>>>>>> d8a75d4f36e6f421f7c4f3e4723009909d85c1b7 + assert f.file_contents == '{\n "a": "b"\n}' # Test sorting of keys f["b"] = "c" @@ -249,19 +244,11 @@ def test_switchclass(self): """ Test the switching of classes in the middle of a grouped write """ f = livejson.File(self.path) with f: -<<<<<<< HEAD assert isinstance(f, livejson.DictFile) - f.set_data([]) + f.data = [] assert isinstance(f, livejson.ListFile) assert f.file_contents == "{}" assert f.file_contents == "[]" -======= - self.assertIsInstance(f, livejson.DictFile) - f.data = [] - self.assertIsInstance(f, livejson.ListFile) - self.assertEqual(f.file_contents, "{}") - self.assertEqual(f.file_contents, "[]") ->>>>>>> d8a75d4f36e6f421f7c4f3e4723009909d85c1b7 def test_misc(self): """ Test miscellaneous other things that seem like they might break @@ -285,25 +272,5 @@ def test_fun_syntax(self): assert fi.read() == "{\"cats\": \"dogs\"}" -<<<<<<< HEAD -class TestAliases(_BaseTest, unittest.TestCase): - def test_Database(self): - db = livejson.Database(self.path) - assert os.path.exists(self.path) - assert db.data == {} - - def test_ListDatabase(self): - db = livejson.ListDatabase(self.path) - assert os.path.exists(self.path) - assert db.data == [] - - def test_DictDatabase(self): - db = livejson.DictDatabase(self.path) - assert os.path.exists(self.path) - assert db.data == {} - - -======= ->>>>>>> d8a75d4f36e6f421f7c4f3e4723009909d85c1b7 if __name__ == "__main__": unittest.main() From 3dd5b90d016ee63f83bd34541845036f4e578a67 Mon Sep 17 00:00:00 2001 From: GideonBear <87426140+GideonBear@users.noreply.github.com> Date: Sun, 9 Jul 2023 09:06:22 +0200 Subject: [PATCH 21/22] Add typing to `_initfile` and replace datatype string with enum --- livejson/__init__.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/livejson/__init__.py b/livejson/__init__.py index ecad9fd..8a66551 100644 --- a/livejson/__init__.py +++ b/livejson/__init__.py @@ -6,16 +6,28 @@ from __future__ import annotations +import enum import json import os from collections.abc import MutableMapping, MutableSequence +from enum import Enum +from typing import Union + + +PathLike = Union[str, os.PathLike] + # MISC HELPERS -def _initfile(path, data="dict"): +class _DataType(Enum): + List = enum.auto() + Dict = enum.auto() + + +def _initfile(path: PathLike, data_type: _DataType = _DataType.Dict) -> bool | None: # TODO: is return really neccessary? Not used anywhere """Initialize an empty JSON file.""" - data = {} if data.lower() == "dict" else [] + data = {} if data_type is _DataType.Dict else [] # The file will need to be created if it doesn't exist if not os.path.exists(path): # The file doesn't exist # Raise exception if the directory that should contain the file doesn't @@ -192,7 +204,7 @@ def __init__(self, path, pretty=False, sort_keys=False): self.indent = 2 # Default indentation level _initfile(self.path, - "list" if isinstance(self, ListFile) else "dict") + _DataType.List if isinstance(self, ListFile) else _DataType.Dict) def _data(self): """A simpler version of data to avoid infinite recursion in some cases. From 0b8649a9191a9553d821ef6d42385609b4fcfdc3 Mon Sep 17 00:00:00 2001 From: GideonBear <87426140+GideonBear@users.noreply.github.com> Date: Sun, 9 Jul 2023 09:08:01 +0200 Subject: [PATCH 22/22] Fix typing in `_initfile` --- livejson/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/livejson/__init__.py b/livejson/__init__.py index 8a66551..4b98b53 100644 --- a/livejson/__init__.py +++ b/livejson/__init__.py @@ -11,8 +11,7 @@ import os from collections.abc import MutableMapping, MutableSequence from enum import Enum -from typing import Union - +from typing import Union, Any PathLike = Union[str, os.PathLike] @@ -27,7 +26,7 @@ class _DataType(Enum): def _initfile(path: PathLike, data_type: _DataType = _DataType.Dict) -> bool | None: # TODO: is return really neccessary? Not used anywhere """Initialize an empty JSON file.""" - data = {} if data_type is _DataType.Dict else [] + data: dict[Any, Any] | list[Any] = {} if data_type is _DataType.Dict else [] # The file will need to be created if it doesn't exist if not os.path.exists(path): # The file doesn't exist # Raise exception if the directory that should contain the file doesn't