summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorptmcg <ptmcg@austin.rr.com>2023-03-06 23:28:15 -0600
committerptmcg <ptmcg@austin.rr.com>2023-03-06 23:28:15 -0600
commit28e4abe1c394e52c39b0dd00537e8312eb2cd9ae (patch)
tree8f13cc87202869b6d390bd45a8895786cf0fa356
parent5d95272d98b8bce1ac53b10bdef6db12b0230dfa (diff)
downloadpyparsing-git-28e4abe1c394e52c39b0dd00537e8312eb2cd9ae.tar.gz
Add ParseResults.deepcopy() - fixes #463
-rw-r--r--CHANGES8
-rw-r--r--docs/HowToUsePyparsing.rst6
-rw-r--r--pyparsing/__init__.py2
-rw-r--r--pyparsing/core.py5
-rw-r--r--pyparsing/results.py29
-rw-r--r--tests/test_unit.py114
6 files changed, 156 insertions, 8 deletions
diff --git a/CHANGES b/CHANGES
index 1977065..e71c9ab 100644
--- a/CHANGES
+++ b/CHANGES
@@ -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"""