Skip to content

Commit

Permalink
Merge branch 'main' into add-docs
Browse files Browse the repository at this point in the history
  • Loading branch information
ItsDrike committed Feb 7, 2023
2 parents 5dfb094 + 5c277ff commit 2824665
Show file tree
Hide file tree
Showing 22 changed files with 527 additions and 298 deletions.
14 changes: 7 additions & 7 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ updates:
schedule:
interval: "daily"
labels:
- "area: dependencies"
- "priority: 3 - low"
- "type: enhancement"
- "a: dependencies"
- "p: 3 - low"
- "t: enhancement"

- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
labels:
- "area: dependencies"
- "area: CI"
- "priority: 3 - low"
- "type: enhancement"
- "a: dependencies"
- "a: CI"
- "p: 3 - low"
- "t: enhancement"
1 change: 0 additions & 1 deletion .github/workflows/changelog-fragment.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
name: Changelog Fragment present

on:
workflow_dispatch:
pull_request:
types: [labeled, unlabeled, opened, reopened, synchronize]
branches:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ jobs:
- name: Upload coverage to codeclimate
uses: paambaati/[email protected]
env:
CC_TEST_REPORTER_ID: ${{ secrets.CODECLIMATE_TEST_REPORTER_ID }}
CC_TEST_REPORTER_ID: 0ec6191ea237656410b90dded9352a5b16d68f8d86d60ea8944abd41d532e869
with:
coverageLocations: .coverage.xml:coverage.py

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
[![discord chat](https://img.shields.io/discord/936788458939224094.svg?logo=Discord)](https://discord.gg/C2wX7zduxC)
![supported python versions](https://img.shields.io/pypi/pyversions/mcproto.svg)
[![current PyPI version](https://img.shields.io/pypi/v/mcproto.svg)](https://pypi.org/project/mcproto/)
[![Test Coverage](https://api.codeclimate.com/v1/badges/9464f1037f07a795de35/test_coverage)](https://codeclimate.com/github/py-mine/mcproto/test_coverage)
[![Validation](https://github.com/ItsDrike/mcproto/actions/workflows/validation.yml/badge.svg)](https://github.com/ItsDrike/mcproto/actions/workflows/validation.yml)
[![Unit tests](https://github.com/ItsDrike/mcproto/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/ItsDrike/mcproto/actions/workflows/unit-tests.yml)
[![Docs](https://img.shields.io/readthedocs/mcproto?label=Docs)](https://mcproto.readthedocs.io/)
Expand Down
1 change: 1 addition & 0 deletions changes/18.docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Rewrite all docstrings into proper Sphinx format, instead of using markdown.
39 changes: 28 additions & 11 deletions mcproto/buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,26 @@ def __init__(self, *args, **kwargs):
self.pos = 0

def write(self, data: bytes) -> None:
"""Write new data into the buffer."""
"""Write/Store given ``data`` into the buffer."""
self.extend(data)

def read(self, length: int) -> bytearray:
"""Read data stored in the buffer.
Reading data doesn't remove that data, rather that data is treated as already read, and
next read will start from the first unread byte. If freeing the data is necessary, check the clear function.
next read will start from the first unread byte. If freeing the data is necessary, check
the :meth:`.clear` function.
Trying to read more data than is available will raise an IOError, however it will deplete the remaining data
and the partial data that was read will be a part of the error message. This behavior is here to mimic reading
from a socket connection.
:param length:
Amount of bytes to be read.
If the requested amount can't be read (buffer doesn't contain that much data/buffer
doesn't contain any data), an :exc:`IOError` will be reaised.
If there were some data in the buffer, but it was less than requested, this remaining
data will still be depleted and the partial data that was read will be a part of the
error message in the :exc:`IOError`. This behavior is here to mimic reading from a real
socket connection.
"""
end = self.pos + length

Expand All @@ -43,11 +51,16 @@ def read(self, length: int) -> bytearray:
self.pos = end

def clear(self, only_already_read: bool = False) -> None:
"""
Clear out the stored data and reset position.
"""Clear out the stored data and reset position.
If `only_already_read` is True, only clear out the data which was already read, and reset the position.
This is mostly useful to avoid keeping large chunks of data in memory for no reason.
:param only_already_read:
When set to ``True``, only the data that was already marked as read will be cleared,
and the position will be reset (to start at the remaining data). This can be useful
for avoiding needlessly storing large amounts of data in memory, if this data is no
longer useful.
Otherwise, if set to ``False``, all of the data is cleared, and the position is reset,
essentially resulting in a blank buffer.
"""
if only_already_read:
del self[: self.pos]
Expand All @@ -56,7 +69,11 @@ def clear(self, only_already_read: bool = False) -> None:
self.pos = 0

def reset(self) -> None:
"""Reset the position in the buffer."""
"""Reset the position in the buffer.
Since the buffer doesn't automatically clear the already read data, it is possible to simply
reset the position and read the data it contains again.
"""
self.pos = 0

def flush(self) -> bytearray:
Expand All @@ -67,5 +84,5 @@ def flush(self) -> bytearray:

@property
def remaining(self) -> int:
"""Get the amount of bytes that's still remaining in be buffer to be read."""
"""Get the amount of bytes that's still remaining in the buffer to be read."""
return len(self) - self.pos
111 changes: 90 additions & 21 deletions mcproto/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@


class SyncConnection(BaseSyncReader, BaseSyncWriter, ABC):
"""Base class for all classes handling synchronous connections."""

__slots__ = ("closed",)

def __init__(self):
Expand All @@ -36,11 +38,19 @@ def __init__(self):
@classmethod
@abstractmethod
def make_client(cls, address: tuple[str, int], timeout: float) -> Self:
"""Construct a client connection to given address."""
"""Construct a client connection (Client -> Server) to given server ``address``.
:param address: Address of the server to connection to.
:param timeout:
Amount of seconds to wait for the connection to be established.
If connection can't be established within this time, :exc:`TimeoutError` will be raised.
This timeout is then also used for any further data receiving.
"""
raise NotImplementedError

@abstractmethod
def _close(self) -> None:
"""Close the underlying connection."""
raise NotImplementedError

def close(self) -> None:
Expand All @@ -58,6 +68,8 @@ def __exit__(self, *a, **kw) -> None:


class AsyncConnection(BaseAsyncReader, BaseAsyncWriter, ABC):
"""Base class for all classes handling asynchronous connections."""

__slots__ = ("closed",)

def __init__(self):
Expand All @@ -66,11 +78,19 @@ def __init__(self):
@classmethod
@abstractmethod
async def make_client(cls, address: tuple[str, int], timeout: float) -> Self:
"""Construct a client connection to given address."""
"""Construct a client connection (Client -> Server) to given server ``address``.
:param address: Address of the server to connection to.
:param timeout:
Amount of seconds to wait for the connection to be established.
If connection can't be established within this time, :exc:`TimeoutError` will be raised.
This timeout is then also used for any further data receiving.
"""
raise NotImplementedError

@abstractmethod
async def _close(self) -> None:
"""Close the underlying connection."""
raise NotImplementedError

async def close(self) -> None:
Expand All @@ -88,6 +108,8 @@ async def __aexit__(self, *a, **kw) -> None:


class TCPSyncConnection(SyncConnection, Generic[T_SOCK]):
"""Synchronous connection using a TCP :class:`~socket.socket`."""

__slots__ = ("socket",)

def __init__(self, socket: T_SOCK):
Expand All @@ -96,16 +118,25 @@ def __init__(self, socket: T_SOCK):

@classmethod
def make_client(cls, address: tuple[str, int], timeout: float) -> Self:
"""Construct a client connection to given address."""
"""Construct a client connection (Client -> Server) to given server ``address``.
:param address: Address of the server to connection to.
:param timeout:
Amount of seconds to wait for the connection to be established.
If connection can't be established within this time, :exc:`TimeoutError` will be raised.
This timeout is then also used for any further data receiving.
"""
sock = socket.create_connection(address, timeout=timeout)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
return cls(sock)

def read(self, length: int) -> bytearray:
"""Receive data sent through the connection.
`length` controls how many bytes we want to receive. If the requested amount
of bytes isn't available to be received, IOError will be raised.
:param length:
Amount of bytes to be received. If the requested amount can't be received
(server didn't send that much data/server didn't send any data), an :exc:`IOError`
will be raised.
"""
result = bytearray()
while len(result) < length:
Expand All @@ -124,15 +155,17 @@ def read(self, length: int) -> bytearray:
return result

def write(self, data: bytes) -> None:
"""Send given data over the connection."""
"""Send given ``data`` over the connection."""
self.socket.send(data)

def _close(self) -> None:
"""Close the connection (it cannot be used after this)."""
"""Close the underlying connection."""
self.socket.close()


class TCPAsyncConnection(AsyncConnection, Generic[T_STREAMREADER, T_STREAMWRITER]):
"""Asynchronous TCP connection using :class:`~asyncio.StreamWriter` and :class:`~asyncio.StreamReader`."""

__slots__ = ("reader", "writer", "timeout")

def __init__(self, reader: T_STREAMREADER, writer: T_STREAMWRITER, timeout: float):
Expand All @@ -143,16 +176,25 @@ def __init__(self, reader: T_STREAMREADER, writer: T_STREAMWRITER, timeout: floa

@classmethod
async def make_client(cls, address: tuple[str, int], timeout: float) -> Self:
"""Construct a client connection to given address."""
"""Construct a client connection (Client -> Server) to given server ``address``.
:param address: Address of the server to connection to.
:param timeout:
Amount of seconds to wait for the connection to be established.
If connection can't be established within this time, :exc:`TimeoutError` will be raised.
This timeout is then also used for any further data receiving.
"""
conn = asyncio.open_connection(address[0], address[1])
reader, writer = await asyncio.wait_for(conn, timeout=timeout)
return cls(reader, writer, timeout)

async def read(self, length: int) -> bytearray:
"""Receive data sent through the connection.
`length` controls how many bytes we want to receive. If the requested amount
of bytes isn't available to be received, IOError will be raised.
:param length:
Amount of bytes to be received. If the requested amount can't be received
(server didn't send that much data/server didn't send any data), an :exc:`IOError`
will be raised.
"""
result = bytearray()
while len(result) < length:
Expand All @@ -171,20 +213,22 @@ async def read(self, length: int) -> bytearray:
return result

async def write(self, data: bytes) -> None:
"""Send given data over the connection."""
"""Send given ``data`` over the connection."""
self.writer.write(data)

async def _close(self) -> None:
"""Close the connection (it cannot be used after this)."""
"""Close the underlying connection."""
self.writer.close()

@property
def socket(self) -> socket.socket:
"""Obtain the underlying socket behind the asyncio transport."""
"""Obtain the underlying socket behind the :class:`~asyncio.Transport`."""
return self.writer.transport._sock # type: ignore


class UDPSyncConnection(SyncConnection, Generic[T_SOCK]):
"""Synchronous connection using a UDP :class:`~socket.socket`."""

__slots__ = ("socket", "address")

BUFFER_SIZE = 65535
Expand All @@ -196,15 +240,27 @@ def __init__(self, socket: T_SOCK, address: tuple[str, int]):

@classmethod
def make_client(cls, address: tuple[str, int], timeout: float) -> Self:
"""Construct a client connection to given address."""
"""Construct a client connection (Client -> Server) to given server ``address``.
:param address: Address of the server to connection to.
:param timeout:
Amount of seconds to wait for the connection to be established.
If connection can't be established within this time, :exc:`TimeoutError` will be raised.
This timeout is then also used for any further data receiving.
"""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(timeout)
return cls(sock, address)

def read(self, length: Optional[int] = None) -> bytearray:
"""Receive data sent through the connection.
For UDP connections, `length` parameter is ignored and not required.
:param length:
For UDP connections, ``length`` parameter is ignored and not required.
Instead, UDP connections always read exactly :attr:`.BUFFER_SIZE` bytes.
If the requested amount can't be received (server didn't send that much
data/server didn't send any data), an :exc:`IOError` will be raised.
"""
result = bytearray()
while len(result) == 0:
Expand All @@ -213,15 +269,17 @@ def read(self, length: Optional[int] = None) -> bytearray:
return result

def write(self, data: bytes) -> None:
"""Send given data over the connection."""
"""Send given ``data`` over the connection."""
self.socket.sendto(data, self.address)

def _close(self) -> None:
"""Close the connection (it cannot be used after this)."""
"""Close the underlying connection."""
self.socket.close()


class UDPAsyncConnection(AsyncConnection, Generic[T_DATAGRAM_CLIENT]):
"""Asynchronous UDP connection using :class:`~asyncio_dgram.DatagramClient`."""

__slots__ = ("stream", "timeout")

def __init__(self, stream: T_DATAGRAM_CLIENT, timeout: float):
Expand All @@ -231,15 +289,26 @@ def __init__(self, stream: T_DATAGRAM_CLIENT, timeout: float):

@classmethod
async def make_client(cls, address: tuple[str, int], timeout: float) -> Self:
"""Construct a client connection to given address."""
"""Construct a client connection (Client -> Server) to given server ``address``.
:param address: Address of the server to connection to.
:param timeout:
Amount of seconds to wait for the connection to be established.
If connection can't be established within this time, :exc:`TimeoutError` will be raised.
This timeout is then also used for any further data receiving.
"""
conn = asyncio_dgram.connect(address)
stream = await asyncio.wait_for(conn, timeout=timeout)
return cls(stream, timeout)

async def read(self, length: Optional[int] = None) -> bytearray:
"""Receive data sent through the connection.
For UDP connections, `length` parameter is ignored and not required.
:param length:
For UDP connections, ``length`` parameter is ignored and not required.
If the requested amount can't be received (server didn't send that much
data/server didn't send any data), an :exc:`IOError` will be raised.
"""
result = bytearray()
while len(result) == 0:
Expand All @@ -248,9 +317,9 @@ async def read(self, length: Optional[int] = None) -> bytearray:
return result

async def write(self, data: bytes) -> None:
"""Send given data over the connection."""
"""Send given ``data`` over the connection."""
await self.stream.send(data)

async def _close(self) -> None:
"""Close the connection (it cannot be used after this)."""
"""Close the underlying connection."""
self.stream.close()
Loading

0 comments on commit 2824665

Please sign in to comment.