diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5493fbb..4ab3532 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -40,10 +40,10 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: "3.13" @@ -58,7 +58,7 @@ jobs: - name: Check distributions run: twine check --strict dist/* - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: dist path: dist/ @@ -74,13 +74,13 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: name: dist path: dist/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1 + uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3 publish-testpypi: name: Publish to TestPyPI @@ -93,12 +93,12 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: name: dist path: dist/ - name: Publish to TestPyPI - uses: pypa/gh-action-pypi-publish@release/v1 + uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3 with: repository-url: https://test.pypi.org/legacy/ diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 6fedd8f..4e637fb 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -15,15 +15,12 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.ref }} + - uses: actions/checkout@v7 - name: Set up Python 3.13 - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: - python-version: 3.13 - allow-prereleases: true + python-version: "3.13" - name: Install dependencies run: | python -m pip install --upgrade pip @@ -39,10 +36,10 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up Python 3.13 - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: "3.13" - name: Install build tools @@ -60,10 +57,10 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up Python 3.13 - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: "3.13" - name: Install dependencies @@ -81,10 +78,10 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up Python 3.13 - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: "3.13" - name: Install dependencies @@ -97,21 +94,26 @@ jobs: test: needs: [ lint ] runs-on: ${{ matrix.os }} + # Pre-release Pythons may fail for reasons unrelated to this package + continue-on-error: ${{ matrix.experimental || false }} strategy: fail-fast: false matrix: os: [ ubuntu-latest ] - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "pypy3.10", "pypy3.11" ] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15", "pypy3.11" ] include: - os: windows-latest - python-version: "3.13" + python-version: "3.15" - os: macos-latest - python-version: "3.13" + python-version: "3.15" + - os: ubuntu-latest + python-version: "3.16" + experimental: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} allow-prereleases: true diff --git a/.readthedocs.yaml b/.readthedocs.yaml index a11f336..bfe1aed 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -11,7 +11,7 @@ build: tools: python: "3.13" -# Build documentation in the docs/ directory with Sphinx +# Build documentation in the doc/ directory with Sphinx sphinx: configuration: doc/conf.py diff --git a/MANIFEST.in b/MANIFEST.in index 9384294..20127ef 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,11 @@ include AUTHORS include LICENSE.txt -include README.md +include makefile +include requirements-dev.txt +include .coveragerc include tests.py include doc/*.rst +include doc/conf.py +include doc/Makefile +include doc/requirements.txt recursive-include jsonpointer-stubs *.pyi diff --git a/README.md b/README.md index 016ac01..601f508 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ 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/) +[![PyPI version](https://img.shields.io/pypi/v/jsonpointer.svg)](https://pypi.org/project/jsonpointer/) +[![Supported Python versions](https://img.shields.io/pypi/pyversions/jsonpointer.svg)](https://pypi.org/project/jsonpointer/) [![Tests](https://github.com/stefankoegl/python-json-pointer/actions/workflows/test.yaml/badge.svg?branch=master)](https://github.com/stefankoegl/python-json-pointer/actions/workflows/test.yaml) [![Coverage Status](https://coveralls.io/repos/github/stefankoegl/python-json-pointer/badge.svg?branch=master)](https://coveralls.io/github/stefankoegl/python-json-pointer?branch=master) @@ -11,12 +11,13 @@ Resolve JSON Pointers in Python ------------------------------- Library to resolve JSON Pointers according to -[RFC 6901](http://tools.ietf.org/html/rfc6901) +[RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901) + +See the documentation for examples. -See source code for examples * Website: https://github.com/stefankoegl/python-json-pointer * Repository: https://github.com/stefankoegl/python-json-pointer.git -* Documentation: https://python-json-pointer.readthedocs.org/ -* PyPI: https://pypi.python.org/pypi/jsonpointer +* Documentation: https://python-json-pointer.readthedocs.io/ +* PyPI: https://pypi.org/project/jsonpointer/ * GitHub Actions: https://github.com/stefankoegl/python-json-pointer/actions * Coveralls: https://coveralls.io/github/stefankoegl/python-json-pointer diff --git a/bin/jsonpointer b/bin/jsonpointer index d4c8cb7..9e0efb2 100755 --- a/bin/jsonpointer +++ b/bin/jsonpointer @@ -13,7 +13,7 @@ parser = argparse.ArgumentParser( # Accept pointer as option, as file or as first positional argument ptr_group = parser.add_mutually_exclusive_group() -ptr_group.add_argument('-f', '--pointer-file', type=argparse.FileType('r', encoding='utf-8'), +ptr_group.add_argument('-f', '--pointer-file', type=str, help='File containing a JSON pointer expression') ptr_group.add_argument('-p', '--pointer', type=str, @@ -37,14 +37,28 @@ def main(): try: ok = resolve_files() except KeyboardInterrupt: - sys.exit(1) + # Conventional status for termination by SIGINT (128 + 2) + sys.exit(130) if not ok: sys.exit(1) +def open_input(name): + """ Open a UTF-8 input file, or return stdin for "-" + + Exits with a usage error if the file cannot be opened. """ + if name == '-': + return sys.stdin + + try: + return open(name, encoding='utf-8') + except OSError as e: + parser.error("can't open '%s': %s" % (name, e)) + + def parse_pointer(args): - """ Return the pointer and the list of files to resolve it on + """ Return the parsed pointer and the list of files to resolve it on If the pointer is given via -f or -p, a POINTER positional argument is actually the first file. """ @@ -57,14 +71,16 @@ def parse_pointer(args): if args.pointer is not None: ptr = args.pointer else: - # 2026-10-11: Trim file line endings without changing member names. + # Strip only the trailing line ending; other whitespace may be + # part of a member name. + pointer_file = open_input(args.pointer_file) try: - ptr = args.pointer_file.read().rstrip("\r\n") + ptr = pointer_file.read().rstrip("\r\n") except UnicodeDecodeError as e: parser.error('could not decode pointer file: %s' % e) finally: - if args.pointer_file is not sys.stdin: - args.pointer_file.close() + if pointer_file is not sys.stdin: + pointer_file.close() elif args.POINTER is not None: ptr = args.POINTER @@ -75,11 +91,11 @@ def parse_pointer(args): # Catches e.g. `jsonpointer a.json b.json`, where the pointer was omitted # and the first file would otherwise be used as the pointer try: - jsonpointer.JsonPointer(ptr) + pointer = jsonpointer.JsonPointer(ptr) except jsonpointer.JsonPointerException as e: parser.error('invalid JSON pointer %r: %s' % (ptr, e)) - return ptr, files + return pointer, files def resolve_files(): @@ -89,14 +105,11 @@ def resolve_files(): or if --continue-on-error skipped a JSON or text decoding error. """ args = parser.parse_args() - ptr, files = parse_pointer(args) + pointer, files = parse_pointer(args) ok = True for name in files: - try: - f = argparse.FileType('r', encoding='utf-8')(name) - except argparse.ArgumentTypeError as e: - parser.error(str(e)) + f = open_input(name) try: doc = json.load(f) @@ -112,7 +125,7 @@ def resolve_files(): f.close() try: - result = jsonpointer.resolve_pointer(doc, ptr) + result = pointer.resolve(doc) if isinstance(result, jsonpointer.EndOfList): raise jsonpointer.JsonPointerException( "'-' does not reference an existing array element") diff --git a/doc/_static/.empty b/doc/_static/.empty deleted file mode 100644 index e69de29..0000000 diff --git a/doc/commandline.rst b/doc/commandline.rst index cbb2a24..e170d02 100644 --- a/doc/commandline.rst +++ b/doc/commandline.rst @@ -2,7 +2,7 @@ The ``jsonpointer`` commandline utility ======================================= The JSON pointer package also installs a ``jsonpointer`` commandline utility -that can be used to resolve a JSON pointers on JSON files. +that can be used to resolve a JSON pointer on JSON files. The program has the following usage :: @@ -19,9 +19,9 @@ The program has the following usage :: options: -h, --help show this help message and exit - -f POINTER_FILE, --pointer-file POINTER_FILE + -f, --pointer-file POINTER_FILE File containing a JSON pointer expression - -p POINTER, --pointer POINTER + -p, --pointer POINTER A JSON pointer expression --indent INDENT Indent output by n spaces --continue-on-error Continue with remaining files after JSON or text @@ -35,8 +35,8 @@ JSON input files and pointer files are read as UTF-8. The exit status is 0 if the pointer could be resolved on all files, 1 if it could not be resolved on at least one file (the remaining files are still -processed) and 2 on usage errors, such as an invalid pointer or a missing -file. +processed), 2 on usage errors, such as an invalid pointer or a missing +file, and 130 if interrupted with Ctrl-C. By default, invalid JSON or a text decoding error stops processing. With ``--continue-on-error``, these errors are reported on standard error with diff --git a/doc/conf.py b/doc/conf.py index f472f57..524b902 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,245 +1,47 @@ -# -# python-json-pointer documentation build configuration file, created by -# sphinx-quickstart on Sat Apr 13 16:52:59 2013. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. +# Sphinx configuration for the python-json-pointer documentation. +# See https://www.sphinx-doc.org/en/master/usage/configuration.html import os import sys -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.join(os.path.abspath('.'), '..')) +# Document the jsonpointer module from this checkout +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')) -import jsonpointer +import jsonpointer # noqa: E402 -# -- General configuration ----------------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' +# -- Project information ------------------------------------------------------- -# General information about the project. project = 'python-json-pointer' +author = 'Stefan Kögl' copyright = jsonpointer.__author__ - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# - -# The short X.Y version. version = jsonpointer.__version__ -# The full version, including alpha/beta/rc tags. release = jsonpointer.__version__ -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# -- General configuration ----------------------------------------------------- -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. +extensions = ['sphinx.ext.autodoc'] exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - - # -- Options for HTML output --------------------------------------------------- -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'default' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. +html_theme = 'classic' htmlhelp_basename = 'python-json-pointerdoc' +# -- Options for other output formats ------------------------------------------ -# -- Options for LaTeX output -------------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'python-json-pointer.tex', 'python-json-pointer Documentation', - 'Stefan Kögl', 'manual'), + ('index', 'python-json-pointer.tex', 'python-json-pointer Documentation', + author, 'manual'), ] -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output -------------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'python-json-pointer', 'python-json-pointer Documentation', - ['Stefan Kögl'], 1) + ('index', 'python-json-pointer', 'python-json-pointer Documentation', + [author], 1), ] -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------------ - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) texinfo_documents = [ - ('index', 'python-json-pointer', 'python-json-pointer Documentation', - 'Stefan Kögl', 'python-json-pointer', 'One line description of project.', - 'Miscellaneous'), + ('index', 'python-json-pointer', 'python-json-pointer Documentation', + author, 'python-json-pointer', jsonpointer.__doc__.strip(), + 'Miscellaneous'), ] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' diff --git a/doc/index.rst b/doc/index.rst index e1e0aae..9a87e25 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -7,7 +7,7 @@ python-json-pointer =================== *python-json-pointer* is a Python library for resolving JSON pointers (`RFC -6901 `_). Python 3.10+ +6901 `_). Python 3.10+ and PyPy are supported. **Contents** @@ -18,7 +18,7 @@ and PyPy are supported. tutorial mod-jsonpointer commandline - RFC 6901 + RFC 6901 Indices and tables diff --git a/doc/mod-jsonpointer.rst b/doc/mod-jsonpointer.rst index 7929d6d..ac48ec7 100644 --- a/doc/mod-jsonpointer.rst +++ b/doc/mod-jsonpointer.rst @@ -1,7 +1,7 @@ -.. mod-jsonpointer: +.. _mod-jsonpointer: The ``jsonpointer`` module -============================ +========================== .. automodule:: jsonpointer :members: diff --git a/doc/requirements.txt b/doc/requirements.txt index 6966869..7dc9833 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1 +1 @@ -sphinx +sphinx==9.2.0 diff --git a/doc/tutorial.rst b/doc/tutorial.rst index d1926e7..384f779 100644 --- a/doc/tutorial.rst +++ b/doc/tutorial.rst @@ -1,9 +1,9 @@ Tutorial ======== -Please refer to `RFC 6901 `_ for the exact -pointer syntax. ``jsonpointer`` has two interfaces. The ``resolve_pointer`` -method is basically a deep ``get``. +Please refer to `RFC 6901 `_ for the exact +pointer syntax. The ``resolve_pointer`` +function is basically a deep ``get``. .. code-block:: python @@ -29,7 +29,7 @@ method is basically a deep ``get``. True -The ``set_pointer`` method allows modifying a portion of an object using +The ``set_pointer`` function allows modifying a portion of an object using JSON pointer notation: .. code-block:: python diff --git a/jsonpointer-stubs/__init__.pyi b/jsonpointer-stubs/__init__.pyi index a011a88..b483df3 100644 --- a/jsonpointer-stubs/__init__.pyi +++ b/jsonpointer-stubs/__init__.pyi @@ -2,7 +2,7 @@ from collections.abc import Iterable, Iterator from re import Pattern from typing import Any, Final, TypeVar -from typing_extensions import Self +from typing_extensions import Self, deprecated _T = TypeVar("_T") @@ -21,6 +21,7 @@ def set_pointer( create: bool = False, ) -> Any: ... def resolve_pointer(doc: Any, pointer: str, default: Any = ...) -> Any: ... +@deprecated("Use itertools.pairwise() instead") def pairwise(iterable: Iterable[_T]) -> Iterator[tuple[_T, _T]]: ... class JsonPointerException(Exception): ... diff --git a/jsonpointer.py b/jsonpointer.py index 3466488..fbdbcc3 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -34,12 +34,14 @@ __author__ = 'Stefan Kögl ' __version__ = '3.11' __website__ = 'https://github.com/stefankoegl/python-json-pointer' -__license__ = 'Modified BSD License' +__license__ = 'BSD-3-Clause' import copy +import itertools import re +import warnings from collections.abc import Mapping, Sequence -from itertools import tee, chain +from itertools import chain _nothing = object() @@ -128,19 +130,13 @@ def pairwise(iterable): s -> (s0,s1), (s1,s2), (s2, s3), ... - >>> list(pairwise([])) - [] - - >>> list(pairwise([1])) - [] - - >>> list(pairwise([1, 2, 3, 4])) - [(1, 2), (2, 3), (3, 4)] + .. deprecated:: 3.12 + Use :func:`itertools.pairwise` instead. """ - a, b = tee(iterable) - for _ in b: - break - return zip(a, b) + warnings.warn('jsonpointer.pairwise() is deprecated, ' + 'use itertools.pairwise() instead', + DeprecationWarning, stacklevel=2) + return itertools.pairwise(iterable) class JsonPointerException(Exception): @@ -163,7 +159,7 @@ class JsonPointer: # Array indices must not contain: # leading zeros, signs, spaces, decimals, etc - _RE_ARRAY_INDEX = re.compile('0|[1-9][0-9]*$') + _RE_ARRAY_INDEX = re.compile('0|[1-9][0-9]*') _RE_INVALID_ESCAPE = re.compile('(~[^01]|~$)') def __init__(self, pointer): @@ -327,8 +323,6 @@ def walk(self, doc, part): part = JsonPointer.get_part(doc, part) - assert hasattr(doc, '__getitem__'), "invalid document type %s" % (type(doc),) - if isinstance(doc, Sequence): if part == '-': return EndOfList(doc) @@ -387,7 +381,7 @@ def __contains__(self, item): return self.contains(item) def join(self, suffix): - """ Returns a new JsonPointer with the given suffix append to this ptr """ + """ Returns a new JsonPointer with the given suffix appended to this ptr """ if isinstance(suffix, JsonPointer): suffix_parts = suffix.parts elif isinstance(suffix, str): @@ -396,8 +390,8 @@ def join(self, suffix): suffix_parts = suffix try: return self.__class__.from_parts(chain(self.parts, suffix_parts)) - except Exception: - raise JsonPointerException("Invalid suffix") + except Exception as e: + raise JsonPointerException("Invalid suffix") from e def __truediv__(self, suffix): return self.join(suffix) @@ -406,7 +400,8 @@ def __truediv__(self, suffix): def path(self): """Returns the string representation of the pointer - >>> ptr = JsonPointer('/~0/0/~1').path == '/~0/0/~1' + >>> JsonPointer('/~0/0/~1').path == '/~0/0/~1' + True """ parts = [escape(part) for part in self.parts] return ''.join('/' + part for part in parts) @@ -414,7 +409,7 @@ def path(self): def __eq__(self, other): """Compares a pointer to another object - Pointers can be compared by comparing their strings (or splitted + Pointers can be compared by comparing their strings (or split strings), because no two different parts can point to the same structure in an object (eg no different number representations) """ @@ -435,7 +430,7 @@ def __repr__(self): @classmethod def from_parts(cls, parts): - """Constructs a JsonPointer from a list of (unescaped) paths + """Constructs a JsonPointer from a list of (unescaped) parts >>> JsonPointer.from_parts(['a', '~', '/', 0]).path == '/a/~0/~1/0' True diff --git a/makefile b/makefile index 3276190..29f45e0 100644 --- a/makefile +++ b/makefile @@ -6,7 +6,7 @@ help: @echo " - coverage: run tests with coverage" @echo @echo "To install jsonpointer, type" - @echo " python3 setup.py install" + @echo " pip install ." @echo test: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..232cef1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=77.0"] +build-backend = "setuptools.build_meta" diff --git a/requirements-dev.txt b/requirements-dev.txt index 239fcca..9c4e869 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,2 @@ -wheel -setuptools coverage flake8 diff --git a/setup.py b/setup.py index 785d397..a80a407 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,5 @@ #!/usr/bin/env python -import io import os.path import re @@ -8,7 +7,8 @@ dirname = os.path.dirname(os.path.abspath(__file__)) filename = os.path.join(dirname, 'jsonpointer.py') -src = io.open(filename, encoding='utf-8').read() +with open(filename, encoding='utf-8') as f: + src = f.read() metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", src)) docstrings = re.findall('"""(.*)"""', src) @@ -21,20 +21,18 @@ AUTHOR_EMAIL = metadata['author'] VERSION = metadata['version'] WEBSITE = metadata['website'] -LICENSE = metadata['license'] DESCRIPTION = docstrings[0] # Extract name and e-mail ("Firstname Lastname ") AUTHOR, EMAIL = re.match(r'(.*) <(.*)>', AUTHOR_EMAIL).groups() -with open('README.md') as readme: +with open(os.path.join(dirname, 'README.md'), encoding='utf-8') as readme: long_description = readme.read() CLASSIFIERS = [ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', @@ -43,6 +41,7 @@ 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', 'Programming Language :: Python :: 3.14', + 'Programming Language :: Python :: 3.15', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Libraries', @@ -58,7 +57,7 @@ long_description_content_type="text/markdown", author=AUTHOR, author_email=EMAIL, - license=LICENSE, + license_expression='BSD-3-Clause', url=WEBSITE, py_modules=MODULES, # PEP 561 stub-only package, shipped in the same distribution diff --git a/tests.py b/tests.py index c1542d7..5bdaac7 100755 --- a/tests.py +++ b/tests.py @@ -216,6 +216,11 @@ def test_join(self): self.assertRaises(JsonPointerException, self.ptr1.join, 0) + def test_join_chains_original_error(self): + with self.assertRaises(JsonPointerException) as caught: + self.ptr1.join(0) + self.assertIsInstance(caught.exception.__cause__, TypeError) + def test_join_magic(self): ptr12a = self.ptr1 / self.ptr2 self.assertEqual(ptr12a.path, "/a/b/c/a/b") @@ -307,6 +312,17 @@ def test_string_not_indexable(self): @unittest.skipUnless(hasattr(sys, 'get_int_max_str_digits'), 'Integer string conversion limits are not available') +class PairwiseTests(unittest.TestCase): + + def test_pairwise_is_deprecated(self): + for items, expected in (([], []), ([1], []), + ([1, 2, 3, 4], [(1, 2), (2, 3), (3, 4)])): + with self.subTest(items=items): + with self.assertWarns(DeprecationWarning): + result = jsonpointer.pairwise(items) + self.assertEqual(list(result), expected) + + class LargeIndexTests(unittest.TestCase): def setUp(self): @@ -755,6 +771,15 @@ def test_keeps_stdin_open(self): self.assertTrue(cli['resolve_files']()) self.assertFalse(stdin.closed) + def test_keyboard_interrupt_exit_status(self): + cli = runpy.run_path(self.SCRIPT) + args = [self.SCRIPT, '/a', os.path.join(self.dir, 'a.json')] + with mock.patch.object(sys, 'argv', args), \ + mock.patch('json.load', side_effect=KeyboardInterrupt): + with self.assertRaises(SystemExit) as caught: + cli['main']() + self.assertEqual(caught.exception.code, 130) + def test_utf8_json_file_without_utf8_mode(self): self._write('unicode.json', '{"value": "🚀"}') proc = self._run('/value', 'unicode.json', utf8=False) @@ -971,6 +996,12 @@ def test_continue_on_error_does_not_skip_missing_files(self): self.assertEqual(proc.stdout, self.A_OUT) self.assertIn('missing.json', proc.stderr) + def test_missing_pointer_file(self): + proc = self._run('-f', 'missing.txt', 'a.json') + self.assertEqual(proc.returncode, 2) + self.assertEqual(proc.stdout, '') + self.assertIn("can't open 'missing.txt'", proc.stderr) + def test_pointer_file_requires_value(self): proc = self._run('-f', '-p', '/a', 'a.json') self.assertEqual(proc.returncode, 2)