diff --git a/.cz.yaml b/.cz.yaml index b906a20..d33569b 100644 --- a/.cz.yaml +++ b/.cz.yaml @@ -1,7 +1,8 @@ +--- commitizen: name: cz_conventional_commits tag_format: v$version - version: 0.31.2 + version: 0.31.4 version_files: - setup.py - docs/conf.py diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..c7d6bfc --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,39 @@ +name: Publish Python 🐍 distributions 📦 to PyPI +on: push +jobs: + build-n-publish: + name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Setup pandoc for changelog conversion + run: sudo apt update && sudo apt install -y pandoc + - name: Write pypi's readme + run: | + cat README.rst > to-pypi.rst + echo "" >> to-pypi.rst + pandoc -s --to rst -o /dev/stdout CHANGELOG.md | tee -a to-pypi.rst + mv to-pypi.rst README.rst + - name: Install pypa/build + run: >- + python -m + pip install + build + --user + - name: Build a binary wheel and a source tarball + run: >- + python -m + build + --sdist + --wheel + --outdir dist/ + . + - name: Publish distribution 📦 to PyPI + if: startsWith(github.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/tests-and-lint.yml b/.github/workflows/tests-and-lint.yml index 44d9527..39bd99c 100644 --- a/.github/workflows/tests-and-lint.yml +++ b/.github/workflows/tests-and-lint.yml @@ -12,12 +12,12 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: [2.7, 3.6, 3.7, 3.8, 3.9, "3.10"] + python-version: [3.7, 3.8, 3.9, "3.10", 3.11] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install Dependencies diff --git a/bmemcached/protocol.py b/bmemcached/protocol.py index e45e010..06d7cb3 100644 --- a/bmemcached/protocol.py +++ b/bmemcached/protocol.py @@ -1,15 +1,15 @@ from datetime import datetime, timedelta import logging -import re import socket import struct import threading try: - from urllib import splitport # type: ignore + from urlparse import SplitResult # type: ignore[import-not-found] except ImportError: - from urllib.parse import splitport # type: ignore + from urllib.parse import SplitResult # type: ignore[import-not-found] import zlib +from ipaddress import ip_address from io import BytesIO import six from six import binary_type, text_type @@ -145,9 +145,7 @@ def _open_connection(self): try: if self.host: - self.connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.connection.settimeout(self.socket_timeout) - self.connection.connect((self.host, self.port)) + self.connection = socket.create_connection((self.host, self.port), self.socket_timeout) if self.tls_context: self.connection = self.tls_context.wrap_socket( @@ -175,18 +173,40 @@ def split_host_port(cls, server): Port defaults to 11211. + When using IPv6 with a specified port, the address must be enclosed in brackets. + If the port is not specified, brackets are optional. + >>> split_host_port('127.0.0.1:11211') ('127.0.0.1', 11211) >>> split_host_port('127.0.0.1') ('127.0.0.1', 11211) + >>> split_host_port('::1') + ('::1', 11211) + >>> split_host_port('[::1]') + ('::1', 11211) + >>> split_host_port('[::1]:11211') + ('::1', 11211) """ - host, port = splitport(server) - if port is None: - port = 11211 - port = int(port) - if re.search(':.*$', host): - host = re.sub(':.*$', '', host) - return host, port + default_port = 11211 + + def is_ip_address(address): + try: + ip_address(address) + return True + except ValueError: + return False + + if is_ip_address(server): + return server, default_port + + if server.startswith('['): + host, _, port = server[1:].partition(']') + if not is_ip_address(host): + raise ValueError('{} is not a valid IPv6 address'.format(server)) + return host, default_port if not port else int(port.lstrip(':')) + + u = SplitResult("", server, "", "", "") + return u.hostname, 11211 if u.port is None else u.port def _read_socket(self, size): """ diff --git a/docs/_static/searchtools.js b/docs/_static/searchtools.js index 56676b2..64d124a 100644 --- a/docs/_static/searchtools.js +++ b/docs/_static/searchtools.js @@ -2,7 +2,7 @@ * searchtools.js_t * ~~~~~~~~~~~~~~~~ * - * Sphinx JavaScript utilties for the full-text search. + * Sphinx JavaScript utilities for the full-text search. * * :copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. @@ -594,7 +594,7 @@ var Search = { * helper function to return a node containing the * search summary for a given text. keywords is a list * of stemmed words, hlwords is the list of normal, unstemmed - * words. the first one is used to find the occurance, the + * words. the first one is used to find the occurrence, the * latter for highlighting it. */ makeSearchSummary : function(text, keywords, hlwords) { diff --git a/docs/_static/websupport.js b/docs/_static/websupport.js index 19fcda5..8e8f0aa 100644 --- a/docs/_static/websupport.js +++ b/docs/_static/websupport.js @@ -2,7 +2,7 @@ * websupport.js * ~~~~~~~~~~~~~ * - * sphinx.websupport utilties for all documentation. + * sphinx.websupport utilities for all documentation. * * :copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. diff --git a/docs/conf.py b/docs/conf.py index 81a001d..64f07fd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -52,7 +52,7 @@ # The short X.Y version. version = '0.31' # The full version, including alpha/beta/rc tags. -release = '0.31.2' +release = '0.31.4' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/requirements_test.txt b/requirements_test.txt index fb5565e..4cf6022 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,7 +1,5 @@ flake8~=4.0; python_version > '2.7' flake8~=3.9; python_version < '3.0' -m2r~=0.2.1 -mistune<2 # m2r breaks with newer versions mock~=4.0; python_version > '2.7' mock~=3.0; python_version < '3.0' pytest-cov~=3.0; python_version > '2.7' diff --git a/setup.py b/setup.py index c6a7080..ce0b628 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ def read(filename): setup( name="python-binary-memcached", - version="0.31.2", + version="0.31.4", author="Jayson Reis", author_email="santosdosreis@gmail.com", description="A pure python module to access memcached via its binary protocol with SASL auth support", diff --git a/test/conftest.py b/test/conftest.py index b4cb3b8..95abf0d 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -41,3 +41,16 @@ def memcached_socket(): yield p p.kill() p.wait() + + +@pytest.yield_fixture(scope="session", autouse=True) +def memcached_ipv6(): + p = subprocess.Popen( + ["memcached", "-l::1"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + time.sleep(0.1) + yield p + p.kill() + p.wait() diff --git a/test/test_server_parsing.py b/test/test_server_parsing.py index a384395..50e18bd 100644 --- a/test/test_server_parsing.py +++ b/test/test_server_parsing.py @@ -26,10 +26,38 @@ def testNoPortGiven(self): self.assertEqual(server.host, os.environ['MEMCACHED_HOST']) self.assertEqual(server.port, 11211) - def testInvalidPort(self): - server = bmemcached.protocol.Protocol('{}:blah'.format(os.environ['MEMCACHED_HOST'])) - self.assertEqual(server.host, os.environ['MEMCACHED_HOST']) + def testIPv6(self): + server = bmemcached.protocol.Protocol('[::1]') + self.assertEqual(server.host, '::1') + self.assertEqual(server.port, 11211) + server = bmemcached.protocol.Protocol('::1') + self.assertEqual(server.host, '::1') + self.assertEqual(server.port, 11211) + server = bmemcached.protocol.Protocol('[2001:db8::2]') + self.assertEqual(server.host, '2001:db8::2') + self.assertEqual(server.port, 11211) + server = bmemcached.protocol.Protocol('2001:db8::2') + self.assertEqual(server.host, '2001:db8::2') self.assertEqual(server.port, 11211) + # Since `2001:db8::2:8080` is a valid IPv6 address, + # it is ambiguous whether to split it into `2001:db8::2` and `8080` + # or treat it as `2001:db8::2:8080`. + # Therefore, it will be treated as `2001:db8::2:8080`. + server = bmemcached.protocol.Protocol('2001:db8::2:8080') + self.assertEqual(server.host, '2001:db8::2:8080') + self.assertEqual(server.port, 11211) + server = bmemcached.protocol.Protocol('[::1]:5000') + self.assertEqual(server.host, '::1') + self.assertEqual(server.port, 5000) + server = bmemcached.protocol.Protocol('[2001:db8::2]:5000') + self.assertEqual(server.host, '2001:db8::2') + self.assertEqual(server.port, 5000) + + def testInvalidPort(self): + with self.assertRaises(ValueError): + bmemcached.protocol.Protocol('{}:blah'.format(os.environ['MEMCACHED_HOST'])) + with self.assertRaises(ValueError): + bmemcached.protocol.Protocol('[::1]:blah') def testNonStandardPort(self): server = bmemcached.protocol.Protocol('{}:5000'.format(os.environ['MEMCACHED_HOST']))