diff options
author | ptmcg <ptmcg@austin.rr.com> | 2023-03-06 23:28:15 -0600 |
---|---|---|
committer | ptmcg <ptmcg@austin.rr.com> | 2023-03-06 23:28:15 -0600 |
commit | 28e4abe1c394e52c39b0dd00537e8312eb2cd9ae (patch) | |
tree | 8f13cc87202869b6d390bd45a8895786cf0fa356 | |
parent | 5d95272d98b8bce1ac53b10bdef6db12b0230dfa (diff) | |
download | pyparsing-git-28e4abe1c394e52c39b0dd00537e8312eb2cd9ae.tar.gz |
Add ParseResults.deepcopy() - fixes #463
-rw-r--r-- | CHANGES | 8 | ||||
-rw-r--r-- | docs/HowToUsePyparsing.rst | 6 | ||||
-rw-r--r-- | pyparsing/__init__.py | 2 | ||||
-rw-r--r-- | pyparsing/core.py | 5 | ||||
-rw-r--r-- | pyparsing/results.py | 29 | ||||
-rw-r--r-- | tests/test_unit.py | 114 |
6 files changed, 156 insertions, 8 deletions
@@ -41,6 +41,14 @@ help from Devin J. Pohly in structuring the code to enable this peaceful transit # or # ident = ppu.Ελληνικά.identifier +- `ParseResults` now has a new method `deepcopy()`, in addition to the current + `copy()` method. `copy()` only makes a shallow copy - any contained `ParseResults` + are copied as references - changes in the copy will be seen as changes in the original. + In many cases, a shallow copy is sufficient, but some applications require a deep copy. + `deepcopy()` makes a deeper copy: any contained `ParseResults` or other mappings or + containers are built with copies from the original, and do not get changed if the + original is later changed. Addresses issue #463, reported by Bryn Pickering. + - Reworked `delimited_list` function into the new `DelimitedList` class. `DelimitedList` has the same constructor interface as `delimited_list`, and in this release, `delimited_list` changes from a function to a synonym for diff --git a/docs/HowToUsePyparsing.rst b/docs/HowToUsePyparsing.rst index 30e5753..fce615c 100644 --- a/docs/HowToUsePyparsing.rst +++ b/docs/HowToUsePyparsing.rst @@ -5,10 +5,10 @@ Using the pyparsing module :author: Paul McGuire :address: ptmcg.pm+pyparsing@gmail.com -:revision: 3.0.10 -:date: July, 2022 +:revision: 3.1.0 +:date: March, 2023 -:copyright: Copyright |copy| 2003-2022 Paul McGuire. +:copyright: Copyright |copy| 2003-2023 Paul McGuire. .. |copy| unicode:: 0xA9 diff --git a/pyparsing/__init__.py b/pyparsing/__init__.py index 9c128ce..22bc21f 100644 --- a/pyparsing/__init__.py +++ b/pyparsing/__init__.py @@ -121,7 +121,7 @@ class version_info(NamedTuple): __version_info__ = version_info(3, 1, 0, "alpha", 1) -__version_time__ = "05 Mar 2023 06:11 UTC" +__version_time__ = "07 Mar 2023 01:33 UTC" __version__ = __version_info__.__version__ __versionTime__ = __version_time__ __author__ = "Paul McGuire <ptmcg.gm+pyparsing@gmail.com>" diff --git a/pyparsing/core.py b/pyparsing/core.py index 9fbb6d0..71c2690 100644 --- a/pyparsing/core.py +++ b/pyparsing/core.py @@ -4147,7 +4147,7 @@ class Or(ParseExpression): raise max_fatal if maxException is not None: - maxException.msg = self.errmsg + # maxException.msg = self.errmsg raise maxException else: raise ParseException( @@ -4260,7 +4260,8 @@ class MatchFirst(ParseExpression): maxExcLoc = len(instring) if maxException is not None: - maxException.msg = self.errmsg + if maxException.msg == self.exprs[0].errmsg: + maxException.msg = self.errmsg raise maxException else: raise ParseException( diff --git a/pyparsing/results.py b/pyparsing/results.py index 5f4b62c..8c52a3a 100644 --- a/pyparsing/results.py +++ b/pyparsing/results.py @@ -1,5 +1,5 @@ # results.py -from collections.abc import MutableMapping, Mapping, MutableSequence, Iterator +from collections.abc import MutableMapping, Mapping, MutableSequence, Iterator, Sequence, Container import pprint from typing import Tuple, Any, Dict, Set, List @@ -539,7 +539,10 @@ class ParseResults: def copy(self) -> "ParseResults": """ - Returns a new copy of a :class:`ParseResults` object. + Returns a new shallow copy of a :class:`ParseResults` object. `ParseResults` + items contained within the source are shared with the copy. Use + :class:`ParseResults.deepcopy()` to create a copy with its own separate + content values. """ ret = ParseResults(self._toklist) ret._tokdict = self._tokdict.copy() @@ -548,6 +551,28 @@ class ParseResults: ret._name = self._name return ret + def deepcopy(self) -> "ParseResults": + """ + Returns a new deep copy of a :class:`ParseResults` object. + """ + ret = self.copy() + # replace values with copies if they are of known mutable types + for i, obj in enumerate(self._toklist): + if isinstance(obj, ParseResults): + self._toklist[i] = obj.deepcopy() + elif isinstance(obj, (str, bytes)): + pass + elif isinstance(obj, MutableMapping): + self._toklist[i] = dest = type(obj)() + for k, v in obj.items(): + dest[k] = v.deepcopy() if isinstance(v, ParseResults) else v + elif isinstance(obj, Container): + self._toklist[i] = type(obj)( + v.deepcopy() if isinstance(v, ParseResults) else v + for v in obj + ) + return ret + def get_name(self): r""" Returns the results name for this token expression. Useful when several diff --git a/tests/test_unit.py b/tests/test_unit.py index 2bb37a5..c2a7160 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -3438,6 +3438,120 @@ class Test02_WithoutPackrat(ppt.TestParseResultsAsserts, TestCase): "ParseResults with empty list but containing a results name evaluated as False", ) + def testParseResultsCopy(self): + expr = pp.Word(pp.nums) + pp.Group(pp.Word(pp.alphas)("key") + '=' + pp.Word(pp.nums)("value"))[...] + result = expr.parse_string("1 a=100 b=200 c=300") + print(result.dump()) + + r2 = result.copy() + print(r2.dump()) + + # check copy is different, but contained results is the same as in original + self.assertFalse(r2 is result, "copy failed") + self.assertTrue(r2[1] is result[1], "shallow copy failed") + + # update result sub-element in place + result[1][0] = 'z' + self.assertParseResultsEquals( + result, + expected_list=['1', ['z', '=', '100'], ['b', '=', '200'], ['c', '=', '300']] + ) + + # update contained results, verify list and dict contents are updated as expected + result[1][0] = result[1]["key"] = 'q' + result[1]["xyz"] = 1000 + print(result.dump()) + self.assertParseResultsEquals( + result, + expected_list=['1', ['q', '=', '100'], ['b', '=', '200'], ['c', '=', '300']], + ) + self.assertParseResultsEquals( + result[1], + expected_dict = {'key': 'q', 'value': '100', 'xyz': 1000} + ) + + # verify that list and dict contents are the same in copy + self.assertParseResultsEquals( + r2, + expected_list=['1', ['q', '=', '100'], ['b', '=', '200'], ['c', '=', '300']], + ) + self.assertParseResultsEquals( + r2[1], + expected_dict = {'key': 'q', 'value': '100', 'xyz': 1000} + ) + + def testParseResultsDeepcopy(self): + expr = pp.Word(pp.nums) + pp.Group(pp.Word(pp.alphas)("key") + '=' + pp.Word(pp.nums)("value"))[...] + result = expr.parse_string("1 a=100 b=200 c=300") + + r2 = result.deepcopy() + print(r2.dump()) + + # check copy and contained results are different from original + self.assertFalse(r2 is result, "copy failed") + self.assertFalse(r2[1] is result[1], "deep copy failed") + + # update contained results + result[1][0] = result[1]["key"] = 'q' + result[1]["xyz"] = 1000 + print(result.dump()) + + # verify that list and dict contents are unchanged in the copy + self.assertParseResultsEquals( + r2, + expected_list=['1', ['a', '=', '100'], ['b', '=', '200'], ['c', '=', '300']], + ) + self.assertParseResultsEquals( + r2[1], + expected_dict = {'key': 'a', 'value': '100'} + ) + + def testParseResultsDeepcopy2(self): + expr = pp.Word(pp.nums) + pp.Group(pp.Word(pp.alphas)("key") + '=' + pp.Word(pp.nums)("value"), aslist=True)[...] + result = expr.parse_string("1 a=100 b=200 c=300") + + r2 = result.deepcopy() + print(r2.dump()) + + # check copy and contained results are different from original + self.assertFalse(r2 is result, "copy failed") + self.assertFalse(r2[1] is result[1], "deep copy failed") + + # update contained results + result[1][0] = 'q' + print(result.dump()) + + # verify that list and dict contents are unchanged in the copy + self.assertParseResultsEquals( + r2, + expected_list=['1', ['a', '=', '100'], ['b', '=', '200'], ['c', '=', '300']], + ) + + def testParseResultsDeepcopy3(self): + expr = pp.Word(pp.nums) + pp.Group( + (pp.Word(pp.alphas)("key") + '=' + pp.Word(pp.nums)("value")).add_parse_action( + lambda t: tuple(t) + ) + )[...] + result = expr.parse_string("1 a=100 b=200 c=300") + + r2 = result.deepcopy() + print(r2.dump()) + + # check copy and contained results are different from original + self.assertFalse(r2 is result, "copy failed") + self.assertFalse(r2[1] is result[1], "deep copy failed") + + # update contained results + result[1][0] = 'q' + print(result.dump()) + + # verify that list and dict contents are unchanged in the copy + self.assertParseResultsEquals( + r2, + expected_list=['1', [('a', '=', '100')], [('b', '=', '200')], [('c', '=', '300')]], + ) + def testIgnoreString(self): """test ParserElement.ignore() passed a string arg""" |