Hello community, here is the log from the commit of package python-jmespath for openSUSE:Factory checked in at 2017-04-20 20:49:26 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-jmespath (Old) and /work/SRC/openSUSE:Factory/.python-jmespath.new (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-jmespath" Thu Apr 20 20:49:26 2017 rev:10 rq:483389 version:0.9.2 Changes: -------- --- /work/SRC/openSUSE:Factory/python-jmespath/python-jmespath.changes 2016-08-17 12:05:28.000000000 +0200 +++ /work/SRC/openSUSE:Factory/.python-jmespath.new/python-jmespath.changes 2017-04-20 20:49:27.780580041 +0200 @@ -1,0 +2,23 @@ +Wed Mar 29 14:15:59 UTC 2017 - [email protected] + +- properly Requires(postun) for %postun section + +------------------------------------------------------------------- +Wed Mar 29 08:16:19 UTC 2017 - [email protected] + +- Only BuildRequire unittest2 for python2 + +------------------------------------------------------------------- +Thu Mar 16 18:43:59 UTC 2017 - [email protected] + +- Switch to single-spec build +- Update to version 0.9.2 + + Fix regression when using ordering comparators on strings (issue 124) +- From 0.9.1 + + Raise LexerError on invalid numbers (issue 98) + + Add support for custom functions (#100) (issue 100) + + Fix ZeroDivisionError for built-in function avg() on empty lists (#115) + (issue 115) + + Properly handle non numerical ordering operators (#117) (issue 117) + +------------------------------------------------------------------- @@ -11,3 +34,2 @@ - * Add support for `JEP 9 <http://jmespath.org/proposals/improved-filters.html>`__, - which introduces "and" expressions, "unary" expressions, "not" expressions, - and "paren" expressions + * Add support for JEP 9 which introduces "and" expressions, "unary" + expressions, "not" expressions, and "paren" expressions @@ -15,3 +37 @@ - (`issue 90 <https://github.com/jmespath/jmespath.py/issues/90>`__, - `issue 88 <https://github.com/jmespath/jmespath.py/issues/88>`__, - `issue 82 <https://github.com/jmespath/jmespath.py/issues/82>`__) + (issue 90, issue 88, issue 82) @@ -19,3 +39,3 @@ - * Improve lexing performance (`issue 84 <https://github.com/jmespath/jmespath.py/pull/84>`__) - * Fix parsing error for multiselect lists (`issue 86 <https://github.com/jmespath/jmespath.py/issues/86>`__) - * Fix issue with escaping single quotes in literal strings (`issue 85 <https://github.com/jmespath/jmespath.py/issues/85>`__) + * Improve lexing performance (issue 84) + * Fix parsing error for multiselect lists (issue 86) + * Fix issue with escaping single quotes in literal strings (issue 85) @@ -23,2 +43,2 @@ - ordered dictionaries (`issue 94 <https://github.com/jmespath/jmespath.py/pull/94>`__) - * Add map() function (`issue 95 <https://github.com/jmespath/jmespath.py/pull/95>`__) + ordered dictionaries (issue 94) + * Add map() function (issue 95) Old: ---- jmespath-0.9.0.tar.gz New: ---- jmespath-0.9.2.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-jmespath.spec ++++++ --- /var/tmp/diff_new_pack.fNcpWs/_old 2017-04-20 20:49:28.308505395 +0200 +++ /var/tmp/diff_new_pack.fNcpWs/_new 2017-04-20 20:49:28.312504829 +0200 @@ -1,7 +1,7 @@ # # spec file for package python-jmespath # -# Copyright (c) 2016 SUSE LINUX GmbH, Nuernberg, Germany. +# Copyright (c) 2017 SUSE LINUX GmbH, Nuernberg, Germany. # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -16,37 +16,37 @@ # -%define baseName jmespath - +%{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-jmespath -Version: 0.9.0 +Version: 0.9.2 Release: 0 Summary: Extract elements from JSON document License: MIT Group: Development/Languages/Python Url: https://github.com/boto/jmespath -Source0: https://pypi.python.org/packages/source/j/%{baseName}/%{baseName}-%{version}.tar.gz +Source: https://pypi.io/packages/source/j/jmespath/jmespath-%{version}.tar.gz +BuildRequires: %{python_module devel} +BuildRequires: %{python_module ply >= 3.4} +BuildRequires: %{python_module setuptools} +BuildRequires: %{python_module simplejson} +BuildRequires: python-rpm-macros +# Testing +BuildRequires: %{python_module nose} +%ifpython2 +BuildRequires: python-unittest2 +%endif Requires: python-base Requires: python-ply >= 3.4 +Requires: python-simplejson Requires(post): update-alternatives -Requires(preun): update-alternatives -BuildRequires: python-devel -BuildRequires: python-ply >= 3.4 -BuildRequires: python-setuptools -# For testing -BuildRequires: python-nose -%if 0%{?suse_version} && 0%{?suse_version} <= 1110 -BuildRequires: python-ordereddict -%endif -BuildRequires: python-simplejson -BuildRequires: python-unittest2 -BuildRoot: %{_tmppath}/%{name}-%{version}-build -%if 0%{?suse_version} && 0%{?suse_version} <= 1110 -%{!?python_sitelib: %global python_sitelib %(python -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")} -%else +Requires(postun): update-alternatives + +%if 0%{?suse_version} > 1110 BuildArch: noarch %endif +%python_subpackages + %description JMESPath (pronounced "jaymz path") allows you to declaratively specify how to extract elements from a JSON document. @@ -79,37 +79,35 @@ The expression: foo.*.name will return ["one", "two"]. %prep -%setup -q -n %{baseName}-%{version} +%setup -q -n jmespath-%{version} %build -python setup.py build +%python_build +#pushd build/scripts-%{$python_bin_suffix} +#mv jp.py jp-%{$python_bin_suffix} +#popd +#pushd build/scripts-%{py_ver} +#mv jp.py jp-%{py_ver} +#popd %install -python setup.py install --prefix=%{_prefix} --root=%{buildroot} --install-scripts=%{_bindir} - -# Prepare for update-alternatives usage -mkdir -p %{buildroot}%{_sysconfdir}/alternatives -mv %{buildroot}%{_bindir}/jp.py %{buildroot}%{_bindir}/jp-%{py_ver} -ln -s -f %{_sysconfdir}/alternatives/jp %{buildroot}%{_bindir}/jp +%python_install +mv %{buildroot}%{_bindir}/jp.py %{buildroot}%{_bindir}/jp +%python_clone -a %{buildroot}%{_bindir}/jp %check -nosetests tests +%python_expand nosetests-%{$python_bin_suffix} tests %post -%_sbindir/update-alternatives --install %{_bindir}/jp jp %{_bindir}/jp-%{py_ver} 30 +%python_install_alternative jp -%preun -if [ $1 -eq 0 ] ; then - %_sbindir/update-alternatives --remove jp %{_bindir}/jp-%{py_ver} -fi +%postun +%python_uninstall_alternative jp -%files +%files %{python_files} %defattr(-,root,root,-) %doc LICENSE.txt README.rst -%{_bindir}/jp -%{_bindir}/jp-%{py_ver} -%ghost %{_sysconfdir}/alternatives/jp -%{python_sitelib}/jmespath/ -%{python_sitelib}/%{baseName}-%{version}-py%{py_ver}.egg-info/ +%{python_sitelib}/* +%python_alternative %{_bindir}/jp %changelog ++++++ jmespath-0.9.0.tar.gz -> jmespath-0.9.2.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jmespath-0.9.0/PKG-INFO new/jmespath-0.9.2/PKG-INFO --- old/jmespath-0.9.0/PKG-INFO 2015-10-01 06:12:12.000000000 +0200 +++ new/jmespath-0.9.2/PKG-INFO 2017-03-11 00:52:50.000000000 +0100 @@ -1,11 +1,11 @@ Metadata-Version: 1.1 Name: jmespath -Version: 0.9.0 +Version: 0.9.2 Summary: JSON Matching Expressions Home-page: https://github.com/jmespath/jmespath.py Author: James Saryerwinnie Author-email: [email protected] -License: UNKNOWN +License: MIT Description: JMESPath ======== @@ -18,6 +18,10 @@ :target: http://travis-ci.org/jmespath/jmespath.py + .. image:: https://codecov.io/github/jmespath/jmespath.py/coverage.svg?branch=develop + :target: https://codecov.io/github/jmespath/jmespath.py?branch=develop + + JMESPath (pronounced "james path") allows you to declaratively specify how to extract elements from a JSON document. @@ -56,7 +60,9 @@ The ``jmespath.py`` library has two functions that operate on python data structures. You can use ``search`` - and give it the jmespath expression and the data:: + and give it the jmespath expression and the data: + + .. code:: python >>> import jmespath >>> path = jmespath.search('foo.bar', {'foo': {'bar': 'baz'}}) @@ -64,7 +70,9 @@ Similar to the ``re`` module, you can use the ``compile`` function to compile the JMESPath expression and use this parsed expression - to perform repeated searches:: + to perform repeated searches: + + .. code:: python >>> import jmespath >>> expression = jmespath.compile('foo.bar') @@ -83,7 +91,9 @@ You can provide an instance of ``jmespath.Options`` to control how a JMESPath expression is evaluated. The most common scenario for using an ``Options`` instance is if you want to have ordered output - of your dict keys. To do this you can use either of these options:: + of your dict keys. To do this you can use either of these options: + + .. code:: python >>> import jmespath >>> jmespath.search('{a: a, b: b}, @@ -98,6 +108,85 @@ ... jmespath.Options(dict_cls=collections.OrderedDict)) + Custom Functions + ~~~~~~~~~~~~~~~~ + + The JMESPath language has numerous + `built-in functions + <http://jmespath.org/specification.html#built-in-functions>`__, but it is + also possible to add your own custom functions. Keep in mind that + custom function support in jmespath.py is experimental and the API may + change based on feedback. + + **If you have a custom function that you've found useful, consider submitting + it to jmespath.site and propose that it be added to the JMESPath language.** + You can submit proposals + `here <https://github.com/jmespath/jmespath.site/issues>`__. + + To create custom functions: + + * Create a subclass of ``jmespath.functions.Functions``. + * Create a method with the name ``_func_<your function name>``. + * Apply the ``jmespath.functions.signature`` decorator that indicates + the expected types of the function arguments. + * Provide an instance of your subclass in a ``jmespath.Options`` object. + + Below are a few examples: + + .. code:: python + + import jmespath + from jmespath import functions + + # 1. Create a subclass of functions.Functions. + # The function.Functions base class has logic + # that introspects all of its methods and automatically + # registers your custom functions in its function table. + class CustomFunctions(functions.Functions): + + # 2 and 3. Create a function that starts with _func_ + # and decorate it with @signature which indicates its + # expected types. + # In this example, we're creating a jmespath function + # called "unique_letters" that accepts a single argument + # with an expected type "string". + @functions.signature({'types': ['string']}) + def _func_unique_letters(self, s): + # Given a string s, return a sorted + # string of unique letters: 'ccbbadd' -> 'abcd' + return ''.join(sorted(set(s))) + + # Here's another example. This is creating + # a jmespath function called "my_add" that expects + # two arguments, both of which should be of type number. + @functions.signature({'types': ['number']}, {'types': ['number']}) + def _func_my_add(self, x, y): + return x + y + + # 4. Provide an instance of your subclass in a Options object. + options = jmespath.Options(custom_functions=CustomFunctions()) + + # Provide this value to jmespath.search: + # This will print 3 + print( + jmespath.search( + 'my_add(`1`, `2`)', {}, options=options) + ) + + # This will print "abcd" + print( + jmespath.search( + 'foo.bar | unique_letters(@)', + {'foo': {'bar': 'ccbbadd'}}, + options=options) + ) + + Again, if you come up with useful functions that you think make + sense in the JMESPath language (and make sense to implement in all + JMESPath libraries, not just python), please let us know at + `jmespath.site <https://github.com/jmespath/jmespath.site/issues>`__. + + Specification ============= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jmespath-0.9.0/README.rst new/jmespath-0.9.2/README.rst --- old/jmespath-0.9.0/README.rst 2015-09-30 06:37:03.000000000 +0200 +++ new/jmespath-0.9.2/README.rst 2017-03-10 01:49:47.000000000 +0100 @@ -10,6 +10,10 @@ :target: http://travis-ci.org/jmespath/jmespath.py +.. image:: https://codecov.io/github/jmespath/jmespath.py/coverage.svg?branch=develop + :target: https://codecov.io/github/jmespath/jmespath.py?branch=develop + + JMESPath (pronounced "james path") allows you to declaratively specify how to extract elements from a JSON document. @@ -48,7 +52,9 @@ The ``jmespath.py`` library has two functions that operate on python data structures. You can use ``search`` -and give it the jmespath expression and the data:: +and give it the jmespath expression and the data: + +.. code:: python >>> import jmespath >>> path = jmespath.search('foo.bar', {'foo': {'bar': 'baz'}}) @@ -56,7 +62,9 @@ Similar to the ``re`` module, you can use the ``compile`` function to compile the JMESPath expression and use this parsed expression -to perform repeated searches:: +to perform repeated searches: + +.. code:: python >>> import jmespath >>> expression = jmespath.compile('foo.bar') @@ -75,7 +83,9 @@ You can provide an instance of ``jmespath.Options`` to control how a JMESPath expression is evaluated. The most common scenario for using an ``Options`` instance is if you want to have ordered output -of your dict keys. To do this you can use either of these options:: +of your dict keys. To do this you can use either of these options: + +.. code:: python >>> import jmespath >>> jmespath.search('{a: a, b: b}, @@ -90,6 +100,85 @@ ... jmespath.Options(dict_cls=collections.OrderedDict)) +Custom Functions +~~~~~~~~~~~~~~~~ + +The JMESPath language has numerous +`built-in functions +<http://jmespath.org/specification.html#built-in-functions>`__, but it is +also possible to add your own custom functions. Keep in mind that +custom function support in jmespath.py is experimental and the API may +change based on feedback. + +**If you have a custom function that you've found useful, consider submitting +it to jmespath.site and propose that it be added to the JMESPath language.** +You can submit proposals +`here <https://github.com/jmespath/jmespath.site/issues>`__. + +To create custom functions: + +* Create a subclass of ``jmespath.functions.Functions``. +* Create a method with the name ``_func_<your function name>``. +* Apply the ``jmespath.functions.signature`` decorator that indicates + the expected types of the function arguments. +* Provide an instance of your subclass in a ``jmespath.Options`` object. + +Below are a few examples: + +.. code:: python + + import jmespath + from jmespath import functions + + # 1. Create a subclass of functions.Functions. + # The function.Functions base class has logic + # that introspects all of its methods and automatically + # registers your custom functions in its function table. + class CustomFunctions(functions.Functions): + + # 2 and 3. Create a function that starts with _func_ + # and decorate it with @signature which indicates its + # expected types. + # In this example, we're creating a jmespath function + # called "unique_letters" that accepts a single argument + # with an expected type "string". + @functions.signature({'types': ['string']}) + def _func_unique_letters(self, s): + # Given a string s, return a sorted + # string of unique letters: 'ccbbadd' -> 'abcd' + return ''.join(sorted(set(s))) + + # Here's another example. This is creating + # a jmespath function called "my_add" that expects + # two arguments, both of which should be of type number. + @functions.signature({'types': ['number']}, {'types': ['number']}) + def _func_my_add(self, x, y): + return x + y + + # 4. Provide an instance of your subclass in a Options object. + options = jmespath.Options(custom_functions=CustomFunctions()) + + # Provide this value to jmespath.search: + # This will print 3 + print( + jmespath.search( + 'my_add(`1`, `2`)', {}, options=options) + ) + + # This will print "abcd" + print( + jmespath.search( + 'foo.bar | unique_letters(@)', + {'foo': {'bar': 'ccbbadd'}}, + options=options) + ) + +Again, if you come up with useful functions that you think make +sense in the JMESPath language (and make sense to implement in all +JMESPath libraries, not just python), please let us know at +`jmespath.site <https://github.com/jmespath/jmespath.site/issues>`__. + + Specification ============= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jmespath-0.9.0/jmespath/__init__.py new/jmespath-0.9.2/jmespath/__init__.py --- old/jmespath-0.9.0/jmespath/__init__.py 2015-10-01 06:11:26.000000000 +0200 +++ new/jmespath-0.9.2/jmespath/__init__.py 2017-03-11 00:45:48.000000000 +0100 @@ -1,7 +1,7 @@ from jmespath import parser from jmespath.visitor import Options -__version__ = '0.9.0' +__version__ = '0.9.2' def compile(expression): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jmespath-0.9.0/jmespath/compat.py new/jmespath-0.9.2/jmespath/compat.py --- old/jmespath-0.9.0/jmespath/compat.py 2015-09-23 07:04:51.000000000 +0200 +++ new/jmespath-0.9.2/jmespath/compat.py 2017-03-10 01:49:47.000000000 +0100 @@ -3,6 +3,15 @@ PY2 = sys.version_info[0] == 2 + +def with_metaclass(meta, *bases): + # Taken from flask/six. + class metaclass(meta): + def __new__(cls, name, this_bases, d): + return meta(name, bases, d) + return type.__new__(metaclass, 'temporary_class', (), {}) + + if PY2: text_type = unicode string_type = basestring diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jmespath-0.9.0/jmespath/functions.py new/jmespath-0.9.2/jmespath/functions.py --- old/jmespath-0.9.0/jmespath/functions.py 2015-09-30 06:37:03.000000000 +0200 +++ new/jmespath-0.9.2/jmespath/functions.py 2017-03-10 01:49:47.000000000 +0100 @@ -1,10 +1,9 @@ import math import json -import weakref from jmespath import exceptions from jmespath.compat import string_type as STRING_TYPE -from jmespath.compat import get_methods +from jmespath.compat import get_methods, with_metaclass # python types -> jmespath types @@ -35,48 +34,39 @@ } -def populate_function_table(cls): - func_table = cls.FUNCTION_TABLE - for name, method in get_methods(cls): - signature = getattr(method, 'signature', None) - if signature is not None: - func_table[name[6:]] = {"function": method, - "signature": signature} - return cls - - -def builtin_function(*arguments): - def _record_arity(func): +def signature(*arguments): + def _record_signature(func): func.signature = arguments return func - return _record_arity + return _record_signature -@populate_function_table -class RuntimeFunctions(object): - # The built in functions are automatically populated in the FUNCTION_TABLE - # using the @builtin_function decorator on methods defined in this class. +class FunctionRegistry(type): + def __init__(cls, name, bases, attrs): + cls._populate_function_table() + super(FunctionRegistry, cls).__init__(name, bases, attrs) + + def _populate_function_table(cls): + function_table = getattr(cls, 'FUNCTION_TABLE', {}) + # Any method with a @signature decorator that also + # starts with "_func_" is registered as a function. + # _func_max_by -> max_by function. + for name, method in get_methods(cls): + if not name.startswith('_func_'): + continue + signature = getattr(method, 'signature', None) + if signature is not None: + function_table[name[6:]] = { + 'function': method, + 'signature': signature, + } + cls.FUNCTION_TABLE = function_table - FUNCTION_TABLE = { - } - def __init__(self): - self._interpreter = None - - @property - def interpreter(self): - if self._interpreter is None: - return None - else: - return self._interpreter() +class Functions(with_metaclass(FunctionRegistry, object)): - @interpreter.setter - def interpreter(self, value): - # A weakref is used because we have - # a cyclic reference and we want to allow - # for the memory to be properly freed when - # the objects are no longer needed. - self._interpreter = weakref.ref(value) + FUNCTION_TABLE = { + } def call_function(self, function_name, resolved_args): try: @@ -170,28 +160,31 @@ raise exceptions.JMESPathTypeError( function_name, element, actual_typename, types) - @builtin_function({'types': ['number']}) + @signature({'types': ['number']}) def _func_abs(self, arg): return abs(arg) - @builtin_function({'types': ['array-number']}) + @signature({'types': ['array-number']}) def _func_avg(self, arg): - return sum(arg) / float(len(arg)) + if arg: + return sum(arg) / float(len(arg)) + else: + return None - @builtin_function({'types': [], 'variadic': True}) + @signature({'types': [], 'variadic': True}) def _func_not_null(self, *arguments): for argument in arguments: if argument is not None: return argument - @builtin_function({'types': []}) + @signature({'types': []}) def _func_to_array(self, arg): if isinstance(arg, list): return arg else: return [arg] - @builtin_function({'types': []}) + @signature({'types': []}) def _func_to_string(self, arg): if isinstance(arg, STRING_TYPE): return arg @@ -199,7 +192,7 @@ return json.dumps(arg, separators=(',', ':'), default=str) - @builtin_function({'types': []}) + @signature({'types': []}) def _func_to_number(self, arg): if isinstance(arg, (list, dict, bool)): return None @@ -216,88 +209,88 @@ except ValueError: return None - @builtin_function({'types': ['array', 'string']}, {'types': []}) + @signature({'types': ['array', 'string']}, {'types': []}) def _func_contains(self, subject, search): return search in subject - @builtin_function({'types': ['string', 'array', 'object']}) + @signature({'types': ['string', 'array', 'object']}) def _func_length(self, arg): return len(arg) - @builtin_function({'types': ['string']}, {'types': ['string']}) + @signature({'types': ['string']}, {'types': ['string']}) def _func_ends_with(self, search, suffix): return search.endswith(suffix) - @builtin_function({'types': ['string']}, {'types': ['string']}) + @signature({'types': ['string']}, {'types': ['string']}) def _func_starts_with(self, search, suffix): return search.startswith(suffix) - @builtin_function({'types': ['array', 'string']}) + @signature({'types': ['array', 'string']}) def _func_reverse(self, arg): if isinstance(arg, STRING_TYPE): return arg[::-1] else: return list(reversed(arg)) - @builtin_function({"types": ['number']}) + @signature({"types": ['number']}) def _func_ceil(self, arg): return math.ceil(arg) - @builtin_function({"types": ['number']}) + @signature({"types": ['number']}) def _func_floor(self, arg): return math.floor(arg) - @builtin_function({"types": ['string']}, {"types": ['array-string']}) + @signature({"types": ['string']}, {"types": ['array-string']}) def _func_join(self, separator, array): return separator.join(array) - @builtin_function({'types': ['expref']}, {'types': ['array']}) + @signature({'types': ['expref']}, {'types': ['array']}) def _func_map(self, expref, arg): result = [] for element in arg: - result.append(self.interpreter.visit(expref.expression, element)) + result.append(expref.visit(expref.expression, element)) return result - @builtin_function({"types": ['array-number', 'array-string']}) + @signature({"types": ['array-number', 'array-string']}) def _func_max(self, arg): if arg: return max(arg) else: return None - @builtin_function({"types": ["object"], "variadic": True}) + @signature({"types": ["object"], "variadic": True}) def _func_merge(self, *arguments): merged = {} for arg in arguments: merged.update(arg) return merged - @builtin_function({"types": ['array-number', 'array-string']}) + @signature({"types": ['array-number', 'array-string']}) def _func_min(self, arg): if arg: return min(arg) else: return None - @builtin_function({"types": ['array-string', 'array-number']}) + @signature({"types": ['array-string', 'array-number']}) def _func_sort(self, arg): return list(sorted(arg)) - @builtin_function({"types": ['array-number']}) + @signature({"types": ['array-number']}) def _func_sum(self, arg): return sum(arg) - @builtin_function({"types": ['object']}) + @signature({"types": ['object']}) def _func_keys(self, arg): # To be consistent with .values() # should we also return the indices of a list? return list(arg.keys()) - @builtin_function({"types": ['object']}) + @signature({"types": ['object']}) def _func_values(self, arg): return list(arg.values()) - @builtin_function({'types': []}) + @signature({'types': []}) def _func_type(self, arg): if isinstance(arg, STRING_TYPE): return "string" @@ -312,7 +305,7 @@ elif arg is None: return "null" - @builtin_function({'types': ['array']}, {'types': ['expref']}) + @signature({'types': ['array']}, {'types': ['expref']}) def _func_sort_by(self, array, expref): if not array: return array @@ -323,34 +316,32 @@ # that validates that type, which requires that remaining array # elements resolve to the same type as the first element. required_type = self._convert_to_jmespath_type( - type(self.interpreter.visit(expref.expression, array[0])).__name__) + type(expref.visit(expref.expression, array[0])).__name__) if required_type not in ['number', 'string']: raise exceptions.JMESPathTypeError( 'sort_by', array[0], required_type, ['string', 'number']) - keyfunc = self._create_key_func(expref.expression, + keyfunc = self._create_key_func(expref, [required_type], 'sort_by') return list(sorted(array, key=keyfunc)) - @builtin_function({'types': ['array']}, {'types': ['expref']}) + @signature({'types': ['array']}, {'types': ['expref']}) def _func_min_by(self, array, expref): - keyfunc = self._create_key_func(expref.expression, + keyfunc = self._create_key_func(expref, ['number', 'string'], 'min_by') return min(array, key=keyfunc) - @builtin_function({'types': ['array']}, {'types': ['expref']}) + @signature({'types': ['array']}, {'types': ['expref']}) def _func_max_by(self, array, expref): - keyfunc = self._create_key_func(expref.expression, + keyfunc = self._create_key_func(expref, ['number', 'string'], 'min_by') return max(array, key=keyfunc) - def _create_key_func(self, expr_node, allowed_types, function_name): - interpreter = self.interpreter - + def _create_key_func(self, expref, allowed_types, function_name): def keyfunc(x): - result = interpreter.visit(expr_node, x) + result = expref.visit(expref.expression, x) actual_typename = type(result).__name__ jmespath_type = self._convert_to_jmespath_type(actual_typename) # allowed_types is in term of jmespath types, not python types. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jmespath-0.9.0/jmespath/lexer.py new/jmespath-0.9.2/jmespath/lexer.py --- old/jmespath-0.9.0/jmespath/lexer.py 2015-10-01 06:11:19.000000000 +0200 +++ new/jmespath-0.9.2/jmespath/lexer.py 2017-03-10 01:49:47.000000000 +0100 @@ -8,7 +8,6 @@ class Lexer(object): START_IDENTIFIER = set(string.ascii_letters + '_') VALID_IDENTIFIER = set(string.ascii_letters + string.digits + '_') - START_NUMBER = set(string.digits + '-') VALID_NUMBER = set(string.digits) WHITESPACE = set(" \t\n\r") SIMPLE_TOKENS = { @@ -63,13 +62,22 @@ yield self._match_or_else('&', 'and', 'expref') elif self._current == '`': yield self._consume_literal() - elif self._current in self.START_NUMBER: + elif self._current in self.VALID_NUMBER: start = self._position - buff = self._current - while self._next() in self.VALID_NUMBER: - buff += self._current + buff = self._consume_number() yield {'type': 'number', 'value': int(buff), 'start': start, 'end': start + len(buff)} + elif self._current == '-': + # Negative number. + start = self._position + buff = self._consume_number() + if len(buff) > 1: + yield {'type': 'number', 'value': int(buff), + 'start': start, 'end': start + len(buff)} + else: + raise LexerError(lexer_position=start, + lexer_value=buff, + message="Unknown token '%s'" % buff) elif self._current == '"': yield self._consume_quoted_identifier() elif self._current == '<': @@ -79,7 +87,15 @@ elif self._current == '!': yield self._match_or_else('=', 'ne', 'not') elif self._current == '=': - yield self._match_or_else('=', 'eq', 'unknown') + if self._next() == '=': + yield {'type': 'eq', 'value': '==', + 'start': self._position - 1, 'end': self._position} + self._next() + else: + raise LexerError( + lexer_position=self._position - 1, + lexer_value='=', + message="Unknown token =") else: raise LexerError(lexer_position=self._position, lexer_value=self._current, @@ -87,6 +103,13 @@ yield {'type': 'eof', 'value': '', 'start': self._length, 'end': self._length} + def _consume_number(self): + start = self._position + buff = self._current + while self._next() in self.VALID_NUMBER: + buff += self._current + return buff + def _initialize_for_expression(self, expression): if not expression: raise EmptyExpressionError() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jmespath-0.9.0/jmespath/parser.py new/jmespath-0.9.2/jmespath/parser.py --- old/jmespath-0.9.0/jmespath/parser.py 2015-10-01 06:11:19.000000000 +0200 +++ new/jmespath-0.9.2/jmespath/parser.py 2017-03-10 01:49:47.000000000 +0100 @@ -39,6 +39,7 @@ 'eof': 0, 'unquoted_identifier': 0, 'quoted_identifier': 0, + 'literal': 0, 'rbracket': 0, 'rparen': 0, 'comma': 0, @@ -133,9 +134,6 @@ current_token = self._current_token() return left - def _token_nud_string_literal(self, token): - return ast.literal(token['value']) - def _token_nud_literal(self, token): return ast.literal(token['value']) @@ -224,17 +222,16 @@ while not current_token == 'rbracket' and index < 3: if current_token == 'colon': index += 1 + if index == 3: + self._raise_parse_error_for_token( + self._lookahead_token(0), 'syntax error') self._advance() elif current_token == 'number': parts[index] = self._lookahead_token(0)['value'] self._advance() else: - t = self._lookahead_token(0) - lex_position = t['start'] - actual_value = t['value'] - actual_type = t['type'] - raise exceptions.ParseError(lex_position, actual_value, - actual_type, 'syntax error') + self._raise_parse_error_for_token( + self._lookahead_token(0), 'syntax error') current_token = self._current_token() self._match('rbracket') return ast.slice(*parts) @@ -274,6 +271,14 @@ return ast.and_expression(left, right) def _token_led_lparen(self, left): + if left['type'] != 'field': + # 0 - first func arg or closing paren. + # -1 - '(' token + # -2 - invalid function "name". + prev_t = self._lookahead_token(-2) + raise exceptions.ParseError( + prev_t['start'], prev_t['value'], prev_t['type'], + "Invalid function name '%s'" % prev_t['value']) name = left['value'] args = [] while not self._current_token() == 'rparen': @@ -396,12 +401,8 @@ self._match('dot') right = self._parse_dot_rhs(binding_power) else: - t = self._lookahead_token(0) - lex_position = t['start'] - actual_value = t['value'] - actual_type = t['type'] - raise exceptions.ParseError(lex_position, actual_value, - actual_type, 'syntax error') + self._raise_parse_error_for_token(self._lookahead_token(0), + 'syntax error') return right def _parse_dot_rhs(self, binding_power): @@ -427,34 +428,19 @@ t = self._lookahead_token(0) allowed = ['quoted_identifier', 'unquoted_identifier', 'lbracket', 'lbrace'] - lex_position = t['start'] - actual_value = t['value'] - actual_type = t['type'] - raise exceptions.ParseError( - lex_position, actual_value, actual_type, - "Expecting: %s, got: %s" % (allowed, - actual_type)) - - def _assert_not_token(self, *token_types): - if self._current_token() in token_types: - t = self._lookahead_token(0) - lex_position = t['start'] - actual_value = t['value'] - actual_type = t['type'] - raise exceptions.ParseError( - lex_position, actual_value, actual_type, - "Token %s not allowed to be: %s" % (actual_type, token_types)) + msg = ( + "Expecting: %s, got: %s" % (allowed, t['type']) + ) + self._raise_parse_error_for_token(t, msg) def _error_nud_token(self, token): if token['type'] == 'eof': raise exceptions.IncompleteExpressionError( token['start'], token['value'], token['type']) - raise exceptions.ParseError(token['start'], token['value'], - token['type'], 'Invalid token.') + self._raise_parse_error_for_token(token, 'invalid token') def _error_led_token(self, token): - raise exceptions.ParseError(token['start'], token['value'], - token['type'], 'Invalid token') + self._raise_parse_error_for_token(token, 'invalid token') def _match(self, token_type=None): # inline'd self._current_token() @@ -462,33 +448,13 @@ # inline'd self._advance() self._advance() else: - t = self._lookahead_token(0) - lex_position = t['start'] - actual_value = t['value'] - actual_type = t['type'] - if actual_type == 'eof': - raise exceptions.IncompleteExpressionError( - lex_position, actual_value, actual_type) - else: - message = 'Expecting: %s, got: %s' % (token_type, - actual_type) - raise exceptions.ParseError( - lex_position, actual_value, actual_type, message) + self._raise_parse_error_maybe_eof( + token_type, self._lookahead_token(0)) def _match_multiple_tokens(self, token_types): if self._current_token() not in token_types: - t = self._lookahead_token(0) - lex_position = t['start'] - actual_value = t['value'] - actual_type = t['type'] - if actual_type == 'eof': - raise exceptions.IncompleteExpressionError( - lex_position, actual_value, actual_type) - else: - message = 'Expecting: %s, got: %s' % (token_types, - actual_type) - raise exceptions.ParseError( - lex_position, actual_value, actual_type, message) + self._raise_parse_error_maybe_eof( + token_types, self._lookahead_token(0)) self._advance() def _advance(self): @@ -503,6 +469,25 @@ def _lookahead_token(self, number): return self._tokens[self._index + number] + def _raise_parse_error_for_token(self, token, reason): + lex_position = token['start'] + actual_value = token['value'] + actual_type = token['type'] + raise exceptions.ParseError(lex_position, actual_value, + actual_type, reason) + + def _raise_parse_error_maybe_eof(self, expected_type, token): + lex_position = token['start'] + actual_value = token['value'] + actual_type = token['type'] + if actual_type == 'eof': + raise exceptions.IncompleteExpressionError( + lex_position, actual_value, actual_type) + message = 'Expecting: %s, got: %s' % (expected_type, + actual_type) + raise exceptions.ParseError( + lex_position, actual_value, actual_type, message) + def _free_cache_entries(self): for key in random.sample(self._CACHE.keys(), int(self._MAX_SIZE / 2)): del self._CACHE[key] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jmespath-0.9.0/jmespath/visitor.py new/jmespath-0.9.2/jmespath/visitor.py --- old/jmespath-0.9.0/jmespath/visitor.py 2015-10-01 06:11:19.000000000 +0200 +++ new/jmespath-0.9.2/jmespath/visitor.py 2017-03-11 00:45:41.000000000 +0100 @@ -1,6 +1,7 @@ import operator from jmespath import functions +from jmespath.compat import string_type def _equals(x, y): @@ -33,9 +34,30 @@ return x is True or x is False +def _is_comparable(x): + # The spec doesn't officially support string types yet, + # but enough people are relying on this behavior that + # it's been added back. This should eventually become + # part of the official spec. + return _is_actual_number(x) or isinstance(x, string_type) + + +def _is_actual_number(x): + # We need to handle python's quirkiness with booleans, + # specifically: + # + # >>> isinstance(False, int) + # True + # >>> isinstance(True, int) + # True + if x is True or x is False: + return False + return isinstance(x, (float, int)) + + class Options(object): """Options to control how a JMESPath function is evaluated.""" - def __init__(self, dict_cls): + def __init__(self, dict_cls=None, custom_functions=None): #: The class to use when creating a dict. The interpreter # may create dictionaries during the evalution of a JMESPath # expression. For example, a multi-select hash will @@ -45,11 +67,16 @@ # want to set a collections.OrderedDict so that you can # have predictible key ordering. self.dict_cls = dict_cls + self.custom_functions = custom_functions class _Expression(object): - def __init__(self, expression): + def __init__(self, expression, interpreter): self.expression = expression + self.interpreter = interpreter + + def visit(self, node, *args, **kwargs): + return self.interpreter.visit(node, *args, **kwargs) class Visitor(object): @@ -71,27 +98,28 @@ class TreeInterpreter(Visitor): COMPARATOR_FUNC = { - 'le': operator.le, + 'eq': _equals, 'ne': lambda x, y: not _equals(x, y), 'lt': operator.lt, - 'lte': operator.le, - 'eq': _equals, 'gt': operator.gt, + 'lte': operator.le, 'gte': operator.ge } + _EQUALITY_OPS = ['eq', 'ne'] MAP_TYPE = dict def __init__(self, options=None): super(TreeInterpreter, self).__init__() - self._options = options self._dict_cls = self.MAP_TYPE - if options is not None and options.dict_cls is not None: + if options is None: + options = Options() + self._options = options + if options.dict_cls is not None: self._dict_cls = self._options.dict_cls - self._functions = functions.RuntimeFunctions() - # Note that .interpreter is a property that uses - # a weakref so that the cyclic reference can be - # properly freed. - self._functions.interpreter = self + if options.custom_functions is not None: + self._functions = self._options.custom_functions + else: + self._functions = functions.Functions() def default_visit(self, node, *args, **kwargs): raise NotImplementedError(node['type']) @@ -109,17 +137,30 @@ return None def visit_comparator(self, node, value): + # Common case: comparator is == or != comparator_func = self.COMPARATOR_FUNC[node['value']] - return comparator_func( - self.visit(node['children'][0], value), - self.visit(node['children'][1], value) - ) + if node['value'] in self._EQUALITY_OPS: + return comparator_func( + self.visit(node['children'][0], value), + self.visit(node['children'][1], value) + ) + else: + # Ordering operators are only valid for numbers. + # Evaluating any other type with a comparison operator + # will yield a None value. + left = self.visit(node['children'][0], value) + right = self.visit(node['children'][1], value) + num_types = (int, float) + if not (_is_comparable(left) and + _is_comparable(right)): + return None + return comparator_func(left, right) def visit_current(self, node, value): return value def visit_expref(self, node, value): - return _Expression(node['children'][0]) + return _Expression(node['children'][0], self) def visit_function_expression(self, node, value): resolved_args = [] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jmespath-0.9.0/jmespath.egg-info/PKG-INFO new/jmespath-0.9.2/jmespath.egg-info/PKG-INFO --- old/jmespath-0.9.0/jmespath.egg-info/PKG-INFO 2015-10-01 06:12:12.000000000 +0200 +++ new/jmespath-0.9.2/jmespath.egg-info/PKG-INFO 2017-03-11 00:52:50.000000000 +0100 @@ -1,11 +1,11 @@ Metadata-Version: 1.1 Name: jmespath -Version: 0.9.0 +Version: 0.9.2 Summary: JSON Matching Expressions Home-page: https://github.com/jmespath/jmespath.py Author: James Saryerwinnie Author-email: [email protected] -License: UNKNOWN +License: MIT Description: JMESPath ======== @@ -18,6 +18,10 @@ :target: http://travis-ci.org/jmespath/jmespath.py + .. image:: https://codecov.io/github/jmespath/jmespath.py/coverage.svg?branch=develop + :target: https://codecov.io/github/jmespath/jmespath.py?branch=develop + + JMESPath (pronounced "james path") allows you to declaratively specify how to extract elements from a JSON document. @@ -56,7 +60,9 @@ The ``jmespath.py`` library has two functions that operate on python data structures. You can use ``search`` - and give it the jmespath expression and the data:: + and give it the jmespath expression and the data: + + .. code:: python >>> import jmespath >>> path = jmespath.search('foo.bar', {'foo': {'bar': 'baz'}}) @@ -64,7 +70,9 @@ Similar to the ``re`` module, you can use the ``compile`` function to compile the JMESPath expression and use this parsed expression - to perform repeated searches:: + to perform repeated searches: + + .. code:: python >>> import jmespath >>> expression = jmespath.compile('foo.bar') @@ -83,7 +91,9 @@ You can provide an instance of ``jmespath.Options`` to control how a JMESPath expression is evaluated. The most common scenario for using an ``Options`` instance is if you want to have ordered output - of your dict keys. To do this you can use either of these options:: + of your dict keys. To do this you can use either of these options: + + .. code:: python >>> import jmespath >>> jmespath.search('{a: a, b: b}, @@ -98,6 +108,85 @@ ... jmespath.Options(dict_cls=collections.OrderedDict)) + Custom Functions + ~~~~~~~~~~~~~~~~ + + The JMESPath language has numerous + `built-in functions + <http://jmespath.org/specification.html#built-in-functions>`__, but it is + also possible to add your own custom functions. Keep in mind that + custom function support in jmespath.py is experimental and the API may + change based on feedback. + + **If you have a custom function that you've found useful, consider submitting + it to jmespath.site and propose that it be added to the JMESPath language.** + You can submit proposals + `here <https://github.com/jmespath/jmespath.site/issues>`__. + + To create custom functions: + + * Create a subclass of ``jmespath.functions.Functions``. + * Create a method with the name ``_func_<your function name>``. + * Apply the ``jmespath.functions.signature`` decorator that indicates + the expected types of the function arguments. + * Provide an instance of your subclass in a ``jmespath.Options`` object. + + Below are a few examples: + + .. code:: python + + import jmespath + from jmespath import functions + + # 1. Create a subclass of functions.Functions. + # The function.Functions base class has logic + # that introspects all of its methods and automatically + # registers your custom functions in its function table. + class CustomFunctions(functions.Functions): + + # 2 and 3. Create a function that starts with _func_ + # and decorate it with @signature which indicates its + # expected types. + # In this example, we're creating a jmespath function + # called "unique_letters" that accepts a single argument + # with an expected type "string". + @functions.signature({'types': ['string']}) + def _func_unique_letters(self, s): + # Given a string s, return a sorted + # string of unique letters: 'ccbbadd' -> 'abcd' + return ''.join(sorted(set(s))) + + # Here's another example. This is creating + # a jmespath function called "my_add" that expects + # two arguments, both of which should be of type number. + @functions.signature({'types': ['number']}, {'types': ['number']}) + def _func_my_add(self, x, y): + return x + y + + # 4. Provide an instance of your subclass in a Options object. + options = jmespath.Options(custom_functions=CustomFunctions()) + + # Provide this value to jmespath.search: + # This will print 3 + print( + jmespath.search( + 'my_add(`1`, `2`)', {}, options=options) + ) + + # This will print "abcd" + print( + jmespath.search( + 'foo.bar | unique_letters(@)', + {'foo': {'bar': 'ccbbadd'}}, + options=options) + ) + + Again, if you come up with useful functions that you think make + sense in the JMESPath language (and make sense to implement in all + JMESPath libraries, not just python), please let us know at + `jmespath.site <https://github.com/jmespath/jmespath.site/issues>`__. + + Specification ============= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jmespath-0.9.0/jmespath.egg-info/SOURCES.txt new/jmespath-0.9.2/jmespath.egg-info/SOURCES.txt --- old/jmespath-0.9.0/jmespath.egg-info/SOURCES.txt 2015-10-01 06:12:12.000000000 +0200 +++ new/jmespath-0.9.2/jmespath.egg-info/SOURCES.txt 2017-03-11 00:52:50.000000000 +0100 @@ -15,7 +15,6 @@ jmespath.egg-info/PKG-INFO jmespath.egg-info/SOURCES.txt jmespath.egg-info/dependency_links.txt -jmespath.egg-info/pbr.json jmespath.egg-info/top_level.txt tests/__init__.py tests/test_compliance.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jmespath-0.9.0/jmespath.egg-info/pbr.json new/jmespath-0.9.2/jmespath.egg-info/pbr.json --- old/jmespath-0.9.0/jmespath.egg-info/pbr.json 2015-04-21 08:34:36.000000000 +0200 +++ new/jmespath-0.9.2/jmespath.egg-info/pbr.json 1970-01-01 01:00:00.000000000 +0100 @@ -1 +0,0 @@ -{"is_release": true, "git_version": "0466cc1"} \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jmespath-0.9.0/setup.py new/jmespath-0.9.2/setup.py --- old/jmespath-0.9.0/setup.py 2015-10-01 06:11:26.000000000 +0200 +++ new/jmespath-0.9.2/setup.py 2017-03-11 00:45:48.000000000 +0100 @@ -9,7 +9,7 @@ setup( name='jmespath', - version='0.9.0', + version='0.9.2', description='JSON Matching Expressions', long_description=io.open('README.rst', encoding='utf-8').read(), author='James Saryerwinnie', @@ -17,6 +17,7 @@ url='https://github.com/jmespath/jmespath.py', scripts=['bin/jp.py'], packages=find_packages(exclude=['tests']), + license='MIT', classifiers=( 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jmespath-0.9.0/tests/test_compliance.py new/jmespath-0.9.2/tests/test_compliance.py --- old/jmespath-0.9.0/tests/test_compliance.py 2015-10-01 06:11:19.000000000 +0200 +++ new/jmespath-0.9.2/tests/test_compliance.py 2017-03-10 01:49:47.000000000 +0100 @@ -5,8 +5,7 @@ from nose.tools import assert_equal -import jmespath -from jmespath.visitor import TreeInterpreter, Options +from jmespath.visitor import Options TEST_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -19,16 +18,17 @@ def test_compliance(): for full_path in _walk_files(): if full_path.endswith('.json'): - for given, expression, result, error in _load_cases(full_path): - if error is NOT_SPECIFIED and result is not NOT_SPECIFIED: - yield (_test_expression, given, expression, - result, os.path.basename(full_path)) - elif result is NOT_SPECIFIED and error is not NOT_SPECIFIED: - yield (_test_error_expression, given, expression, - error, os.path.basename(full_path)) - else: - parts = (given, expression, result, error) - raise RuntimeError("Invalid test description: %s" % parts) + for given, test_type, test_data in load_cases(full_path): + t = test_data + # Benchmark tests aren't run as part of the normal + # test suite, so we only care about 'result' and + # 'error' test_types. + if test_type == 'result': + yield (_test_expression, given, t['expression'], + t['result'], os.path.basename(full_path)) + elif test_type == 'error': + yield (_test_error_expression, given, t['expression'], + t['error'], os.path.basename(full_path)) def _walk_files(): @@ -47,14 +47,20 @@ yield os.path.join(root, filename) -def _load_cases(full_path): +def load_cases(full_path): all_test_data = json.load(open(full_path), object_pairs_hook=OrderedDict) for test_data in all_test_data: given = test_data['given'] for case in test_data['cases']: - yield (given, case['expression'], - case.get('result', NOT_SPECIFIED), - case.get('error', NOT_SPECIFIED)) + if 'result' in case: + test_type = 'result' + elif 'error' in case: + test_type = 'error' + elif 'bench' in case: + test_type = 'bench' + else: + raise RuntimeError("Unknown test type: %s" % json.dumps(case)) + yield (given, test_type, case) def _test_expression(given, expression, expected, filename): @@ -85,9 +91,16 @@ try: parsed = jmespath.compile(expression) parsed.search(given) - except ValueError as e: + except ValueError: # Test passes, it raised a parse error as expected. pass + except Exception as e: + # Failure because an unexpected exception was raised. + error_msg = ("\n\n (%s) The expression '%s' was suppose to be a " + "syntax error, but it raised an unexpected error:\n\n%s" % ( + filename, expression, e)) + error_msg = error_msg.replace(r'\n', '\n') + raise AssertionError(error_msg) else: error_msg = ("\n\n (%s) The expression '%s' was suppose to be a " "syntax error, but it successfully parsed as:\n\n%s" % (
