jenkins-bot merged this change.

View Change

Approvals: Dvorapa: Looks good to me, approved jenkins-bot: Verified
[deps] require ipaddress for Python 2

- ipaddress is backported from Python 3 and will be mandatory with next
release. ipaddr is superseeded by ipaddress and no longer maintained
- change requirements.txt and setup.py accordingly
- move is_IP function from ip subfolder to tools
- implement a fallback to the current ip.is_IP implementation until
ip subfolder will be removed later
- deprecation warnings will be shown for old module
- use the new location in api.py, page.py, site.py
- remove IPRegexTestCase for regex tests
- merge IPAddressModuleTestCase into TestIPBase
- split test_T76286_failures into two subtests with
expected failures
- test_T105443_failures is solves within ipaddress and
does not need a separate test from test_ipaddress_module

Bug: T105443
Bug: T243171
Change-Id: Icc094eec294039539e3673584d5b771bc9f1f893
---
M HISTORY.rst
M pywikibot/data/api.py
M pywikibot/page.py
M pywikibot/site.py
M pywikibot/tools/__init__.py
M pywikibot/tools/ip.py
M requirements.txt
M setup.py
M tests/dry_api_tests.py
M tests/tools_ip_tests.py
10 files changed, 82 insertions(+), 100 deletions(-)

diff --git a/HISTORY.rst b/HISTORY.rst
index 05fdd57..3bcdd32 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -4,6 +4,9 @@
Current release
---------------

+* ipaddress module is required for Python 2 (T243171)
+* tools.ip will be dropped in favour of tools.is_IP (T243171)
+* tools.ip_regexp is deprecatd for 5 years and will be removed with next release
* backports.py will be removed with next release (T244664)
* stdnum package is required for ISBN scripts and cosmetic_changes (T132919, T144288, T241141)
* preload urllib.quote() with Python 2 (T243710, T222623)
diff --git a/pywikibot/data/api.py b/pywikibot/data/api.py
index 886997b..8cccf44 100644
--- a/pywikibot/data/api.py
+++ b/pywikibot/data/api.py
@@ -38,7 +38,7 @@
Error, TimeoutError, InvalidTitle, UnsupportedPage
)
from pywikibot.tools import (
- deprecated, itergroup, ip, PY2, PYTHON_VERSION,
+ deprecated, itergroup, is_IP, PY2, PYTHON_VERSION,
getargspec, UnicodeType, remove_last_args
)
from pywikibot.tools.formatter import color_format
@@ -1253,7 +1253,7 @@
raise Error('API write action attempted without userinfo')
assert('name' in self.site._userinfo)

- if ip.is_IP(self.site._userinfo['name']):
+ if is_IP(self.site._userinfo['name']):
raise Error('API write action attempted as IP %r'
% self.site._userinfo['name'])

diff --git a/pywikibot/page.py b/pywikibot/page.py
index c154adf..46115d0 100644
--- a/pywikibot/page.py
+++ b/pywikibot/page.py
@@ -50,7 +50,8 @@
first_upper, redirect_func, remove_last_args, UnicodeType,
StringTypes
)
-from pywikibot.tools.ip import is_IP, ip_regexp
+from pywikibot.tools.ip import ip_regexp # deprecated
+from pywikibot.tools import is_IP

if not PY2:
from html import entities as htmlentitydefs
diff --git a/pywikibot/site.py b/pywikibot/site.py
index 55351fe..93ffe1a 100644
--- a/pywikibot/site.py
+++ b/pywikibot/site.py
@@ -83,7 +83,7 @@
filter_unique,
UnicodeType
)
-from pywikibot.tools.ip import is_IP
+from pywikibot.tools import is_IP

if not PY2:
from itertools import zip_longest
diff --git a/pywikibot/tools/__init__.py b/pywikibot/tools/__init__.py
index 2d9b6d5..a4d2219 100644
--- a/pywikibot/tools/__init__.py
+++ b/pywikibot/tools/__init__.py
@@ -42,11 +42,16 @@
import queue
StringTypes = (str, bytes)
UnicodeType = str
+ from ipaddress import ip_address
else:
from itertools import izip_longest as zip_longest
import Queue as queue # noqa: N813
StringTypes = types.StringTypes
UnicodeType = types.UnicodeType
+ try:
+ from ipaddress import ip_address
+ except ImportError:
+ ip_address = None

