33
44import codecs
55import json
6+ import re
67from functools import reduce
78from io import IOBase
89from operator import getitem
1516from typing import Union
1617from urllib .parse import unquote
1718
18- from .exceptions import JSONPointerError
19- from .exceptions import JSONPointerIndexError
20- from .exceptions import JSONPointerKeyError
21- from .exceptions import JSONPointerResolutionError
22- from .exceptions import JSONPointerTypeError
19+ from jsonpath .exceptions import JSONPointerError
20+ from jsonpath .exceptions import JSONPointerIndexError
21+ from jsonpath .exceptions import JSONPointerKeyError
22+ from jsonpath .exceptions import JSONPointerResolutionError
23+ from jsonpath .exceptions import JSONPointerTypeError
24+ from jsonpath .exceptions import RelativeJSONPointerIndexError
25+ from jsonpath .exceptions import RelativeJSONPointerSyntaxError
2326
2427if TYPE_CHECKING :
2528 from .match import JSONPathMatch
@@ -125,22 +128,30 @@ def _getitem(self, obj: Any, key: Any) -> Any: # noqa: PLR0912
125128 return getitem (obj , str (key ))
126129 except KeyError :
127130 raise JSONPointerKeyError (key ) from err
128- # Handle non-standard keys selector
131+ # Handle non-standard keys/property selector/pointer.
129132 if (
130133 isinstance (key , str )
131134 and isinstance (obj , Mapping )
132- and key .startswith (self .keys_selector )
135+ and key .startswith (( self .keys_selector , "#" ) )
133136 and key [1 :] in obj
134137 ):
135138 return key [1 :]
139+ # Handle non-standard index/property pointer (`#`)
136140 raise JSONPointerKeyError (key ) from err
137141 except TypeError as err :
138- if isinstance (obj , Sequence ):
142+ if isinstance (obj , Sequence ) and not isinstance ( obj , str ) :
139143 if key == "-" :
140144 # "-" is a valid index when appending to a JSON array
141145 # with JSON Patch, but not when resolving a JSON Pointer.
142146 raise JSONPointerIndexError ("index out of range" ) from None
143-
147+ # Handle non-standard index pointer.
148+ if isinstance (key , str ) and key .startswith ("#" ):
149+ _index = int (key [1 :])
150+ if _index >= len (obj ):
151+ raise JSONPointerIndexError (
152+ f"index out of range: { _index } "
153+ ) from err
154+ return _index
144155 # Try int index. Reject non-zero ints that start with a zero.
145156 if isinstance (key , str ):
146157 index = self ._index (key )
@@ -151,7 +162,7 @@ def _getitem(self, obj: Any, key: Any) -> Any: # noqa: PLR0912
151162 raise JSONPointerIndexError (
152163 f"index out of range: { key } "
153164 ) from index_err
154- raise JSONPointerTypeError (f"pointer type error: { key } : { err } " ) from err
165+ raise JSONPointerTypeError (f"{ key } : { err } " ) from err
155166 except IndexError as err :
156167 raise JSONPointerIndexError (f"index out of range: { key } " ) from err
157168
@@ -408,6 +419,189 @@ def join(self, *parts: str) -> JSONPointer:
408419 pointer = pointer / part
409420 return pointer
410421
422+ def to (
423+ self ,
424+ rel : Union [RelativeJSONPointer , str ],
425+ * ,
426+ unicode_escape : bool = True ,
427+ uri_decode : bool = False ,
428+ ) -> JSONPointer :
429+ """Return a new pointer relative to this pointer.
430+
431+ Args:
432+ rel: A `RelativeJSONPointer` or a string following "Relative JSON
433+ Pointer" syntax.
434+ unicode_escape: If `True`, UTF-16 escape sequences will be decoded
435+ before parsing the pointer.
436+ uri_decode: If `True`, the pointer will be unescaped using _urllib_
437+ before being parsed.
438+
439+ See https://www.ietf.org/id/draft-hha-relative-json-pointer-00.html
440+ """
441+ relative_pointer = (
442+ RelativeJSONPointer (
443+ rel , unicode_escape = unicode_escape , uri_decode = uri_decode
444+ )
445+ if isinstance (rel , str )
446+ else rel
447+ )
448+
449+ return relative_pointer .to (self )
450+
451+
452+ RE_RELATIVE_POINTER = re .compile (
453+ r"(?P<ORIGIN>\d+)(?P<INDEX_G>(?P<SIGN>[+\-])(?P<INDEX>\d))?(?P<POINTER>.*)" ,
454+ re .DOTALL ,
455+ )
456+
457+
458+ class RelativeJSONPointer :
459+ """A Relative JSON Pointer.
460+
461+ See https://www.ietf.org/id/draft-hha-relative-json-pointer-00.html
462+
463+ Args:
464+ rel: A string following Relative JSON Pointer syntax.
465+ unicode_escape: If `True`, UTF-16 escape sequences will be decoded
466+ before parsing the pointer.
467+ uri_decode: If `True`, the pointer will be unescaped using _urllib_
468+ before being parsed.
469+ """
470+
471+ __slots__ = ("origin" , "index" , "pointer" )
472+
473+ def __init__ (
474+ self ,
475+ rel : str ,
476+ * ,
477+ unicode_escape : bool = True ,
478+ uri_decode : bool = False ,
479+ ) -> None :
480+ self .origin , self .index , self .pointer = self ._parse (
481+ rel , unicode_escape = unicode_escape , uri_decode = uri_decode
482+ )
483+
484+ def __str__ (self ) -> str :
485+ sign = "+" if self .index > 0 else ""
486+ index = "" if self .index == 0 else f"{ sign } { self .index } "
487+ return f"{ self .origin } { index } { self .pointer } "
488+
489+ def __eq__ (self , __value : object ) -> bool :
490+ return isinstance (__value , RelativeJSONPointer ) and str (self ) == str (__value )
491+
492+ def _parse (
493+ self ,
494+ rel : str ,
495+ * ,
496+ unicode_escape : bool = True ,
497+ uri_decode : bool = False ,
498+ ) -> Tuple [int , int , Union [JSONPointer , str ]]:
499+ rel = rel .lstrip ()
500+ match = RE_RELATIVE_POINTER .match (rel )
501+ if not match :
502+ raise RelativeJSONPointerSyntaxError ("" , rel )
503+
504+ # Steps to move
505+ origin = self ._zero_or_positive (match .group ("ORIGIN" ), rel )
506+
507+ # Optional index manipulation
508+ if match .group ("INDEX_G" ):
509+ index = self ._zero_or_positive (match .group ("INDEX" ), rel )
510+ if index == 0 :
511+ raise RelativeJSONPointerSyntaxError ("index offset can't be zero" , rel )
512+ if match .group ("SIGN" ) == "-" :
513+ index = - index
514+ else :
515+ index = 0
516+
517+ # Pointer or '#'. Empty string is OK.
518+ _pointer = match .group ("POINTER" ).strip ()
519+ pointer = (
520+ JSONPointer (
521+ _pointer ,
522+ unicode_escape = unicode_escape ,
523+ uri_decode = uri_decode ,
524+ )
525+ if _pointer != "#"
526+ else _pointer
527+ )
528+
529+ return (origin , index , pointer )
530+
531+ def _zero_or_positive (self , s : str , rel : str ) -> int :
532+ # TODO: accept start and stop index for better error messages
533+ if s .startswith ("0" ) and len (s ) > 1 :
534+ raise RelativeJSONPointerSyntaxError ("unexpected leading zero" , rel )
535+ try :
536+ return int (s )
537+ except ValueError as err :
538+ raise RelativeJSONPointerSyntaxError (
539+ "expected positive int or zero" , rel
540+ ) from err
541+
542+ def _int_like (self , obj : Any ) -> bool :
543+ if isinstance (obj , int ):
544+ return True
545+ try :
546+ int (obj )
547+ except ValueError :
548+ return False
549+ return True
550+
551+ def to (
552+ self ,
553+ pointer : Union [JSONPointer , str ],
554+ * ,
555+ unicode_escape : bool = True ,
556+ uri_decode : bool = False ,
557+ ) -> JSONPointer :
558+ """Return a new JSONPointer relative to _pointer_.
559+
560+ Args:
561+ pointer: A `JSONPointer` instance or a string following JSON
562+ Pointer syntax.
563+ unicode_escape: If `True`, UTF-16 escape sequences will be decoded
564+ before parsing the pointer.
565+ uri_decode: If `True`, the pointer will be unescaped using _urllib_
566+ before being parsed.
567+ """
568+ _pointer = (
569+ JSONPointer (pointer , unicode_escape = unicode_escape , uri_decode = uri_decode )
570+ if isinstance (pointer , str )
571+ else pointer
572+ )
573+
574+ # Move to origin
575+ if self .origin > len (_pointer .parts ):
576+ raise RelativeJSONPointerIndexError (
577+ f"origin ({ self .origin } ) exceeds root ({ len (_pointer .parts )} )"
578+ )
579+
580+ if self .origin < 1 :
581+ parts = list (_pointer .parts )
582+ else :
583+ parts = list (_pointer .parts [: - self .origin ])
584+
585+ # Array index offset
586+ if self .index and parts and self ._int_like (parts [- 1 ]):
587+ new_index = int (parts [- 1 ]) + self .index
588+ if new_index < 0 :
589+ raise RelativeJSONPointerIndexError (
590+ f"index offset out of range { new_index } "
591+ )
592+ parts [- 1 ] = int (parts [- 1 ]) + self .index
593+
594+ # Pointer or index/property
595+ if isinstance (self .pointer , JSONPointer ):
596+ parts .extend (self .pointer .parts )
597+ else :
598+ assert self .pointer == "#"
599+ parts [- 1 ] = f"#{ parts [- 1 ]} "
600+
601+ return JSONPointer .from_parts (
602+ parts , unicode_escape = unicode_escape , uri_decode = uri_decode
603+ )
604+
411605
412606def resolve (
413607 pointer : Union [str , Iterable [Union [str , int ]]],
0 commit comments