diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ca35544..67ecf47 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,10 +22,10 @@ jobs: awk -F '\/' '{ print tolower($2) }' | tr '_' '-' ) - - name: Set up Python 3.7 + - name: Set up Python 3.8 uses: actions/setup-python@v1 with: - python-version: 3.7 + python-version: 3.8 - name: Versions run: | python3 --version @@ -71,5 +71,9 @@ jobs: python setup.py sdist python setup.py bdist_wheel --universal twine check dist/* + - name: Test Python package + run: | + pip install tox==3.24.5 + tox - name: Setup problem matchers uses: adafruit/circuitpython-action-library-ci-problem-matchers@v1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6d0015a..1421ff9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,7 +69,7 @@ jobs: if: contains(steps.need-pypi.outputs.setup-py, 'setup.py') uses: actions/setup-python@v1 with: - python-version: '3.x' + python-version: '3.8' - name: Install dependencies if: contains(steps.need-pypi.outputs.setup-py, 'setup.py') run: | diff --git a/.gitignore b/.gitignore index 9647e71..bbe230d 100755 --- a/.gitignore +++ b/.gitignore @@ -4,10 +4,12 @@ *.mpy .idea +.vscode __pycache__ _build *.pyc .env +.tox bundles *.DS_Store .eggs diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 1335112..a2f6d2d 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -9,7 +9,7 @@ version: 2 python: - version: "3.7" + version: "3.8" install: - requirements: docs/requirements.txt - requirements: requirements.txt diff --git a/adafruit_requests.py b/adafruit_requests.py index ee857b8..6ec2542 100644 --- a/adafruit_requests.py +++ b/adafruit_requests.py @@ -37,35 +37,116 @@ __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Requests.git" import errno +import sys + +if sys.implementation.name == "circuitpython": + + def cast(_t, value): + """No-op shim for the typing.cast() function which is not available in CircuitPython.""" + return value + + +else: + from ssl import SSLContext + from types import ModuleType, TracebackType + from typing import Any, Dict, List, Optional, Protocol, Tuple, Type, Union, cast + + # Based on https://github.com/python/typeshed/blob/master/stdlib/_socket.pyi + class CommonSocketType(Protocol): + """Describes the common structure every socket type must have.""" + + def send(self, data: bytes, flags: int = ...) -> None: + """Send data to the socket. The meaning of the optional flags kwarg is + implementation-specific.""" + ... + + def settimeout(self, value: Optional[float]) -> None: + """Set a timeout on blocking socket operations.""" + ... + + def close(self) -> None: + """Close the socket.""" + ... + + class CommonCircuitPythonSocketType(CommonSocketType, Protocol): + """Describes the common structure every CircuitPython socket type must have.""" + + def connect( + self, + address: Tuple[str, int], + conntype: Optional[int] = ..., + ) -> None: + """Connect to a remote socket at the provided (host, port) address. The conntype + kwarg optionally may indicate SSL or not, depending on the underlying interface.""" + ... + + class LegacyCircuitPythonSocketType(CommonCircuitPythonSocketType, Protocol): + """Describes the structure a legacy CircuitPython socket type must have.""" + + def recv(self, bufsize: int = ...) -> bytes: + """Receive data from the socket. The return value is a bytes object representing + the data received. The maximum amount of data to be received at once is specified + by bufsize.""" + ... + + class SupportsRecvWithFlags(Protocol): + """Describes a type that posseses a socket recv() method supporting the flags kwarg.""" + + def recv(self, bufsize: int = ..., flags: int = ...) -> bytes: + """Receive data from the socket. The return value is a bytes object representing + the data received. The maximum amount of data to be received at once is specified + by bufsize. The meaning of the optional flags kwarg is implementation-specific.""" + ... + + class SupportsRecvInto(Protocol): + """Describes a type that possesses a socket recv_into() method.""" + + def recv_into( + self, buffer: bytearray, nbytes: int = ..., flags: int = ... + ) -> int: + """Receive up to nbytes bytes from the socket, storing the data into the provided + buffer. If nbytes is not specified (or 0), receive up to the size available in the + given buffer. The meaning of the optional flags kwarg is implementation-specific. + Returns the number of bytes received.""" + ... + + class CircuitPythonSocketType( + CommonCircuitPythonSocketType, + SupportsRecvInto, + SupportsRecvWithFlags, + Protocol, + ): # pylint: disable=too-many-ancestors + """Describes the structure every modern CircuitPython socket type must have.""" + + ... + + class StandardPythonSocketType( + CommonSocketType, SupportsRecvInto, SupportsRecvWithFlags, Protocol + ): + """Describes the structure every standard Python socket type must have.""" -try: - from typing import Union, TypeVar, Optional, Dict, Any, List, Type - import types - from types import TracebackType - import ssl - import adafruit_esp32spi.adafruit_esp32spi_socket as esp32_socket - import adafruit_wiznet5k.adafruit_wiznet5k_socket as wiznet_socket - import adafruit_fona.adafruit_fona_socket as cellular_socket - from adafruit_esp32spi.adafruit_esp32spi import ESP_SPIcontrol - from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K - from adafruit_fona.adafruit_fona import FONA - import socket as cpython_socket - - SocketType = TypeVar( - "SocketType", - esp32_socket.socket, - wiznet_socket.socket, - cellular_socket.socket, - cpython_socket.socket, - ) - SocketpoolModuleType = types.ModuleType - SSLContextType = ( - ssl.SSLContext - ) # Can use either CircuitPython or CPython ssl module - InterfaceType = TypeVar("InterfaceType", ESP_SPIcontrol, WIZNET5K, FONA) + def connect(self, address: Union[Tuple[Any, ...], str, bytes]) -> None: + """Connect to a remote socket at the provided address.""" + ... + + SocketType = Union[ + LegacyCircuitPythonSocketType, + CircuitPythonSocketType, + StandardPythonSocketType, + ] + + SocketpoolModuleType = ModuleType + + class InterfaceType(Protocol): + """Describes the structure every interface type must have.""" + + @property + def TLS_MODE(self) -> int: # pylint: disable=invalid-name + """Constant representing that a socket's connection mode is TLS.""" + ... + + SSLContextType = Union[SSLContext, "_FakeSSLContext"] -except ImportError: - pass # CircuitPython 6.0 does not have the bytearray.split method. # This function emulates buf.split(needle)[0], which is the functionality @@ -157,7 +238,7 @@ def _recv_into(self, buf: bytearray, size: int = 0) -> int: read_size = len(b) buf[:read_size] = b return read_size - return self.socket.recv_into(buf, size) + return cast("SupportsRecvInto", self.socket).recv_into(buf, size) @staticmethod def _find(buf: bytes, needle: bytes, start: int, end: int) -> int: @@ -440,7 +521,7 @@ def _free_sockets(self) -> None: def _get_socket( self, host: str, port: int, proto: str, *, timeout: float = 1 - ) -> SocketType: + ) -> CircuitPythonSocketType: # pylint: disable=too-many-branches key = (host, port, proto) if key in self._open_sockets: @@ -693,7 +774,7 @@ def delete(self, url: str, **kw) -> Response: class _FakeSSLSocket: - def __init__(self, socket: SocketType, tls_mode: int) -> None: + def __init__(self, socket: CircuitPythonSocketType, tls_mode: int) -> None: self._socket = socket self._mode = tls_mode self.settimeout = socket.settimeout @@ -701,7 +782,7 @@ def __init__(self, socket: SocketType, tls_mode: int) -> None: self.recv = socket.recv self.close = socket.close - def connect(self, address: Union[bytes, str]) -> None: + def connect(self, address: Tuple[str, int]) -> None: """connect wrapper to add non-standard mode parameter""" try: return self._socket.connect(address, self._mode) @@ -714,7 +795,7 @@ def __init__(self, iface: InterfaceType) -> None: self._iface = iface def wrap_socket( - self, socket: SocketType, server_hostname: Optional[str] = None + self, socket: CircuitPythonSocketType, server_hostname: Optional[str] = None ) -> _FakeSSLSocket: """Return the same socket""" # pylint: disable=unused-argument diff --git a/docs/conf.py b/docs/conf.py index 4a485bc..c0e0389 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,7 +25,7 @@ # Uncomment the below if you use native CircuitPython modules such as # digitalio, micropython and busio. List the modules you use. Without it, the # autodoc module docs will fail to generate with a warning. -autodoc_mock_imports = ["adafruit_esp32spi", "adafruit_wiznet5k", "adafruit_fona"] +# autodoc_mock_imports = ["digitalio", "busio"] intersphinx_mapping = { diff --git a/setup.py b/setup.py index d92f376..c18e1ce 100755 --- a/setup.py +++ b/setup.py @@ -33,6 +33,7 @@ # Author details author="Adafruit Industries", author_email="circuitpython@adafruit.com", + python_requires=">=3.8", install_requires=["Adafruit-Blinka"], # Choose your license license="MIT", @@ -44,8 +45,7 @@ "Topic :: System :: Hardware", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.8", ], # What does your project relate to? keywords="adafruit blinka circuitpython micropython requests requests, networking", diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..ab2df5e --- /dev/null +++ b/tox.ini @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2022 Kevin Conley +# +# SPDX-License-Identifier: MIT + +[tox] +envlist = py38 + +[testenv] +changedir = {toxinidir}/tests +deps = pytest==6.2.5 +commands = pytest