try:
import bz2
@@ -373,6 +378,33 @@
'%s: %s' % (self.__class__.__name__, self.__doc__))


+def is_IP(IP): # noqa N802, N803
+ """Verify the IP address provided is valid.
+
+ No logging is performed. Use ip_address instead to catch errors.
+
+ @param IP: IP address
+ @type IP: str
+ @rtype: bool
+ """
+ method = ip_address
+ if not ip_address: # Python 2 needs ipaddress to be installed
+ issue_deprecation_warning(
+ 'ipaddr module or tools.ip.ip_regexp', 'ipaddress module',
+ warning_class=FutureWarning, since='20200120')
+ from pywikibot.tools import ip
+ with suppress_warnings('pywikibot.tools.ip.is_IP is deprecated'):
+ method = ip.is_IP
+
+ try:
+ method(IP)
+ except ValueError:
+ pass
+ else:
+ return True
+ return False
+
+
def has_module(module, version=None):
"""Check whether a module can be imported."""
try:
@@ -685,7 +717,8 @@
def __getattr__(self, attr):
"""Issue deprecation warning."""
issue_deprecation_warning(
- self._name, self._instead, since=self._since)
+ self._name, self._instead, warning_class=FutureWarning,
+ since=self._since)
return super(DeprecatedRegex, self).__getattr__(attr)


