| @@ -4,6 +4,7 @@ v0.6.5 (unreleased): | |||||
| - Added support for Python 3.11. | - Added support for Python 3.11. | ||||
| - Fixed parsing of leading zeros in named HTML entities. (#288) | - Fixed parsing of leading zeros in named HTML entities. (#288) | ||||
| - Fixed memory leak parsing tags. (#303) | - Fixed memory leak parsing tags. (#303) | ||||
| - Fixed pickling SmartList objects. (#289) | |||||
| v0.6.4 (released February 14, 2022): | v0.6.4 (released February 14, 2022): | ||||
| @@ -13,6 +13,8 @@ Unreleased | |||||
| (`#288 <https://github.com/earwig/mwparserfromhell/issues/288>`_) | (`#288 <https://github.com/earwig/mwparserfromhell/issues/288>`_) | ||||
| - Fixed memory leak parsing tags. | - Fixed memory leak parsing tags. | ||||
| (`#303 <https://github.com/earwig/mwparserfromhell/issues/303>`_) | (`#303 <https://github.com/earwig/mwparserfromhell/issues/303>`_) | ||||
| - Fixed pickling SmartList objects. | |||||
| (`#289 <https://github.com/earwig/mwparserfromhell/issues/289>`_) | |||||
| v0.6.4 | v0.6.4 | ||||
| ------ | ------ | ||||
| @@ -1,4 +1,4 @@ | |||||
| # Copyright (C) 2012-2020 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| # Copyright (C) 2012-2023 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| # Copyright (C) 2019-2020 Yuri Astrakhan <YuriAstrakhan@gmail.com> | # Copyright (C) 2019-2020 Yuri Astrakhan <YuriAstrakhan@gmail.com> | ||||
| # | # | ||||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| @@ -19,6 +19,8 @@ | |||||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| # SOFTWARE. | # SOFTWARE. | ||||
| import weakref | |||||
| from .utils import _SliceNormalizerMixIn, inheritdoc | from .utils import _SliceNormalizerMixIn, inheritdoc | ||||
| @@ -30,11 +32,21 @@ class ListProxy(_SliceNormalizerMixIn, list): | |||||
| it builds it dynamically using the :meth:`_render` method. | it builds it dynamically using the :meth:`_render` method. | ||||
| """ | """ | ||||
| __slots__ = ("__weakref__", "_parent", "_sliceinfo") | |||||
| def __init__(self, parent, sliceinfo): | def __init__(self, parent, sliceinfo): | ||||
| super().__init__() | super().__init__() | ||||
| self._parent = parent | self._parent = parent | ||||
| self._sliceinfo = sliceinfo | self._sliceinfo = sliceinfo | ||||
| def __reduce_ex__(self, protocol: int) -> tuple: | |||||
| return (ListProxy, (self._parent, self._sliceinfo), ()) | |||||
| def __setstate__(self, state: tuple) -> None: | |||||
| # Reregister with the parent | |||||
| child_ref = weakref.ref(self, self._parent._delete_child) | |||||
| self._parent._children[id(child_ref)] = (child_ref, self._sliceinfo) | |||||
| def __repr__(self): | def __repr__(self): | ||||
| return repr(self._render()) | return repr(self._render()) | ||||
| @@ -1,4 +1,4 @@ | |||||
| # Copyright (C) 2012-2020 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| # Copyright (C) 2012-2023 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| # Copyright (C) 2019-2020 Yuri Astrakhan <YuriAstrakhan@gmail.com> | # Copyright (C) 2019-2020 Yuri Astrakhan <YuriAstrakhan@gmail.com> | ||||
| # | # | ||||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| @@ -49,12 +49,16 @@ class SmartList(_SliceNormalizerMixIn, list): | |||||
| [0, 1, 2, 3, 4] | [0, 1, 2, 3, 4] | ||||
| """ | """ | ||||
| def __init__(self, iterable=None): | |||||
| if iterable: | |||||
| super().__init__(iterable) | |||||
| else: | |||||
| super().__init__() | |||||
| self._children = {} | |||||
| __slots__ = ("_children",) | |||||
| def __new__(cls, *args, **kwargs): | |||||
| obj = super().__new__(cls, *args, **kwargs) | |||||
| obj._children = {} | |||||
| return obj | |||||
| def __reduce_ex__(self, protocol: int) -> tuple: | |||||
| # Detach children when pickling | |||||
| return (SmartList, (), None, iter(self)) | |||||
| def __getitem__(self, key): | def __getitem__(self, key): | ||||
| if not isinstance(key, slice): | if not isinstance(key, slice): | ||||
| @@ -1,4 +1,4 @@ | |||||
| # Copyright (C) 2012-2016 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| # Copyright (C) 2012-2023 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| # Copyright (C) 2019-2020 Yuri Astrakhan <YuriAstrakhan@gmail.com> | # Copyright (C) 2019-2020 Yuri Astrakhan <YuriAstrakhan@gmail.com> | ||||
| # | # | ||||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| @@ -37,6 +37,8 @@ def inheritdoc(method): | |||||
| class _SliceNormalizerMixIn: | class _SliceNormalizerMixIn: | ||||
| """MixIn that provides a private method to normalize slices.""" | """MixIn that provides a private method to normalize slices.""" | ||||
| __slots__ = () | |||||
| def _normalize_slice(self, key, clamp=False): | def _normalize_slice(self, key, clamp=False): | ||||
| """Return a slice equivalent to the input *key*, standardized.""" | """Return a slice equivalent to the input *key*, standardized.""" | ||||
| if key.start is None: | if key.start is None: | ||||
| @@ -1,4 +1,4 @@ | |||||
| # Copyright (C) 2012-2020 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| # Copyright (C) 2012-2023 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| # | # | ||||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| # of this software and associated documentation files (the "Software"), to deal | # of this software and associated documentation files (the "Software"), to deal | ||||
| @@ -23,10 +23,20 @@ This module contains accessory functions for other parts of the library. Parser | |||||
| users generally won't need stuff from here. | users generally won't need stuff from here. | ||||
| """ | """ | ||||
| from __future__ import annotations | |||||
| __all__ = ["parse_anything"] | __all__ = ["parse_anything"] | ||||
| import typing | |||||
| from typing import Any | |||||
| if typing.TYPE_CHECKING: | |||||
| from .wikicode import Wikicode | |||||
| def parse_anything(value, context=0, skip_style_tags=False): | |||||
| def parse_anything( | |||||
| value: Any, context: int = 0, *, skip_style_tags: bool = False | |||||
| ) -> Wikicode: | |||||
| """Return a :class:`.Wikicode` for *value*, allowing multiple types. | """Return a :class:`.Wikicode` for *value*, allowing multiple types. | ||||
| This differs from :meth:`.Parser.parse` in that we accept more than just a | This differs from :meth:`.Parser.parse` in that we accept more than just a | ||||
| @@ -58,11 +68,13 @@ def parse_anything(value, context=0, skip_style_tags=False): | |||||
| if value is None: | if value is None: | ||||
| return Wikicode(SmartList()) | return Wikicode(SmartList()) | ||||
| if hasattr(value, "read"): | if hasattr(value, "read"): | ||||
| return parse_anything(value.read(), context, skip_style_tags) | |||||
| return parse_anything(value.read(), context, skip_style_tags=skip_style_tags) | |||||
| try: | try: | ||||
| nodelist = SmartList() | nodelist = SmartList() | ||||
| for item in value: | for item in value: | ||||
| nodelist += parse_anything(item, context, skip_style_tags).nodes | |||||
| nodelist += parse_anything( | |||||
| item, context, skip_style_tags=skip_style_tags | |||||
| ).nodes | |||||
| return Wikicode(nodelist) | return Wikicode(nodelist) | ||||
| except TypeError as exc: | except TypeError as exc: | ||||
| error = ( | error = ( | ||||
| @@ -1,4 +1,4 @@ | |||||
| # Copyright (C) 2012-2020 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| # Copyright (C) 2012-2023 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| # | # | ||||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| # of this software and associated documentation files (the "Software"), to deal | # of this software and associated documentation files (the "Software"), to deal | ||||
| @@ -22,6 +22,8 @@ | |||||
| Test cases for the SmartList class and its child, ListProxy. | Test cases for the SmartList class and its child, ListProxy. | ||||
| """ | """ | ||||
| import pickle | |||||
| import pytest | import pytest | ||||
| from mwparserfromhell.smart_list import SmartList | from mwparserfromhell.smart_list import SmartList | ||||
| @@ -432,3 +434,27 @@ def test_influence(): | |||||
| assert [6, 5, 2, 3, 4, 1] == parent | assert [6, 5, 2, 3, 4, 1] == parent | ||||
| assert [4, 3, 2] == child2 | assert [4, 3, 2] == child2 | ||||
| assert 0 == len(parent._children) | assert 0 == len(parent._children) | ||||
| @pytest.mark.parametrize("protocol", range(pickle.HIGHEST_PROTOCOL + 1)) | |||||
| def test_pickling(protocol: int): | |||||
| """test SmartList objects behave properly when pickling""" | |||||
| parent = SmartList([0, 1, 2, 3, 4, 5]) | |||||
| enc = pickle.dumps(parent, protocol=protocol) | |||||
| assert pickle.loads(enc) == parent | |||||
| child = parent[1:3] | |||||
| assert len(parent._children) == 1 | |||||
| assert list(parent._children.values())[0][0]() is child | |||||
| enc = pickle.dumps(parent, protocol=protocol) | |||||
| parent2 = pickle.loads(enc) | |||||
| assert parent2 == parent | |||||
| assert parent2._children == {} | |||||
| enc = pickle.dumps(child, protocol=protocol) | |||||
| child2 = pickle.loads(enc) | |||||
| assert child2 == child | |||||
| assert child2._parent == parent | |||||
| assert child2._parent is not parent | |||||
| assert len(child2._parent._children) == 1 | |||||
| assert list(child2._parent._children.values())[0][0]() is child2 | |||||
| @@ -725,7 +725,7 @@ def test_formatting(): | |||||
| ), | ), | ||||
| ] | ] | ||||
| for (original, expected) in tests: | |||||
| for original, expected in tests: | |||||
| code = parse(original) | code = parse(original) | ||||
| template = code.filter_templates()[0] | template = code.filter_templates()[0] | ||||
| template.add("pop", "12345<ref>example ref</ref>") | template.add("pop", "12345<ref>example ref</ref>") | ||||
| @@ -1,4 +1,4 @@ | |||||
| # Copyright (C) 2012-2020 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| # Copyright (C) 2012-2023 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| # | # | ||||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| # of this software and associated documentation files (the "Software"), to deal | # of this software and associated documentation files (the "Software"), to deal | ||||
| @@ -24,6 +24,7 @@ Tests for the Wikicode class, which manages a list of nodes. | |||||
| from functools import partial | from functools import partial | ||||
| import re | import re | ||||
| import pickle | |||||
| from types import GeneratorType | from types import GeneratorType | ||||
| import pytest | import pytest | ||||
| @@ -60,6 +61,14 @@ def test_nodes(): | |||||
| code.__setattr__("nodes", object) | code.__setattr__("nodes", object) | ||||
| @pytest.mark.parametrize("protocol", range(pickle.HIGHEST_PROTOCOL + 1)) | |||||
| def test_pickling(protocol: int): | |||||
| """test Wikicode objects can be pickled""" | |||||
| code = parse("Have a {{template}} and a [[page|link]]") | |||||
| enc = pickle.dumps(code, protocol=protocol) | |||||
| assert pickle.loads(enc) == code | |||||
| def test_get(): | def test_get(): | ||||
| """test Wikicode.get()""" | """test Wikicode.get()""" | ||||
| code = parse("Have a {{template}} and a [[page|link]]") | code = parse("Have a {{template}} and a [[page|link]]") | ||||