diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 21829ce..9184d24 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,8 @@ name: release on: - push: - branches: - - "main" - - "master" + workflow_dispatch: + jobs: pypi_version: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 87ada9d..e3b1024 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ env: FORCE_COLOR: "1" jobs: - run: + tests: name: Python ${{ matrix.python-version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} runs-on: ${{ matrix.os }} strategy: @@ -44,4 +44,57 @@ jobs: run: hatch fmt --check - name: Run tests - run: hatch run test.py${{ matrix.python-version }}:cov + run: hatch run test.py${{ matrix.python-version }}:test-cov + + - name: Upload coverage data + uses: actions/upload-artifact@v3 + with: + name: covdata + path: .coverage.* + include-hidden-files: true + + coverage: + name: Coverage + needs: + tests + runs-on: ubuntu-latest + steps: + - name: Check out repo + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.12 + + - name: Install dependencies + run: + python -m pip install --upgrade pip coverage[toml] + + - name: Download coverage data + uses: actions/download-artifact@v3 + with: + name: covdata + + - name: Combine + run: | + python -m compileall examples src tests + coverage combine + coverage report -i + coverage json -i + export TOTAL=$(cat coverage.json | jq -r .totals.percent_covered_display) + echo "total=${TOTAL}" >> $GITHUB_ENV + echo "### Total coverage: ${TOTAL} %" >> $GITHUB_STEP_SUMMARY + + - name: Make badge + uses: schneegans/dynamic-badges-action@v1.7.0 + with: + auth: ${{ secrets.GIST_SECRET }} + gistID: 12a0c0616d67fc2b8b9cda9eda30be5d + filename: sliplib_coverage.svg + label: Coverage + message: ${{ env.total }} % + minColorRange: 50 + maxColorRange: 90 + valColorRange: ${{ env.total }} + diff --git a/.gitignore b/.gitignore index 4c613d9..a476127 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.pyc .cache .coverage +.coverage.* .externalToolBuilders .idea .mypy_cache @@ -20,3 +21,4 @@ Lib pyvenv.cfg Scripts venv +/coverage.json diff --git a/CHANGELOG.md b/CHANGELOG.md index e24c8fc..f06a66b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,20 @@ -# Changelog +Changelog +========= ## Unpublished ### Upgrade steps +- Implementations that use the `Driver` class directly must update their code + with respect to handling received data and retrieving messages. + Use `Driver.receive()` to receive data and add it to the internal buffer. + Use `Driver.get()` to obtain the next message. + ### Breaking Changes - Removed support for Python version 3.6 and lower. +- `Driver.receive()` no longer returns a list of messages. + Instead, use `Driver.get()` to retrieve the next message. ### New Features @@ -14,8 +22,6 @@ ### Improvements -- Added `block` and `timeout` arguments to the `Driver.get()` method. - ### Other Changes - Converted project to use `hatch`. @@ -37,6 +43,7 @@ - Updated documentation and examples + ## v0.5.0 ### New Features diff --git a/README.rst b/README.md similarity index 58% rename from README.rst rename to README.md index 5b5d653..369d27d 100644 --- a/README.rst +++ b/README.md @@ -1,106 +1,90 @@ - -.. image:: https://readthedocs.org/projects/sliplib/badge/?version=latest - :target: http://sliplib.readthedocs.org/en/master/?badge=master - :alt: ReadTheDocs Documentation Status - -.. image:: https://travis-ci.org/rhjdjong/SlipLib.svg - :target: https://travis-ci.org/rhjdjong/SlipLib - :alt: Travis Test Status - -.. image:: https://ci.appveyor.com/api/projects/status/d1nwwn34xoaxh3tt/branch/master?svg=true - :target: https://ci.appveyor.com/project/RuuddeJong/sliplib/branch/master - :alt: AppVeyor Test Status - - -============================================== -``sliplib`` --- A module for the SLIP protocol -============================================== - - -The `sliplib` module implements the encoding and decoding -functionality for SLIP packets, as described in :rfc:`1055`. -It defines encoding, decoding, and validation functions, -as well as a driver class that can be used to implement -a SLIP protocol stack, and higher-level classes that -apply the SLIP protocol to TCP connections or IO streams. -Read the `documentation `_ -for detailed information. - -Background -========== - -The SLIP protocol is described in :rfc:`1055` (:title:`A Nonstandard for -Transmission of IP Datagrams over Serial Lines: SLIP`, J. Romkey, -June 1988). The original purpose of the protocol is -to provide a mechanism to indicate the boundaries of IP packets, -in particular when the IP packets are sent over a connection that -does not provide a framing mechanism, such as serial lines or -dial-up connections. - -There is, however, nothing specific to IP in the SLIP protocol. -SLIP offers a generic framing method that can be used for any -type of data that must be transmitted over a (continuous) byte stream. -In fact, the main reason for creating this module -was the need to communicate with a third-party application that -used SLIP over TCP (which is a continuous byte stream) -to frame variable length data structures. - - -Usage -===== - -Installation ------------- - -To install the `sliplib` module, use - -.. code:: - - pip install sliplib - -Low-level usage ---------------- - -The recommended basic usage is to run all encoding and decoding operations -through an instantiation `driver` of the `Driver` class, in combination -with the appropriate I/O code. -The `Driver` class itself works without any I/O, and can therefore be used with -any networking code, or any bytestream like pipes, serial I/O, etc. -It can work in synchronous as well as in asynchronous environments. - -The `Driver` class offers the methods -`send` and `receive` to handle -the conversion between messages and SLIP-encoded packets. - -High-level usage ----------------- - -The module also provides a `SlipWrapper` abstract baseclass -that provides the methods `send_msg` and `recv_msg` to send -and receive single SLIP-encoded messages. This base class -wraps an instance of the `Driver` class with a user-provided stream. - -Two concrete subclasses of `SlipWrapper` are provided: - -* `SlipStream` allows the wrapping of a byte IO stream. -* `SlipSocket` allows the wrapping of a TCP socket. - -In addition, the module also provides a `SlipRequestHandler` -to facilitate the creation of TCP servers that can handle -SLIP-encoded messages. - - -Error Handling -============== - -Contrary to the reference implementation described in :rfc:`1055`, -which chooses to essentially ignore protocol errors, -the functions and classes in the `sliplib` module -use a `ProtocolError` exception -to indicate protocol errors, i.e. SLIP packets with invalid byte sequences. -The `Driver` class raises the `ProtocolError` exception -as soon as a complete SLIP packet with an invalid byte sequence is received. -The `SlipWrapper` class and its subclasses catch the `ProtocolError`\s -raised by the `Driver` class, and re-raise them when -an attempt is made to read the contents of a SLIP packet that contained -invalid data. +[![Stable Version](https://img.shields.io/pypi/v/sliplib?color=blue)](https://pypi.org/project/sliplib/) +[![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg)](https://github.com/pypa/hatch) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) +[![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/) +![tests](https://github.com/rhjdjong/SlipLib/actions/workflows/test.yml/badge.svg) +![coverage](https://gist.githubusercontent.com/rhjdjong/12a0c0616d67fc2b8b9cda9eda30be5d/raw/sliplib_coverage.svg) + +# `sliplib` — A module for the SLIP protocol + +The `sliplib` module implements the encoding and decoding +functionality for SLIP packets, as described in +[RFC 1055][rfc1055]. +It defines encoding, decoding, and validation functions, +as well as a driver class that can be used to implement +a SLIP protocol stack, and higher-level classes that +apply the SLIP protocol to TCP connections or IO streams. +Read the [documentation](http://sliplib.readthedocs.org/en/master/) +for detailed information. + +## Background + +The SLIP protocol is described in [RFC 1055][rfc1055] (*A Nonstandard for +Transmission of IP Datagrams over Serial Lines: SLIP*, J. Romkey, +June 1988). The original purpose of the protocol is +to provide a mechanism to indicate the boundaries of IP packets, +in particular when the IP packets are sent over a connection that +does not provide a framing mechanism, such as serial lines or +dial-up connections. + +There is, however, nothing specific to IP in the SLIP protocol. +SLIP offers a generic framing method that can be used for any +type of data that must be transmitted over a (continuous) byte stream. +In fact, the main reason for creating this module +was the need to communicate with a third-party application that +used SLIP over TCP (which is a continuous byte stream) +to frame variable length data structures. + + +## Usage + +### Installation + +To install the `sliplib` module, use + +``` +pip install sliplib +``` + +### Low-level usage + +The recommended basic usage is to run all encoding and decoding operations +through an instantiation `driver` of the `Driver` class, in combination +with the appropriate I/O code. +The `Driver` class itself works without any I/O, and can therefore be used with +any networking code, or any bytestream like pipes, serial I/O, etc. +It can work in synchronous as well as in asynchronous environments. + +The `Driver` class offers the methods +`send` and `receive` to handle +the conversion between messages and SLIP-encoded packets. + +### High-level usage + +The module also provides a `SlipWrapper` abstract baseclass +that provides the methods `send_msg` and `recv_msg` to send +and receive single SLIP-encoded messages. This base class +wraps an instance of the `Driver` class with a user-provided stream. + +Two concrete subclasses of `SlipWrapper` are provided: + +* `SlipStream` allows the wrapping of a byte IO stream. +* `SlipSocket` allows the wrapping of a TCP socket. + +In addition, the module also provides a `SlipRequestHandler` +to facilitate the creation of TCP servers that can handle +SLIP-encoded messages. + + +## Error Handling + +Contrary to the reference implementation described in :rfc:`1055`, +which chooses to essentially ignore protocol errors, +the functions and classes in the `sliplib` module +use a `ProtocolError` exception +to indicate protocol errors, i.e. SLIP packets with invalid byte sequences. +The `Driver` class raises a `ProtocolError` exception +when an attempt is made to `get()` a message from such a packet. + +[rfc1055]: http://tools.ietf.org/html/rfc1055.html diff --git a/docs/source/changes.md b/docs/source/changes.md new file mode 100644 index 0000000..3139cd4 --- /dev/null +++ b/docs/source/changes.md @@ -0,0 +1,2 @@ +```{include} ../../CHANGELOG.md +``` diff --git a/docs/source/conf.py b/docs/source/conf.py index 5d05f9f..30259e0 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -40,18 +40,20 @@ "sphinx_toolbox.more_autodoc.typevars", "sphinx_toolbox.more_autodoc.variables", "sphinx_autodoc_typehints", + "myst_parser", ] templates_path = ["_templates"] exclude_patterns = [] napoleon_google_docstring = True -# napoleon_use_rtype = False autoclass_content = "both" autodoc_typehints = "description" autodoc_type_aliases = {} add_module_names = False -intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), +} # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output diff --git a/docs/source/index.rst b/docs/source/index.rst index bd679a3..94ced03 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -7,8 +7,8 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -.. include:: ../../README.rst -.. include:: ../../CHANGES.rst +.. include:: ../../README.md + :parser: myst_parser.sphinx_ .. toctree:: :maxdepth: 2 @@ -16,6 +16,7 @@ module example + changes Indices and tables diff --git a/examples/echoserver/__init__.py b/examples/echoserver/__init__.py index ac28400..242500d 100644 --- a/examples/echoserver/__init__.py +++ b/examples/echoserver/__init__.py @@ -76,23 +76,26 @@ By running the server with the argument ``ipv6``, an IPv6-based connection will be established. -In the server terminal window: - -.. code:: bash - - $ python server.py ipv6 - Slip server listening on localhost, port 59454 - Incoming connection from ('::1', 59458, 0, 0) - ... - -In the client terminal window: - -.. code:: bash - - $ python client.py 59454 - Connecting to server on port 59454 - Connected to ('::1', 59454, 0, 0) - Message> - ... - +.. list-table:: + :header-rows: 1 + :widths: 50 50 + + * - Server + - Client + * - .. code:: bash + + $ python server.py ipv6 + Slip server listening on localhost, port 59454 + \u200b + Incoming connection from ('::1', 59458, 0, 0) + \u200b + ... + - .. code:: bash + + \u200b + \u200b + $ python client.py 59454 + Connecting to server on port 59454 + Message> + ... """ diff --git a/pyproject.toml b/pyproject.toml index 9da3b04..337aa54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" name = "sliplib" dynamic = ["version"] description = "Slip package" -readme = "README.rst" +readme = "README.md" license = "MIT" authors = [ { name = "Ruud de Jong", email = "ruud.de.jong@xs4all.nl" }, @@ -66,6 +66,7 @@ test-cov = "coverage run -m pytest {args:tests}" cov-report = [ "- coverage combine", "coverage report", + "coverage json", ] cov = [ "test-cov", @@ -132,6 +133,7 @@ include = [ [tool.hatch.envs.doc] python = "3.12" dependencies = [ + "myst-parser", "sphinx", "sphinx_rtd_theme", "sphinx-toolbox", diff --git a/readthedocs.yaml b/readthedocs.yaml index 1f2b89d..e083a99 100644 --- a/readthedocs.yaml +++ b/readthedocs.yaml @@ -1,10 +1,5 @@ version: 2 -build: - os: ubuntu-22.04 - tools: - python: "3.12" - sphinx: configuration: docs/source/conf.py diff --git a/src/sliplib/__init__.py b/src/sliplib/__init__.py index 09661d0..f2ca7cb 100644 --- a/src/sliplib/__init__.py +++ b/src/sliplib/__init__.py @@ -13,22 +13,6 @@ as well as various classes that can be used to to wrap the SLIP protocol over different kinds of byte streams. -The SLIP protocol is described in :rfc:`1055` (:title:`A Nonstandard for -Transmission of IP Datagrams over Serial Lines: SLIP`, J. Romkey, -June 1988). The original purpose of the protocol is -to provide a mechanism to indicate the boundaries of IP packets, -in particular when the IP packets are sent over a connection that -does not provide a framing mechanism, such as serial lines or -dial-up connections. - -There is, however, nothing specific to IP in the SLIP protocol. -The protocol describes a generic framing method that can be used for any -type of data that must be transmitted over a (continuous) byte stream. -In fact, the main reason for creating this module -was the need to communicate with a third-party application that -used SLIP over TCP (which is a continuous byte stream) -to frame variable length data structures. - The SLIP protocol uses four special byte values: =============== ================ ============================================= @@ -41,16 +25,16 @@ =============== ================ ============================================= An :const:`END` byte in the message is encoded as the sequence -:code:`ESC+ESC_END` (:code:`b'\\xdb\\xdc'`) +:const:`ESC+ESC_END` (:code:`b'\\\\xdb\\\\xdc'`) in the slip packet, and an :const:`ESC` byte in the message is encoded -as the sequence :code:`ESC+ESC_ESC` (:code:`b'\\xdb\\xdd'`). +as the sequence :const:`ESC+ESC_ESC` (:code:`b'\\\\xdb\\\\xdd'`). .. csv-table:: :header: "Decoded", "Encoded" - :code:`b'\\xc0'`, :code:`b'\\xdb\\xdc'` - :code:`b'\\xdb'`, :code:`b'\\xdb\\xdd'` + :code:`b'\\\\xc0'`, :code:`b'\\\\xdb\\\\xdc'` + :code:`b'\\\\xdb'`, :code:`b'\\\\xdb\\\\xdd'` As a consequence, an :const:`ESC` byte in an encoded SLIP packet must always be followed by an :const:`ESC_END` or an :const:`ESC_ESC` byte; diff --git a/src/sliplib/slip.py b/src/sliplib/slip.py index bfffeac..6702a60 100644 --- a/src/sliplib/slip.py +++ b/src/sliplib/slip.py @@ -48,8 +48,8 @@ END = b"\xc0" #: The SLIP `END` byte. ESC = b"\xdb" #: The SLIP `ESC` byte. -ESC_END = b"\xdc" #: The SLIP byte that, when preceded by an `ESC` byte, represents an escaped `END` byte. -ESC_ESC = b"\xdd" #: The SLIP byte that, when preceded by an `ESC` byte, represents an escaped `ESC` byte. +ESC_END = b"\xdc" #: When preceded by an `ESC` byte, this represents an escaped `END` byte. +ESC_ESC = b"\xdd" #: When preceded by an `ESC` byte, this represents an escaped `ESC` byte. class ProtocolError(ValueError): @@ -143,13 +143,13 @@ def send(self, message: bytes) -> bytes: return encode(message) def receive(self, data: bytes | int) -> None: - """Decodes data to extract the SLIP-encoded messages. + """Extract SLIP packets. Processes :obj:`data`, which must be a bytes-like object, - and extracts and buffers the SLIP messages contained therein. + and extracts and buffers the SLIP packets contained therein. A non-terminated SLIP packet in :obj:`data` - is also buffered, and processed with the next call to :meth:`receive`. + is also buffered, and extended with the next call to :meth:`receive`. Args: data: A bytes-like object to be processed. @@ -205,13 +205,23 @@ def get(self, *, block: bool = True, timeout: float | None = None) -> bytes | No """Get the next decoded message. Remove and decode a SLIP packet from the internal buffer, and return the resulting message. - If `block` is `True` and `timeout` is `None`(the default), then this method blocks until a message is available. + If `block` is `True` and `timeout` is `None` (the default), + then this method blocks until a message is available. If `timeout` is a positive number, the blocking will last for at most `timeout` seconds, and the method will return `None` if no message became available in that time. If `block` is `False` the method returns immediately with either a message or `None`. + Note: + `block` and `timeout` are keyword-only parameters. + + Args: + block: If `True`, block for at most timeout seconds. Otherwise return immediately. + timeout: Number of seconds to wait for a message to become available. + Returns: - A decoded SLIP message, or an empty bytestring `b""` if no further message will come available. + - `None` if no message is available, + - a decoded SLIP message, or + - an empty bytestring `b""` if no further messages will come available. Raises: ProtocolError: When the packet that contained the message had an invalid byte sequence. diff --git a/src/sliplib/slipserver.py b/src/sliplib/slipserver.py index 612c084..f6a417a 100644 --- a/src/sliplib/slipserver.py +++ b/src/sliplib/slipserver.py @@ -41,7 +41,7 @@ class SlipRequestHandler(BaseRequestHandler): """ def __init__(self, request: socket.socket | SlipSocket, client_address: TCPAddress, server: TCPServer): - """Initializes the request handler. + """To initialize the request handler, a request, client address, and server instance must be provided. The type of the :attr:`request` parameter depends on the type of server that instantiates the request handler. diff --git a/src/sliplib/slipwrapper.py b/src/sliplib/slipwrapper.py index 0dc844d..7af19a6 100644 --- a/src/sliplib/slipwrapper.py +++ b/src/sliplib/slipwrapper.py @@ -28,6 +28,7 @@ from __future__ import annotations +import abc from typing import TYPE_CHECKING, Generic, TypeVar if TYPE_CHECKING: @@ -39,7 +40,7 @@ ByteStream = TypeVar("ByteStream") -class SlipWrapper(Generic[ByteStream]): +class SlipWrapper(abc.ABC, Generic[ByteStream]): """Base class that provides a message based interface to a byte stream :class:`SlipWrapper` combines a :class:`Driver` instance with a (generic) byte stream. @@ -72,6 +73,7 @@ def __init__(self, stream: ByteStream): #: The :class:`SlipWrapper`'s :class:`Driver` instance. self.driver = Driver() + @abc.abstractmethod def send_bytes(self, packet: bytes) -> None: """Send a packet over the stream. @@ -80,8 +82,8 @@ def send_bytes(self, packet: bytes) -> None: Args: packet: the packet to send over the stream """ - raise NotImplementedError + @abc.abstractmethod def recv_bytes(self) -> bytes: """Receive data from the stream. @@ -98,7 +100,6 @@ def recv_bytes(self) -> bytes: Returns: The bytes received from the stream """ - raise NotImplementedError def send_msg(self, message: bytes) -> None: """Send a SLIP-encoded message over the stream. diff --git a/tests/unit/test_slipwrapper.py b/tests/unit/test_slipwrapper.py index cb6aec3..df59167 100644 --- a/tests/unit/test_slipwrapper.py +++ b/tests/unit/test_slipwrapper.py @@ -13,22 +13,10 @@ class TestSlipWrapper: """Basic tests for SlipWrapper.""" - @pytest.fixture(autouse=True) - def setup(self) -> None: - """Prepare the test.""" - self.slipwrapper = SlipWrapper("not a valid byte stream") - self.subwrapper = type("SubSlipWrapper", (SlipWrapper,), {})(None) # Dummy subclass without implementation - - def test_slip_wrapper_recv_bytes_is_not_implemented(self) -> None: - """Verify that calling recv_msg on a SlipWrapper instance that does not implement read_bytes fails.""" - with pytest.raises(NotImplementedError): - _ = self.slipwrapper.recv_msg() - with pytest.raises(NotImplementedError): - _ = self.subwrapper.recv_msg() - - def test_slip_wrapper_send_bytes_is_not_implemented(self) -> None: - """Verify that calling send_msg on a SlipWrapper instance that does not implement send_bytes fails.""" - with pytest.raises(NotImplementedError): - self.slipwrapper.send_msg(b"oops") - with pytest.raises(NotImplementedError): - self.subwrapper.send_msg(b"oops") + def test_slip_wrapper_cannot_be_instantiated(self) -> None: + with pytest.raises(TypeError): + SlipWrapper("just a random byte stream") # type: ignore [abstract] + + def test_slip_wrapper_cannot_be_subclassed_without_concrete_implementations(self) -> None: + with pytest.raises(TypeError): + type("SubSlipWrapper", (SlipWrapper,), {})(None) # Dummy subclass without implementation