From f60f5a912974ad42dfa709597911379d29d1f654 Mon Sep 17 00:00:00 2001 From: Cameron Simpson Date: Thu, 7 Aug 2025 12:47:17 +1000 Subject: [PATCH 1/7] edtf.util: new @remapparams decorator to rename obsolete parameters to their modern names --- edtf/util.py | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 edtf/util.py diff --git a/edtf/util.py b/edtf/util.py new file mode 100644 index 0000000..b1d5673 --- /dev/null +++ b/edtf/util.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 + +''' +Assorted utility functions. +''' + +from functools import update_wrapper +from logging import warning +from traceback import extract_stack + +def remapparams(**remap): + ''' + Remap the specified named parameters. + + Example to support an obsolete `parseAll` parameter: + + @remapparams(parseAll='parse_all') + def parse(s, parse_all=True): + + ''' + if not remap: + raise ValueError('no parameters specified for remapping') + for old, new in remap.items(): + if new in remap: + raise ValueError(f'{old}={new!r}: {new!r} is also remapped') + + def remapparams_decorator(func): + '''The decorator to apply the remappings.''' + # a record of callers whose parameters were remapped + remapped_callers = set() + + def remapparams_wrapper(*a, **kw): + remappings = {} + for param, value in list(kw.items()): + try: + remapped = remap[param] + except KeyError: + continue + if remapped in kw: + raise ValueError(f'remap {param}= to {remapped}=: this is already present in the keyword arguments') + del kw[param] + kw[remapped] = value + remappings[param] = remapped + if remappings: + caller_frame = extract_stack(limit=2)[-2] + caller_key = caller_frame.filename, caller_frame.lineno + if caller_key not in remapped_callers: + warning( + "call of %s.%s() from %s:%d: remapped the following obsolete parameters: %s", + func.__module__, func.__name__, + caller_frame.filename, caller_frame.lineno, + ", ".join(sorted(f'{old}->{new}' for old, new in remappings.items())), + ) + remapped_callers.add(caller_key) + return func(*a, **kw) + + update_wrapper(remapparams_wrapper, func) + return remapparams_wrapper + + return remapparams_decorator + +if __name__ == '__main__': + + @remapparams(parseAll='parse_all') + def parser(s, parse_all=True): + pass + + assert parser.__name__ == 'parser' + parser('foo') + # this should not warn + parser('foo', parse_all=False) + # this should warn, but only once + for _ in 1, 2: + parser('foo', parseAll=False) + try: + parser('foo', parseAll=False, parse_all=True) + except ValueError: + pass + else: + assert False, "expected ValueError because of duplicated parameters" + + try: + @remapparams() + def no_remappings(): + pass + except ValueError: + pass + else: + assert False, "expected ValueError from @remapparams() because no remappings" + try: + @remapparams(p1='p2', p2='p3') + def no_remappings(): + pass + except ValueError: + pass + else: + assert False, "expected ValueError from @remapparams() because p1 remaps to another remapped parameter" From 94ec9c21c329fb2f45ae6003426d843900445503 Mon Sep 17 00:00:00 2001 From: Cameron Simpson Date: Thu, 7 Aug 2025 12:57:09 +1000 Subject: [PATCH 2/7] edtf.parser.grammar: decorate parse_edtf() to accept the old parseAll parameter --- edtf/parser/grammar.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/edtf/parser/grammar.py b/edtf/parser/grammar.py index de84633..4480555 100644 --- a/edtf/parser/grammar.py +++ b/edtf/parser/grammar.py @@ -5,6 +5,7 @@ import pyparsing from edtf.appsettings import DEBUG_PYPARSING +from edtf.util import remapparams pyparsing.ParserElement.enablePackrat() @@ -343,6 +344,7 @@ def f(toks): ) +@remapparams(parseAll='parse_all') def parse_edtf( input_string: str, parse_all: bool = True, From d142a7544197d2ceba02e7d337e9df8f9daa7abb Mon Sep 17 00:00:00 2001 From: Cameron Simpson Date: Thu, 7 Aug 2025 13:39:33 +1000 Subject: [PATCH 3/7] edtf.utils: clean lint --- edtf/util.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/edtf/util.py b/edtf/util.py index b1d5673..11d0c6e 100644 --- a/edtf/util.py +++ b/edtf/util.py @@ -8,6 +8,7 @@ from logging import warning from traceback import extract_stack + def remapparams(**remap): ''' Remap the specified named parameters. @@ -65,7 +66,7 @@ def remapparams_wrapper(*a, **kw): def parser(s, parse_all=True): pass - assert parser.__name__ == 'parser' + assert parser.__name__ == 'parser' # noqa: S101 parser('foo') # this should not warn parser('foo', parse_all=False) @@ -77,7 +78,9 @@ def parser(s, parse_all=True): except ValueError: pass else: - assert False, "expected ValueError because of duplicated parameters" + raise AssertionError( + "expected ValueError because of duplicated parameters" + ) try: @remapparams() @@ -86,7 +89,9 @@ def no_remappings(): except ValueError: pass else: - assert False, "expected ValueError from @remapparams() because no remappings" + raise AssertionError( + "expected ValueError from @remapparams() because no remappings" + ) try: @remapparams(p1='p2', p2='p3') def no_remappings(): @@ -94,4 +99,6 @@ def no_remappings(): except ValueError: pass else: - assert False, "expected ValueError from @remapparams() because p1 remaps to another remapped parameter" + raise AssertionError( + "expected ValueError from @remapparams() because p1 remaps to another remapped parameter" + ) From 36fa3462d79d0497960bbd4f425ccb147b9bb4a1 Mon Sep 17 00:00:00 2001 From: Cameron Simpson Date: Thu, 7 Aug 2025 13:45:40 +1000 Subject: [PATCH 4/7] ruff format --- edtf/parser/grammar.py | 2 +- edtf/util.py | 51 ++++++++++++++++++++++++------------------ 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/edtf/parser/grammar.py b/edtf/parser/grammar.py index 4480555..cdb64dc 100644 --- a/edtf/parser/grammar.py +++ b/edtf/parser/grammar.py @@ -344,7 +344,7 @@ def f(toks): ) -@remapparams(parseAll='parse_all') +@remapparams(parseAll="parse_all") def parse_edtf( input_string: str, parse_all: bool = True, diff --git a/edtf/util.py b/edtf/util.py index 11d0c6e..5241ae8 100644 --- a/edtf/util.py +++ b/edtf/util.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -''' +""" Assorted utility functions. -''' +""" from functools import update_wrapper from logging import warning @@ -10,7 +10,7 @@ def remapparams(**remap): - ''' + """ Remap the specified named parameters. Example to support an obsolete `parseAll` parameter: @@ -18,15 +18,15 @@ def remapparams(**remap): @remapparams(parseAll='parse_all') def parse(s, parse_all=True): - ''' + """ if not remap: - raise ValueError('no parameters specified for remapping') + raise ValueError("no parameters specified for remapping") for old, new in remap.items(): if new in remap: - raise ValueError(f'{old}={new!r}: {new!r} is also remapped') + raise ValueError(f"{old}={new!r}: {new!r} is also remapped") def remapparams_decorator(func): - '''The decorator to apply the remappings.''' + """The decorator to apply the remappings.""" # a record of callers whose parameters were remapped remapped_callers = set() @@ -38,7 +38,9 @@ def remapparams_wrapper(*a, **kw): except KeyError: continue if remapped in kw: - raise ValueError(f'remap {param}= to {remapped}=: this is already present in the keyword arguments') + raise ValueError( + f"remap {param}= to {remapped}=: this is already present in the keyword arguments" + ) del kw[param] kw[remapped] = value remappings[param] = remapped @@ -48,9 +50,13 @@ def remapparams_wrapper(*a, **kw): if caller_key not in remapped_callers: warning( "call of %s.%s() from %s:%d: remapped the following obsolete parameters: %s", - func.__module__, func.__name__, - caller_frame.filename, caller_frame.lineno, - ", ".join(sorted(f'{old}->{new}' for old, new in remappings.items())), + func.__module__, + func.__name__, + caller_frame.filename, + caller_frame.lineno, + ", ".join( + sorted(f"{old}->{new}" for old, new in remappings.items()) + ), ) remapped_callers.add(caller_key) return func(*a, **kw) @@ -60,29 +66,29 @@ def remapparams_wrapper(*a, **kw): return remapparams_decorator -if __name__ == '__main__': - @remapparams(parseAll='parse_all') +if __name__ == "__main__": + + @remapparams(parseAll="parse_all") def parser(s, parse_all=True): pass - assert parser.__name__ == 'parser' # noqa: S101 - parser('foo') + assert parser.__name__ == "parser" # noqa: S101 + parser("foo") # this should not warn - parser('foo', parse_all=False) + parser("foo", parse_all=False) # this should warn, but only once for _ in 1, 2: - parser('foo', parseAll=False) + parser("foo", parseAll=False) try: - parser('foo', parseAll=False, parse_all=True) + parser("foo", parseAll=False, parse_all=True) except ValueError: pass else: - raise AssertionError( - "expected ValueError because of duplicated parameters" - ) + raise AssertionError("expected ValueError because of duplicated parameters") try: + @remapparams() def no_remappings(): pass @@ -93,7 +99,8 @@ def no_remappings(): "expected ValueError from @remapparams() because no remappings" ) try: - @remapparams(p1='p2', p2='p3') + + @remapparams(p1="p2", p2="p3") def no_remappings(): pass except ValueError: From f4a73fe3097b56045b12c54c7f7fe8bb69cce3c3 Mon Sep 17 00:00:00 2001 From: Cameron Simpson Date: Fri, 8 Aug 2025 09:47:34 +1000 Subject: [PATCH 5/7] edit.utils.remapparams: move the tests from the main section into edtf.tests --- edtf/tests.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ edtf/util.py | 44 -------------------------------------------- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/edtf/tests.py b/edtf/tests.py index 9812b65..7ee9805 100644 --- a/edtf/tests.py +++ b/edtf/tests.py @@ -4,6 +4,7 @@ from time import struct_time from edtf import convert +from edtf.utils import remapparams def test_dt_to_struct_time_for_datetime(): @@ -107,3 +108,46 @@ def test_roll_negative_time_fields(): assert convert._roll_negative_time_fields( year, month, day, hour, minute, second ) == (-102, 5, 24, 21, 41, 47) + +def test_remapparams(): + + @remapparams(parseAll="parse_all") + def parser(s, parse_all=True): + pass + + assert parser.__name__ == "parser" # noqa: S101 + parser("foo") + # this should not warn + parser("foo", parse_all=False) + # this should warn, but only once + for _ in 1, 2: + parser("foo", parseAll=False) + try: + parser("foo", parseAll=False, parse_all=True) + except ValueError: + pass + else: + raise AssertionError("expected ValueError because of duplicated parameters") + + try: + + @remapparams() + def no_remappings(): + pass + except ValueError: + pass + else: + raise AssertionError( + "expected ValueError from @remapparams() because no remappings" + ) + try: + + @remapparams(p1="p2", p2="p3") + def no_remappings(): + pass + except ValueError: + pass + else: + raise AssertionError( + "expected ValueError from @remapparams() because p1 remaps to another remapped parameter" + ) diff --git a/edtf/util.py b/edtf/util.py index 5241ae8..146eec2 100644 --- a/edtf/util.py +++ b/edtf/util.py @@ -65,47 +65,3 @@ def remapparams_wrapper(*a, **kw): return remapparams_wrapper return remapparams_decorator - - -if __name__ == "__main__": - - @remapparams(parseAll="parse_all") - def parser(s, parse_all=True): - pass - - assert parser.__name__ == "parser" # noqa: S101 - parser("foo") - # this should not warn - parser("foo", parse_all=False) - # this should warn, but only once - for _ in 1, 2: - parser("foo", parseAll=False) - try: - parser("foo", parseAll=False, parse_all=True) - except ValueError: - pass - else: - raise AssertionError("expected ValueError because of duplicated parameters") - - try: - - @remapparams() - def no_remappings(): - pass - except ValueError: - pass - else: - raise AssertionError( - "expected ValueError from @remapparams() because no remappings" - ) - try: - - @remapparams(p1="p2", p2="p3") - def no_remappings(): - pass - except ValueError: - pass - else: - raise AssertionError( - "expected ValueError from @remapparams() because p1 remaps to another remapped parameter" - ) From dedb361b2f39ad8366e5c483a9f28f13146bd69e Mon Sep 17 00:00:00 2001 From: Cameron Simpson Date: Fri, 8 Aug 2025 10:07:15 +1000 Subject: [PATCH 6/7] edtf.tests: exercise parse_edtf using the obsolete and modern mode parameters --- edtf/tests.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/edtf/tests.py b/edtf/tests.py index 7ee9805..cc0f31e 100644 --- a/edtf/tests.py +++ b/edtf/tests.py @@ -4,7 +4,9 @@ from time import struct_time from edtf import convert -from edtf.utils import remapparams +from edtf.parser.edtf_exceptions import EDTFParseException +from edtf.parser.grammar import parse_edtf +from edtf.util import remapparams def test_dt_to_struct_time_for_datetime(): @@ -151,3 +153,31 @@ def no_remappings(): raise AssertionError( "expected ValueError from @remapparams() because p1 remaps to another remapped parameter" ) + +def test_remapparams_parse_edtf(): + edtf_s = '2005-09-24T10:00:00' # ISO8601 example from the EDTF spec + dat = parse_edtf(edtf_s) # implicit parse_all=True + assert dat.isoformat() == edtf_s + assert parse_edtf(edtf_s, parse_all=True).isoformat() == edtf_s + assert parse_edtf(edtf_s, parseAll=True).isoformat() == edtf_s + assert parse_edtf(f'{edtf_s} SNORT', parse_all=False).isoformat() == edtf_s + assert parse_edtf(f'{edtf_s} SNORT', parseAll=False).isoformat() == edtf_s + # make sure parse_all=True fails the SNORT parse + try: + parse_edtf(f'{edtf_s} SNORT') + except EDTFParseException: + pass + else: + raise AssertionError('expected EDTFParseException') + try: + parse_edtf(f'{edtf_s} SNORT', parse_all=True) + except EDTFParseException: + pass + else: + raise AssertionError('expected EDTFParseException') + try: + parse_edtf(f'{edtf_s} SNORT', parseAll=True) + except EDTFParseException: + pass + else: + raise AssertionError('expected EDTFParseException') From 83c8ba2d07f1f63bdb666397cb118ee59b2fc3f1 Mon Sep 17 00:00:00 2001 From: Cameron Simpson Date: Fri, 8 Aug 2025 10:07:43 +1000 Subject: [PATCH 7/7] edtf.tests: autoformat --- edtf/tests.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/edtf/tests.py b/edtf/tests.py index cc0f31e..837e580 100644 --- a/edtf/tests.py +++ b/edtf/tests.py @@ -111,8 +111,8 @@ def test_roll_negative_time_fields(): year, month, day, hour, minute, second ) == (-102, 5, 24, 21, 41, 47) -def test_remapparams(): +def test_remapparams(): @remapparams(parseAll="parse_all") def parser(s, parse_all=True): pass @@ -154,30 +154,31 @@ def no_remappings(): "expected ValueError from @remapparams() because p1 remaps to another remapped parameter" ) + def test_remapparams_parse_edtf(): - edtf_s = '2005-09-24T10:00:00' # ISO8601 example from the EDTF spec - dat = parse_edtf(edtf_s) # implicit parse_all=True + edtf_s = "2005-09-24T10:00:00" # ISO8601 example from the EDTF spec + dat = parse_edtf(edtf_s) # implicit parse_all=True assert dat.isoformat() == edtf_s assert parse_edtf(edtf_s, parse_all=True).isoformat() == edtf_s assert parse_edtf(edtf_s, parseAll=True).isoformat() == edtf_s - assert parse_edtf(f'{edtf_s} SNORT', parse_all=False).isoformat() == edtf_s - assert parse_edtf(f'{edtf_s} SNORT', parseAll=False).isoformat() == edtf_s + assert parse_edtf(f"{edtf_s} SNORT", parse_all=False).isoformat() == edtf_s + assert parse_edtf(f"{edtf_s} SNORT", parseAll=False).isoformat() == edtf_s # make sure parse_all=True fails the SNORT parse try: - parse_edtf(f'{edtf_s} SNORT') + parse_edtf(f"{edtf_s} SNORT") except EDTFParseException: pass else: - raise AssertionError('expected EDTFParseException') + raise AssertionError("expected EDTFParseException") try: - parse_edtf(f'{edtf_s} SNORT', parse_all=True) + parse_edtf(f"{edtf_s} SNORT", parse_all=True) except EDTFParseException: pass else: - raise AssertionError('expected EDTFParseException') + raise AssertionError("expected EDTFParseException") try: - parse_edtf(f'{edtf_s} SNORT', parseAll=True) + parse_edtf(f"{edtf_s} SNORT", parseAll=True) except EDTFParseException: pass else: - raise AssertionError('expected EDTFParseException') + raise AssertionError("expected EDTFParseException")