diff --git a/pywikibot/tools/ip.py b/pywikibot/tools/ip.py
index 5a9ea2f..9130251 100644
--- a/pywikibot/tools/ip.py
+++ b/pywikibot/tools/ip.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""IP address tools module."""
#
-# (C) Pywikibot team, 2014-2019
+# (C) Pywikibot team, 2014-2020
#
# Distributed under the terms of the MIT license.
#
@@ -12,7 +12,8 @@
from distutils.version import StrictVersion
from warnings import warn

-from pywikibot.tools import DeprecatedRegex, PY2, UnicodeType
+from pywikibot.tools import (DeprecatedRegex, PY2, UnicodeType,
+ ModuleDeprecationWrapper)

_ipaddress_e = _ipaddr_e = _ipaddr_version = None

@@ -87,7 +88,7 @@
r'(([\dA-F]{1,4}(\4|:\b|$)|\2){2}|'
r'(((2[0-4]|1\d|[1-9])?\d|25[0-5])\.?\b){4}))\Z',
re.IGNORECASE,
- 'page.ip_regexp', 'tools.ip.is_IP', since='20150212')
+ 'page.ip_regexp', 'tools.is_IP', since='20150212')


def is_IP(IP):
@@ -105,3 +106,10 @@
return True
except ValueError:
return False
+
+
+wrapper = ModuleDeprecationWrapper(__name__)
+wrapper._add_deprecated_attr('is_IP',
+ replacement_name='tools.is_IP',
+ future_warning=True,
+ since='20200120')
diff --git a/requirements.txt b/requirements.txt
index 9464a2e..6e9e858 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -18,15 +18,14 @@
# or
# $ awk -F '[#>=]' '{print $1}' requirements.txt | xargs apt-cache search

-# mandatory; see README.conversion.txt
+# mandatory; see README-conversion.txt
requests>=2.20.1,<2.22.0; python_version == '3.4'
requests>=2.20.1; python_version != '3.4'
enum34>=1.1.6; python_version < '3'
+ipaddress>=1.0.23 ; python_version < '3'
# requests security extra
requests[security] ; python_full_version > '2.7.6' and python_full_version < '2.7.9'

-ipaddr>=2.1.10 ; python_version < '3'
-
# OAuth support
# mwoauth 0.2.4 is needed because it supports getting identity information
# about the user
diff --git a/setup.py b/setup.py
index f51970e..d3b462c 100644
--- a/setup.py
+++ b/setup.py
@@ -97,18 +97,11 @@
extra_deps.update(script_deps)

# ------- setup install_requires ------- #
+# packages which are mandatory
dependencies = ['requests>=2.20.1,<2.22.0; python_version == "3.4"',
'requests>=2.20.1; python_version != "3.4"',
- 'enum34>=1.1.6; python_version < "3"']
-# tools.ip does not have a hard dependency on an IP address module,
-# as it falls back to using regexes if one is not available.
-# The functional backport of py3 ipaddress is acceptable:
-# https://pypi.org/project/ipaddress
-# However the Debian package python-ipaddr is also supported:
-# https://pypi.org/project/ipaddr
-# Other backports are likely broken.
-# ipaddr 2.1.10+ is distributed with Debian and Fedora. See T105443.
-dependencies.append('ipaddr>=2.1.10;python_version<"3"')
+ 'enum34>=1.1.6; python_version < "3"',
+ 'ipaddress>=1.0.23; python_version < "3"']

# version.package_version() uses pathlib which is a python 3 library.
# pathlib2 is required for python 2.7
diff --git a/tests/dry_api_tests.py b/tests/dry_api_tests.py
index 348a887..a0f0b63 100644
--- a/tests/dry_api_tests.py
+++ b/tests/dry_api_tests.py
@@ -248,11 +248,10 @@
Request, site=site,
parameters={'action': 'edit'})

- # Explicitly using str as the test expects it to be str (without the
- # u-prefix) in Python 2 and this module is using unicode_literals
- site._userinfo = {'name': str('1.2.3.4'), 'groups': []}
+ site._userinfo = {'name': '1.2.3.4', 'groups': []}

- self.assertRaisesRegex(pywikibot.Error, " as IP '1.2.3.4'",
+ # unicode string with "u" is returned with Python 2
+ self.assertRaisesRegex(pywikibot.Error, " as IP u?'1.2.3.4'",
Request, site=site,
parameters={'action': 'edit'})

diff --git a/tests/tools_ip_tests.py b/tests/tools_ip_tests.py
index 88cd474..09aff29 100644
--- a/tests/tools_ip_tests.py
+++ b/tests/tools_ip_tests.py
@@ -7,16 +7,14 @@
# Distributed under the terms of the MIT license.
from __future__ import absolute_import, division, unicode_literals

-from distutils.version import StrictVersion
-
-from pywikibot.tools import ip, PY2, PYTHON_VERSION
+from pywikibot.tools import is_IP, PY2, PYTHON_VERSION

from tests import unittest_print
-from tests.aspects import unittest, TestCase, DeprecationTestCase
+from tests.aspects import unittest, TestCase
from tests.utils import expected_failure_if


-class TestIPBase(TestCase):
+class IPAddressModuleTestCase(TestCase):

"""Unit test class base for IP matching."""

@@ -25,11 +23,11 @@
def setUp(self):
"""Set up test."""
self.total = 0
- super(TestIPBase, self).setUp()
+ super(IPAddressModuleTestCase, self).setUp()

def tearDown(self):
"""Tear down test."""
- super(TestIPBase, self).tearDown()
+ super(IPAddressModuleTestCase, self).tearDown()
unittest_print('{} subtests done'.format(self.total))

def ipv6test(self, result, ip_address):
@@ -38,13 +36,10 @@
with self.subTest(ip_address=ip_address):
msg = '"{}" match should be {} - not OK'.format(
ip_address, result) if PY2 else None
- if result:
- self.assertTrue(self._do_ip_test(ip_address), msg)
- else:
- self.assertFalse(self._do_ip_test(ip_address), msg)
+ self.assertEqual(result, is_IP(ip_address), msg)

- def _run_tests(self):
- """Test various IP."""
+ def test_ipaddress_module(self):
+ """Test ipaddress module."""
# test from http://download.dartware.com/thirdparty/test-ipv6-regex.pl
self.ipv6test(False, '') # empty string
self.ipv6test(True, '::1') # loopback, compressed, non-routable
@@ -635,82 +630,33 @@
self.ipv6test(True, 'a:b:c:d:e:f:0::')
self.ipv6test(False, "':10.0.0.1")

- def _test_T76286_failures(self):
+ # Known bugs with ipaddr v2.1.10 but works with ipaddress
+ self.ipv6test(False, '02001:0000:1234:0000:0000:C1C0:ABCD:0876')
+ self.ipv6test(False, '2001:0000:1234:0000:00001:C1C0:ABCD:0876')
+
+ @unittest.expectedFailure
+ def test_T76286a_failures(self):
"""Test known bugs in the ipaddress module."""
# The following fail with the ipaddress module. See T76286
self.ipv6test(False, '1111:2222:3333:4444:5555:6666:00.00.00.00')
+
+ @unittest.expectedFailure
+ def test_T76286b_failures(self):
+ """Test known bugs in the ipaddress module."""
self.ipv6test(False, '1111:2222:3333:4444:5555:6666:000.000.000.000')

- def _test_T105443_failures(self):
- """Test known bugs with ipaddr v2.1.10."""
- # extra 0 not allowed!
- self.ipv6test(False, '02001:0000:1234:0000:0000:C1C0:ABCD:0876')
- # extra 0 not allowed!
- self.ipv6test(False, '2001:0000:1234:0000:00001:C1C0:ABCD:0876')
-
- def _test_T240060_failures(self):
+ @expected_failure_if(PYTHON_VERSION >= (3, 8))
+ def test_T240060_failures(self):
"""Test known deviated behaviour in Python 3.8."""
# Testing IPv4 addresses represented as dotted-quads
# Leading zero's in IPv4 addresses not allowed: some systems treat the
# leading "0" in ".086" as the start of an octal number
# Update: The BNF in RFC-3986 explicitly defines the dec-octet
# (for IPv4 addresses) not to have a leading zero
- self.ipv6test(PYTHON_VERSION >= (3, 8),
+ self.ipv6test(False,
'fe80:0000:0000:0000:0204:61ff:254.157.241.086')


-class IPRegexTestCase(TestIPBase, DeprecationTestCase):
-
- """Test IP regex."""
-
- def _do_ip_test(self, address):
- return bool(ip.ip_regexp.match(address))
-
- def test_regex(self):
- """Test IP regex."""
- self._run_tests()
- self._test_T76286_failures()
- self._test_T105443_failures()
- self.assertDeprecationParts('page.ip_regexp', 'tools.ip.is_IP')
- self.assertLength(self.deprecation_messages, self.total)
-
-
-class IPAddressModuleTestCase(TestIPBase):
-
- """Test ipaddress module."""
-
- def _do_ip_test(self, address):
- return ip.is_IP(address)
-
- @classmethod
- def setUpClass(cls):
- """Check ipaddress module is available."""
- if ip.ip_address.__name__ == 'ip_address_fake':
- raise unittest.SkipTest('module ipaddress not available')
-
- super(IPAddressModuleTestCase, cls).setUpClass()
-
- def test_ipaddress_module(self):
- """Test ipaddress module."""
- self._run_tests()
-
- @expected_failure_if(ip.ip_address.__module__ == 'ipaddress'
- or ip.ip_address.__name__ == 'ip_address_patched')
- def test_T76286_failures(self):
- """Test known bugs in the ipaddress module."""
- self._test_T76286_failures()
-
- @expected_failure_if(ip.ip_address.__module__ == 'ipaddr'
- and ip._ipaddr_version == StrictVersion('2.1.10'))
- def test_T105443_failures(self):
- """Test known bugs in the ipaddr module."""
- self._test_T105443_failures()
-
- def test_T240060_failures(self):
- """Test known bugs in the ipaddr module."""
- self._test_T240060_failures()
-
-
if __name__ == '__main__': # pragma: no cover
try:
unittest.main()

To view, visit change 566013. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-MessageType: merged
Gerrit-Change-Id: Icc094eec294039539e3673584d5b771bc9f1f893
Gerrit-Change-Number: 566013
Gerrit-PatchSet: 9
Gerrit-Owner: Xqt <info@gno.de>
Gerrit-Reviewer: Dvorapa <dvorapa@seznam.cz>
Gerrit-Reviewer: Xqt <info@gno.de>
Gerrit-Reviewer: jenkins-bot (75)