diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..fa172c1 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,58 @@ +name: Python package + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + lint: + name: "flake8 on code" + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: 3.12 + allow-prereleases: true + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 + - name: Run flake8 + shell: bash + run: | + flake8 + + test: + needs: [ lint ] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11", "3.12" ] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + + - name: Test + run: | + make coverage diff --git a/.gitignore b/.gitignore index ce09cfb..861d6b2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ dist *.swp doc/_build *.egg-info +.idea diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..b8c2f2b --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,22 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: doc/conf.py + +# We recommend specifying your dependencies to enable reproducible builds: +# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +# python: +# install: +# - requirements: docs/requirements.txt diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 62ad875..0000000 --- a/.travis.yml +++ /dev/null @@ -1,32 +0,0 @@ -language: python -python: -- '2.7' -- '3.5' -- '3.6' -- '3.7' -- '3.8' -- '3.9' -- 3.10-dev -- nightly -- pypy3 -install: -- travis_retry pip install coveralls -script: -- coverage run --source=jsonpointer tests.py -after_script: -- coveralls -sudo: false -addons: - apt: - packages: - - pandoc -before_deploy: -- pip install -r requirements-dev.txt -deploy: - provider: pypi - user: skoegl - password: - secure: bKET/1sK+uWetPM3opPTt4qHHfZ6bMcjuLGe3Z/EUfNnGazcbDezy9CHJSqofuMXynF3xc8yluEojjfaqos3Ge/Y4o8pdFMY8ABp8KkxMnAJYGtYzbneSHgdgxPKsmdcUMVtIfioqkeNJTJClWUhRikWSlpKZ7TtkK4AmWtKNwc= - on: - tags: true - distributions: sdist bdist_wheel diff --git a/README.md b/README.md index 210b07b..06c8aef 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ python-json-pointer [![PyPI version](https://img.shields.io/pypi/v/jsonpointer.svg)](https://pypi.python.org/pypi/jsonpointer/) [![Supported Python versions](https://img.shields.io/pypi/pyversions/jsonpointer.svg)](https://pypi.python.org/pypi/jsonpointer/) -[![Build Status](https://travis-ci.org/stefankoegl/python-json-pointer.svg?branch=master)](https://travis-ci.org/stefankoegl/python-json-pointer) [![Coverage Status](https://coveralls.io/repos/stefankoegl/python-json-pointer/badge.svg?branch=master)](https://coveralls.io/r/stefankoegl/python-json-pointer?branch=master) diff --git a/bin/jsonpointer b/bin/jsonpointer index d577d01..ba2117c 100755 --- a/bin/jsonpointer +++ b/bin/jsonpointer @@ -1,14 +1,12 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from __future__ import print_function -import sys -import os.path -import json -import jsonpointer import argparse +import json +import sys +import jsonpointer parser = argparse.ArgumentParser( description='Resolve a JSON pointer on JSON files') @@ -20,7 +18,7 @@ ptr_group.add_argument('-f', '--pointer-file', type=argparse.FileType('r'), nargs='?', help='File containing a JSON pointer expression') -ptr_group.add_argument('POINTER', type=str, nargs='?', +ptr_group.add_argument('POINTER', type=str, nargs='?', help='A JSON pointer expression') parser.add_argument('FILE', type=argparse.FileType('r'), nargs='+', diff --git a/jsonpointer.py b/jsonpointer.py index b45684d..3e97add 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -32,38 +32,22 @@ """ Identify specific nodes in a JSON document (RFC 6901) """ -from __future__ import unicode_literals - # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '2.3' +__version__ = '3.0.0' __website__ = 'https://github.com/stefankoegl/python-json-pointer' __license__ = 'Modified BSD License' - -try: - from itertools import izip - str = unicode - encode_str = lambda u: u.encode("raw_unicode_escape") -except ImportError: # Python 3 - izip = zip - encode_str = lambda u: u - -try: - from collections.abc import Mapping, Sequence -except ImportError: # Python 3 - from collections import Mapping, Sequence - -from itertools import tee, chain -import re import copy - +import re +from collections.abc import Mapping, Sequence +from itertools import tee, chain _nothing = object() def set_pointer(doc, pointer, value, inplace=True): - """Resolves pointer against doc and sets the value of the target within doc. + """Resolves a pointer against doc and sets the value of the target within doc. With inplace set to true, doc is modified as long as pointer is not the root. @@ -145,7 +129,7 @@ def pairwise(iterable): a, b = tee(iterable) for _ in b: break - return izip(a, b) + return zip(a, b) class JsonPointerException(Exception): @@ -259,12 +243,11 @@ def get_part(cls, doc, part): else: raise JsonPointerException("Document '%s' does not support indexing, " "must be mapping/sequence or support __getitem__" % type(doc)) - + def get_parts(self): """Returns the list of the parts. For example, JsonPointer('/a/b').get_parts() == ['a', 'b']""" - - return self.parts + return self.parts def walk(self, doc, part): """ Walks one step in doc and returns the referenced part """ @@ -281,7 +264,7 @@ def walk(self, doc, part): return doc[part] except IndexError: - raise JsonPointerException("index '%s' is out of bounds" % (part, )) + raise JsonPointerException("index '%s' is out of bounds" % (part,)) # Else the object is a mapping or supports __getitem__(so assume custom indexing) try: @@ -290,7 +273,6 @@ def walk(self, doc, part): except KeyError: raise JsonPointerException("member '%s' not found in %s" % (part, doc)) - def contains(self, ptr): """ Returns True if self contains the given ptr """ return self.parts[:len(ptr.parts)] == ptr.parts @@ -309,12 +291,11 @@ def join(self, suffix): suffix_parts = suffix try: return JsonPointer.from_parts(chain(self.parts, suffix_parts)) - except: + except: # noqa E722 raise JsonPointerException("Invalid suffix") - def __truediv__(self, suffix): # Python 3 + def __truediv__(self, suffix): # Python 3 return self.join(suffix) - __div__ = __truediv__ # Python 2 @property def path(self): @@ -342,10 +323,10 @@ def __hash__(self): return hash(tuple(self.parts)) def __str__(self): - return encode_str(self.path) + return self.path def __repr__(self): - return "JsonPointer(" + repr(self.path) + ")" + return type(self).__name__ + "(" + repr(self.path) + ")" @classmethod def from_parts(cls, parts): @@ -362,5 +343,6 @@ def from_parts(cls, parts): def escape(s): return s.replace('~', '~0').replace('/', '~1') + def unescape(s): return s.replace('~1', '/').replace('~0', '~') diff --git a/makefile b/makefile index e0a2fcf..c40b485 100644 --- a/makefile +++ b/makefile @@ -10,7 +10,7 @@ help: @echo test: - python tests.py + python -munittest coverage: coverage run --source=jsonpointer tests.py diff --git a/requirements-dev.txt b/requirements-dev.txt index 9fcb076..239fcca 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,4 @@ wheel -twine>=1.11.0 -setuptools>=38.6.0 +setuptools +coverage +flake8 diff --git a/setup.cfg b/setup.cfg index 2a9acf1..ab8f354 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,6 @@ [bdist_wheel] universal = 1 + +[flake8] +max-line-length = 120 +exclude = .git,.tox,dist,doc,*egg,build,.venv \ No newline at end of file diff --git a/setup.py b/setup.py index da27f3f..3e87a4c 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,10 @@ #!/usr/bin/env python -from setuptools import setup -import re import io import os.path +import re + +from setuptools import setup dirname = os.path.dirname(os.path.abspath(__file__)) filename = os.path.join(dirname, 'jsonpointer.py') @@ -14,7 +15,7 @@ PACKAGE = 'jsonpointer' MODULES = ( - 'jsonpointer', + 'jsonpointer', ) AUTHOR_EMAIL = metadata['author'] @@ -26,10 +27,8 @@ # Extract name and e-mail ("Firstname Lastname ") AUTHOR, EMAIL = re.match(r'(.*) <(.*)>', AUTHOR_EMAIL).groups() - with open('README.md') as readme: - long_description = readme.read() - + long_description = readme.read() CLASSIFIERS = [ 'Development Status :: 5 - Production/Stable', @@ -38,14 +37,13 @@ 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', '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', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Libraries', @@ -64,5 +62,5 @@ py_modules=MODULES, scripts=['bin/jsonpointer'], classifiers=CLASSIFIERS, - python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', -) + python_requires='>=3.7', + ) diff --git a/tests.py b/tests.py index 9252369..7b1cdac 100755 --- a/tests.py +++ b/tests.py @@ -3,19 +3,21 @@ from __future__ import unicode_literals +import copy import doctest -import unittest import sys -import copy +import unittest + +import jsonpointer from jsonpointer import resolve_pointer, EndOfList, JsonPointerException, \ - JsonPointer, set_pointer + JsonPointer, set_pointer class SpecificationTests(unittest.TestCase): """ Tests all examples from the JSON Pointer specification """ def test_example(self): - doc = { + doc = { "foo": ["bar", "baz"], "": 0, "a/b": 1, @@ -41,7 +43,6 @@ def test_example(self): self.assertEqual(resolve_pointer(doc, "/ "), 7) self.assertEqual(resolve_pointer(doc, "/m~0n"), 8) - def test_eol(self): doc = { "foo": ["bar", "baz"] @@ -164,19 +165,16 @@ def test_eq_hash(self): self.assertFalse(p1 == "/something/1/b") def test_contains(self): - self.assertTrue(self.ptr1.contains(self.ptr2)) self.assertTrue(self.ptr1.contains(self.ptr1)) self.assertFalse(self.ptr1.contains(self.ptr3)) def test_contains_magic(self): - self.assertTrue(self.ptr2 in self.ptr1) self.assertTrue(self.ptr1 in self.ptr1) self.assertFalse(self.ptr3 in self.ptr1) def test_join(self): - ptr12a = self.ptr1.join(self.ptr2) self.assertEqual(ptr12a.path, "/a/b/c/a/b") @@ -195,7 +193,6 @@ def test_join(self): self.assertRaises(JsonPointerException, self.ptr1.join, 0) def test_join_magic(self): - ptr12a = self.ptr1 / self.ptr2 self.assertEqual(ptr12a.path, "/a/b/c/a/b") @@ -211,6 +208,7 @@ def test_join_magic(self): ptr12e = self.ptr1 / ["a", "b"] self.assertEqual(ptr12e.path, "/a/b/c/a/b") + class WrongInputTests(unittest.TestCase): def test_no_start_slash(self): @@ -243,7 +241,6 @@ def test_empty_path(self): self.assertEqual(doc, last) self.assertTrue(nxt is None) - def test_path(self): doc = {'a': [{'b': 1, 'c': 2}, 5]} ptr = JsonPointer('/a/0/b') @@ -255,7 +252,7 @@ def test_path(self): class SetTests(unittest.TestCase): def test_set(self): - doc = { + doc = { "foo": ["bar", "baz"], "": 0, "a/b": 1, @@ -284,7 +281,7 @@ def test_set(self): newdoc = set_pointer(doc, "/fud", {}, inplace=False) newdoc = set_pointer(newdoc, "/fud/gaw", [1, 2, 3], inplace=False) - self.assertEqual(resolve_pointer(newdoc, "/fud"), {'gaw' : [1, 2, 3]}) + self.assertEqual(resolve_pointer(newdoc, "/fud"), {'gaw': [1, 2, 3]}) newdoc = set_pointer(doc, "", 9, inplace=False) self.assertEqual(newdoc, 9) @@ -306,14 +303,13 @@ def test_set(self): self.assertRaises(JsonPointerException, set_pointer, doc, "/fud/gaw", 9) set_pointer(doc, "/fud", {}) - set_pointer(doc, "/fud/gaw", [1, 2, 3] ) - self.assertEqual(resolve_pointer(doc, "/fud"), {'gaw' : [1, 2, 3]}) + set_pointer(doc, "/fud/gaw", [1, 2, 3]) + self.assertEqual(resolve_pointer(doc, "/fud"), {'gaw': [1, 2, 3]}) self.assertRaises(JsonPointerException, set_pointer, doc, "", 9) class AltTypesTests(unittest.TestCase): - class Node(object): def __init__(self, name, parent=None): self.name = name @@ -348,13 +344,13 @@ def __setitem__(self, key, val): class mdict(object): def __init__(self, d): self._d = d + def __getitem__(self, item): return self._d[item] mdict = mdict({'root': {'1': {'2': '3'}}}) Node = Node - def test_alttypes(self): Node = self.Node @@ -410,23 +406,6 @@ def test_mock_dict_raises_key_error(self): self.assertRaises(JsonPointerException, resolve_pointer, doc, '/root/1/2/3/4') - -suite = unittest.TestSuite() -suite.addTest(unittest.makeSuite(SpecificationTests)) -suite.addTest(unittest.makeSuite(ComparisonTests)) -suite.addTest(unittest.makeSuite(WrongInputTests)) -suite.addTest(unittest.makeSuite(ToLastTests)) -suite.addTest(unittest.makeSuite(SetTests)) -suite.addTest(unittest.makeSuite(AltTypesTests)) - -modules = ['jsonpointer'] - -for module in modules: - m = __import__(module, fromlist=[module]) - suite.addTest(doctest.DocTestSuite(m)) - -runner = unittest.TextTestRunner(verbosity=1) -result = runner.run(suite) - -if not result.wasSuccessful(): - sys.exit(1) +def load_tests(loader, tests, ignore): + tests.addTests(doctest.DocTestSuite(jsonpointer)) + return tests