Skip to content

Commit

Permalink
Drop support for Python 3.7, 3.8 and 3.9
Browse files Browse the repository at this point in the history
Some older workarounds are removed as part of that; the OBJECT_SECURITY
/ object_security alias to the OSCORE option is now recognizably
deprecated.

Closes: #275
  • Loading branch information
chrysn committed May 6, 2024
2 parents da1973c + b6a8459 commit 38af05c
Show file tree
Hide file tree
Showing 26 changed files with 80 additions and 95 deletions.
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ the list or in the excluded items, file a wishlist item at the same location).
Dependencies
------------

Basic aiocoap works out of the box on Python_ 3.7 or newer (also works on
Basic aiocoap works out of the box on Python_ 3.10 or newer (also works on
PyPy3_). For full support (DTLS, OSCORE and link-format handling) follow the
installation_ instructions as these require additional libraries.

Expand Down
5 changes: 2 additions & 3 deletions aiocoap/cli/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
from aiocoap.util import contenttype
from aiocoap.util.cli import ActionNoYes
from aiocoap.numbers import ContentFormat
from ..util.asyncio import py38args

def build_parser():
p = argparse.ArgumentParser(description=__doc__)
Expand Down Expand Up @@ -382,7 +381,7 @@ async def interactive():

current_task = asyncio.create_task(
single_request(line, context=context),
**py38args(name="Interactive prompt command %r" % line)
name="Interactive prompt command %r" % line,
)
interactive_expecting_keyboard_interrupt = asyncio.get_event_loop().create_future()

Expand Down Expand Up @@ -420,7 +419,7 @@ def sync_main(args=None):
loop = asyncio.get_event_loop()
task = loop.create_task(
interactive(),
**py38args(name="Interactive prompt")
name="Interactive prompt",
)

while not task.done():
Expand Down
3 changes: 1 addition & 2 deletions aiocoap/cli/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,7 @@ async def server_context_from_arguments(site, namespace, **kwargs):
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_context.load_cert_chain(certfile=namespace.tls_server_certificate, keyfile=namespace.tls_server_key)
ssl_context.set_alpn_protocols(["coap"])
if hasattr(ssl_context, 'sni_callback'): # starting python 3.7
ssl_context.sni_callback = lambda obj, name, context: setattr(obj, "indicated_server_name", name)
ssl_context.sni_callback = lambda obj, name, context: setattr(obj, "indicated_server_name", name)
else:
ssl_context = None

Expand Down
3 changes: 1 addition & 2 deletions aiocoap/cli/fileserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
from aiocoap.cli.common import (add_server_arguments,
server_context_from_arguments, extract_server_arguments)
from aiocoap.resourcedirectory.client.register import Registerer
from ..util.asyncio import py38args

class InvalidPathError(error.ConstructionRenderableError):
code = codes.BAD_REQUEST
Expand Down Expand Up @@ -347,7 +346,7 @@ async def start_with_options(self, path, verbosity=0, register=False,

self.refreshes = asyncio.create_task(
server.check_files_for_refreshes(),
**py38args(name="Refresh on %r" % (path,))
name="Refresh on %r" % (path,),
)

if register is not False:
Expand Down
3 changes: 1 addition & 2 deletions aiocoap/cli/rd.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
import aiocoap.proxy.server

from aiocoap.util.linkformat import Link, LinkFormat, parse
from ..util.asyncio import py38args

from ..util.linkformat import link_header

Expand Down Expand Up @@ -219,7 +218,7 @@ async def longwait(delay, callback):
callback()
self.timeout = asyncio.create_task(
longwait(delay, self.delete),
**py38args(name="RD Timeout for %r" % self)
name="RD Timeout for %r" % self,
)

def refresh_timeout(self):
Expand Down
8 changes: 5 additions & 3 deletions aiocoap/numbers/optionnumbers.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,7 @@ class OptionNumber(ExtensibleIntEnum):
OBSERVE = 6
URI_PORT = 7
LOCATION_PATH = 8
# The OBJECT_SECURITY name is to be deprecated when Python 3.8 support is
# dropped, and we can do that easily using a class property.
OSCORE = OBJECT_SECURITY = 9
OSCORE = 9
URI_PATH = 11
CONTENT_FORMAT = 12
MAX_AGE = 14
Expand All @@ -60,6 +58,10 @@ class OptionNumber(ExtensibleIntEnum):
# going to be used in overhead comparisons.
REQUEST_HASH = 548

_deprecated_aliases = {
"OBJECT_SECURITY": "OSCORE",
}

def __add__(self, delta):
"""Addition makes sense on these due to the delta encoding in CoAP
serialization"""
Expand Down
14 changes: 12 additions & 2 deletions aiocoap/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# SPDX-License-Identifier: MIT

from itertools import chain
from warnings import warn

from .numbers.optionnumbers import OptionNumber
from .error import UnparsableMessage
Expand Down Expand Up @@ -39,7 +40,7 @@ def _write_extended_field_value(value):
raise ValueError("Value out of range.")


def _single_value_view(option_number, doc=None):
def _single_value_view(option_number, doc=None, deprecated=None):
"""Generate a property for a given option number, where the option is not
repeatable. For getting, it will return the value of the first option
object with matching number. For setting, it will remove all options with
Expand All @@ -51,18 +52,26 @@ def _single_value_view(option_number, doc=None):
for any of them)."""

def _getter(self, option_number=option_number):
if deprecated is not None:
warn(deprecated, stacklevel=2)
options = self.get_option(option_number)
if not options:
return None
else:
return options[0].value

def _setter(self, value, option_number=option_number):
if deprecated is not None:
# Stack level 3 is what it takes to get out of the typical
# `Message(opt=val)` construction, and probably most useful.
warn(deprecated, stacklevel=3)
self.delete_option(option_number)
if value is not None:
self.add_option(option_number.create_option(value=value))

def _deleter(self, option_number=option_number):
if deprecated is not None:
warn(deprecated, stacklevel=2)
self.delete_option(option_number)

return property(_getter, _setter, _deleter, doc or "Single-value view on the %s option." % option_number)
Expand Down Expand Up @@ -206,7 +215,8 @@ def option_list(self):
proxy_uri = _single_value_view(OptionNumber.PROXY_URI)
proxy_scheme = _single_value_view(OptionNumber.PROXY_SCHEME)
size1 = _single_value_view(OptionNumber.SIZE1)
object_security = _single_value_view(OptionNumber.OBJECT_SECURITY)
oscore = _single_value_view(OptionNumber.OSCORE)
object_security = _single_value_view(OptionNumber.OSCORE, deprecated="Use `oscore` instead of `object_security`")
max_age = _single_value_view(OptionNumber.MAX_AGE)
if_match = _items_view(OptionNumber.IF_MATCH)
no_response = _single_value_view(OptionNumber.NO_RESPONSE)
Expand Down
12 changes: 6 additions & 6 deletions aiocoap/oscore.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,8 +583,8 @@ def _extract_external_aad(self, message, request_id, local_is_sender: bool) -> b
# observation span group rekeyings
external_aad.append(self.id_context)

assert message.opt.object_security is not None, "Double OSCORE"
external_aad.append(message.opt.object_security)
assert message.opt.oscore is not None, "Double OSCORE"
external_aad.append(message.opt.oscore)

if local_is_sender:
external_aad.append(self.sender_auth_cred)
Expand Down Expand Up @@ -707,10 +707,10 @@ def protect(self, message, request_id=None, *, kid_context=True):
# Putting in a dummy value as the signature calculation will already need some of the compression result
if self.is_signing:
unprotected[COSE_COUNTERSIGNATURE0] = b""
# FIXME: Running this twice quite needlessly (just to get the object_security option for sending)
# FIXME: Running this twice quite needlessly (just to get the oscore option for sending)
option_data, _ = self._compress(protected, unprotected, b"")

outer_message.opt.object_security = option_data
outer_message.opt.oscore = option_data

external_aad = self._extract_external_aad(outer_message, request_id, local_is_sender=True)

Expand Down Expand Up @@ -1058,10 +1058,10 @@ def _uncompress(option_data, payload):

@classmethod
def _extract_encrypted0(cls, message):
if message.opt.object_security is None:
if message.opt.oscore is None:
raise NotAProtectedMessage("No Object-Security option present", message)

protected_serialized, protected, unprotected, ciphertext = cls._uncompress(message.opt.object_security, message.payload)
protected_serialized, protected, unprotected, ciphertext = cls._uncompress(message.opt.oscore, message.payload)
return protected_serialized, protected, unprotected, ciphertext

# implementation defined
Expand Down
8 changes: 1 addition & 7 deletions aiocoap/pipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

from . import error
from .numbers import INTERNAL_SERVER_ERROR
from .util.asyncio import py38args

class Pipe:
"""Low-level meeting point between a request and a any responses that come
Expand Down Expand Up @@ -201,15 +200,10 @@ async def wrapped():
# Not doing anything special about cancellation: it indicates the
# peer's loss of interest, so there's no use in sending anythign out to
# someone not listening any more
#
# (We'd *like* to do something on the Python 3.7 versions whose
# cancelled threads show errors, but there we can't stop it in here
# because catching the CancelledError doesn't remove the taint from the
# task).

task = asyncio.create_task(
wrapped(),
**py38args(name=name),
name=name,
)
if sys.version_info < (3, 8):
# These Python versions used to complain about cancelled tasks, where
Expand Down
20 changes: 10 additions & 10 deletions aiocoap/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
from . import error
from .numbers import (INTERNAL_SERVER_ERROR, NOT_FOUND,
CONTINUE, SHUTDOWN_TIMEOUT)
from .util.asyncio import py38args

import warnings
import logging
Expand Down Expand Up @@ -317,7 +316,7 @@ async def shutdown(self):
done, pending = await asyncio.wait([
asyncio.create_task(
ri.shutdown(),
**py38args(name="Shutdown of %r" % ri)
name="Shutdown of %r" % ri,
)
for ri
in self.request_interfaces],
Expand Down Expand Up @@ -359,7 +358,7 @@ async def send():
return
self.loop.create_task(
send(),
**py38args(name="Request processing of %r" % result)
name="Request processing of %r" % result,
)
return result

Expand Down Expand Up @@ -595,13 +594,14 @@ def __init__(self, protocol, app_request):
self.observation = None

self._runner = protocol.loop.create_task(self._run_outer(
app_request,
self.response,
weakref.ref(self.observation) if self.observation is not None else lambda: None,
self.protocol,
self.log,
app_request,
self.response,
weakref.ref(self.observation) if self.observation is not None else lambda: None,
self.protocol,
self.log,
),
**py38args(name="Blockwise runner for %r" % app_request))
name="Blockwise runner for %r" % app_request,
)
self.response.add_done_callback(self._response_cancellation_handler)

def _response_cancellation_handler(self, response_future):
Expand Down Expand Up @@ -769,7 +769,7 @@ async def _run(cls, app_request, response, weak_observation, protocol, log):
future_weak_observation,
protocol,
log),
**py38args(name="Blockwise observation for %r" % app_request)
name="Blockwise observation for %r" % app_request,
)
future_weak_observation.set_result(weakref.ref(obs, lambda obs: subtask.cancel()))
obs.on_cancel(subtask.cancel)
Expand Down
1 change: 0 additions & 1 deletion aiocoap/proxy/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from ..protocol import ClientObservation

from ..util import hostportsplit
from ..util.asyncio import py38args

class ProxyForwarder(interfaces.RequestProvider):
"""Object that behaves like a Context but only provides the request
Expand Down
5 changes: 2 additions & 3 deletions aiocoap/transports/oscore.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@

from .. import interfaces, credentials, oscore
from ..numbers import UNAUTHORIZED, MAX_REGULAR_BLOCK_SIZE_EXP
from ..util.asyncio import py38args

class OSCOREAddress(
namedtuple("_OSCOREAddress", ["security_context", "underlying_address"]),
Expand Down Expand Up @@ -128,7 +127,7 @@ def __init__(self, context, forward_context):
async def fill_or_recognize_remote(self, message):
if isinstance(message.remote, OSCOREAddress):
return True
if message.opt.object_security is not None:
if message.opt.oscore is not None:
# double oscore is not specified; using this fact to make `._wire
# is ._context` an option
return False
Expand All @@ -149,7 +148,7 @@ async def fill_or_recognize_remote(self, message):
def request(self, request):
t = self.loop.create_task(
self._request(request),
**py38args(name="OSCORE request %r" % request)
name="OSCORE request %r" % request,
)
self._tasks.add(t)
t.add_done_callback(lambda _, _tasks=self._tasks, _t=t: _tasks.remove(_t))
Expand Down
3 changes: 1 addition & 2 deletions aiocoap/transports/simple6.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
from aiocoap import interfaces
from aiocoap import COAP_PORT
from ..util import hostportjoin
from ..util.asyncio import py38args
from .generic_udp import GenericMessageInterface

class _Connection(asyncio.DatagramProtocol, interfaces.EndpointAddress):
Expand Down Expand Up @@ -239,7 +238,7 @@ async def shutdown(self):
done, pending = await asyncio.wait([
asyncio.create_task(
s.shutdown(),
**py38args(name="Socket shutdown of %r" % s)
name="Socket shutdown of %r" % s,
)
for s
in self._sockets.values()])
Expand Down
12 changes: 7 additions & 5 deletions aiocoap/transports/tcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from aiocoap import interfaces, error, util
from aiocoap import COAP_PORT, Message
from aiocoap import defaults
from ..util.asyncio import py38args

def _extract_message_size(data: bytes):
"""Read out the full length of a CoAP messsage represented by data.
Expand Down Expand Up @@ -326,13 +325,15 @@ async def shutdown(self):
shutdowns = [
asyncio.create_task(
c.release(),
**py38args(name="Close client %s" % c))
name="Close client %s" % c,
)
for c
in self._pool
]
shutdowns.append(asyncio.create_task(
self.server.wait_closed(),
**py38args(name="Close server %s" % self)))
name="Close server %s" % self),
)
# There is at least one member, so we can just .wait()
await asyncio.wait(shutdowns)

Expand Down Expand Up @@ -422,8 +423,9 @@ async def shutdown(self):
self._tokenmanager = None

shutdowns = [asyncio.create_task(
c.release(),
**py38args(name="Close client %s" % c))
c.release(),
name="Close client %s" % c,
)
for c in self._pool.values()]
if not shutdowns:
# wait is documented to require a non-empty set
Expand Down
9 changes: 2 additions & 7 deletions aiocoap/transports/tinydtls.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
import warnings

from ..util import hostportjoin, hostportsplit
from ..util.asyncio import py38args
from ..message import Message
from .. import interfaces, error
from ..numbers import COAPS_PORT
Expand Down Expand Up @@ -150,7 +149,7 @@ def send(self, message):

self._retransmission_task = asyncio.create_task(
self._run_retransmissions(),
**py38args(name="DTLS handshake retransmissions")
name="DTLS handshake retransmissions",
)

log = property(lambda self: self.coaptransport.log)
Expand Down Expand Up @@ -195,7 +194,7 @@ async def _start(self):

self._retransmission_task = asyncio.create_task(
self._run_retransmissions(),
**py38args(name="DTLS handshake retransmissions")
name="DTLS handshake retransmissions",
)

self._connecting = asyncio.get_running_loop().create_future()
Expand All @@ -213,10 +212,6 @@ async def _start(self):

return

except asyncio.CancelledError:
# Can be removed starting with Python 3.8 as it's a workaround for
# https://bugs.python.org/issue32528
raise
except Exception as e:
self.coaptransport.ctx.dispatch_error(e, self)
finally:
Expand Down
Loading

0 comments on commit 38af05c

Please sign in to comment.