diff --git a/.github/workflows/build-linux-wheels.yml b/.github/workflows/build-linux-wheels.yml new file mode 100644 index 0000000..067f84d --- /dev/null +++ b/.github/workflows/build-linux-wheels.yml @@ -0,0 +1,24 @@ +name: Build manylinux1 wheels + +on: push + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Build manylinux1 Python wheels + uses: RalfG/python-wheels-manylinux-build@0c24cb31441c7a1e6ea90d6a6408d406b2fee279 + with: + python-versions: 'cp35-cp35m cp36-cp36m cp37-cp37m cp38-cp38 cp39-cp39' + - name: Move to dist/ + run: | + mkdir -p dist + cp -v wheelhouse/*-manylinux1_x86_64.whl dist/ + - name: Publish package to PyPI + # Only actually publish if a new tag was pushed + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@37e305e7413032d8422456179fee28fac7d25187 + with: + user: __token__ + password: ${{ secrets.pypi_password }} diff --git a/.github/workflows/build-macos-wheels.yml b/.github/workflows/build-macos-wheels.yml new file mode 100644 index 0000000..5e93c1f --- /dev/null +++ b/.github/workflows/build-macos-wheels.yml @@ -0,0 +1,30 @@ +name: Build macOS wheels + +on: push + +jobs: + build: + runs-on: macos-latest + strategy: + matrix: + python-version: [3.5, 3.6, 3.7, 3.8, 3.9] + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Build wheels + run: | + python -m pip install --upgrade pip wheel setuptools + pip wheel . -w dist/ + ls dist/ + - name: Publish package to PyPI + # Only actually publish if a new tag was pushed + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + # We can't use the pypa action because of https://github.com/pypa/gh-action-pypi-publish/issues/15 + run: | + pip install twine + TWINE_USERNAME="__token__" \ + TWINE_PASSWORD="${{ secrets.pypi_password }}" \ + twine upload dist/* diff --git a/.travis.yml b/.travis.yml index 0ecf3fe..7668ab8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,14 @@ dist: xenial language: python python: - - 2.7 - - 3.4 - 3.5 - 3.6 - 3.7 - 3.8 + - 3.9 +arch: + - amd64 + - ppc64le install: - pip install coveralls - python setup.py develop diff --git a/CHANGELOG b/CHANGELOG index 53b3548..6d0cf86 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,9 +1,17 @@ v0.6 (unreleased): -- Added support for Python 3.8. +Thanks to everyone for their patience with this release! + +- Breaking change: dropped support for end-of-life Python 2.7 and 3.4. +- Added support for Python 3.8 and 3.9. +- Added binary wheels for Linux and macOS. - Updated Wikicode.matches() to recognize underscores as being equivalent to spaces. (#216) +- Add a 'default' parameter to Template.get, and implement dict-style item + access for template parameters. (#252) - Fixed a rare parsing bug involving deeply nested style tags. (#224) +- Updated HTML tag definitions. +- Internal refactoring and cleanup. v0.5.4 (released May 15, 2019): diff --git a/README.rst b/README.rst index a94f4e2..45db387 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,7 @@ mwparserfromhell **mwparserfromhell** (the *MediaWiki Parser from Hell*) is a Python package that provides an easy-to-use and outrageously powerful parser for MediaWiki_ -wikicode. It supports Python 2 and Python 3. +wikicode. It supports Python 3.5+. Developed by Earwig_ with contributions from `Σ`_, Legoktm_, and others. Full documentation is available on ReadTheDocs_. Development occurs on GitHub_. @@ -41,8 +41,7 @@ Normal usage is rather straightforward (where ``text`` is page text): >>> wikicode = mwparserfromhell.parse(text) ``wikicode`` is a ``mwparserfromhell.Wikicode`` object, which acts like an -ordinary ``str`` object (or ``unicode`` in Python 2) with some extra methods. -For example: +ordinary ``str`` object with some extra methods. For example: >>> text = "I has a template! {{foo|bar|baz|eggs=spam}} See it?" >>> wikicode = mwparserfromhell.parse(text) @@ -111,8 +110,6 @@ saving the page!) by calling ``str()`` on it: >>> text == code True -Likewise, use ``unicode(code)`` in Python 2. - Limitations ----------- diff --git a/appveyor.yml b/appveyor.yml index 20f9e35..a39024f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -22,22 +22,6 @@ environment: secure: gOIcvPxSC2ujuhwOzwj3v8xjq3CCYd8keFWVnguLM+gcL0e02qshDHy7gwZZwj0+ matrix: - - PYTHON: "C:\\Python27" - PYTHON_VERSION: "2.7" - PYTHON_ARCH: "32" - - - PYTHON: "C:\\Python27-x64" - PYTHON_VERSION: "2.7" - PYTHON_ARCH: "64" - - - PYTHON: "C:\\Python34" - PYTHON_VERSION: "3.4" - PYTHON_ARCH: "32" - - - PYTHON: "C:\\Python34-x64" - PYTHON_VERSION: "3.4" - PYTHON_ARCH: "64" - - PYTHON: "C:\\Python35" PYTHON_VERSION: "3.5" PYTHON_ARCH: "32" @@ -70,6 +54,14 @@ environment: PYTHON_VERSION: "3.8" PYTHON_ARCH: "64" + - PYTHON: "C:\\Python39" + PYTHON_VERSION: "3.9" + PYTHON_ARCH: "32" + + - PYTHON: "C:\\Python39-x64" + PYTHON_VERSION: "3.9" + PYTHON_ARCH: "64" + install: - "%PIP% install --disable-pip-version-check --user --upgrade pip" - "%PIP% install wheel twine" diff --git a/docs/api/mwparserfromhell.nodes.rst b/docs/api/mwparserfromhell.nodes.rst index 2cbaa1c..38058f2 100644 --- a/docs/api/mwparserfromhell.nodes.rst +++ b/docs/api/mwparserfromhell.nodes.rst @@ -9,6 +9,14 @@ nodes Package .. autoclass:: mwparserfromhell.nodes.Node :special-members: +:mod:`_base` Module +---------------------- + +.. automodule:: mwparserfromhell.nodes._base + :members: + :undoc-members: + :show-inheritance: + :mod:`argument` Module ---------------------- diff --git a/docs/api/mwparserfromhell.parser.rst b/docs/api/mwparserfromhell.parser.rst index c7c8639..72ee9eb 100644 --- a/docs/api/mwparserfromhell.parser.rst +++ b/docs/api/mwparserfromhell.parser.rst @@ -23,6 +23,13 @@ parser Package :members: :undoc-members: +:mod:`errors` Module +-------------------- + +.. automodule:: mwparserfromhell.parser.errors + :members: + :undoc-members: + :mod:`tokenizer` Module ----------------------- diff --git a/docs/api/mwparserfromhell.rst b/docs/api/mwparserfromhell.rst index 63af111..c0bdc88 100644 --- a/docs/api/mwparserfromhell.rst +++ b/docs/api/mwparserfromhell.rst @@ -8,27 +8,12 @@ mwparserfromhell Package :members: :undoc-members: -:mod:`compat` Module --------------------- - -.. automodule:: mwparserfromhell.compat - :members: - :undoc-members: - :mod:`definitions` Module ------------------------- .. automodule:: mwparserfromhell.definitions :members: -:mod:`smart_list` Module ------------------------- - -.. automodule:: mwparserfromhell.smart_list - :members: SmartList, _ListProxy - :undoc-members: - :show-inheritance: - :mod:`string_mixin` Module -------------------------- @@ -58,3 +43,4 @@ Subpackages mwparserfromhell.nodes mwparserfromhell.parser + mwparserfromhell.smart_list diff --git a/docs/api/mwparserfromhell.smart_list.rst b/docs/api/mwparserfromhell.smart_list.rst new file mode 100644 index 0000000..9312374 --- /dev/null +++ b/docs/api/mwparserfromhell.smart_list.rst @@ -0,0 +1,30 @@ +smart_list Package +================== + +:mod:`smart_list` Package +------------------------- + +.. automodule:: mwparserfromhell.smart_list + :members: + :undoc-members: + +:mod:`list_proxy` Module +--------------------- + +.. automodule:: mwparserfromhell.smart_list.list_proxy + :members: + :undoc-members: + +:mod:`smart_list` Module +--------------------- + +.. automodule:: mwparserfromhell.smart_list.smart_list + :members: + :undoc-members: + +:mod:`utils` Module +--------------------- + +.. automodule:: mwparserfromhell.smart_list.utils + :members: + :undoc-members: diff --git a/docs/changelog.rst b/docs/changelog.rst index 1ca7411..59bedc8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,11 +7,21 @@ v0.6 Unreleased (`changes `__): -- Added support for Python 3.8. -- Updated Wikicode.matches() to recognize underscores as being equivalent - to spaces. (`#216 `_) +Thanks to everyone for their patience with this release! + +- Breaking change: dropped support for end-of-life Python 2.7 and 3.4. +- Added support for Python 3.8 and 3.9. +- Added binary wheels for Linux and macOS. +- Updated :meth:`.Wikicode.matches` to recognize underscores as being + equivalent to spaces. + (`#216 `_) +- Add a `default` parameter to :meth:`.Template.get`, and implement dict-style + item access for template parameters. + (`#252 `_) - Fixed a rare parsing bug involving deeply nested style tags. (`#224 `_) +- Updated HTML tag definitions. +- Internal refactoring and cleanup. v0.5.4 ------ diff --git a/docs/conf.py b/docs/conf.py index 9666cd0..9946f3b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -42,7 +42,7 @@ master_doc = 'index' # General information about the project. project = u'mwparserfromhell' -copyright = u'2012–2019 Ben Kurtovic' +copyright = u'2012–2020 Ben Kurtovic' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/docs/index.rst b/docs/index.rst index 8a1621f..6d9fcf9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,7 +3,7 @@ MWParserFromHell v\ |version| Documentation :mod:`mwparserfromhell` (the *MediaWiki Parser from Hell*) is a Python package that provides an easy-to-use and outrageously powerful parser for MediaWiki_ -wikicode. It supports Python 2 and Python 3. +wikicode. It supports Python 3.5+. Developed by Earwig_ with contributions from `Σ`_, Legoktm_, and others. Development occurs on GitHub_. diff --git a/docs/usage.rst b/docs/usage.rst index ee667fd..2cdc690 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -7,8 +7,7 @@ Normal usage is rather straightforward (where ``text`` is page text):: >>> wikicode = mwparserfromhell.parse(text) ``wikicode`` is a :class:`mwparserfromhell.Wikicode <.Wikicode>` object, which -acts like an ordinary ``str`` object (or ``unicode`` in Python 2) with some -extra methods. For example:: +acts like an ordinary ``str`` object with some extra methods. For example:: >>> text = "I has a template! {{foo|bar|baz|eggs=spam}} See it?" >>> wikicode = mwparserfromhell.parse(text) @@ -78,7 +77,6 @@ saving the page!) by calling :func:`str` on it:: >>> text == code True -(Likewise, use :func:`unicode(code) ` in Python 2.) For more tips, check out :class:`Wikicode's full method list <.Wikicode>` and the :mod:`list of Nodes <.nodes>`. diff --git a/mwparserfromhell/__init__.py b/mwparserfromhell/__init__.py index f867e26..841f992 100644 --- a/mwparserfromhell/__init__.py +++ b/mwparserfromhell/__init__.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2019 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -27,12 +25,12 @@ outrageously powerful parser for `MediaWiki `_ wikico """ __author__ = "Ben Kurtovic" -__copyright__ = "Copyright (C) 2012-2019 Ben Kurtovic" +__copyright__ = "Copyright (C) 2012-2020 Ben Kurtovic" __license__ = "MIT License" __version__ = "0.6.dev0" __email__ = "ben.kurtovic@gmail.com" -from . import (compat, definitions, nodes, parser, smart_list, string_mixin, +from . import (definitions, nodes, parser, smart_list, string_mixin, utils, wikicode) parse = utils.parse_anything diff --git a/mwparserfromhell/compat.py b/mwparserfromhell/compat.py deleted file mode 100644 index 85f9d48..0000000 --- a/mwparserfromhell/compat.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Implements support for both Python 2 and Python 3 by defining common types in -terms of their Python 2/3 variants. For example, :class:`str` is set to -:class:`unicode` on Python 2 but :class:`str` on Python 3; likewise, -:class:`bytes` is :class:`str` on 2 but :class:`bytes` on 3. These types are -meant to be imported directly from within the parser's modules. -""" - -import sys - -py3k = (sys.version_info[0] == 3) - -if py3k: - bytes = bytes - str = str - range = range - import html.entities as htmlentities - -else: - bytes = str - str = unicode - range = xrange - import htmlentitydefs as htmlentities - -del sys diff --git a/mwparserfromhell/definitions.py b/mwparserfromhell/definitions.py index 4399970..c8d37cd 100644 --- a/mwparserfromhell/definitions.py +++ b/mwparserfromhell/definitions.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -28,36 +26,77 @@ When updating this file, please also update the the C tokenizer version: - mwparserfromhell/parser/ctokenizer/definitions.h """ -from __future__ import unicode_literals - __all__ = ["get_html_tag", "is_parsable", "is_visible", "is_single", "is_single_only", "is_scheme"] URI_SCHEMES = { - # [mediawiki/core.git]/includes/DefaultSettings.php @ 374a0ad943 - "http": True, "https": True, "ftp": True, "ftps": True, "ssh": True, - "sftp": True, "irc": True, "ircs": True, "xmpp": False, "sip": False, - "sips": False, "gopher": True, "telnet": True, "nntp": True, - "worldwind": True, "mailto": False, "tel": False, "sms": False, - "news": False, "svn": True, "git": True, "mms": True, "bitcoin": False, - "magnet": False, "urn": False, "geo": False + # [wikimedia/mediawiki.git]/includes/DefaultSettings.php @ 5c660de5d0 + "bitcoin": False, + "ftp": True, + "ftps": True, + "geo": False, + "git": True, + "gopher": True, + "http": True, + "https": True, + "irc": True, + "ircs": True, + "magnet": False, + "mailto": False, + "mms": True, + "news": False, + "nntp": True, + "redis": True, + "sftp": True, + "sip": False, + "sips": False, + "sms": False, + "ssh": True, + "svn": True, + "tel": False, + "telnet": True, + "urn": False, + "worldwind": True, + "xmpp": False, } PARSER_BLACKLIST = [ - # enwiki extensions @ 2013-06-28 - "categorytree", "gallery", "hiero", "imagemap", "inputbox", "math", - "nowiki", "pre", "score", "section", "source", "syntaxhighlight", - "templatedata", "timeline" + # https://www.mediawiki.org/wiki/Parser_extension_tags @ 2020-12-21 + "categorytree", + "ce", + "chem", + "gallery", + "graph", + "hiero", + "imagemap", + "inputbox", + "math", + "nowiki", + "pre", + "score", + "section", + "source", + "syntaxhighlight", + "templatedata", + "timeline", ] INVISIBLE_TAGS = [ - # enwiki extensions @ 2013-06-28 - "categorytree", "gallery", "imagemap", "inputbox", "math", "score", - "section", "templatedata", "timeline" + # https://www.mediawiki.org/wiki/Parser_extension_tags @ 2020-12-21 + "categorytree", + "gallery", + "graph", + "imagemap", + "inputbox", + "math", + "score", + "section", + "templatedata", + "timeline" ] -# [mediawiki/core.git]/includes/Sanitizer.php @ 065bec63ea -SINGLE_ONLY = ["br", "hr", "meta", "link", "img", "wbr"] +# [wikimedia/mediawiki.git]/includes/parser/Sanitizer.php @ 95e17ee645 +SINGLE_ONLY = ["br", "wbr", "hr", "meta", "link", "img"] SINGLE = SINGLE_ONLY + ["li", "dt", "dd", "th", "td", "tr"] MARKUP_TO_HTML = { diff --git a/mwparserfromhell/nodes/__init__.py b/mwparserfromhell/nodes/__init__.py index 1e38254..18a1780 100644 --- a/mwparserfromhell/nodes/__init__.py +++ b/mwparserfromhell/nodes/__init__.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -29,44 +27,8 @@ the name of a :class:`.Template` is a :class:`.Wikicode` object that can contain text or more templates. """ -from __future__ import unicode_literals - -from ..compat import str -from ..string_mixin import StringMixIn - -__all__ = ["Argument", "Comment", "ExternalLink", "HTMLEntity", "Heading", - "Node", "Tag", "Template", "Text", "Wikilink"] - -class Node(StringMixIn): - """Represents the base Node type, demonstrating the methods to override. - - :meth:`__unicode__` must be overridden. It should return a ``unicode`` or - (``str`` in py3k) representation of the node. If the node contains - :class:`.Wikicode` objects inside of it, :meth:`__children__` should be a - generator that iterates over them. If the node is printable - (shown when the page is rendered), :meth:`__strip__` should return its - printable version, stripping out any formatting marks. It does not have to - return a string, but something that can be converted to a string with - ``str()``. Finally, :meth:`__showtree__` can be overridden to build a - nice tree representation of the node, if desired, for - :meth:`~.Wikicode.get_tree`. - """ - def __unicode__(self): - raise NotImplementedError() - - def __children__(self): - return - # pylint: disable=unreachable - yield # pragma: no cover (this is a generator that yields nothing) - - def __strip__(self, **kwargs): - return None - - def __showtree__(self, write, get, mark): - write(str(self)) - - from . import extras +from ._base import Node from .text import Text from .argument import Argument from .comment import Comment @@ -76,3 +38,6 @@ from .html_entity import HTMLEntity from .tag import Tag from .template import Template from .wikilink import Wikilink + +__all__ = ["Argument", "Comment", "ExternalLink", "HTMLEntity", "Heading", + "Node", "Tag", "Template", "Text", "Wikilink"] diff --git a/mwparserfromhell/nodes/_base.py b/mwparserfromhell/nodes/_base.py new file mode 100644 index 0000000..e6b2a50 --- /dev/null +++ b/mwparserfromhell/nodes/_base.py @@ -0,0 +1,50 @@ +# Copyright (C) 2012-2020 Ben Kurtovic +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ..string_mixin import StringMixIn + +__all__ = ["Node"] + +class Node(StringMixIn): + """Represents the base Node type, demonstrating the methods to override. + + :meth:`__str__` must be overridden. It should return a ``str`` + representation of the node. If the node contains :class:`.Wikicode` + objects inside of it, :meth:`__children__` should be a generator that + iterates over them. If the node is printable (shown when the page is + rendered), :meth:`__strip__` should return its printable version, + stripping out any formatting marks. It does not have to return a string, + but something that can be converted to a string with ``str()``. Finally, + :meth:`__showtree__` can be overridden to build a nice tree representation + of the node, if desired, for :meth:`~.Wikicode.get_tree`. + """ + def __str__(self): + raise NotImplementedError() + + def __children__(self): + return + # pylint: disable=unreachable + yield # pragma: no cover (this is a generator that yields nothing) + + def __strip__(self, **kwargs): + return None + + def __showtree__(self, write, get, mark): + write(str(self)) diff --git a/mwparserfromhell/nodes/argument.py b/mwparserfromhell/nodes/argument.py index 2da1467..501788f 100644 --- a/mwparserfromhell/nodes/argument.py +++ b/mwparserfromhell/nodes/argument.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2019 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,10 +18,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals -from . import Node -from ..compat import str +from ._base import Node from ..utils import parse_anything __all__ = ["Argument"] @@ -32,11 +28,11 @@ class Argument(Node): """Represents a template argument substitution, like ``{{{foo}}}``.""" def __init__(self, name, default=None): - super(Argument, self).__init__() + super().__init__() self.name = name self.default = default - def __unicode__(self): + def __str__(self): start = "{{{" + str(self.name) if self.default is not None: return start + "|" + str(self.default) + "}}}" diff --git a/mwparserfromhell/nodes/comment.py b/mwparserfromhell/nodes/comment.py index 40224ba..fd8a9cc 100644 --- a/mwparserfromhell/nodes/comment.py +++ b/mwparserfromhell/nodes/comment.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2019 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,10 +18,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals -from . import Node -from ..compat import str +from ._base import Node __all__ = ["Comment"] @@ -31,10 +27,10 @@ class Comment(Node): """Represents a hidden HTML comment, like ````.""" def __init__(self, contents): - super(Comment, self).__init__() + super().__init__() self.contents = contents - def __unicode__(self): + def __str__(self): return "" @property diff --git a/mwparserfromhell/nodes/external_link.py b/mwparserfromhell/nodes/external_link.py index 22b2ef7..0423e2a 100644 --- a/mwparserfromhell/nodes/external_link.py +++ b/mwparserfromhell/nodes/external_link.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2019 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,10 +18,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals -from . import Node -from ..compat import str +from ._base import Node from ..utils import parse_anything __all__ = ["ExternalLink"] @@ -32,12 +28,12 @@ class ExternalLink(Node): """Represents an external link, like ``[http://example.com/ Example]``.""" def __init__(self, url, title=None, brackets=True): - super(ExternalLink, self).__init__() + super().__init__() self.url = url self.title = title self.brackets = brackets - def __unicode__(self): + def __str__(self): if self.brackets: if self.title is not None: return "[" + str(self.url) + " " + str(self.title) + "]" @@ -82,6 +78,7 @@ class ExternalLink(Node): @url.setter def url(self, value): + # pylint: disable=import-outside-toplevel from ..parser import contexts self._url = parse_anything(value, contexts.EXT_LINK_URI) diff --git a/mwparserfromhell/nodes/extras/__init__.py b/mwparserfromhell/nodes/extras/__init__.py index 9347c61..ef76125 100644 --- a/mwparserfromhell/nodes/extras/__init__.py +++ b/mwparserfromhell/nodes/extras/__init__.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright (C) 2012-2016 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/mwparserfromhell/nodes/extras/attribute.py b/mwparserfromhell/nodes/extras/attribute.py index 4312199..9e7b7cd 100644 --- a/mwparserfromhell/nodes/extras/attribute.py +++ b/mwparserfromhell/nodes/extras/attribute.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2019 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,9 +18,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals -from ...compat import str from ...string_mixin import StringMixIn from ...utils import parse_anything @@ -38,7 +34,7 @@ class Attribute(StringMixIn): def __init__(self, name, value=None, quotes='"', pad_first=" ", pad_before_eq="", pad_after_eq=""): - super(Attribute, self).__init__() + super().__init__() self.name = name self._quotes = None self.value = value @@ -47,7 +43,7 @@ class Attribute(StringMixIn): self.pad_before_eq = pad_before_eq self.pad_after_eq = pad_after_eq - def __unicode__(self): + def __str__(self): result = self.pad_first + str(self.name) + self.pad_before_eq if self.value is not None: result += "=" + self.pad_after_eq diff --git a/mwparserfromhell/nodes/extras/parameter.py b/mwparserfromhell/nodes/extras/parameter.py index fb0aac0..44fb3aa 100644 --- a/mwparserfromhell/nodes/extras/parameter.py +++ b/mwparserfromhell/nodes/extras/parameter.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2019 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,10 +18,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals import re -from ...compat import str from ...string_mixin import StringMixIn from ...utils import parse_anything @@ -39,12 +35,12 @@ class Parameter(StringMixIn): """ def __init__(self, name, value, showkey=True): - super(Parameter, self).__init__() + super().__init__() self.name = name self.value = value self.showkey = showkey - def __unicode__(self): + def __str__(self): if self.showkey: return str(self.name) + "=" + str(self.value) return str(self.value) diff --git a/mwparserfromhell/nodes/heading.py b/mwparserfromhell/nodes/heading.py index 426e742..77f2f68 100644 --- a/mwparserfromhell/nodes/heading.py +++ b/mwparserfromhell/nodes/heading.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2019 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,10 +18,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals -from . import Node -from ..compat import str +from ._base import Node from ..utils import parse_anything __all__ = ["Heading"] @@ -32,11 +28,11 @@ class Heading(Node): """Represents a section heading in wikicode, like ``== Foo ==``.""" def __init__(self, title, level): - super(Heading, self).__init__() + super().__init__() self.title = title self.level = level - def __unicode__(self): + def __str__(self): return ("=" * self.level) + str(self.title) + ("=" * self.level) def __children__(self): diff --git a/mwparserfromhell/nodes/html_entity.py b/mwparserfromhell/nodes/html_entity.py index ea534e9..fa3fa4d 100644 --- a/mwparserfromhell/nodes/html_entity.py +++ b/mwparserfromhell/nodes/html_entity.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,10 +18,9 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals +import html.entities as htmlentities -from . import Node -from ..compat import htmlentities, py3k, str +from ._base import Node __all__ = ["HTMLEntity"] @@ -31,7 +28,7 @@ class HTMLEntity(Node): """Represents an HTML entity, like `` ``, either named or unnamed.""" def __init__(self, value, named=None, hexadecimal=False, hex_char="x"): - super(HTMLEntity, self).__init__() + super().__init__() self._value = value if named is None: # Try to guess whether or not the entity is named try: @@ -51,7 +48,7 @@ class HTMLEntity(Node): self._hexadecimal = hexadecimal self._hex_char = hex_char - def __unicode__(self): + def __str__(self): if self.named: return "&{};".format(self.value) if self.hexadecimal: @@ -63,32 +60,6 @@ class HTMLEntity(Node): return self.normalize() return self - if not py3k: - @staticmethod - def _unichr(value): - """Implement builtin unichr() with support for non-BMP code points. - - On wide Python builds, this functions like the normal unichr(). On - narrow builds, this returns the value's encoded surrogate pair. - """ - try: - return unichr(value) - except ValueError: - # Test whether we're on the wide or narrow Python build. Check - # the length of a non-BMP code point - # (U+1F64A, SPEAK-NO-EVIL MONKEY): - if len("\U0001F64A") == 1: # pragma: no cover - raise - # Ensure this is within the range we can encode: - if value > 0x10FFFF: - raise ValueError("unichr() arg not in range(0x110000)") - code = value - 0x10000 - if value < 0: # Invalid code point - raise - lead = 0xD800 + (code >> 10) - trail = 0xDC00 + (code % (1 << 10)) - return unichr(lead) + unichr(trail) - @property def value(self): """The string value of the HTML entity.""" @@ -126,21 +97,23 @@ class HTMLEntity(Node): int(newval) except ValueError: try: - int(newval, 16) + intval = int(newval, 16) except ValueError: if newval not in htmlentities.entitydefs: - raise ValueError("entity value is not a valid name") + raise ValueError( + "entity value {!r} is not a valid name".format(newval)) from None self._named = True self._hexadecimal = False else: - if int(newval, 16) < 0 or int(newval, 16) > 0x10FFFF: - raise ValueError("entity value is not in range(0x110000)") + if intval < 0 or intval > 0x10FFFF: + raise ValueError( + "entity value 0x{:x} is not in range(0x110000)".format(intval)) from None self._named = False self._hexadecimal = True else: test = int(newval, 16 if self.hexadecimal else 10) if test < 0 or test > 0x10FFFF: - raise ValueError("entity value is not in range(0x110000)") + raise ValueError("entity value {} is not in range(0x110000)".format(test)) self._named = False self._value = newval @@ -148,13 +121,13 @@ class HTMLEntity(Node): def named(self, newval): newval = bool(newval) if newval and self.value not in htmlentities.entitydefs: - raise ValueError("entity value is not a valid name") + raise ValueError("entity value {!r} is not a valid name".format(self.value)) if not newval: try: int(self.value, 16) - except ValueError: - err = "current entity value is not a valid Unicode codepoint" - raise ValueError(err) + except ValueError as exc: + raise ValueError("current entity value {!r} is not a valid " + "Unicode codepoint".format(self.value)) from exc self._named = newval @hexadecimal.setter @@ -173,9 +146,8 @@ class HTMLEntity(Node): def normalize(self): """Return the unicode character represented by the HTML entity.""" - chrfunc = chr if py3k else HTMLEntity._unichr if self.named: - return chrfunc(htmlentities.name2codepoint[self.value]) + return chr(htmlentities.name2codepoint[self.value]) if self.hexadecimal: - return chrfunc(int(self.value, 16)) - return chrfunc(int(self.value)) + return chr(int(self.value, 16)) + return chr(int(self.value)) diff --git a/mwparserfromhell/nodes/tag.py b/mwparserfromhell/nodes/tag.py index 70a2876..eb59c5b 100644 --- a/mwparserfromhell/nodes/tag.py +++ b/mwparserfromhell/nodes/tag.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2019 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,11 +18,9 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals -from . import Node +from ._base import Node from .extras import Attribute -from ..compat import str from ..definitions import is_visible from ..utils import parse_anything @@ -37,7 +33,7 @@ class Tag(Node): self_closing=False, invalid=False, implicit=False, padding="", closing_tag=None, wiki_style_separator=None, closing_wiki_markup=None): - super(Tag, self).__init__() + super().__init__() self.tag = tag self.contents = contents self._attrs = attrs if attrs else [] @@ -53,7 +49,7 @@ class Tag(Node): if closing_wiki_markup is not None: self.closing_wiki_markup = closing_wiki_markup - def __unicode__(self): + def __str__(self): if self.wiki_markup: if self.attributes: attrs = "".join([str(attr) for attr in self.attributes]) @@ -63,10 +59,9 @@ class Tag(Node): separator = self.wiki_style_separator or "" if self.self_closing: return self.wiki_markup + attrs + padding + separator - else: - close = self.closing_wiki_markup or "" - return self.wiki_markup + attrs + padding + separator + \ - str(self.contents) + close + close = self.closing_wiki_markup or "" + return self.wiki_markup + attrs + padding + separator + \ + str(self.contents) + close result = (" +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,36 +18,37 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals from collections import defaultdict import re -from . import HTMLEntity, Node, Text +from ._base import Node +from .html_entity import HTMLEntity +from .text import Text from .extras import Parameter -from ..compat import range, str from ..utils import parse_anything __all__ = ["Template"] FLAGS = re.DOTALL | re.UNICODE +# Used to allow None as a valid fallback value +_UNSET = object() class Template(Node): """Represents a template in wikicode, like ``{{foo}}``.""" def __init__(self, name, params=None): - super(Template, self).__init__() + super().__init__() self.name = name if params: self._params = params else: self._params = [] - def __unicode__(self): + def __str__(self): if self.params: params = "|".join([str(param) for param in self.params]) return "{{" + str(self.name) + "|" + params + "}}" - else: - return "{{" + str(self.name) + "}}" + return "{{" + str(self.name) + "}}" def __children__(self): yield self.name @@ -103,6 +102,7 @@ class Template(Node): confidence = float(best) / sum(values) if confidence > 0.5: return tuple(theories.keys())[values.index(best)] + return None @staticmethod def _blank_param_value(value): @@ -211,23 +211,29 @@ class Template(Node): return True return False - has_param = lambda self, name, ignore_empty=False: \ - self.has(name, ignore_empty) - has_param.__doc__ = "Alias for :meth:`has`." + def has_param(self, name, ignore_empty=False): + """Alias for :meth:`has`.""" + return self.has(name, ignore_empty) - def get(self, name): + def get(self, name, default=_UNSET): """Get the parameter whose name is *name*. The returned object is a :class:`.Parameter` instance. Raises - :exc:`ValueError` if no parameter has this name. Since multiple - parameters can have the same name, we'll return the last match, since - the last parameter is the only one read by the MediaWiki parser. + :exc:`ValueError` if no parameter has this name. If *default* is set, + returns that instead. Since multiple parameters can have the same name, + we'll return the last match, since the last parameter is the only one + read by the MediaWiki parser. """ name = str(name).strip() for param in reversed(self.params): if param.name.strip() == name: return param - raise ValueError(name) + if default is _UNSET: + raise ValueError(name) + return default + + def __getitem__(self, name): + return self.get(name) def add(self, name, value, showkey=None, before=None, preserve_spacing=True): @@ -309,6 +315,9 @@ class Template(Node): self.params.append(param) return param + def __setitem__(self, name, value): + return self.add(name, value) + def remove(self, param, keep_field=False): """Remove a parameter from the template, identified by *param*. @@ -330,19 +339,20 @@ class Template(Node): hidden name, if it exists, or the first instance). """ if isinstance(param, Parameter): - return self._remove_exact(param, keep_field) + self._remove_exact(param, keep_field) + return name = str(param).strip() removed = False to_remove = [] - for i, param in enumerate(self.params): - if param.name.strip() == name: + for i, par in enumerate(self.params): + if par.name.strip() == name: if keep_field: if self._should_remove(i, name): to_remove.append(i) else: - self._blank_param_value(param.value) + self._blank_param_value(par.value) keep_field = False else: self._fix_dependendent_params(i) @@ -354,3 +364,6 @@ class Template(Node): raise ValueError(name) for i in reversed(to_remove): self.params.pop(i) + + def __delitem__(self, param): + return self.remove(param) diff --git a/mwparserfromhell/nodes/text.py b/mwparserfromhell/nodes/text.py index 1c47c7b..3242170 100644 --- a/mwparserfromhell/nodes/text.py +++ b/mwparserfromhell/nodes/text.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2019 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,10 +18,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals -from . import Node -from ..compat import str +from ._base import Node __all__ = ["Text"] @@ -31,10 +27,10 @@ class Text(Node): """Represents ordinary, unformatted text with no special properties.""" def __init__(self, value): - super(Text, self).__init__() + super().__init__() self.value = value - def __unicode__(self): + def __str__(self): return self.value def __strip__(self, **kwargs): diff --git a/mwparserfromhell/nodes/wikilink.py b/mwparserfromhell/nodes/wikilink.py index 265a100..7304168 100644 --- a/mwparserfromhell/nodes/wikilink.py +++ b/mwparserfromhell/nodes/wikilink.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2019 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,10 +18,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals -from . import Node -from ..compat import str +from ._base import Node from ..utils import parse_anything __all__ = ["Wikilink"] @@ -32,11 +28,11 @@ class Wikilink(Node): """Represents an internal wikilink, like ``[[Foo|Bar]]``.""" def __init__(self, title, text=None): - super(Wikilink, self).__init__() + super().__init__() self.title = title self.text = text - def __unicode__(self): + def __str__(self): if self.text is not None: return "[[" + str(self.title) + "|" + str(self.text) + "]]" return "[[" + str(self.title) + "]]" diff --git a/mwparserfromhell/parser/__init__.py b/mwparserfromhell/parser/__init__.py index dc769a2..3fad93b 100644 --- a/mwparserfromhell/parser/__init__.py +++ b/mwparserfromhell/parser/__init__.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -26,20 +24,8 @@ modules: the :mod:`.tokenizer` and the :mod:`.builder`. This module joins them together into one interface. """ -class ParserError(Exception): - """Exception raised when an internal error occurs while parsing. - - This does not mean that the wikicode was invalid, because invalid markup - should still be parsed correctly. This means that the parser caught itself - with an impossible internal state and is bailing out before other problems - can happen. Its appearance indicates a bug. - """ - def __init__(self, extra): - msg = "This is a bug and should be reported. Info: {}.".format(extra) - super(ParserError, self).__init__(msg) - - from .builder import Builder +from .errors import ParserError try: from ._tokenizer import CTokenizer use_c = True @@ -50,7 +36,7 @@ except ImportError: __all__ = ["use_c", "Parser", "ParserError"] -class Parser(object): +class Parser: """Represents a parser for wikicode. Actual parsing is a two-step process: first, the text is split up into a diff --git a/mwparserfromhell/parser/builder.py b/mwparserfromhell/parser/builder.py index f1b9689..2f58455 100644 --- a/mwparserfromhell/parser/builder.py +++ b/mwparserfromhell/parser/builder.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,10 +18,9 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals -from . import tokens, ParserError -from ..compat import str +from . import tokens +from .errors import ParserError from ..nodes import (Argument, Comment, ExternalLink, Heading, HTMLEntity, Tag, Template, Text, Wikilink) from ..nodes.extras import Attribute, Parameter @@ -45,7 +42,7 @@ def _add_handler(token_type): return decorator -class Builder(object): +class Builder: """Builds a tree of nodes out of a sequence of tokens. To use, pass a list of :class:`.Token`\\ s to the :meth:`build` method. The @@ -201,8 +198,7 @@ class Builder(object): if isinstance(token, tokens.HeadingEnd): title = self._pop() return Heading(title, level) - else: - self._write(self._handle_token(token)) + self._write(self._handle_token(token)) raise ParserError("_handle_heading() missed a close token") @_add_handler(tokens.CommentStart) @@ -214,8 +210,7 @@ class Builder(object): if isinstance(token, tokens.CommentEnd): contents = self._pop() return Comment(contents) - else: - self._write(self._handle_token(token)) + self._write(self._handle_token(token)) raise ParserError("_handle_comment() missed a close token") def _handle_attribute(self, start): @@ -286,7 +281,7 @@ class Builder(object): return _HANDLERS[type(token)](self, token) except KeyError: err = "_handle_token() got unexpected {0}" - raise ParserError(err.format(type(token).__name__)) + raise ParserError(err.format(type(token).__name__)) from None def build(self, tokenlist): """Build a Wikicode object from a list tokens and return it.""" diff --git a/mwparserfromhell/parser/contexts.py b/mwparserfromhell/parser/contexts.py index cac5250..aecc2f3 100644 --- a/mwparserfromhell/parser/contexts.py +++ b/mwparserfromhell/parser/contexts.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright (C) 2012-2019 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/mwparserfromhell/parser/ctokenizer/common.h b/mwparserfromhell/parser/ctokenizer/common.h index f3d51f4..22a6b81 100644 --- a/mwparserfromhell/parser/ctokenizer/common.h +++ b/mwparserfromhell/parser/ctokenizer/common.h @@ -23,7 +23,7 @@ SOFTWARE. #pragma once #ifndef PY_SSIZE_T_CLEAN -#define PY_SSIZE_T_CLEAN // See: https://docs.python.org/2/c-api/arg.html +#define PY_SSIZE_T_CLEAN // See: https://docs.python.org/3/c-api/arg.html #endif #include @@ -34,10 +34,6 @@ SOFTWARE. /* Compatibility macros */ -#if PY_MAJOR_VERSION >= 3 -#define IS_PY3K -#endif - #ifndef uint64_t #define uint64_t unsigned PY_LONG_LONG #endif @@ -48,20 +44,8 @@ SOFTWARE. /* Unicode support macros */ -#if defined(IS_PY3K) && PY_MINOR_VERSION >= 3 -#define PEP_393 -#endif - -#ifdef PEP_393 -#define Unicode Py_UCS4 #define PyUnicode_FROM_SINGLE(chr) \ PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, &(chr), 1) -#else -#define Unicode Py_UNICODE -#define PyUnicode_FROM_SINGLE(chr) \ - PyUnicode_FromUnicode(&(chr), 1) -#define PyUnicode_GET_LENGTH PyUnicode_GET_SIZE -#endif /* Error handling macros */ @@ -85,13 +69,9 @@ extern PyObject* definitions; typedef struct { Py_ssize_t capacity; Py_ssize_t length; -#ifdef PEP_393 PyObject* object; int kind; void* data; -#else - Py_UNICODE* data; -#endif } Textbuffer; typedef struct { @@ -111,12 +91,8 @@ typedef struct Stack Stack; typedef struct { PyObject* object; /* base PyUnicodeObject object */ Py_ssize_t length; /* length of object, in code points */ -#ifdef PEP_393 int kind; /* object's kind value */ void* data; /* object's raw unicode buffer */ -#else - Py_UNICODE* buf; /* object's internal buffer */ -#endif } TokenizerInput; typedef struct avl_tree_node avl_tree; diff --git a/mwparserfromhell/parser/ctokenizer/definitions.c b/mwparserfromhell/parser/ctokenizer/definitions.c index e247234..323d8a1 100644 --- a/mwparserfromhell/parser/ctokenizer/definitions.c +++ b/mwparserfromhell/parser/ctokenizer/definitions.c @@ -1,5 +1,5 @@ /* -Copyright (C) 2012-2016 Ben Kurtovic +Copyright (C) 2012-2020 Ben Kurtovic Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in @@ -28,29 +28,79 @@ SOFTWARE. */ static const char* URI_SCHEMES[] = { - "http", "https", "ftp", "ftps", "ssh", "sftp", "irc", "ircs", "xmpp", - "sip", "sips", "gopher", "telnet", "nntp", "worldwind", "mailto", "tel", - "sms", "news", "svn", "git", "mms", "bitcoin", "magnet", "urn", "geo", NULL + "bitcoin", + "ftp", + "ftps", + "geo", + "git", + "gopher", + "http", + "https", + "irc", + "ircs", + "magnet", + "mailto", + "mms", + "news", + "nntp", + "redis", + "sftp", + "sip", + "sips", + "sms", + "ssh", + "svn", + "tel", + "telnet", + "urn", + "worldwind", + "xmpp", + NULL, }; static const char* URI_SCHEMES_AUTHORITY_OPTIONAL[] = { - "xmpp", "sip", "sips", "mailto", "tel", "sms", "news", "bitcoin", "magnet", - "urn", "geo", NULL + "bitcoin", + "geo", + "magnet", + "mailto", + "news", + "sip", + "sips", + "sms", + "tel", + "urn", + "xmpp", + NULL, }; static const char* PARSER_BLACKLIST[] = { - "categorytree", "gallery", "hiero", "imagemap", "inputbox", "math", - "nowiki", "pre", "score", "section", "source", "syntaxhighlight", - "templatedata", "timeline", NULL + "categorytree", + "ce", + "chem", + "gallery", + "graph", + "hiero", + "imagemap", + "inputbox", + "math", + "nowiki", + "pre", + "score", + "section", + "source", + "syntaxhighlight", + "templatedata", + "timeline", + NULL, }; static const char* SINGLE[] = { - "br", "hr", "meta", "link", "img", "li", "dt", "dd", "th", "td", "tr", - "wbr", NULL + "br", "wbr", "hr", "meta", "link", "img", "li", "dt", "dd", "th", "td", + "tr", NULL }; static const char* SINGLE_ONLY[] = { - "br", "hr", "meta", "link", "img", "wbr", NULL + "br", "wbr", "hr", "meta", "link", "img", NULL }; /* diff --git a/mwparserfromhell/parser/ctokenizer/tag_data.h b/mwparserfromhell/parser/ctokenizer/tag_data.h index c2e9303..7e8edcb 100644 --- a/mwparserfromhell/parser/ctokenizer/tag_data.h +++ b/mwparserfromhell/parser/ctokenizer/tag_data.h @@ -32,7 +32,7 @@ typedef struct { Textbuffer* pad_first; Textbuffer* pad_before_eq; Textbuffer* pad_after_eq; - Unicode quoter; + Py_UCS4 quoter; Py_ssize_t reset; } TagData; diff --git a/mwparserfromhell/parser/ctokenizer/textbuffer.c b/mwparserfromhell/parser/ctokenizer/textbuffer.c index 3fd129f..e37b7c3 100644 --- a/mwparserfromhell/parser/ctokenizer/textbuffer.c +++ b/mwparserfromhell/parser/ctokenizer/textbuffer.c @@ -29,23 +29,16 @@ SOFTWARE. /* Internal allocation function for textbuffers. */ -static int internal_alloc(Textbuffer* self, Unicode maxchar) +static int internal_alloc(Textbuffer* self, Py_UCS4 maxchar) { self->capacity = INITIAL_CAPACITY; self->length = 0; -#ifdef PEP_393 self->object = PyUnicode_New(self->capacity, maxchar); if (!self->object) return -1; self->kind = PyUnicode_KIND(self->object); self->data = PyUnicode_DATA(self->object); -#else - (void) maxchar; // Unused - self->data = malloc(sizeof(Unicode) * self->capacity); - if (!self->data) - return -1; -#endif return 0; } @@ -55,11 +48,7 @@ static int internal_alloc(Textbuffer* self, Unicode maxchar) */ static void internal_dealloc(Textbuffer* self) { -#ifdef PEP_393 Py_DECREF(self->object); -#else - free(self->data); -#endif } /* @@ -67,7 +56,6 @@ static void internal_dealloc(Textbuffer* self) */ static int internal_resize(Textbuffer* self, Py_ssize_t new_cap) { -#ifdef PEP_393 PyObject *newobj; void *newdata; @@ -79,10 +67,6 @@ static int internal_resize(Textbuffer* self, Py_ssize_t new_cap) Py_DECREF(self->object); self->object = newobj; self->data = newdata; -#else - if (!(self->data = realloc(self->data, sizeof(Unicode) * new_cap))) - return -1; -#endif self->capacity = new_cap; return 0; @@ -94,11 +78,9 @@ static int internal_resize(Textbuffer* self, Py_ssize_t new_cap) Textbuffer* Textbuffer_new(TokenizerInput* text) { Textbuffer* self = malloc(sizeof(Textbuffer)); - Unicode maxchar = 0; + Py_UCS4 maxchar = 0; -#ifdef PEP_393 maxchar = PyUnicode_MAX_CHAR_VALUE(text->object); -#endif if (!self) goto fail_nomem; @@ -127,11 +109,9 @@ void Textbuffer_dealloc(Textbuffer* self) */ int Textbuffer_reset(Textbuffer* self) { - Unicode maxchar = 0; + Py_UCS4 maxchar = 0; -#ifdef PEP_393 maxchar = PyUnicode_MAX_CHAR_VALUE(self->object); -#endif internal_dealloc(self); if (internal_alloc(self, maxchar)) @@ -142,18 +122,14 @@ int Textbuffer_reset(Textbuffer* self) /* Write a Unicode codepoint to the given textbuffer. */ -int Textbuffer_write(Textbuffer* self, Unicode code) +int Textbuffer_write(Textbuffer* self, Py_UCS4 code) { if (self->length >= self->capacity) { if (internal_resize(self, self->capacity * RESIZE_FACTOR) < 0) return -1; } -#ifdef PEP_393 PyUnicode_WRITE(self->kind, self->data, self->length++, code); -#else - self->data[self->length++] = code; -#endif return 0; } @@ -163,13 +139,9 @@ int Textbuffer_write(Textbuffer* self, Unicode code) This function does not check for bounds. */ -Unicode Textbuffer_read(Textbuffer* self, Py_ssize_t index) +Py_UCS4 Textbuffer_read(Textbuffer* self, Py_ssize_t index) { -#ifdef PEP_393 return PyUnicode_READ(self->kind, self->data, index); -#else - return self->data[index]; -#endif } /* @@ -177,11 +149,7 @@ Unicode Textbuffer_read(Textbuffer* self, Py_ssize_t index) */ PyObject* Textbuffer_render(Textbuffer* self) { -#ifdef PEP_393 return PyUnicode_FromKindAndData(self->kind, self->data, self->length); -#else - return PyUnicode_FromUnicode(self->data, self->length); -#endif } /* @@ -196,14 +164,9 @@ int Textbuffer_concat(Textbuffer* self, Textbuffer* other) return -1; } -#ifdef PEP_393 assert(self->kind == other->kind); memcpy(((Py_UCS1*) self->data) + self->kind * self->length, other->data, other->length * other->kind); -#else - memcpy(self->data + self->length, other->data, - other->length * sizeof(Unicode)); -#endif self->length = newlen; return 0; @@ -215,18 +178,12 @@ int Textbuffer_concat(Textbuffer* self, Textbuffer* other) void Textbuffer_reverse(Textbuffer* self) { Py_ssize_t i, end = self->length - 1; - Unicode tmp; + Py_UCS4 tmp; for (i = 0; i < self->length / 2; i++) { -#ifdef PEP_393 tmp = PyUnicode_READ(self->kind, self->data, i); PyUnicode_WRITE(self->kind, self->data, i, PyUnicode_READ(self->kind, self->data, end - i)); PyUnicode_WRITE(self->kind, self->data, end - i, tmp); -#else - tmp = self->data[i]; - self->data[i] = self->data[end - i]; - self->data[end - i] = tmp; -#endif } } diff --git a/mwparserfromhell/parser/ctokenizer/textbuffer.h b/mwparserfromhell/parser/ctokenizer/textbuffer.h index 35579fd..85b39bc 100644 --- a/mwparserfromhell/parser/ctokenizer/textbuffer.h +++ b/mwparserfromhell/parser/ctokenizer/textbuffer.h @@ -29,8 +29,8 @@ SOFTWARE. Textbuffer* Textbuffer_new(TokenizerInput*); void Textbuffer_dealloc(Textbuffer*); int Textbuffer_reset(Textbuffer*); -int Textbuffer_write(Textbuffer*, Unicode); -Unicode Textbuffer_read(Textbuffer*, Py_ssize_t); +int Textbuffer_write(Textbuffer*, Py_UCS4); +Py_UCS4 Textbuffer_read(Textbuffer*, Py_ssize_t); PyObject* Textbuffer_render(Textbuffer*); int Textbuffer_concat(Textbuffer*, Textbuffer*); void Textbuffer_reverse(Textbuffer*); diff --git a/mwparserfromhell/parser/ctokenizer/tok_parse.c b/mwparserfromhell/parser/ctokenizer/tok_parse.c index fdcf695..e73b3ef 100644 --- a/mwparserfromhell/parser/ctokenizer/tok_parse.c +++ b/mwparserfromhell/parser/ctokenizer/tok_parse.c @@ -52,7 +52,7 @@ static int Tokenizer_parse_tag(Tokenizer*); /* Determine whether the given code point is a marker. */ -static int is_marker(Unicode this) +static int is_marker(Py_UCS4 this) { int i; @@ -442,7 +442,7 @@ static int Tokenizer_parse_bracketed_uri_scheme(Tokenizer* self) static const char* valid = URISCHEME; Textbuffer* buffer; PyObject* scheme; - Unicode this; + Py_UCS4 this; int slashes, i; if (Tokenizer_check_route(self, LC_EXT_LINK_URI) < 0) @@ -463,7 +463,7 @@ static int Tokenizer_parse_bracketed_uri_scheme(Tokenizer* self) while (1) { if (!valid[i]) goto end_of_loop; - if (this == (Unicode) valid[i]) + if (this == (Py_UCS4) valid[i]) break; i++; } @@ -516,7 +516,7 @@ static int Tokenizer_parse_free_uri_scheme(Tokenizer* self) static const char* valid = URISCHEME; Textbuffer *scheme_buffer = Textbuffer_new(&self->text); PyObject *scheme; - Unicode chunk; + Py_UCS4 chunk; Py_ssize_t i; int slashes, j; uint64_t new_context; @@ -536,7 +536,7 @@ static int Tokenizer_parse_free_uri_scheme(Tokenizer* self) FAIL_ROUTE(0); return 0; } - } while (chunk != (Unicode) valid[j++]); + } while (chunk != (Py_UCS4) valid[j++]); Textbuffer_write(scheme_buffer, chunk); } end_of_loop: @@ -580,7 +580,7 @@ static int Tokenizer_parse_free_uri_scheme(Tokenizer* self) Handle text in a free external link, including trailing punctuation. */ static int Tokenizer_handle_free_link_text( - Tokenizer* self, int* parens, Textbuffer* tail, Unicode this) + Tokenizer* self, int* parens, Textbuffer* tail, Py_UCS4 this) { #define PUSH_TAIL_BUFFER(tail, error) \ if (tail && tail->length > 0) { \ @@ -607,10 +607,10 @@ static int Tokenizer_handle_free_link_text( Return whether the current head is the end of a free link. */ static int -Tokenizer_is_free_link(Tokenizer* self, Unicode this, Unicode next) +Tokenizer_is_free_link(Tokenizer* self, Py_UCS4 this, Py_UCS4 next) { // Built from Tokenizer_parse()'s end sentinels: - Unicode after = Tokenizer_read(self, 2); + Py_UCS4 after = Tokenizer_read(self, 2); uint64_t ctx = self->topstack->context; return (!this || this == '\n' || this == '[' || this == ']' || @@ -628,7 +628,7 @@ static PyObject* Tokenizer_really_parse_external_link(Tokenizer* self, int brackets, Textbuffer* extra) { - Unicode this, next; + Py_UCS4 this, next; int parens = 0; if (brackets ? Tokenizer_parse_bracketed_uri_scheme(self) : @@ -816,11 +816,7 @@ static int Tokenizer_parse_heading(Tokenizer* self) if (!heading) { return -1; } -#ifdef IS_PY3K level = PyLong_FromSsize_t(heading->level); -#else - level = PyInt_FromSsize_t(heading->level); -#endif if (!level) { Py_DECREF(heading->title); free(heading); @@ -933,7 +929,7 @@ static HeadingData* Tokenizer_handle_heading_end(Tokenizer* self) static int Tokenizer_really_parse_entity(Tokenizer* self) { PyObject *kwargs, *charobj, *textobj; - Unicode this; + Py_UCS4 this; int numeric, hexadecimal, i, j, zeroes, test; char *valid, *text, *buffer, *def; @@ -1014,7 +1010,7 @@ static int Tokenizer_really_parse_entity(Tokenizer* self) while (1) { if (!valid[j]) FAIL_ROUTE_AND_EXIT() - if (this == (Unicode) valid[j]) + if (this == (Py_UCS4) valid[j]) break; j++; } @@ -1111,7 +1107,7 @@ static int Tokenizer_parse_comment(Tokenizer* self) { Py_ssize_t reset = self->head + 3; PyObject *comment; - Unicode this; + Py_UCS4 this; self->head += 4; if (Tokenizer_push(self, 0)) @@ -1211,7 +1207,7 @@ static int Tokenizer_push_tag_buffer(Tokenizer* self, TagData* data) Handle whitespace inside of an HTML open tag. */ static int Tokenizer_handle_tag_space( - Tokenizer* self, TagData* data, Unicode text) + Tokenizer* self, TagData* data, Py_UCS4 text) { uint64_t ctx = data->context; uint64_t end_of_value = (ctx & TAG_ATTR_VALUE && @@ -1243,9 +1239,9 @@ static int Tokenizer_handle_tag_space( /* Handle regular text inside of an HTML open tag. */ -static int Tokenizer_handle_tag_text(Tokenizer* self, Unicode text) +static int Tokenizer_handle_tag_text(Tokenizer* self, Py_UCS4 text) { - Unicode next = Tokenizer_read(self, 1); + Py_UCS4 next = Tokenizer_read(self, 1); if (!is_marker(text) || !Tokenizer_CAN_RECURSE(self)) return Tokenizer_emit_char(self, text); @@ -1262,7 +1258,7 @@ static int Tokenizer_handle_tag_text(Tokenizer* self, Unicode text) Handle all sorts of text data inside of an HTML open tag. */ static int Tokenizer_handle_tag_data( - Tokenizer* self, TagData* data, Unicode chunk) + Tokenizer* self, TagData* data, Py_UCS4 chunk) { PyObject *trash; int first_time, escaped; @@ -1444,7 +1440,7 @@ static PyObject* Tokenizer_handle_blacklisted_tag(Tokenizer* self) { Textbuffer* buffer; PyObject *buf_tmp, *end_tag, *start_tag; - Unicode this, next; + Py_UCS4 this, next; Py_ssize_t reset; int cmp; @@ -1600,7 +1596,7 @@ static PyObject* Tokenizer_really_parse_tag(Tokenizer* self) { TagData *data = TagData_new(&self->text); PyObject *token, *text, *trash; - Unicode this, next; + Py_UCS4 this, next; int can_exit; if (!data) @@ -1686,7 +1682,7 @@ static int Tokenizer_handle_invalid_tag_start(Tokenizer* self) Py_ssize_t reset = self->head + 1, pos = 0; Textbuffer* buf; PyObject *name, *tag; - Unicode this; + Py_UCS4 this; self->head += 2; buf = Textbuffer_new(&self->text); @@ -1988,7 +1984,7 @@ static PyObject* Tokenizer_parse_style(Tokenizer* self) static int Tokenizer_handle_list_marker(Tokenizer* self) { PyObject *kwargs, *markup; - Unicode code = Tokenizer_read(self, 0); + Py_UCS4 code = Tokenizer_read(self, 0); if (code == ';') self->topstack->context |= LC_DLTERM; @@ -2015,7 +2011,7 @@ static int Tokenizer_handle_list_marker(Tokenizer* self) */ static int Tokenizer_handle_list(Tokenizer* self) { - Unicode marker = Tokenizer_read(self, 1); + Py_UCS4 marker = Tokenizer_read(self, 1); if (Tokenizer_handle_list_marker(self)) return -1; @@ -2169,11 +2165,11 @@ Tokenizer_emit_table_tag(Tokenizer* self, const char* open_open_markup, /* Handle style attributes for a table until an ending token. */ -static PyObject* Tokenizer_handle_table_style(Tokenizer* self, Unicode end_token) +static PyObject* Tokenizer_handle_table_style(Tokenizer* self, Py_UCS4 end_token) { TagData *data = TagData_new(&self->text); PyObject *padding, *trash; - Unicode this; + Py_UCS4 this; int can_exit; if (!data) @@ -2483,7 +2479,7 @@ static PyObject* Tokenizer_handle_end(Tokenizer* self, uint64_t context) everything is safe, or -1 if the route must be failed. */ static int -Tokenizer_verify_safe(Tokenizer* self, uint64_t context, Unicode data) +Tokenizer_verify_safe(Tokenizer* self, uint64_t context, Py_UCS4 data) { if (context & LC_FAIL_NEXT) return -1; @@ -2568,7 +2564,7 @@ Tokenizer_verify_safe(Tokenizer* self, uint64_t context, Unicode data) static int Tokenizer_has_leading_whitespace(Tokenizer* self) { int offset = 1; - Unicode current_character; + Py_UCS4 current_character; while (1) { current_character = Tokenizer_read_backwards(self, offset); if (!current_character || current_character == '\n') @@ -2586,7 +2582,7 @@ static int Tokenizer_has_leading_whitespace(Tokenizer* self) PyObject* Tokenizer_parse(Tokenizer* self, uint64_t context, int push) { uint64_t this_context; - Unicode this, next, next_next, last; + Py_UCS4 this, next, next_next, last; PyObject* temp; if (push) { diff --git a/mwparserfromhell/parser/ctokenizer/tok_parse.h b/mwparserfromhell/parser/ctokenizer/tok_parse.h index 9d98b00..bdae573 100644 --- a/mwparserfromhell/parser/ctokenizer/tok_parse.h +++ b/mwparserfromhell/parser/ctokenizer/tok_parse.h @@ -24,7 +24,7 @@ SOFTWARE. #include "common.h" -static const Unicode MARKERS[] = { +static const Py_UCS4 MARKERS[] = { '{', '}', '[', ']', '<', '>', '|', '=', '&', '\'', '#', '*', ';', ':', '/', '-', '!', '\n', '\0'}; diff --git a/mwparserfromhell/parser/ctokenizer/tok_support.c b/mwparserfromhell/parser/ctokenizer/tok_support.c index 30dc2a1..bf554f6 100644 --- a/mwparserfromhell/parser/ctokenizer/tok_support.c +++ b/mwparserfromhell/parser/ctokenizer/tok_support.c @@ -275,7 +275,7 @@ int Tokenizer_emit_token_kwargs(Tokenizer* self, PyObject* token, /* Write a Unicode codepoint to the current textbuffer. */ -int Tokenizer_emit_char(Tokenizer* self, Unicode code) +int Tokenizer_emit_char(Tokenizer* self, Py_UCS4 code) { return Textbuffer_write(self->topstack->textbuffer, code); } @@ -389,19 +389,15 @@ int Tokenizer_emit_text_then_stack(Tokenizer* self, const char* text) /* Internal function to read the codepoint at the given index from the input. */ -static Unicode read_codepoint(TokenizerInput* text, Py_ssize_t index) +static Py_UCS4 read_codepoint(TokenizerInput* text, Py_ssize_t index) { -#ifdef PEP_393 return PyUnicode_READ(text->kind, text->data, index); -#else - return text->buf[index]; -#endif } /* Read the value at a relative point in the wikicode, forwards. */ -Unicode Tokenizer_read(Tokenizer* self, Py_ssize_t delta) +Py_UCS4 Tokenizer_read(Tokenizer* self, Py_ssize_t delta) { Py_ssize_t index = self->head + delta; @@ -413,7 +409,7 @@ Unicode Tokenizer_read(Tokenizer* self, Py_ssize_t delta) /* Read the value at a relative point in the wikicode, backwards. */ -Unicode Tokenizer_read_backwards(Tokenizer* self, Py_ssize_t delta) +Py_UCS4 Tokenizer_read_backwards(Tokenizer* self, Py_ssize_t delta) { Py_ssize_t index; diff --git a/mwparserfromhell/parser/ctokenizer/tok_support.h b/mwparserfromhell/parser/ctokenizer/tok_support.h index f65d102..d08f5c4 100644 --- a/mwparserfromhell/parser/ctokenizer/tok_support.h +++ b/mwparserfromhell/parser/ctokenizer/tok_support.h @@ -38,14 +38,14 @@ void Tokenizer_free_bad_route_tree(Tokenizer*); int Tokenizer_emit_token(Tokenizer*, PyObject*, int); int Tokenizer_emit_token_kwargs(Tokenizer*, PyObject*, PyObject*, int); -int Tokenizer_emit_char(Tokenizer*, Unicode); +int Tokenizer_emit_char(Tokenizer*, Py_UCS4); int Tokenizer_emit_text(Tokenizer*, const char*); int Tokenizer_emit_textbuffer(Tokenizer*, Textbuffer*); int Tokenizer_emit_all(Tokenizer*, PyObject*); int Tokenizer_emit_text_then_stack(Tokenizer*, const char*); -Unicode Tokenizer_read(Tokenizer*, Py_ssize_t); -Unicode Tokenizer_read_backwards(Tokenizer*, Py_ssize_t); +Py_UCS4 Tokenizer_read(Tokenizer*, Py_ssize_t); +Py_UCS4 Tokenizer_read_backwards(Tokenizer*, Py_ssize_t); /* Macros */ diff --git a/mwparserfromhell/parser/ctokenizer/tokenizer.c b/mwparserfromhell/parser/ctokenizer/tokenizer.c index 24d0b4a..a501032 100644 --- a/mwparserfromhell/parser/ctokenizer/tokenizer.c +++ b/mwparserfromhell/parser/ctokenizer/tokenizer.c @@ -85,12 +85,8 @@ static void init_tokenizer_text(TokenizerInput* text) text->object = Py_None; Py_INCREF(Py_None); text->length = 0; -#ifdef PEP_393 text->kind = PyUnicode_WCHAR_KIND; text->data = NULL; -#else - text->buf = NULL; -#endif } /* @@ -119,14 +115,10 @@ static int load_tokenizer_text(TokenizerInput* text, PyObject *input) dealloc_tokenizer_text(text); text->object = input; -#ifdef PEP_393 if (PyUnicode_READY(input) < 0) return -1; text->kind = PyUnicode_KIND(input); text->data = PyUnicode_DATA(input); -#else - text->buf = PyUnicode_AS_UNICODE(input); -#endif text->length = PyUnicode_GET_LENGTH(input); return 0; } @@ -192,11 +184,9 @@ static int load_entities(void) { PyObject *tempmod, *defmap, *deflist; unsigned numdefs, i; -#ifdef IS_PY3K PyObject *string; -#endif - tempmod = PyImport_ImportModule(ENTITYDEFS_MODULE); + tempmod = PyImport_ImportModule("html.entities"); if (!tempmod) return -1; defmap = PyObject_GetAttrString(tempmod, "entitydefs"); @@ -212,14 +202,10 @@ static int load_entities(void) if (!entitydefs) return -1; for (i = 0; i < numdefs; i++) { -#ifdef IS_PY3K string = PyUnicode_AsASCIIString(PyList_GET_ITEM(deflist, i)); if (!string) return -1; entitydefs[i] = PyBytes_AsString(string); -#else - entitydefs[i] = PyBytes_AsString(PyList_GET_ITEM(deflist, i)); -#endif if (!entitydefs[i]) return -1; } @@ -233,7 +219,7 @@ static int load_tokens(void) *globals = PyEval_GetGlobals(), *locals = PyEval_GetLocals(), *fromlist = PyList_New(1), - *modname = IMPORT_NAME_FUNC("tokens"); + *modname = PyUnicode_FromString("tokens"); char *name = "mwparserfromhell.parser"; if (!fromlist || !modname) @@ -256,7 +242,7 @@ static int load_defs(void) *globals = PyEval_GetGlobals(), *locals = PyEval_GetLocals(), *fromlist = PyList_New(1), - *modname = IMPORT_NAME_FUNC("definitions"); + *modname = PyUnicode_FromString("definitions"); char *name = "mwparserfromhell"; if (!fromlist || !modname) @@ -277,7 +263,7 @@ static int load_exceptions(void) *globals = PyEval_GetGlobals(), *locals = PyEval_GetLocals(), *fromlist = PyList_New(1), - *modname = IMPORT_NAME_FUNC("parser"); + *modname = PyUnicode_FromString("parser"); char *name = "mwparserfromhell"; if (!fromlist || !modname) @@ -294,24 +280,22 @@ static int load_exceptions(void) return 0; } -PyMODINIT_FUNC INIT_FUNC_NAME(void) +PyMODINIT_FUNC PyInit__tokenizer(void) { PyObject *module; TokenizerType.tp_new = PyType_GenericNew; if (PyType_Ready(&TokenizerType) < 0) - INIT_ERROR; - module = CREATE_MODULE; + return NULL; + module = PyModule_Create(&module_def); if (!module) - INIT_ERROR; + return NULL; Py_INCREF(&TokenizerType); PyModule_AddObject(module, "CTokenizer", (PyObject*) &TokenizerType); Py_INCREF(Py_True); PyDict_SetItemString(TokenizerType.tp_dict, "USES_C", Py_True); NOARGS = PyTuple_New(0); if (!NOARGS || load_entities() || load_tokens() || load_defs()) - INIT_ERROR; -#ifdef IS_PY3K + return NULL; return module; -#endif } diff --git a/mwparserfromhell/parser/ctokenizer/tokenizer.h b/mwparserfromhell/parser/ctokenizer/tokenizer.h index 6050ce0..ac98d79 100644 --- a/mwparserfromhell/parser/ctokenizer/tokenizer.h +++ b/mwparserfromhell/parser/ctokenizer/tokenizer.h @@ -32,22 +32,6 @@ static void Tokenizer_dealloc(Tokenizer*); static int Tokenizer_init(Tokenizer*, PyObject*, PyObject*); static PyObject* Tokenizer_tokenize(Tokenizer*, PyObject*); -/* Compatibility macros */ - -#ifdef IS_PY3K - #define IMPORT_NAME_FUNC PyUnicode_FromString - #define CREATE_MODULE PyModule_Create(&module_def); - #define ENTITYDEFS_MODULE "html.entities" - #define INIT_FUNC_NAME PyInit__tokenizer - #define INIT_ERROR return NULL -#else - #define IMPORT_NAME_FUNC PyBytes_FromString - #define CREATE_MODULE Py_InitModule("_tokenizer", NULL); - #define ENTITYDEFS_MODULE "htmlentitydefs" - #define INIT_FUNC_NAME init_tokenizer - #define INIT_ERROR return -#endif - /* Structs */ static PyMethodDef Tokenizer_methods[] = { @@ -101,11 +85,9 @@ static PyTypeObject TokenizerType = { Tokenizer_new, /* tp_new */ }; -#ifdef IS_PY3K static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, "_tokenizer", "Creates a list of tokens from a string of wikicode.", -1, NULL, NULL, NULL, NULL, NULL }; -#endif diff --git a/mwparserfromhell/parser/errors.py b/mwparserfromhell/parser/errors.py new file mode 100644 index 0000000..f94e62b --- /dev/null +++ b/mwparserfromhell/parser/errors.py @@ -0,0 +1,33 @@ +# Copyright (C) 2012-2020 Ben Kurtovic +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +__all__ = ["ParserError"] + +class ParserError(Exception): + """Exception raised when an internal error occurs while parsing. + + This does not mean that the wikicode was invalid, because invalid markup + should still be parsed correctly. This means that the parser caught itself + with an impossible internal state and is bailing out before other problems + can happen. Its appearance indicates a bug. + """ + def __init__(self, extra): + msg = "This is a bug and should be reported. Info: {}.".format(extra) + super().__init__(msg) diff --git a/mwparserfromhell/parser/tokenizer.py b/mwparserfromhell/parser/tokenizer.py index afdfd5a..ab61f92 100644 --- a/mwparserfromhell/parser/tokenizer.py +++ b/mwparserfromhell/parser/tokenizer.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2019 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,12 +18,12 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals +import html.entities as htmlentities from math import log import re -from . import contexts, tokens, ParserError -from ..compat import htmlentities, range +from . import contexts, tokens +from .errors import ParserError from ..definitions import (get_html_tag, is_parsable, is_single, is_single_only, is_scheme) @@ -35,11 +33,11 @@ class BadRoute(Exception): """Raised internally when the current tokenization route is invalid.""" def __init__(self, context=0): - super(BadRoute, self).__init__() + super().__init__() self.context = context -class _TagOpenData(object): +class _TagOpenData: """Stores data about an HTML open tag, like ````.""" CX_NAME = 1 << 0 CX_ATTR_READY = 1 << 1 @@ -57,7 +55,7 @@ class _TagOpenData(object): self.reset = 0 -class Tokenizer(object): +class Tokenizer: """Creates a list of tokens from a string of wikicode.""" USES_C = False START = object() @@ -325,7 +323,7 @@ class Tokenizer(object): self._head += 2 try: # If the wikilink looks like an external link, parse it as such: - link, extra, delta = self._really_parse_external_link(True) + link, _extra, _delta = self._really_parse_external_link(True) except BadRoute: self._head = reset + 1 try: @@ -435,17 +433,17 @@ class Tokenizer(object): self._emit_text(this) return punct, tail - def _is_free_link_end(self, this, next): + def _is_free_link_end(self, this, nxt): """Return whether the current head is the end of a free link.""" # Built from _parse()'s end sentinels: after, ctx = self._read(2), self._context equal_sign_contexts = contexts.TEMPLATE_PARAM_KEY | contexts.HEADING return (this in (self.END, "\n", "[", "]", "<", ">") or - this == next == "'" or + this == nxt == "'" or (this == "|" and ctx & contexts.TEMPLATE) or (this == "=" and ctx & equal_sign_contexts) or - (this == next == "}" and ctx & contexts.TEMPLATE) or - (this == next == after == "}" and ctx & contexts.ARGUMENT)) + (this == nxt == "}" and ctx & contexts.TEMPLATE) or + (this == nxt == after == "}" and ctx & contexts.ARGUMENT)) def _really_parse_external_link(self, brackets): """Really parse an external link.""" @@ -460,23 +458,23 @@ class Tokenizer(object): self._fail_route() tail = "" while True: - this, next = self._read(), self._read(1) + this, nxt = self._read(), self._read(1) if this == "&": if tail: self._emit_text(tail) tail = "" self._parse_entity() - elif (this == "<" and next == "!" and self._read(2) == + elif (this == "<" and nxt == "!" and self._read(2) == self._read(3) == "-"): if tail: self._emit_text(tail) tail = "" self._parse_comment() - elif not brackets and self._is_free_link_end(this, next): + elif not brackets and self._is_free_link_end(this, nxt): return self._pop(), tail, -1 elif this is self.END or this == "\n": self._fail_route() - elif this == next == "{" and self._can_recurse(): + elif this == nxt == "{" and self._can_recurse(): if tail: self._emit_text(tail) tail = "" @@ -704,12 +702,12 @@ class Tokenizer(object): def _handle_tag_text(self, text): """Handle regular *text* inside of an HTML open tag.""" - next = self._read(1) + nxt = self._read(1) if not self._can_recurse() or text not in self.MARKERS: self._emit_text(text) - elif text == next == "{": + elif text == nxt == "{": self._parse_template_or_argument() - elif text == next == "[": + elif text == nxt == "[": self._parse_wikilink() elif text == "<": self._parse_tag() @@ -798,10 +796,10 @@ class Tokenizer(object): """Handle the body of an HTML tag that is parser-blacklisted.""" strip = lambda text: text.rstrip().lower() while True: - this, next = self._read(), self._read(1) + this, nxt = self._read(), self._read(1) if this is self.END: self._fail_route() - elif this == "<" and next == "/": + elif this == "<" and nxt == "/": self._head += 3 if self._read() != ">" or (strip(self._read(-1)) != strip(self._stack[1].text)): @@ -856,7 +854,7 @@ class Tokenizer(object): self._push(contexts.TAG_OPEN) self._emit(tokens.TagOpenOpen()) while True: - this, next = self._read(), self._read(1) + this, nxt = self._read(), self._read(1) can_exit = (not data.context & (data.CX_QUOTED | data.CX_NAME) or data.context & data.CX_NOTE_SPACE) if this is self.END: @@ -878,7 +876,7 @@ class Tokenizer(object): if is_parsable(self._stack[1].text): return self._parse(push=False) return self._handle_blacklisted_tag() - elif this == "/" and next == ">" and can_exit: + elif this == "/" and nxt == ">" and can_exit: self._handle_tag_close_open(data, tokens.TagCloseSelfclose) return self._pop() else: @@ -935,9 +933,11 @@ class Tokenizer(object): stack = self._parse(new_ctx) except BadRoute: self._head = reset - return self._emit_text("''") + self._emit_text("''") + return else: - return self._emit_text("''") + self._emit_text("''") + return self._emit_style_tag("i", "''", stack) def _parse_bold(self): @@ -950,7 +950,7 @@ class Tokenizer(object): if self._context & contexts.STYLE_SECOND_PASS: self._emit_text("'") return True - elif self._context & contexts.STYLE_ITALICS: + if self._context & contexts.STYLE_ITALICS: self._context |= contexts.STYLE_PASS_AGAIN self._emit_text("'''") else: @@ -958,6 +958,7 @@ class Tokenizer(object): self._parse_italics() else: self._emit_style_tag("b", "'''", stack) + return False def _parse_italics_and_bold(self): """Parse wiki-style italics and bold together (i.e., five ticks).""" @@ -1019,7 +1020,7 @@ class Tokenizer(object): if ticks == 5: self._head -= 3 if italics else 2 return self._pop() - elif not self._can_recurse(): + if not self._can_recurse(): if ticks == 3: if self._context & contexts.STYLE_SECOND_PASS: self._emit_text("'") @@ -1103,7 +1104,7 @@ class Tokenizer(object): if this.isspace(): data.padding_buffer["first"] += this return data.padding_buffer["first"] - elif this is self.END or this == end_token: + if this is self.END or this == end_token: if self._context & contexts.TAG_ATTR: if data.context & data.CX_QUOTED: # Unclosed attribute quote: reset, don't die @@ -1243,9 +1244,9 @@ class Tokenizer(object): if context & contexts.FAIL_NEXT: return False if context & contexts.WIKILINK_TITLE: - if this == "]" or this == "{": + if this in ("]", "{"): self._context |= contexts.FAIL_NEXT - elif this == "\n" or this == "[" or this == "}" or this == ">": + elif this in ("\n", "[", "}", ">"): return False elif this == "<": if self._read(1) == "!": @@ -1253,16 +1254,16 @@ class Tokenizer(object): else: return False return True - elif context & contexts.EXT_LINK_TITLE: + if context & contexts.EXT_LINK_TITLE: return this != "\n" - elif context & contexts.TEMPLATE_NAME: + if context & contexts.TEMPLATE_NAME: if this == "{": self._context |= contexts.HAS_TEMPLATE | contexts.FAIL_NEXT return True if this == "}" or (this == "<" and self._read(1) == "!"): self._context |= contexts.FAIL_NEXT return True - if this == "[" or this == "]" or this == "<" or this == ">": + if this in ("[", "]", "<", ">"): return False if this == "|": return True @@ -1275,30 +1276,29 @@ class Tokenizer(object): elif this is self.END or not this.isspace(): self._context |= contexts.HAS_TEXT return True - elif context & contexts.TAG_CLOSE: + if context & contexts.TAG_CLOSE: return this != "<" - else: - if context & contexts.FAIL_ON_EQUALS: - if this == "=": - return False - elif context & contexts.FAIL_ON_LBRACE: - if this == "{" or (self._read(-1) == self._read(-2) == "{"): - if context & contexts.TEMPLATE: - self._context |= contexts.FAIL_ON_EQUALS - else: - self._context |= contexts.FAIL_NEXT - return True - self._context ^= contexts.FAIL_ON_LBRACE - elif context & contexts.FAIL_ON_RBRACE: - if this == "}": + if context & contexts.FAIL_ON_EQUALS: + if this == "=": + return False + elif context & contexts.FAIL_ON_LBRACE: + if this == "{" or (self._read(-1) == self._read(-2) == "{"): + if context & contexts.TEMPLATE: + self._context |= contexts.FAIL_ON_EQUALS + else: self._context |= contexts.FAIL_NEXT - return True - self._context ^= contexts.FAIL_ON_RBRACE - elif this == "{": - self._context |= contexts.FAIL_ON_LBRACE - elif this == "}": - self._context |= contexts.FAIL_ON_RBRACE - return True + return True + self._context ^= contexts.FAIL_ON_LBRACE + elif context & contexts.FAIL_ON_RBRACE: + if this == "}": + self._context |= contexts.FAIL_NEXT + return True + self._context ^= contexts.FAIL_ON_RBRACE + elif this == "{": + self._context |= contexts.FAIL_ON_LBRACE + elif this == "}": + self._context |= contexts.FAIL_ON_RBRACE + return True def _parse(self, context=0, push=True): """Parse the wikicode string, using *context* for when to stop.""" @@ -1317,8 +1317,8 @@ class Tokenizer(object): continue if this is self.END: return self._handle_end() - next = self._read(1) - if this == next == "{": + nxt = self._read(1) + if this == nxt == "{": if self._can_recurse(): self._parse_template_or_argument() else: @@ -1326,27 +1326,26 @@ class Tokenizer(object): elif this == "|" and self._context & contexts.TEMPLATE: self._handle_template_param() elif this == "=" and self._context & contexts.TEMPLATE_PARAM_KEY: - if not self._global & contexts.GL_HEADING and self._read(-1) in ("\n", self.START) and next == "=": + if not self._global & contexts.GL_HEADING and self._read(-1) in ("\n", self.START) and nxt == "=": self._parse_heading() else: self._handle_template_param_value() - elif this == next == "}" and self._context & contexts.TEMPLATE: + elif this == nxt == "}" and self._context & contexts.TEMPLATE: return self._handle_template_end() elif this == "|" and self._context & contexts.ARGUMENT_NAME: self._handle_argument_separator() - elif this == next == "}" and self._context & contexts.ARGUMENT: + elif this == nxt == "}" and self._context & contexts.ARGUMENT: if self._read(2) == "}": return self._handle_argument_end() - else: - self._emit_text("}") - elif this == next == "[" and self._can_recurse(): + self._emit_text("}") + elif this == nxt == "[" and self._can_recurse(): if not self._context & contexts.NO_WIKILINKS: self._parse_wikilink() else: self._emit_text("[") elif this == "|" and self._context & contexts.WIKILINK_TITLE: self._handle_wikilink_separator() - elif this == next == "]" and self._context & contexts.WIKILINK: + elif this == nxt == "]" and self._context & contexts.WIKILINK: return self._handle_wikilink_end() elif this == "[": self._parse_external_link(True) @@ -1365,12 +1364,12 @@ class Tokenizer(object): self._fail_route() elif this == "&": self._parse_entity() - elif this == "<" and next == "!": + elif this == "<" and nxt == "!": if self._read(2) == self._read(3) == "-": self._parse_comment() else: self._emit_text(this) - elif this == "<" and next == "/" and self._read(2) is not self.END: + elif this == "<" and nxt == "/" and self._read(2) is not self.END: if self._context & contexts.TAG_BODY: self._handle_tag_open_close() else: @@ -1382,14 +1381,14 @@ class Tokenizer(object): self._emit_text("<") elif this == ">" and self._context & contexts.TAG_CLOSE: return self._handle_tag_close_close() - elif this == next == "'" and not self._skip_style_tags: + elif this == nxt == "'" and not self._skip_style_tags: result = self._parse_style() if result is not None: return result elif self._read(-1) in ("\n", self.START) and this in ("#", "*", ";", ":"): self._handle_list() elif self._read(-1) in ("\n", self.START) and ( - this == next == self._read(2) == self._read(3) == "-"): + this == nxt == self._read(2) == self._read(3) == "-"): self._handle_hr() elif this in ("\n", ":") and self._context & contexts.DL_TERM: self._handle_dl_term() @@ -1397,7 +1396,7 @@ class Tokenizer(object): # Kill potential table contexts self._context &= ~contexts.TABLE_CELL_LINE_CONTEXTS # Start of table parsing - elif this == "{" and next == "|" and ( + elif this == "{" and nxt == "|" and ( self._read(-1) in ("\n", self.START) or (self._read(-2) in ("\n", self.START) and self._read(-1).isspace())): if self._can_recurse(): @@ -1405,15 +1404,15 @@ class Tokenizer(object): else: self._emit_text("{") elif self._context & contexts.TABLE_OPEN: - if this == next == "|" and self._context & contexts.TABLE_TD_LINE: + if this == nxt == "|" and self._context & contexts.TABLE_TD_LINE: if self._context & contexts.TABLE_CELL_OPEN: return self._handle_table_cell_end() self._handle_table_cell("||", "td", contexts.TABLE_TD_LINE) - elif this == next == "|" and self._context & contexts.TABLE_TH_LINE: + elif this == nxt == "|" and self._context & contexts.TABLE_TH_LINE: if self._context & contexts.TABLE_CELL_OPEN: return self._handle_table_cell_end() self._handle_table_cell("||", "th", contexts.TABLE_TH_LINE) - elif this == next == "!" and self._context & contexts.TABLE_TH_LINE: + elif this == nxt == "!" and self._context & contexts.TABLE_TH_LINE: if self._context & contexts.TABLE_CELL_OPEN: return self._handle_table_cell_end() self._handle_table_cell("!!", "th", contexts.TABLE_TH_LINE) @@ -1425,13 +1424,13 @@ class Tokenizer(object): self._emit_text(this) elif (self._read(-1) in ("\n", self.START) or (self._read(-2) in ("\n", self.START) and self._read(-1).isspace())): - if this == "|" and next == "}": + if this == "|" and nxt == "}": if self._context & contexts.TABLE_CELL_OPEN: return self._handle_table_cell_end() if self._context & contexts.TABLE_ROW_OPEN: return self._handle_table_row_end() return self._handle_table_end() - elif this == "|" and next == "-": + if this == "|" and nxt == "-": if self._context & contexts.TABLE_CELL_OPEN: return self._handle_table_cell_end() if self._context & contexts.TABLE_ROW_OPEN: @@ -1463,10 +1462,10 @@ class Tokenizer(object): self._skip_style_tags = skip_style_tags try: - tokens = self._parse(context) - except BadRoute: # pragma: no cover (untestable/exceptional case) - raise ParserError("Python tokenizer exited with BadRoute") + result = self._parse(context) + except BadRoute as exc: # pragma: no cover (untestable/exceptional case) + raise ParserError("Python tokenizer exited with BadRoute") from exc if self._stacks: # pragma: no cover (untestable/exceptional case) err = "Python tokenizer exited with non-empty token stack" raise ParserError(err) - return tokens + return result diff --git a/mwparserfromhell/parser/tokens.py b/mwparserfromhell/parser/tokens.py index 3110179..f274123 100644 --- a/mwparserfromhell/parser/tokens.py +++ b/mwparserfromhell/parser/tokens.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -28,10 +26,6 @@ a syntactically valid form by the :class:`.Tokenizer`, and then converted into the :class`.Wikicode` tree by the :class:`.Builder`. """ -from __future__ import unicode_literals - -from ..compat import py3k, str - __all__ = ["Token"] class Token(dict): @@ -65,7 +59,7 @@ class Token(dict): def make(name): """Create a new Token class using ``type()`` and add it to ``__all__``.""" __all__.append(name) - return type(name if py3k else name.encode("utf8"), (Token,), {}) + return type(name, (Token,), {}) Text = make("Text") diff --git a/mwparserfromhell/smart_list.py b/mwparserfromhell/smart_list.py deleted file mode 100644 index e7fa59f..0000000 --- a/mwparserfromhell/smart_list.py +++ /dev/null @@ -1,456 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2016 Ben Kurtovic -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -""" -This module contains the :class:`.SmartList` type, as well as its -:class:`._ListProxy` child, which together implement a list whose sublists -reflect changes made to the main list, and vice-versa. -""" - -from __future__ import unicode_literals -from sys import maxsize -from weakref import ref - -from .compat import py3k - -__all__ = ["SmartList"] - -def inheritdoc(method): - """Set __doc__ of *method* to __doc__ of *method* in its parent class. - - Since this is used on :class:`.SmartList`, the "parent class" used is - ``list``. This function can be used as a decorator. - """ - method.__doc__ = getattr(list, method.__name__).__doc__ - return method - - -class _SliceNormalizerMixIn(object): - """MixIn that provides a private method to normalize slices.""" - - def _normalize_slice(self, key, clamp=False): - """Return a slice equivalent to the input *key*, standardized.""" - if key.start is None: - start = 0 - else: - start = (len(self) + key.start) if key.start < 0 else key.start - if key.stop is None or key.stop == maxsize: - stop = len(self) if clamp else None - else: - stop = (len(self) + key.stop) if key.stop < 0 else key.stop - return slice(start, stop, key.step or 1) - - -class SmartList(_SliceNormalizerMixIn, list): - """Implements the ``list`` interface with special handling of sublists. - - When a sublist is created (by ``list[i:j]``), any changes made to this - list (such as the addition, removal, or replacement of elements) will be - reflected in the sublist, or vice-versa, to the greatest degree possible. - This is implemented by having sublists - instances of the - :class:`._ListProxy` type - dynamically determine their elements by storing - their slice info and retrieving that slice from the parent. Methods that - change the size of the list also change the slice info. For example:: - - >>> parent = SmartList([0, 1, 2, 3]) - >>> parent - [0, 1, 2, 3] - >>> child = parent[2:] - >>> child - [2, 3] - >>> child.append(4) - >>> child - [2, 3, 4] - >>> parent - [0, 1, 2, 3, 4] - """ - - def __init__(self, iterable=None): - if iterable: - super(SmartList, self).__init__(iterable) - else: - super(SmartList, self).__init__() - self._children = {} - - def __getitem__(self, key): - if not isinstance(key, slice): - return super(SmartList, self).__getitem__(key) - key = self._normalize_slice(key, clamp=False) - sliceinfo = [key.start, key.stop, key.step] - child = _ListProxy(self, sliceinfo) - child_ref = ref(child, self._delete_child) - self._children[id(child_ref)] = (child_ref, sliceinfo) - return child - - def __setitem__(self, key, item): - if not isinstance(key, slice): - return super(SmartList, self).__setitem__(key, item) - item = list(item) - super(SmartList, self).__setitem__(key, item) - key = self._normalize_slice(key, clamp=True) - diff = len(item) + (key.start - key.stop) // key.step - if not diff: - return - values = self._children.values if py3k else self._children.itervalues - for child, (start, stop, step) in values(): - if start > key.stop: - self._children[id(child)][1][0] += diff - if stop is not None and stop >= key.stop: - self._children[id(child)][1][1] += diff - - def __delitem__(self, key): - super(SmartList, self).__delitem__(key) - if isinstance(key, slice): - key = self._normalize_slice(key, clamp=True) - else: - key = slice(key, key + 1, 1) - diff = (key.stop - key.start) // key.step - values = self._children.values if py3k else self._children.itervalues - for child, (start, stop, step) in values(): - if start > key.start: - self._children[id(child)][1][0] -= diff - if stop is not None and stop >= key.stop: - self._children[id(child)][1][1] -= diff - - if not py3k: - def __getslice__(self, start, stop): - return self.__getitem__(slice(start, stop)) - - def __setslice__(self, start, stop, iterable): - self.__setitem__(slice(start, stop), iterable) - - def __delslice__(self, start, stop): - self.__delitem__(slice(start, stop)) - - def __add__(self, other): - return SmartList(list(self) + other) - - def __radd__(self, other): - return SmartList(other + list(self)) - - def __iadd__(self, other): - self.extend(other) - return self - - def _delete_child(self, child_ref): - """Remove a child reference that is about to be garbage-collected.""" - del self._children[id(child_ref)] - - def _detach_children(self): - """Remove all children and give them independent parent copies.""" - children = [val[0] for val in self._children.values()] - for child in children: - child()._parent = list(self) - self._children.clear() - - @inheritdoc - def append(self, item): - head = len(self) - self[head:head] = [item] - - @inheritdoc - def extend(self, item): - head = len(self) - self[head:head] = item - - @inheritdoc - def insert(self, index, item): - self[index:index] = [item] - - @inheritdoc - def pop(self, index=None): - if index is None: - index = len(self) - 1 - item = self[index] - del self[index] - return item - - @inheritdoc - def remove(self, item): - del self[self.index(item)] - - @inheritdoc - def reverse(self): - self._detach_children() - super(SmartList, self).reverse() - - if py3k: - @inheritdoc - def sort(self, key=None, reverse=None): - self._detach_children() - kwargs = {} - if key is not None: - kwargs["key"] = key - if reverse is not None: - kwargs["reverse"] = reverse - super(SmartList, self).sort(**kwargs) - else: - @inheritdoc - def sort(self, cmp=None, key=None, reverse=None): - self._detach_children() - kwargs = {} - if cmp is not None: - kwargs["cmp"] = cmp - if key is not None: - kwargs["key"] = key - if reverse is not None: - kwargs["reverse"] = reverse - super(SmartList, self).sort(**kwargs) - - -class _ListProxy(_SliceNormalizerMixIn, list): - """Implement the ``list`` interface by getting elements from a parent. - - This is created by a :class:`.SmartList` object when slicing. It does not - actually store the list at any time; instead, whenever the list is needed, - it builds it dynamically using the :meth:`_render` method. - """ - - def __init__(self, parent, sliceinfo): - super(_ListProxy, self).__init__() - self._parent = parent - self._sliceinfo = sliceinfo - - def __repr__(self): - return repr(self._render()) - - def __lt__(self, other): - if isinstance(other, _ListProxy): - return self._render() < list(other) - return self._render() < other - - def __le__(self, other): - if isinstance(other, _ListProxy): - return self._render() <= list(other) - return self._render() <= other - - def __eq__(self, other): - if isinstance(other, _ListProxy): - return self._render() == list(other) - return self._render() == other - - def __ne__(self, other): - if isinstance(other, _ListProxy): - return self._render() != list(other) - return self._render() != other - - def __gt__(self, other): - if isinstance(other, _ListProxy): - return self._render() > list(other) - return self._render() > other - - def __ge__(self, other): - if isinstance(other, _ListProxy): - return self._render() >= list(other) - return self._render() >= other - - if py3k: - def __bool__(self): - return bool(self._render()) - else: - def __nonzero__(self): - return bool(self._render()) - - def __len__(self): - return max((self._stop - self._start) // self._step, 0) - - def __getitem__(self, key): - if isinstance(key, slice): - key = self._normalize_slice(key, clamp=True) - keystart = min(self._start + key.start, self._stop) - keystop = min(self._start + key.stop, self._stop) - adjusted = slice(keystart, keystop, key.step) - return self._parent[adjusted] - else: - return self._render()[key] - - def __setitem__(self, key, item): - if isinstance(key, slice): - key = self._normalize_slice(key, clamp=True) - keystart = min(self._start + key.start, self._stop) - keystop = min(self._start + key.stop, self._stop) - adjusted = slice(keystart, keystop, key.step) - self._parent[adjusted] = item - else: - length = len(self) - if key < 0: - key = length + key - if key < 0 or key >= length: - raise IndexError("list assignment index out of range") - self._parent[self._start + key] = item - - def __delitem__(self, key): - if isinstance(key, slice): - key = self._normalize_slice(key, clamp=True) - keystart = min(self._start + key.start, self._stop) - keystop = min(self._start + key.stop, self._stop) - adjusted = slice(keystart, keystop, key.step) - del self._parent[adjusted] - else: - length = len(self) - if key < 0: - key = length + key - if key < 0 or key >= length: - raise IndexError("list assignment index out of range") - del self._parent[self._start + key] - - def __iter__(self): - i = self._start - while i < self._stop: - yield self._parent[i] - i += self._step - - def __reversed__(self): - i = self._stop - 1 - while i >= self._start: - yield self._parent[i] - i -= self._step - - def __contains__(self, item): - return item in self._render() - - if not py3k: - def __getslice__(self, start, stop): - return self.__getitem__(slice(start, stop)) - - def __setslice__(self, start, stop, iterable): - self.__setitem__(slice(start, stop), iterable) - - def __delslice__(self, start, stop): - self.__delitem__(slice(start, stop)) - - def __add__(self, other): - return SmartList(list(self) + other) - - def __radd__(self, other): - return SmartList(other + list(self)) - - def __iadd__(self, other): - self.extend(other) - return self - - def __mul__(self, other): - return SmartList(list(self) * other) - - def __rmul__(self, other): - return SmartList(other * list(self)) - - def __imul__(self, other): - self.extend(list(self) * (other - 1)) - return self - - @property - def _start(self): - """The starting index of this list, inclusive.""" - return self._sliceinfo[0] - - @property - def _stop(self): - """The ending index of this list, exclusive.""" - if self._sliceinfo[1] is None: - return len(self._parent) - return self._sliceinfo[1] - - @property - def _step(self): - """The number to increase the index by between items.""" - return self._sliceinfo[2] - - def _render(self): - """Return the actual list from the stored start/stop/step.""" - return list(self._parent)[self._start:self._stop:self._step] - - @inheritdoc - def append(self, item): - self._parent.insert(self._stop, item) - - @inheritdoc - def count(self, item): - return self._render().count(item) - - @inheritdoc - def index(self, item, start=None, stop=None): - if start is not None: - if stop is not None: - return self._render().index(item, start, stop) - return self._render().index(item, start) - return self._render().index(item) - - @inheritdoc - def extend(self, item): - self._parent[self._stop:self._stop] = item - - @inheritdoc - def insert(self, index, item): - if index < 0: - index = len(self) + index - self._parent.insert(self._start + index, item) - - @inheritdoc - def pop(self, index=None): - length = len(self) - if index is None: - index = length - 1 - elif index < 0: - index = length + index - if index < 0 or index >= length: - raise IndexError("pop index out of range") - return self._parent.pop(self._start + index) - - @inheritdoc - def remove(self, item): - index = self.index(item) - del self._parent[self._start + index] - - @inheritdoc - def reverse(self): - item = self._render() - item.reverse() - self._parent[self._start:self._stop:self._step] = item - - if py3k: - @inheritdoc - def sort(self, key=None, reverse=None): - item = self._render() - kwargs = {} - if key is not None: - kwargs["key"] = key - if reverse is not None: - kwargs["reverse"] = reverse - item.sort(**kwargs) - self._parent[self._start:self._stop:self._step] = item - else: - @inheritdoc - def sort(self, cmp=None, key=None, reverse=None): - item = self._render() - kwargs = {} - if cmp is not None: - kwargs["cmp"] = cmp - if key is not None: - kwargs["key"] = key - if reverse is not None: - kwargs["reverse"] = reverse - item.sort(**kwargs) - self._parent[self._start:self._stop:self._step] = item - - -del inheritdoc diff --git a/mwparserfromhell/smart_list/__init__.py b/mwparserfromhell/smart_list/__init__.py new file mode 100644 index 0000000..92c75e2 --- /dev/null +++ b/mwparserfromhell/smart_list/__init__.py @@ -0,0 +1,29 @@ +# Copyright (C) 2012-2020 Ben Kurtovic +# Copyright (C) 2019-2020 Yuri Astrakhan +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +This module contains the :class:`.SmartList` type, as well as its +:class:`.ListProxy` child, which together implement a list whose sublists +reflect changes made to the main list, and vice-versa. +""" + +from .list_proxy import ListProxy as _ListProxy +from .smart_list import SmartList diff --git a/mwparserfromhell/smart_list/list_proxy.py b/mwparserfromhell/smart_list/list_proxy.py new file mode 100644 index 0000000..d2d89e9 --- /dev/null +++ b/mwparserfromhell/smart_list/list_proxy.py @@ -0,0 +1,229 @@ +# Copyright (C) 2012-2020 Ben Kurtovic +# Copyright (C) 2019-2020 Yuri Astrakhan +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from .utils import _SliceNormalizerMixIn, inheritdoc + + +class ListProxy(_SliceNormalizerMixIn, list): + """Implement the ``list`` interface by getting elements from a parent. + + This is created by a :class:`.SmartList` object when slicing. It does not + actually store the list at any time; instead, whenever the list is needed, + it builds it dynamically using the :meth:`_render` method. + """ + + def __init__(self, parent, sliceinfo): + super().__init__() + self._parent = parent + self._sliceinfo = sliceinfo + + def __repr__(self): + return repr(self._render()) + + def __lt__(self, other): + if isinstance(other, ListProxy): + return self._render() < list(other) + return self._render() < other + + def __le__(self, other): + if isinstance(other, ListProxy): + return self._render() <= list(other) + return self._render() <= other + + def __eq__(self, other): + if isinstance(other, ListProxy): + return self._render() == list(other) + return self._render() == other + + def __ne__(self, other): + if isinstance(other, ListProxy): + return self._render() != list(other) + return self._render() != other + + def __gt__(self, other): + if isinstance(other, ListProxy): + return self._render() > list(other) + return self._render() > other + + def __ge__(self, other): + if isinstance(other, ListProxy): + return self._render() >= list(other) + return self._render() >= other + + def __bool__(self): + return bool(self._render()) + + def __len__(self): + return max((self._stop - self._start) // self._step, 0) + + def __getitem__(self, key): + if isinstance(key, slice): + key = self._normalize_slice(key, clamp=True) + keystart = min(self._start + key.start, self._stop) + keystop = min(self._start + key.stop, self._stop) + adjusted = slice(keystart, keystop, key.step) + return self._parent[adjusted] + return self._render()[key] + + def __setitem__(self, key, item): + if isinstance(key, slice): + key = self._normalize_slice(key, clamp=True) + keystart = min(self._start + key.start, self._stop) + keystop = min(self._start + key.stop, self._stop) + adjusted = slice(keystart, keystop, key.step) + self._parent[adjusted] = item + else: + length = len(self) + if key < 0: + key = length + key + if key < 0 or key >= length: + raise IndexError("list assignment index out of range") + self._parent[self._start + key] = item + + def __delitem__(self, key): + if isinstance(key, slice): + key = self._normalize_slice(key, clamp=True) + keystart = min(self._start + key.start, self._stop) + keystop = min(self._start + key.stop, self._stop) + adjusted = slice(keystart, keystop, key.step) + del self._parent[adjusted] + else: + length = len(self) + if key < 0: + key = length + key + if key < 0 or key >= length: + raise IndexError("list assignment index out of range") + del self._parent[self._start + key] + + def __iter__(self): + i = self._start + while i < self._stop: + yield self._parent[i] + i += self._step + + def __reversed__(self): + i = self._stop - 1 + while i >= self._start: + yield self._parent[i] + i -= self._step + + def __contains__(self, item): + return item in self._render() + + def __add__(self, other): + return type(self._parent)(list(self) + other) + + def __radd__(self, other): + return type(self._parent)(other + list(self)) + + def __iadd__(self, other): + self.extend(other) + return self + + def __mul__(self, other): + return type(self._parent)(list(self) * other) + + def __rmul__(self, other): + return type(self._parent)(other * list(self)) + + def __imul__(self, other): + self.extend(list(self) * (other - 1)) + return self + + @property + def _start(self): + """The starting index of this list, inclusive.""" + return self._sliceinfo[0] + + @property + def _stop(self): + """The ending index of this list, exclusive.""" + if self._sliceinfo[1] is None: + return len(self._parent) + return self._sliceinfo[1] + + @property + def _step(self): + """The number to increase the index by between items.""" + return self._sliceinfo[2] + + def _render(self): + """Return the actual list from the stored start/stop/step.""" + return list(self._parent)[self._start:self._stop:self._step] + + @inheritdoc + def append(self, item): + self._parent.insert(self._stop, item) + + @inheritdoc + def count(self, item): + return self._render().count(item) + + @inheritdoc + def index(self, item, start=None, stop=None): + if start is not None: + if stop is not None: + return self._render().index(item, start, stop) + return self._render().index(item, start) + return self._render().index(item) + + @inheritdoc + def extend(self, item): + self._parent[self._stop:self._stop] = item + + @inheritdoc + def insert(self, index, item): + if index < 0: + index = len(self) + index + self._parent.insert(self._start + index, item) + + @inheritdoc + def pop(self, index=None): + length = len(self) + if index is None: + index = length - 1 + elif index < 0: + index = length + index + if index < 0 or index >= length: + raise IndexError("pop index out of range") + return self._parent.pop(self._start + index) + + @inheritdoc + def remove(self, item): + index = self.index(item) + del self._parent[self._start + index] + + @inheritdoc + def reverse(self): + item = self._render() + item.reverse() + self._parent[self._start:self._stop:self._step] = item + + @inheritdoc + def sort(self, key=None, reverse=None): + item = self._render() + kwargs = {} + if key is not None: + kwargs["key"] = key + if reverse is not None: + kwargs["reverse"] = reverse + item.sort(**kwargs) + self._parent[self._start:self._stop:self._step] = item diff --git a/mwparserfromhell/smart_list/smart_list.py b/mwparserfromhell/smart_list/smart_list.py new file mode 100644 index 0000000..e2fd87f --- /dev/null +++ b/mwparserfromhell/smart_list/smart_list.py @@ -0,0 +1,158 @@ +# Copyright (C) 2012-2020 Ben Kurtovic +# Copyright (C) 2019-2020 Yuri Astrakhan +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from weakref import ref + +from .list_proxy import ListProxy +from .utils import _SliceNormalizerMixIn, inheritdoc + + +class SmartList(_SliceNormalizerMixIn, list): + """Implements the ``list`` interface with special handling of sublists. + + When a sublist is created (by ``list[i:j]``), any changes made to this + list (such as the addition, removal, or replacement of elements) will be + reflected in the sublist, or vice-versa, to the greatest degree possible. + This is implemented by having sublists - instances of the + :class:`.ListProxy` type - dynamically determine their elements by storing + their slice info and retrieving that slice from the parent. Methods that + change the size of the list also change the slice info. For example:: + + >>> parent = SmartList([0, 1, 2, 3]) + >>> parent + [0, 1, 2, 3] + >>> child = parent[2:] + >>> child + [2, 3] + >>> child.append(4) + >>> child + [2, 3, 4] + >>> parent + [0, 1, 2, 3, 4] + """ + + def __init__(self, iterable=None): + if iterable: + super().__init__(iterable) + else: + super().__init__() + self._children = {} + + def __getitem__(self, key): + if not isinstance(key, slice): + return super().__getitem__(key) + key = self._normalize_slice(key, clamp=False) + sliceinfo = [key.start, key.stop, key.step] + child = ListProxy(self, sliceinfo) + child_ref = ref(child, self._delete_child) + self._children[id(child_ref)] = (child_ref, sliceinfo) + return child + + def __setitem__(self, key, item): + if not isinstance(key, slice): + super().__setitem__(key, item) + return + item = list(item) + super().__setitem__(key, item) + key = self._normalize_slice(key, clamp=True) + diff = len(item) + (key.start - key.stop) // key.step + if not diff: + return + for child, (start, stop, _step) in self._children.values(): + if start > key.stop: + self._children[id(child)][1][0] += diff + if stop is not None and stop >= key.stop: + self._children[id(child)][1][1] += diff + + def __delitem__(self, key): + super().__delitem__(key) + if isinstance(key, slice): + key = self._normalize_slice(key, clamp=True) + else: + key = slice(key, key + 1, 1) + diff = (key.stop - key.start) // key.step + for child, (start, stop, _step) in self._children.values(): + if start > key.start: + self._children[id(child)][1][0] -= diff + if stop is not None and stop >= key.stop: + self._children[id(child)][1][1] -= diff + + def __add__(self, other): + return SmartList(list(self) + other) + + def __radd__(self, other): + return SmartList(other + list(self)) + + def __iadd__(self, other): + self.extend(other) + return self + + def _delete_child(self, child_ref): + """Remove a child reference that is about to be garbage-collected.""" + del self._children[id(child_ref)] + + def _detach_children(self): + """Remove all children and give them independent parent copies.""" + children = [val[0] for val in self._children.values()] + for child in children: + child()._parent = list(self) + self._children.clear() + + @inheritdoc + def append(self, item): + head = len(self) + self[head:head] = [item] + + @inheritdoc + def extend(self, item): + head = len(self) + self[head:head] = item + + @inheritdoc + def insert(self, index, item): + self[index:index] = [item] + + @inheritdoc + def pop(self, index=None): + if index is None: + index = len(self) - 1 + item = self[index] + del self[index] + return item + + @inheritdoc + def remove(self, item): + del self[self.index(item)] + + @inheritdoc + def reverse(self): + self._detach_children() + super().reverse() + + @inheritdoc + def sort(self, key=None, reverse=None): + self._detach_children() + kwargs = {} + if key is not None: + kwargs["key"] = key + if reverse is not None: + kwargs["reverse"] = reverse + super().sort(**kwargs) diff --git a/mwparserfromhell/smart_list/utils.py b/mwparserfromhell/smart_list/utils.py new file mode 100644 index 0000000..1a36d0b --- /dev/null +++ b/mwparserfromhell/smart_list/utils.py @@ -0,0 +1,50 @@ +# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2019-2020 Yuri Astrakhan +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from sys import maxsize + +__all__ = [] + + +def inheritdoc(method): + """Set __doc__ of *method* to __doc__ of *method* in its parent class. + + Since this is used on :class:`.SmartList`, the "parent class" used is + ``list``. This function can be used as a decorator. + """ + method.__doc__ = getattr(list, method.__name__).__doc__ + return method + + +class _SliceNormalizerMixIn: + """MixIn that provides a private method to normalize slices.""" + + def _normalize_slice(self, key, clamp=False): + """Return a slice equivalent to the input *key*, standardized.""" + if key.start is None: + start = 0 + else: + start = (len(self) + key.start) if key.start < 0 else key.start + if key.stop is None or key.stop == maxsize: + stop = len(self) if clamp else None + else: + stop = (len(self) + key.stop) if key.stop < 0 else key.stop + return slice(start, stop, key.step or 1) diff --git a/mwparserfromhell/string_mixin.py b/mwparserfromhell/string_mixin.py index 3664a09..2aeabf5 100644 --- a/mwparserfromhell/string_mixin.py +++ b/mwparserfromhell/string_mixin.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -22,14 +20,11 @@ """ This module contains the :class:`.StringMixIn` type, which implements the -interface for the ``unicode`` type (``str`` on py3k) in a dynamic manner. +interface for the ``str`` type in a dynamic manner. """ -from __future__ import unicode_literals from sys import getdefaultencoding -from .compat import bytes, py3k, str - __all__ = ["StringMixIn"] def inheritdoc(method): @@ -41,80 +36,67 @@ def inheritdoc(method): method.__doc__ = getattr(str, method.__name__).__doc__ return method -class StringMixIn(object): - """Implement the interface for ``unicode``/``str`` in a dynamic manner. +class StringMixIn: + """Implement the interface for ``str`` in a dynamic manner. - To use this class, inherit from it and override the :meth:`__unicode__` - method (same on py3k) to return the string representation of the object. - The various string methods will operate on the value of :meth:`__unicode__` - instead of the immutable ``self`` like the regular ``str`` type. + To use this class, inherit from it and override the :meth:`__str__` method + to return the string representation of the object. The various string + methods will operate on the value of :meth:`__str__` instead of the + immutable ``self`` like the regular ``str`` type. """ - if py3k: - def __str__(self): - return self.__unicode__() - - def __bytes__(self): - return bytes(self.__unicode__(), getdefaultencoding()) - else: - def __str__(self): - return bytes(self.__unicode__()) - - def __unicode__(self): + def __str__(self): raise NotImplementedError() + def __bytes__(self): + return bytes(self.__str__(), getdefaultencoding()) + def __repr__(self): - return repr(self.__unicode__()) + return repr(self.__str__()) def __lt__(self, other): - return self.__unicode__() < other + return self.__str__() < other def __le__(self, other): - return self.__unicode__() <= other + return self.__str__() <= other def __eq__(self, other): - return self.__unicode__() == other + return self.__str__() == other def __ne__(self, other): - return self.__unicode__() != other + return self.__str__() != other def __gt__(self, other): - return self.__unicode__() > other + return self.__str__() > other def __ge__(self, other): - return self.__unicode__() >= other + return self.__str__() >= other - if py3k: - def __bool__(self): - return bool(self.__unicode__()) - else: - def __nonzero__(self): - return bool(self.__unicode__()) + def __bool__(self): + return bool(self.__str__()) def __len__(self): - return len(self.__unicode__()) + return len(self.__str__()) def __iter__(self): - for char in self.__unicode__(): - yield char + yield from self.__str__() def __getitem__(self, key): - return self.__unicode__()[key] + return self.__str__()[key] def __reversed__(self): - return reversed(self.__unicode__()) + return reversed(self.__str__()) def __contains__(self, item): - return str(item) in self.__unicode__() + return str(item) in self.__str__() def __getattr__(self, attr): if not hasattr(str, attr): raise AttributeError("{!r} object has no attribute {!r}".format( type(self).__name__, attr)) - return getattr(self.__unicode__(), attr) + return getattr(self.__str__(), attr) - if py3k: - maketrans = str.maketrans # Static method can't rely on __getattr__ + maketrans = str.maketrans # Static method can't rely on __getattr__ del inheritdoc diff --git a/mwparserfromhell/utils.py b/mwparserfromhell/utils.py index d30a2da..5d262b9 100644 --- a/mwparserfromhell/utils.py +++ b/mwparserfromhell/utils.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2019 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -25,50 +23,47 @@ This module contains accessory functions for other parts of the library. Parser users generally won't need stuff from here. """ -from __future__ import unicode_literals - -from .compat import bytes, str -from .nodes import Node -from .smart_list import SmartList - __all__ = ["parse_anything"] def parse_anything(value, context=0, skip_style_tags=False): """Return a :class:`.Wikicode` for *value*, allowing multiple types. This differs from :meth:`.Parser.parse` in that we accept more than just a - string to be parsed. Unicode objects (strings in py3k), strings (bytes in - py3k), integers (converted to strings), ``None``, existing :class:`.Node` - or :class:`.Wikicode` objects, as well as an iterable of these types, are - supported. This is used to parse input on-the-fly by various methods of - :class:`.Wikicode` and others like :class:`.Template`, such as - :meth:`wikicode.insert() <.Wikicode.insert>` or setting - :meth:`template.name <.Template.name>`. + string to be parsed. Strings, bytes, integers (converted to strings), + ``None``, existing :class:`.Node` or :class:`.Wikicode` objects, as well + as an iterable of these types, are supported. This is used to parse input + on-the-fly by various methods of :class:`.Wikicode` and others like + :class:`.Template`, such as :meth:`wikicode.insert() <.Wikicode.insert>` + or setting :meth:`template.name <.Template.name>`. Additional arguments are passed directly to :meth:`.Parser.parse`. """ + # pylint: disable=cyclic-import,import-outside-toplevel + from .nodes import Node from .parser import Parser + from .smart_list import SmartList from .wikicode import Wikicode if isinstance(value, Wikicode): return value - elif isinstance(value, Node): + if isinstance(value, Node): return Wikicode(SmartList([value])) - elif isinstance(value, str): + if isinstance(value, str): return Parser().parse(value, context, skip_style_tags) - elif isinstance(value, bytes): + if isinstance(value, bytes): return Parser().parse(value.decode("utf8"), context, skip_style_tags) - elif isinstance(value, int): + if isinstance(value, int): return Parser().parse(str(value), context, skip_style_tags) - elif value is None: + if value is None: return Wikicode(SmartList()) - elif hasattr(value, "read"): + if hasattr(value, "read"): return parse_anything(value.read(), context, skip_style_tags) try: nodelist = SmartList() for item in value: nodelist += parse_anything(item, context, skip_style_tags).nodes return Wikicode(nodelist) - except TypeError: - error = "Needs string, Node, Wikicode, file, int, None, or iterable of these, but got {0}: {1}" - raise ValueError(error.format(type(value).__name__, value)) + except TypeError as exc: + error = ("Needs string, Node, Wikicode, file, int, None, or " + "iterable of these, but got {0}: {1}") + raise ValueError(error.format(type(value).__name__, value)) from exc diff --git a/mwparserfromhell/wikicode.py b/mwparserfromhell/wikicode.py index 840d8ed..bbd38a2 100644 --- a/mwparserfromhell/wikicode.py +++ b/mwparserfromhell/wikicode.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2019 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,14 +18,12 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals -from itertools import chain import re +from itertools import chain -from .compat import bytes, py3k, range, str from .nodes import (Argument, Comment, ExternalLink, Heading, HTMLEntity, Node, Tag, Template, Text, Wikilink) -from .smart_list import _ListProxy +from .smart_list.list_proxy import ListProxy from .string_mixin import StringMixIn from .utils import parse_anything @@ -48,10 +44,10 @@ class Wikicode(StringMixIn): RECURSE_OTHERS = 2 def __init__(self, nodes): - super(Wikicode, self).__init__() + super().__init__() self._nodes = nodes - def __unicode__(self): + def __str__(self): return "".join([str(node) for node in self.nodes]) @staticmethod @@ -63,8 +59,7 @@ class Wikicode(StringMixIn): for code in node.__children__(): for child in code.nodes: sub = Wikicode._get_children(child, contexts, restrict, code) - for result in sub: - yield result + yield from sub @staticmethod def _slice_replace(code, index, old, new): @@ -112,7 +107,7 @@ class Wikicode(StringMixIn): def _is_child_wikicode(self, obj, recursive=True): """Return whether the given :class:`.Wikicode` is a descendant.""" def deref(nodes): - if isinstance(nodes, _ListProxy): + if isinstance(nodes, ListProxy): return nodes._parent # pylint: disable=protected-access return nodes @@ -252,13 +247,13 @@ class Wikicode(StringMixIn): self.ifilter(forcetype=ftype, *a, **kw)) make_filter = lambda ftype: (lambda self, *a, **kw: self.filter(forcetype=ftype, *a, **kw)) - for name, ftype in (meths.items() if py3k else meths.iteritems()): - ifilter = make_ifilter(ftype) - filter = make_filter(ftype) - ifilter.__doc__ = doc.format(name, "ifilter", ftype) - filter.__doc__ = doc.format(name, "filter", ftype) - setattr(cls, "ifilter_" + name, ifilter) - setattr(cls, "filter_" + name, filter) + for name, ftype in meths.items(): + ifilt = make_ifilter(ftype) + filt = make_filter(ftype) + ifilt.__doc__ = doc.format(name, "ifilter", ftype) + filt.__doc__ = doc.format(name, "filter", ftype) + setattr(cls, "ifilter_" + name, ifilt) + setattr(cls, "filter_" + name, filt) @property def nodes(self): @@ -355,6 +350,7 @@ class Wikicode(StringMixIn): ancestors = _get_ancestors(code, needle) if ancestors is not None: return [node] + ancestors + return None if isinstance(obj, Wikicode): obj = obj.get(0) @@ -447,13 +443,13 @@ class Wikicode(StringMixIn): """ if isinstance(obj, (Node, Wikicode)): context, index = self._do_strong_search(obj, recursive) - for i in range(index.start, index.stop): + for _ in range(index.start, index.stop): context.nodes.pop(index.start) context.insert(index.start, value) else: for exact, context, index in self._do_weak_search(obj, recursive): if exact: - for i in range(index.start, index.stop): + for _ in range(index.start, index.stop): context.nodes.pop(index.start) context.insert(index.start, value) else: @@ -482,12 +478,12 @@ class Wikicode(StringMixIn): """ if isinstance(obj, (Node, Wikicode)): context, index = self._do_strong_search(obj, recursive) - for i in range(index.start, index.stop): + for _ in range(index.start, index.stop): context.nodes.pop(index.start) else: for exact, context, index in self._do_weak_search(obj, recursive): if exact: - for i in range(index.start, index.stop): + for _ in range(index.start, index.stop): context.nodes.pop(index.start) else: self._slice_replace(context, index, str(obj), "") @@ -649,8 +645,7 @@ class Wikicode(StringMixIn): while "\n\n\n" in stripped: stripped = stripped.replace("\n\n\n", "\n\n") return stripped - else: - return "".join(nodes) + return "".join(nodes) def get_tree(self): """Return a hierarchical tree representation of the object. diff --git a/scripts/memtest.py b/scripts/memtest.py index 64e8c6b..6f0d1ab 100644 --- a/scripts/memtest.py +++ b/scripts/memtest.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -21,10 +19,10 @@ # SOFTWARE. """ -Tests for memory leaks in the CTokenizer. Python 2 and 3 compatible. +Tests for memory leaks in the CTokenizer. This appears to work mostly fine under Linux, but gives an absurd number of -false positives on OS X. I'm not sure why. Running the tests multiple times +false positives on macOS. I'm not sure why. Running the tests multiple times yields different results (tests don't always leak, and the amount they leak by varies). Increasing the number of loops results in a smaller bytes/loop value, too, indicating the increase in memory usage might be due to something else. @@ -32,7 +30,6 @@ Actual memory leaks typically leak very large amounts of memory (megabytes) and scale with the number of loops. """ -from __future__ import unicode_literals, print_function from locale import LC_ALL, setlocale from multiprocessing import Process, Pipe from os import listdir, path @@ -40,22 +37,18 @@ import sys import psutil -from mwparserfromhell.compat import py3k from mwparserfromhell.parser._tokenizer import CTokenizer -if sys.version_info[0] == 2: - range = xrange - LOOPS = 10000 -class Color(object): +class Color: GRAY = "\x1b[30;1m" GREEN = "\x1b[92m" YELLOW = "\x1b[93m" RESET = "\x1b[0m" -class MemoryTest(object): +class MemoryTest: """Manages a memory test.""" def __init__(self): @@ -88,8 +81,6 @@ class MemoryTest(object): def load_file(filename): with open(filename, "rU") as fp: text = fp.read() - if not py3k: - text = text.decode("utf8") name = path.split(filename)[1][:0-len(extension)] self._parse_file(name, text) @@ -154,13 +145,13 @@ class MemoryTest(object): def _runner(text, child): r1, r2 = range(250), range(LOOPS) - for i in r1: + for _ in r1: CTokenizer().tokenize(text) child.send("OK") child.recv() child.send("OK") child.recv() - for i in r2: + for _ in r2: CTokenizer().tokenize(text) child.send("OK") child.recv() diff --git a/setup.py b/setup.py index 97abef0..3c7d2cb 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,6 @@ #! /usr/bin/env python -# -*- coding: utf-8 -*- # -# Copyright (C) 2012-2018 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -21,23 +20,17 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import print_function from distutils.errors import DistutilsError, CCompilerError from glob import glob from os import environ import sys -if ((sys.version_info[0] == 2 and sys.version_info[1] < 7) or - (sys.version_info[1] == 3 and sys.version_info[1] < 4)): - raise RuntimeError("mwparserfromhell needs Python 2.7 or 3.4+") - from setuptools import setup, find_packages, Extension from setuptools.command.build_ext import build_ext from mwparserfromhell import __version__ -from mwparserfromhell.compat import py3k -with open("README.rst", **({'encoding':'utf-8'} if py3k else {})) as fp: +with open("README.rst") as fp: long_docs = fp.read() use_extension = True @@ -84,6 +77,7 @@ setup( ext_modules = [tokenizer] if use_extension else [], test_suite = "tests", version = __version__, + python_requires = ">= 3.5", author = "Ben Kurtovic", author_email = "ben.kurtovic@gmail.com", url = "https://github.com/earwig/mwparserfromhell", @@ -98,14 +92,12 @@ setup( "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", "Topic :: Text Processing :: Markup" ], ) diff --git a/tests/__init__.py b/tests/__init__.py index 89907bf..e69de29 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/tests/_test_tokenizer.py b/tests/_test_tokenizer.py index 4d19dd4..6a749c7 100644 --- a/tests/_test_tokenizer.py +++ b/tests/_test_tokenizer.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,22 +18,19 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals import codecs from os import listdir, path import sys import warnings -from mwparserfromhell.compat import py3k, str from mwparserfromhell.parser import tokens from mwparserfromhell.parser.builder import Builder class _TestParseError(Exception): """Raised internally when a test could not be parsed.""" - pass -class TokenizerTestCase(object): +class TokenizerTestCase: """A base test case for tokenizers, whose tests are loaded dynamically. Subclassed along with unittest.TestCase to form TestPyTokenizer and @@ -44,7 +39,7 @@ class TokenizerTestCase(object): """ @staticmethod - def _build_test_method(funcname, data): + def _build_test_method(data): """Create and return a method to be treated as a test case method. *data* is a dict containing multiple keys: the *input* text to be @@ -60,8 +55,6 @@ class TokenizerTestCase(object): actual = self.tokenizer().tokenize(data["input"]) self.assertEqual(expected, actual) - if not py3k: - inner.__name__ = funcname.encode("utf8") inner.__doc__ = data["label"] return inner @@ -84,7 +77,7 @@ class TokenizerTestCase(object): try: data["output"] = eval(raw, vars(tokens)) except Exception as err: - raise _TestParseError(err) + raise _TestParseError(err) from err @classmethod def _load_tests(cls, filename, name, text, restrict=None): @@ -120,7 +113,7 @@ class TokenizerTestCase(object): continue fname = "test_{}{}_{}".format(name, number, data["name"]) - meth = cls._build_test_method(fname, data) + meth = cls._build_test_method(data) setattr(cls, fname, meth) @classmethod diff --git a/tests/_test_tree_equality.py b/tests/_test_tree_equality.py index aba54d1..3dc28c0 100644 --- a/tests/_test_tree_equality.py +++ b/tests/_test_tree_equality.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2019 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,13 +18,10 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals from unittest import TestCase -from mwparserfromhell.compat import range from mwparserfromhell.nodes import (Argument, Comment, Heading, HTMLEntity, Tag, Template, Text, Wikilink) -from mwparserfromhell.nodes.extras import Attribute, Parameter from mwparserfromhell.smart_list import SmartList from mwparserfromhell.wikicode import Wikicode diff --git a/tests/compat.py b/tests/compat.py deleted file mode 100644 index d5b3fba..0000000 --- a/tests/compat.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Serves the same purpose as mwparserfromhell.compat, but only for objects -required by unit tests. This avoids unnecessary imports (like urllib) within -the main library. -""" - -from mwparserfromhell.compat import py3k - -if py3k: - from io import StringIO - from urllib.parse import urlencode - from urllib.request import urlopen - -else: - from StringIO import StringIO - from urllib import urlencode, urlopen diff --git a/tests/test_argument.py b/tests/test_argument.py index 16b4d0c..ccf429d 100644 --- a/tests/test_argument.py +++ b/tests/test_argument.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,10 +18,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals import unittest -from mwparserfromhell.compat import str from mwparserfromhell.nodes import Argument, Text from ._test_tree_equality import TreeEqualityTestCase, wrap, wraptext @@ -31,8 +27,8 @@ from ._test_tree_equality import TreeEqualityTestCase, wrap, wraptext class TestArgument(TreeEqualityTestCase): """Test cases for the Argument node.""" - def test_unicode(self): - """test Argument.__unicode__()""" + def test_str(self): + """test Argument.__str__()""" node = Argument(wraptext("foobar")) self.assertEqual("{{{foobar}}}", str(node)) node2 = Argument(wraptext("foo"), wraptext("bar")) diff --git a/tests/test_attribute.py b/tests/test_attribute.py index e9f2528..74739b7 100644 --- a/tests/test_attribute.py +++ b/tests/test_attribute.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2019 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,10 +18,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals import unittest -from mwparserfromhell.compat import str from mwparserfromhell.nodes import Template from mwparserfromhell.nodes.extras import Attribute @@ -32,8 +28,8 @@ from ._test_tree_equality import TreeEqualityTestCase, wrap, wraptext class TestAttribute(TreeEqualityTestCase): """Test cases for the Attribute node extra.""" - def test_unicode(self): - """test Attribute.__unicode__()""" + def test_str(self): + """test Attribute.__str__()""" node = Attribute(wraptext("foo")) self.assertEqual(" foo", str(node)) node2 = Attribute(wraptext("foo"), wraptext("bar")) diff --git a/tests/test_builder.py b/tests/test_builder.py index 7343077..59f3445 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright (C) 2012-2019 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy @@ -20,10 +18,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals import unittest -from mwparserfromhell.compat import py3k from mwparserfromhell.nodes import (Argument, Comment, ExternalLink, Heading, HTMLEntity, Tag, Template, Text, Wikilink) from mwparserfromhell.nodes.extras import Attribute, Parameter @@ -428,9 +424,8 @@ class TestBuilder(TreeEqualityTestCase): [tokens.TagOpenOpen()] ] - func = self.assertRaisesRegex if py3k else self.assertRaisesRegexp msg = r"_handle_token\(\) got unexpected TemplateClose" - func(ParserError, msg, self.builder.build, [tokens.TemplateClose()]) + self.assertRaisesRegex(ParserError, msg, self.builder.build, [tokens.TemplateClose()]) for test in missing_closes: self.assertRaises(ParserError, self.builder.build, test) diff --git a/tests/test_comment.py b/tests/test_comment.py index cf2f14d..071d1d8 100644 --- a/tests/test_comment.py +++ b/tests/test_comment.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,10 +18,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals import unittest -from mwparserfromhell.compat import str from mwparserfromhell.nodes import Comment from ._test_tree_equality import TreeEqualityTestCase @@ -31,8 +27,8 @@ from ._test_tree_equality import TreeEqualityTestCase class TestComment(TreeEqualityTestCase): """Test cases for the Comment node.""" - def test_unicode(self): - """test Comment.__unicode__()""" + def test_str(self): + """test Comment.__str__()""" node = Comment("foobar") self.assertEqual("", str(node)) diff --git a/tests/test_ctokenizer.py b/tests/test_ctokenizer.py index 3552a02..53d6700 100644 --- a/tests/test_ctokenizer.py +++ b/tests/test_ctokenizer.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright (C) 2012-2016 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy @@ -20,7 +18,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals import unittest try: diff --git a/tests/test_docs.py b/tests/test_docs.py index 8559493..e478a5f 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,23 +18,22 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import print_function, unicode_literals import json +from io import StringIO import os import unittest +from urllib.parse import urlencode +from urllib.request import urlopen import mwparserfromhell -from mwparserfromhell.compat import py3k, str - -from .compat import StringIO, urlencode, urlopen class TestDocs(unittest.TestCase): """Integration test cases for mwparserfromhell's documentation.""" - def assertPrint(self, input, output): - """Assertion check that *input*, when printed, produces *output*.""" + def assertPrint(self, value, output): + """Assertion check that *value*, when printed, produces *output*.""" buff = StringIO() - print(input, end="", file=buff) + print(value, end="", file=buff) buff.seek(0) self.assertEqual(output, buff.read()) @@ -47,16 +44,10 @@ class TestDocs(unittest.TestCase): self.assertPrint(wikicode, "I has a template! {{foo|bar|baz|eggs=spam}} See it?") templates = wikicode.filter_templates() - if py3k: - self.assertPrint(templates, "['{{foo|bar|baz|eggs=spam}}']") - else: - self.assertPrint(templates, "[u'{{foo|bar|baz|eggs=spam}}']") + self.assertPrint(templates, "['{{foo|bar|baz|eggs=spam}}']") template = templates[0] self.assertPrint(template.name, "foo") - if py3k: - self.assertPrint(template.params, "['bar', 'baz', 'eggs=spam']") - else: - self.assertPrint(template.params, "[u'bar', u'baz', u'eggs=spam']") + self.assertPrint(template.params, "['bar', 'baz', 'eggs=spam']") self.assertPrint(template.get(1).value, "bar") self.assertPrint(template.get("eggs").value, "spam") @@ -64,21 +55,14 @@ class TestDocs(unittest.TestCase): """test a block of example code in the README""" text = "{{foo|{{bar}}={{baz|{{spam}}}}}}" temps = mwparserfromhell.parse(text).filter_templates() - if py3k: - res = "['{{foo|{{bar}}={{baz|{{spam}}}}}}', '{{bar}}', '{{baz|{{spam}}}}', '{{spam}}']" - else: - res = "[u'{{foo|{{bar}}={{baz|{{spam}}}}}}', u'{{bar}}', u'{{baz|{{spam}}}}', u'{{spam}}']" + res = "['{{foo|{{bar}}={{baz|{{spam}}}}}}', '{{bar}}', '{{baz|{{spam}}}}', '{{spam}}']" self.assertPrint(temps, res) def test_readme_3(self): """test a block of example code in the README""" code = mwparserfromhell.parse("{{foo|this {{includes a|template}}}}") - if py3k: - self.assertPrint(code.filter_templates(recursive=False), - "['{{foo|this {{includes a|template}}}}']") - else: - self.assertPrint(code.filter_templates(recursive=False), - "[u'{{foo|this {{includes a|template}}}}']") + self.assertPrint(code.filter_templates(recursive=False), + "['{{foo|this {{includes a|template}}}}']") foo = code.filter_templates(recursive=False)[0] self.assertPrint(foo.get(1).value, "this {{includes a|template}}") self.assertPrint(foo.get(1).value.filter_templates()[0], @@ -98,10 +82,7 @@ class TestDocs(unittest.TestCase): code.replace("{{uncategorized}}", "{{bar-stub}}") res = "{{cleanup|date=July 2012}} '''Foo''' is a [[bar]]. {{bar-stub}}" self.assertPrint(code, res) - if py3k: - res = "['{{cleanup|date=July 2012}}', '{{bar-stub}}']" - else: - res = "[u'{{cleanup|date=July 2012}}', u'{{bar-stub}}']" + res = "['{{cleanup|date=July 2012}}', '{{bar-stub}}']" self.assertPrint(code.filter_templates(), res) text = str(code) res = "{{cleanup|date=July 2012}} '''Foo''' is a [[bar]]. {{bar-stub}}" @@ -126,14 +107,14 @@ class TestDocs(unittest.TestCase): } try: raw = urlopen(url1, urlencode(data).encode("utf8")).read() - except IOError: + except OSError: self.skipTest("cannot continue because of unsuccessful web call") res = json.loads(raw.decode("utf8")) revision = res["query"]["pages"][0]["revisions"][0] text = revision["slots"]["main"]["content"] try: expected = urlopen(url2.format(title)).read().decode("utf8") - except IOError: + except OSError: self.skipTest("cannot continue because of unsuccessful web call") actual = mwparserfromhell.parse(text) self.assertEqual(expected, actual) diff --git a/tests/test_external_link.py b/tests/test_external_link.py index c70905a..462a438 100644 --- a/tests/test_external_link.py +++ b/tests/test_external_link.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,10 +18,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals import unittest -from mwparserfromhell.compat import str from mwparserfromhell.nodes import ExternalLink, Text from ._test_tree_equality import TreeEqualityTestCase, wrap, wraptext @@ -31,8 +27,8 @@ from ._test_tree_equality import TreeEqualityTestCase, wrap, wraptext class TestExternalLink(TreeEqualityTestCase): """Test cases for the ExternalLink node.""" - def test_unicode(self): - """test ExternalLink.__unicode__()""" + def test_str(self): + """test ExternalLink.__str__()""" node = ExternalLink(wraptext("http://example.com/"), brackets=False) self.assertEqual("http://example.com/", str(node)) node2 = ExternalLink(wraptext("http://example.com/")) diff --git a/tests/test_heading.py b/tests/test_heading.py index e5ec470..d043bc7 100644 --- a/tests/test_heading.py +++ b/tests/test_heading.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,10 +18,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals import unittest -from mwparserfromhell.compat import str from mwparserfromhell.nodes import Heading, Text from ._test_tree_equality import TreeEqualityTestCase, wrap, wraptext @@ -31,8 +27,8 @@ from ._test_tree_equality import TreeEqualityTestCase, wrap, wraptext class TestHeading(TreeEqualityTestCase): """Test cases for the Heading node.""" - def test_unicode(self): - """test Heading.__unicode__()""" + def test_str(self): + """test Heading.__str__()""" node = Heading(wraptext("foobar"), 2) self.assertEqual("==foobar==", str(node)) node2 = Heading(wraptext(" zzz "), 5) diff --git a/tests/test_html_entity.py b/tests/test_html_entity.py index fc09fde..85a23ec 100644 --- a/tests/test_html_entity.py +++ b/tests/test_html_entity.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,19 +18,17 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals import unittest -from mwparserfromhell.compat import str from mwparserfromhell.nodes import HTMLEntity -from ._test_tree_equality import TreeEqualityTestCase, wrap +from ._test_tree_equality import TreeEqualityTestCase class TestHTMLEntity(TreeEqualityTestCase): """Test cases for the HTMLEntity node.""" - def test_unicode(self): - """test HTMLEntity.__unicode__()""" + def test_str(self): + """test HTMLEntity.__str__()""" node1 = HTMLEntity("nbsp", named=True, hexadecimal=False) node2 = HTMLEntity("107", named=False, hexadecimal=False) node3 = HTMLEntity("6b", named=False, hexadecimal=True) diff --git a/tests/test_parameter.py b/tests/test_parameter.py index be09448..fe527f1 100644 --- a/tests/test_parameter.py +++ b/tests/test_parameter.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,20 +18,17 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals import unittest -from mwparserfromhell.compat import str -from mwparserfromhell.nodes import Text from mwparserfromhell.nodes.extras import Parameter -from ._test_tree_equality import TreeEqualityTestCase, wrap, wraptext +from ._test_tree_equality import TreeEqualityTestCase, wraptext class TestParameter(TreeEqualityTestCase): """Test cases for the Parameter node extra.""" - def test_unicode(self): - """test Parameter.__unicode__()""" + def test_str(self): + """test Parameter.__str__()""" node = Parameter(wraptext("1"), wraptext("foo"), showkey=False) self.assertEqual("foo", str(node)) node2 = Parameter(wraptext("foo"), wraptext("bar")) diff --git a/tests/test_parser.py b/tests/test_parser.py index 5b12a0e..b16449f 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright (C) 2012-2016 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy @@ -20,11 +18,9 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals import unittest from mwparserfromhell import parser -from mwparserfromhell.compat import range from mwparserfromhell.nodes import Tag, Template, Text, Wikilink from mwparserfromhell.nodes.extras import Parameter diff --git a/tests/test_pytokenizer.py b/tests/test_pytokenizer.py index a4c9bc1..3015b14 100644 --- a/tests/test_pytokenizer.py +++ b/tests/test_pytokenizer.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright (C) 2012-2019 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy @@ -20,7 +18,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals import unittest from mwparserfromhell.parser import contexts diff --git a/tests/test_roundtripping.py b/tests/test_roundtripping.py index 50f9c1f..b5e204c 100644 --- a/tests/test_roundtripping.py +++ b/tests/test_roundtripping.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright (C) 2012-2016 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy @@ -20,7 +18,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals import unittest from ._test_tokenizer import TokenizerTestCase diff --git a/tests/test_smart_list.py b/tests/test_smart_list.py index 3c9f711..0cdb79a 100644 --- a/tests/test_smart_list.py +++ b/tests/test_smart_list.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,14 +18,14 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals import unittest -from mwparserfromhell.compat import py3k, range -from mwparserfromhell.smart_list import SmartList, _ListProxy +from mwparserfromhell.smart_list import SmartList +from mwparserfromhell.smart_list.list_proxy import ListProxy + class TestSmartList(unittest.TestCase): - """Test cases for the SmartList class and its child, _ListProxy.""" + """Test cases for the SmartList class and its child, ListProxy.""" def _test_get_set_del_item(self, builder): """Run tests on __get/set/delitem__ of a list built with *builder*.""" @@ -126,14 +124,9 @@ class TestSmartList(unittest.TestCase): list3 = builder([0, 2, 3, 4]) list4 = builder([0, 1, 2]) - if py3k: - self.assertEqual("[0, 1, 2, 3, 'one', 'two']", str(list1)) - self.assertEqual(b"\x00\x01\x02", bytes(list4)) - self.assertEqual("[0, 1, 2, 3, 'one', 'two']", repr(list1)) - else: - self.assertEqual("[0, 1, 2, 3, u'one', u'two']", unicode(list1)) - self.assertEqual(b"[0, 1, 2, 3, u'one', u'two']", str(list1)) - self.assertEqual(b"[0, 1, 2, 3, u'one', u'two']", repr(list1)) + self.assertEqual("[0, 1, 2, 3, 'one', 'two']", str(list1)) + self.assertEqual(b"\x00\x01\x02", bytes(list4)) + self.assertEqual("[0, 1, 2, 3, 'one', 'two']", repr(list1)) self.assertLess(list1, list3) self.assertLessEqual(list1, list3) @@ -184,7 +177,7 @@ class TestSmartList(unittest.TestCase): gen1 = iter(list1) out = [] - for i in range(len(list1)): + for _ in range(len(list1)): out.append(next(gen1)) self.assertRaises(StopIteration, next, gen1) self.assertEqual([0, 1, 2, 3, "one", "two"], out) @@ -261,18 +254,13 @@ class TestSmartList(unittest.TestCase): self.assertEqual([0, 2, 2, 3, 4, 5], list1) list1.sort(reverse=True) self.assertEqual([5, 4, 3, 2, 2, 0], list1) - if not py3k: - func = lambda x, y: abs(3 - x) - abs(3 - y) # Distance from 3 - list1.sort(cmp=func) - self.assertEqual([3, 4, 2, 2, 5, 0], list1) - list1.sort(cmp=func, reverse=True) - self.assertEqual([0, 5, 4, 2, 2, 3], list1) list3.sort(key=lambda i: i[1]) self.assertEqual([("d", 2), ("c", 3), ("a", 5), ("b", 8)], list3) list3.sort(key=lambda i: i[1], reverse=True) self.assertEqual([("b", 8), ("a", 5), ("c", 3), ("d", 2)], list3) - def _dispatch_test_for_children(self, meth): + @staticmethod + def _dispatch_test_for_children(meth): """Run a test method on various different types of children.""" meth(lambda L: SmartList(list(L))[:]) meth(lambda L: SmartList([999] + list(L))[1:]) @@ -280,13 +268,13 @@ class TestSmartList(unittest.TestCase): meth(lambda L: SmartList([101, 102] + list(L) + [201, 202])[2:-2]) def test_docs(self): - """make sure the methods of SmartList/_ListProxy have docstrings""" + """make sure the methods of SmartList/ListProxy have docstrings""" methods = ["append", "count", "extend", "index", "insert", "pop", "remove", "reverse", "sort"] for meth in methods: expected = getattr(list, meth).__doc__ smartlist_doc = getattr(SmartList, meth).__doc__ - listproxy_doc = getattr(_ListProxy, meth).__doc__ + listproxy_doc = getattr(ListProxy, meth).__doc__ self.assertEqual(expected, smartlist_doc) self.assertEqual(expected, listproxy_doc) @@ -317,19 +305,19 @@ class TestSmartList(unittest.TestCase): self._test_list_methods(SmartList) def test_child_get_set_del(self): - """make sure _ListProxy's getitem/setitem/delitem work""" + """make sure ListProxy's getitem/setitem/delitem work""" self._dispatch_test_for_children(self._test_get_set_del_item) def test_child_add(self): - """make sure _ListProxy's add/radd/iadd work""" + """make sure ListProxy's add/radd/iadd work""" self._dispatch_test_for_children(self._test_add_radd_iadd) def test_child_other_magics(self): - """make sure _ListProxy's other magically implemented features work""" + """make sure ListProxy's other magically implemented features work""" self._dispatch_test_for_children(self._test_other_magic_methods) def test_child_methods(self): - """make sure _ListProxy's non-magic methods work, like append()""" + """make sure ListProxy's non-magic methods work, like append()""" self._dispatch_test_for_children(self._test_list_methods) def test_influence(self): diff --git a/tests/test_string_mixin.py b/tests/test_string_mixin.py index 11ee6b7..e6d1ff4 100644 --- a/tests/test_string_mixin.py +++ b/tests/test_string_mixin.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,19 +18,17 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals from sys import getdefaultencoding from types import GeneratorType import unittest -from mwparserfromhell.compat import bytes, py3k, range, str from mwparserfromhell.string_mixin import StringMixIn class _FakeString(StringMixIn): def __init__(self, data): self._data = data - def __unicode__(self): + def __str__(self): return self._data @@ -42,18 +38,16 @@ class TestStringMixIn(unittest.TestCase): def test_docs(self): """make sure the various methods of StringMixIn have docstrings""" methods = [ - "capitalize", "center", "count", "encode", "endswith", - "expandtabs", "find", "format", "index", "isalnum", "isalpha", - "isdecimal", "isdigit", "islower", "isnumeric", "isspace", - "istitle", "isupper", "join", "ljust", "lower", "lstrip", - "partition", "replace", "rfind", "rindex", "rjust", "rpartition", - "rsplit", "rstrip", "split", "splitlines", "startswith", "strip", - "swapcase", "title", "translate", "upper", "zfill"] - if py3k: - methods.extend(["casefold", "format_map", "isidentifier", "isprintable", - "maketrans"]) - else: - methods.append("decode") + "capitalize", "casefold", "center", "count", "encode", "endswith", + "expandtabs", "find", "format", "format_map", "index", "isalnum", + "isalpha", "isdecimal", "isdigit", "isidentifier", "islower", + "isnumeric", "isprintable", "isspace", "istitle", "isupper", + "join", "ljust", "lower", "lstrip", "maketrans", "partition", + "replace", "rfind", "rindex", "rjust", "rpartition", "rsplit", + "rstrip", "split", "splitlines", "startswith", "strip", "swapcase", + "title", "translate", "upper", "zfill" + ] + for meth in methods: expected = getattr("foo", meth).__doc__ actual = getattr(_FakeString("foo"), meth).__doc__ @@ -64,17 +58,11 @@ class TestStringMixIn(unittest.TestCase): fstr = _FakeString("fake string") self.assertEqual(str(fstr), "fake string") self.assertEqual(bytes(fstr), b"fake string") - if py3k: - self.assertEqual(repr(fstr), "'fake string'") - else: - self.assertEqual(repr(fstr), b"u'fake string'") + self.assertEqual(repr(fstr), "'fake string'") self.assertIsInstance(str(fstr), str) self.assertIsInstance(bytes(fstr), bytes) - if py3k: - self.assertIsInstance(repr(fstr), str) - else: - self.assertIsInstance(repr(fstr), bytes) + self.assertIsInstance(repr(fstr), str) def test_comparisons(self): """make sure comparison operators work""" @@ -139,7 +127,7 @@ class TestStringMixIn(unittest.TestCase): self.assertIsInstance(gen2, GeneratorType) out = [] - for i in range(len(str1)): + for _ in range(len(str1)): out.append(next(gen1)) self.assertRaises(StopIteration, next, gen1) self.assertEqual(expected, out) @@ -179,14 +167,6 @@ class TestStringMixIn(unittest.TestCase): self.assertEqual(1, str1.count("r", 5, 9)) self.assertEqual(0, str1.count("r", 5, 7)) - if not py3k: - str2 = _FakeString("fo") - self.assertEqual(str1, str1.decode()) - actual = _FakeString("\\U00010332\\U0001033f\\U00010344") - self.assertEqual("𐌲𐌿𐍄", actual.decode("unicode_escape")) - self.assertRaises(UnicodeError, str2.decode, "punycode") - self.assertEqual("", str2.decode("punycode", "ignore")) - str3 = _FakeString("𐌲𐌿𐍄") actual = b"\xF0\x90\x8C\xB2\xF0\x90\x8C\xBF\xF0\x90\x8D\x84" self.assertEqual(b"fake string", str1.encode()) @@ -233,10 +213,9 @@ class TestStringMixIn(unittest.TestCase): self.assertEqual("foobarbazbuzz", str7.format("bar", abc="baz")) self.assertRaises(IndexError, str8.format, "abc") - if py3k: - self.assertEqual("fake string", str1.format_map({})) - self.assertEqual("foobarbaz", str6.format_map({"abc": "bar"})) - self.assertRaises(ValueError, str5.format_map, {0: "abc"}) + self.assertEqual("fake string", str1.format_map({})) + self.assertEqual("foobarbaz", str6.format_map({"abc": "bar"})) + self.assertRaises(ValueError, str5.format_map, {0: "abc"}) self.assertEqual(3, str1.index("e")) self.assertRaises(ValueError, str1.index, "z") @@ -269,11 +248,10 @@ class TestStringMixIn(unittest.TestCase): self.assertFalse(str13.isdigit()) self.assertTrue(str14.isdigit()) - if py3k: - self.assertTrue(str9.isidentifier()) - self.assertTrue(str10.isidentifier()) - self.assertFalse(str11.isidentifier()) - self.assertFalse(str12.isidentifier()) + self.assertTrue(str9.isidentifier()) + self.assertTrue(str10.isidentifier()) + self.assertFalse(str11.isidentifier()) + self.assertFalse(str12.isidentifier()) str15 = _FakeString("") str16 = _FakeString("FooBar") @@ -286,13 +264,12 @@ class TestStringMixIn(unittest.TestCase): self.assertTrue(str13.isnumeric()) self.assertTrue(str14.isnumeric()) - if py3k: - str16B = _FakeString("\x01\x02") - self.assertTrue(str9.isprintable()) - self.assertTrue(str13.isprintable()) - self.assertTrue(str14.isprintable()) - self.assertTrue(str15.isprintable()) - self.assertFalse(str16B.isprintable()) + str16B = _FakeString("\x01\x02") + self.assertTrue(str9.isprintable()) + self.assertTrue(str13.isprintable()) + self.assertTrue(str14.isprintable()) + self.assertTrue(str15.isprintable()) + self.assertFalse(str16B.isprintable()) str17 = _FakeString(" ") str18 = _FakeString("\t \t \r\n") @@ -323,10 +300,9 @@ class TestStringMixIn(unittest.TestCase): self.assertEqual("", str15.lower()) self.assertEqual("foobar", str16.lower()) self.assertEqual("ß", str22.lower()) - if py3k: - self.assertEqual("", str15.casefold()) - self.assertEqual("foobar", str16.casefold()) - self.assertEqual("ss", str22.casefold()) + self.assertEqual("", str15.casefold()) + self.assertEqual("foobar", str16.casefold()) + self.assertEqual("ss", str22.casefold()) str23 = _FakeString(" fake string ") self.assertEqual("fake string", str1.lstrip()) @@ -372,9 +348,8 @@ class TestStringMixIn(unittest.TestCase): self.assertEqual(actual, str25.rsplit(None, 3)) actual = [" this is a sentence with", "", "whitespace", ""] self.assertEqual(actual, str25.rsplit(" ", 3)) - if py3k: - actual = [" this is a", "sentence", "with", "whitespace"] - self.assertEqual(actual, str25.rsplit(maxsplit=3)) + actual = [" this is a", "sentence", "with", "whitespace"] + self.assertEqual(actual, str25.rsplit(maxsplit=3)) self.assertEqual("fake string", str1.rstrip()) self.assertEqual(" fake string", str23.rstrip()) @@ -390,9 +365,8 @@ class TestStringMixIn(unittest.TestCase): self.assertEqual(actual, str25.split(None, 3)) actual = ["", "", "", "this is a sentence with whitespace "] self.assertEqual(actual, str25.split(" ", 3)) - if py3k: - actual = ["this", "is", "a", "sentence with whitespace "] - self.assertEqual(actual, str25.split(maxsplit=3)) + actual = ["this", "is", "a", "sentence with whitespace "] + self.assertEqual(actual, str25.split(maxsplit=3)) str26 = _FakeString("lines\nof\ntext\r\nare\r\npresented\nhere") self.assertEqual(["lines", "of", "text", "are", "presented", "here"], @@ -411,17 +385,13 @@ class TestStringMixIn(unittest.TestCase): self.assertEqual("Fake String", str1.title()) - if py3k: - table1 = StringMixIn.maketrans({97: "1", 101: "2", 105: "3", - 111: "4", 117: "5"}) - table2 = StringMixIn.maketrans("aeiou", "12345") - table3 = StringMixIn.maketrans("aeiou", "12345", "rts") - self.assertEqual("f1k2 str3ng", str1.translate(table1)) - self.assertEqual("f1k2 str3ng", str1.translate(table2)) - self.assertEqual("f1k2 3ng", str1.translate(table3)) - else: - table = {97: "1", 101: "2", 105: "3", 111: "4", 117: "5"} - self.assertEqual("f1k2 str3ng", str1.translate(table)) + table1 = StringMixIn.maketrans({97: "1", 101: "2", 105: "3", + 111: "4", 117: "5"}) + table2 = StringMixIn.maketrans("aeiou", "12345") + table3 = StringMixIn.maketrans("aeiou", "12345", "rts") + self.assertEqual("f1k2 str3ng", str1.translate(table1)) + self.assertEqual("f1k2 str3ng", str1.translate(table2)) + self.assertEqual("f1k2 3ng", str1.translate(table3)) self.assertEqual("", str15.upper()) self.assertEqual("FOOBAR", str16.upper()) diff --git a/tests/test_tag.py b/tests/test_tag.py index c8c9808..2bea194 100644 --- a/tests/test_tag.py +++ b/tests/test_tag.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,10 +18,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals import unittest -from mwparserfromhell.compat import str from mwparserfromhell.nodes import Tag, Template, Text from mwparserfromhell.nodes.extras import Attribute from ._test_tree_equality import TreeEqualityTestCase, wrap, wraptext @@ -37,8 +33,8 @@ agenpnv = lambda name, a, b, c: Attribute(wraptext(name), None, '"', a, b, c) class TestTag(TreeEqualityTestCase): """Test cases for the Tag node.""" - def test_unicode(self): - """test Tag.__unicode__()""" + def test_str(self): + """test Tag.__str__()""" node1 = Tag(wraptext("ref")) node2 = Tag(wraptext("span"), wraptext("foo"), [agen("style", "color: red;")]) @@ -230,7 +226,7 @@ class TestTag(TreeEqualityTestCase): node.wiki_markup = "{" self.assertEqual("{|\n{", node) node2 = Tag(wraptext("table"), wraptext("\n"), wiki_style_separator="|") - self.assertEqual("|", node.wiki_style_separator) + self.assertEqual("|", node2.wiki_style_separator) def test_closing_wiki_markup(self): """test getter/setter for closing_wiki_markup attribute""" diff --git a/tests/test_template.py b/tests/test_template.py index e03a564..cf812a6 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2017 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,11 +18,9 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals from difflib import unified_diff import unittest -from mwparserfromhell.compat import str from mwparserfromhell.nodes import HTMLEntity, Template, Text from mwparserfromhell.nodes.extras import Parameter from mwparserfromhell import parse @@ -37,8 +33,8 @@ pgenh = lambda k, v: Parameter(wraptext(k), wraptext(v), showkey=False) class TestTemplate(TreeEqualityTestCase): """Test cases for the Template node.""" - def test_unicode(self): - """test Template.__unicode__()""" + def test_str(self): + """test Template.__str__()""" node = Template(wraptext("foobar")) self.assertEqual("{{foobar}}", str(node)) node2 = Template(wraptext("foo"), diff --git a/tests/test_text.py b/tests/test_text.py index 4464418..96dff16 100644 --- a/tests/test_text.py +++ b/tests/test_text.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,17 +18,15 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals import unittest -from mwparserfromhell.compat import str from mwparserfromhell.nodes import Text class TestText(unittest.TestCase): """Test cases for the Text node.""" - def test_unicode(self): - """test Text.__unicode__()""" + def test_str(self): + """test Text.__str__()""" node = Text("foobar") self.assertEqual("foobar", str(node)) node2 = Text("fóóbar") diff --git a/tests/test_tokens.py b/tests/test_tokens.py index e766002..42f54bf 100644 --- a/tests/test_tokens.py +++ b/tests/test_tokens.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,10 +18,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals import unittest -from mwparserfromhell.compat import py3k from mwparserfromhell.parser import tokens class TestTokens(unittest.TestCase): @@ -64,17 +60,9 @@ class TestTokens(unittest.TestCase): hundredchars = ("earwig" * 100)[:97] + "..." self.assertEqual("Token()", repr(token1)) - if py3k: - token2repr1 = "Token(foo='bar', baz=123)" - token2repr2 = "Token(baz=123, foo='bar')" - token3repr = "Text(text='" + hundredchars + "')" - else: - token2repr1 = "Token(foo=u'bar', baz=123)" - token2repr2 = "Token(baz=123, foo=u'bar')" - token3repr = "Text(text=u'" + hundredchars + "')" - token2repr = repr(token2) - self.assertTrue(token2repr == token2repr1 or token2repr == token2repr2) - self.assertEqual(token3repr, repr(token3)) + self.assertTrue(repr(token2) in ( + "Token(foo='bar', baz=123)", "Token(baz=123, foo='bar')")) + self.assertEqual("Text(text='" + hundredchars + "')", repr(token3)) def test_equality(self): """check that equivalent tokens are considered equal""" diff --git a/tests/test_utils.py b/tests/test_utils.py index b79b544..89ba93c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright (C) 2012-2016 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy @@ -20,7 +18,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals import unittest from mwparserfromhell.nodes import Template, Text diff --git a/tests/test_wikicode.py b/tests/test_wikicode.py index 307ee9a..6e017de 100644 --- a/tests/test_wikicode.py +++ b/tests/test_wikicode.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,15 +18,12 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals from functools import partial import re from types import GeneratorType import unittest -from mwparserfromhell.compat import py3k, str -from mwparserfromhell.nodes import (Argument, Comment, Heading, HTMLEntity, - Node, Tag, Template, Text, Wikilink) +from mwparserfromhell.nodes import Argument, Heading, Template, Text from mwparserfromhell.smart_list import SmartList from mwparserfromhell.wikicode import Wikicode from mwparserfromhell import parse @@ -38,8 +33,8 @@ from ._test_tree_equality import TreeEqualityTestCase, wrap, wraptext class TestWikicode(TreeEqualityTestCase): """Tests for the Wikicode class, which manages a list of nodes.""" - def test_unicode(self): - """test Wikicode.__unicode__()""" + def test_str(self): + """test Wikicode.__str__()""" code1 = parse("foobar") code2 = parse("Have a {{template}} and a [[page|link]]") self.assertEqual("foobar", str(code1)) diff --git a/tests/test_wikilink.py b/tests/test_wikilink.py index 487b7af..1f331e0 100644 --- a/tests/test_wikilink.py +++ b/tests/test_wikilink.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2020 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,10 +18,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import unicode_literals import unittest -from mwparserfromhell.compat import str from mwparserfromhell.nodes import Text, Wikilink from ._test_tree_equality import TreeEqualityTestCase, wrap, wraptext @@ -31,8 +27,8 @@ from ._test_tree_equality import TreeEqualityTestCase, wrap, wraptext class TestWikilink(TreeEqualityTestCase): """Test cases for the Wikilink node.""" - def test_unicode(self): - """test Wikilink.__unicode__()""" + def test_str(self): + """test Wikilink.__str__()""" node = Wikilink(wraptext("foobar")) self.assertEqual("[[foobar]]", str(node)) node2 = Wikilink(wraptext("foo"), wraptext("bar"))