Skip to content

Commit

Permalink
Merge pull request #68 from SSS-Says-Snek/typecast-overhaul
Browse files Browse the repository at this point in the history
Overhaul typecasting system (remove manual typecasting)
  • Loading branch information
SSS-Says-Snek authored Aug 6, 2023
2 parents 1f39c2a + 61fbbff commit eff4209
Show file tree
Hide file tree
Showing 11 changed files with 413 additions and 626 deletions.
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import sys
import time
import random

from hisock import start_server, get_local_ip
from hisock import ClientInfo, start_server, get_local_ip

ADDR = get_local_ip()
PORT = 6969
Expand All @@ -68,16 +68,16 @@ print(f"Serving at {ADDR}")
server = start_server((ADDR, PORT))

@server.on("join")
def client_join(client_data):
print(f"Cool, {client_data.ip_as_str} joined!")
if client_data['name'] is not None:
def client_join(client: ClientInfo):
print(f"Cool, {client.ipstr} joined!")
if client.name is not None:
print(f" - With a sick name \"{client_data.name}\", very cool!")
if client_data['group'] is not None:
if client.group is not None:
print(f" - In a sick group \"{client_data.group}\", cool!")

print("I'm gonna send them a quick hello message")

server.send_client(client_data['ip'], "hello_message", str(time.time()).encode())
server.send_client(client, "hello_message", str(time.time()).encode())

@server.on("processing1")
def process(client_data, process_request: str):
Expand All @@ -93,7 +93,7 @@ def process(client_data, process_request: str):
result = eval(process_request) # Insecure, but I'm lazy, so...
print(f"Cool! The result is {result}! I'mma send it to the client")

server.send_client(client_data, "something", str(result))
server.send_client(client_data, "something", result)


server.start()
Expand Down Expand Up @@ -133,7 +133,7 @@ client = connect(
join_time = time.time()


@client.on("hello_message")
@client.on("hello_message", threaded=True)
def handle_hello(msg: str):
print("Thanks, server, for sending a hello, just for me!")
print(f"Looks like, the message was sent on timestamp {msg}, "
Expand Down
52 changes: 16 additions & 36 deletions docs/source/tutorials/beginner_tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ When a function is prefaced with the ``on`` decorator, it will run on something.

The ``on`` decorator takes a maximum of three parameters. One of the parameters is the command to listen on. The second (optional) parameter is whether to run the listener in its own thread or not. The third (optional) parameter is whether to override a reserved command, and this tutorial won't be covering it.

For the server: The ``on`` decorator will send a maximum of two parameters to the function it is decorating (there are a few exceptions we will touch on). The first parameter is the client info. It is an instance of :class:`ClientInfo` that includes the client's name, client IP address, and the group the client is in (can be type-casted to a dict). The second parameter is the data that is being received.
For the server: The ``on`` decorator will send a maximum of two parameters to the function it is decorating (there are a few exceptions we will touch on). The first parameter is the client info. It is an instance of :class:`ClientInfo` that includes the client's name, client IP address, and the group the client is in. The second parameter is the data that is being received.

For the client: the ``on`` decorator will send a maximum of one parameter to the function it is decorating, which will be the message or content the client receives (in most cases).

Expand Down Expand Up @@ -209,9 +209,6 @@ As I stated before, not every receiver has a maximum of two parameters passed to

:mod:`HiSock` has reserved events. These events shouldn't be sent by the client or server explicitly as it is currently unsupported.

.. note::
Besides for ``string`` and ``bytes`` for ``message``, these reserved events do not have type casting.

Here is a list of the reserved events:

Server:
Expand Down Expand Up @@ -254,49 +251,32 @@ Client:
----

============
Type-casting
Typecasting
============
:mod:`HiSock` has a system called "type-casting" when transmitting data.

Data sent and received can be one of the following types:
.. versionchanged:: 3.0
Previously, :mod:`HiSock` used manual type casting, where the type hints of the event's function actually determined
the type the data was supposed to be interpreted. However, now, it doesn't matter. Even if it doesn't matter, it's still recommended
to type hint your functions!

You shouldn't worry about this too much, but :mod:`HiSock` has a system called "typecasting" when transmitting data.
This system is how hisock will convert supported datatypes to and from bytes to get sent along the network.

Data sent and received can be one of the following supported types:

- ``bytes``
- ``str``
- ``int``
- ``float``
- ``bool``
- ``None``
- ``NoneType``
- ``list`` (with the types listed here)
- ``dict`` (with the types listed here)

.. note::
There is a type hint in ``hisock.utils`` called ``Sendable`` which has these.

The type that the data gets type-casted to depends on the type hint for the message argument for the function for the event receiving the data. If there is no type hint for the argument, the data received will be bytes.

Here are a few examples this server-side code block:

.. code-block:: python
@server.on("string_sent")
def on_string_sent(client: hisock.ClientInfo, message: str):
"""``message`` will be of type ``string``"""
...
@server.on("integer_sent")
def on_integer_sent(client: hisock.ClientInfo, integer: int):
"""``integer`` will be of type ``int``"""
...
@server.on("dictionary_sent")
def on_dictionary_sent(client: hisock.ClientInfo, dictionary: dict):
"""``dictionary`` will be of type ``dict``"""
...
- ``dict`` (with the immutable types listed here)

.. note::
Although these examples are on the server-side, they work the exact same for the client-side.
There is a type alias in ``hisock.utils`` called ``Sendable`` which is a union of all of these.

Of course, you need to be careful that the type-casting will work. Turning ``b"hello there"`` to ``int`` will fail.
If you have a type that isn't one of these supported types, such as a class, consider manually converting to and from bytes.

----

Expand All @@ -309,7 +289,7 @@ In :mod:`HiSock` with an _unreserved_ event, the function to handle it can be ca

As an example, for the server: If an event has 1 argument, it will only be called with the client info. If it has 2 arguments, it will be called with the client info and the message. If it has 0 arguments, it'll be called as a void (no arguments).

Data can be sent similarly. If there is no data sent, the server will receive the equivalent of ``None`` for the type-casted data.
Data can be sent similarly. If there is no data sent, the server will receive the equivalent of ``None`` for the typecasted data.

Here are a few examples of this with a server-side code block.

Expand Down
4 changes: 2 additions & 2 deletions examples/basic/example_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ def handle_hello(msg: str):
)
print("In response, I'm going to send the server a request to do some processing")

client.send("processing1", b"randnum**2")
result = client.recv("something", int)
client.send("processing1", "randnum**2")
result = client.recv("something")

print(f"WHOAAA! The result is {result}! Thanks server!")

Expand Down
123 changes: 33 additions & 90 deletions hisock/_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@

import inspect
import threading
from typing import Any, Callable, Union
from typing import Any, Callable, Optional, Union

try:
from . import _typecast
from .utils import (ClientInfo, FunctionNotFoundException,
MessageCacheMember, Sendable,
_str_type_to_type_annotations_dict, _type_cast,
MessageCacheMember, Sendable, make_header,
validate_command_not_reserved)
except ImportError:
import _typecast
from utils import (ClientInfo, FunctionNotFoundException,
MessageCacheMember, Sendable,
_str_type_to_type_annotations_dict, _type_cast,
MessageCacheMember, Sendable, make_header,
validate_command_not_reserved)


Expand Down Expand Up @@ -56,50 +56,6 @@ def __init__(self, addr: tuple[str, int], header_len: int = 16, cache_size: int

# Internal methods

def _type_cast_client_info(self, command: str, client_info: ClientInfo) -> Union[ClientInfo, dict]:
"""
Type cast client info accordingly.
If the type hint is None, then the client info is returned as is (a dict).
:param command: The name of the function that called this method.
:type command: str
:param client_info: The client info to type cast.
:type client_info: dict
:return: The type casted client info from the type hint.
:rtype: Union[ClientInfo, dict]
"""

type_cast_to = self.funcs[command]["type_hint"]["client_info"]
if type_cast_to is None:
type_cast_to = ClientInfo

if type_cast_to is ClientInfo:
return client_info
return client_info.as_dict()

@staticmethod
def _send_type_cast(content: Sendable) -> bytes:
"""
Type casting content for the send methods.
This method exists so type casting can easily be changed without changing
all the send methods.
:param content: The content to type cast
:type content: Sendable
:return: The type casted content
:rtype: bytes
:raises InvalidTypeCast: If the content cannot be type casted
"""

return _type_cast(
type_cast=bytes,
content_to_type_cast=content,
func_name="<sending function>",
)

def _cache(
self,
has_listener: bool,
Expand Down Expand Up @@ -131,46 +87,34 @@ def _cache(

def _call_wildcard_function(
self,
command: Union[str, None],
content: bytes,
client_info: Union[dict, None] = None,
command: str,
content: Sendable,
client_info: Optional[ClientInfo] = None,
):
"""
Call the wildcard command.
:param command: The command that was sent. If None, then it is just
random data.
:type command: str, optional
:param content: The data to pass to the wildcard command. Will be
type-casted accordingly.
:param content: The data to pass to the wildcard command. Will NOT be
type-casted.
:type content: bytes
:param client_info: The client info. If None, then there is no client info.
:type client_info: dict, optional
:type client_info: ClientInfo, optional
:raises FunctionNotFoundException: If there is no wildcard listener.
"""

try:
wildcard_func = self.funcs["*"]
self.funcs["*"]
except KeyError:
raise FunctionNotFoundException("A wildcard function doesn't exist.") from None

arguments = []
if client_info is not None:
arguments.append(self._type_cast_client_info("*", client_info))

arguments += [
_type_cast(
type_cast=wildcard_func["type_hint"]["command"],
content_to_type_cast=command,
func_name="<wildcard function> <command>",
),
_type_cast(
type_cast=wildcard_func["type_hint"]["message"],
content_to_type_cast=content,
func_name="<wildcard function> <data>",
),
]
arguments.append(client_info)
arguments.extend([command, content])

self._call_function(
"*",
Expand Down Expand Up @@ -244,6 +188,14 @@ def _call_function(self, func_name: str, *args, **kwargs):
)
function_thread.start()

def _prepare_send(self, command: str, content: Optional[Sendable] = None) -> bytes:
fmt, encoded_content = _typecast.write_fmt(content) if content is not None else ("", b"")

data_to_send = b"$CMD$" + command.encode() + b"$MSG$" + make_header(fmt, 8) + fmt.encode() + encoded_content
data_header = make_header(data_to_send, self.header_len)

return data_header + data_to_send

class _on: # NOSONAR (it's used in child classes)
"""Decorator for handling a command"""

Expand Down Expand Up @@ -277,28 +229,12 @@ def __call__(self, func: Callable) -> Callable:

self._assert_num_func_args_valid(len(func_args))

# Store annotations of function
annotations = _str_type_to_type_annotations_dict(
inspect.getfullargspec(func).annotations
) # {"param": type}
parameter_annotations = {}

# Map function arguments into type hint compliant ones
type_cast_arguments: tuple
if self.command in self.outer._reserved_funcs:
type_cast_arguments = (self.outer._reserved_funcs[self.command]["type_cast_arguments"],)[0]
else:
type_cast_arguments = self.outer._unreserved_func_arguments

for func_argument, argument_name in zip(func_args, type_cast_arguments):
parameter_annotations[argument_name] = annotations.get(func_argument, None)

# Add function
self.outer.funcs[self.command] = {
"func": func,
"name": func.__name__,
"type_hint": parameter_annotations,
"threaded": self.threaded,
"num_args": len(func_args),
"override": self.override,
}

Expand All @@ -319,7 +255,7 @@ def _assert_num_func_args_valid(self, number_of_func_args: int):

# Reserved commands
if self.command in self.outer._reserved_funcs:
needed_number_of_args = (self.outer._reserved_funcs[self.command]["number_arguments"],)[0]
needed_number_of_args = self.outer._reserved_funcs[self.command]
valid = number_of_func_args == needed_number_of_args

# Unreserved commands
Expand Down Expand Up @@ -366,7 +302,7 @@ def _handle_recv_commands(self, command: str, content: bytes):

return False

def recv(self, recv_on: str = None, recv_as: Sendable = bytes) -> Sendable:
def recv(self, recv_on: str = None) -> Sendable:
"""
Receive data from the server while blocking.
Can receive on a command, which is used as like one-time on decorator.
Expand Down Expand Up @@ -410,5 +346,12 @@ def recv(self, recv_on: str = None, recv_as: Sendable = bytes) -> Sendable:
data = self._recv_on_events[listen_on]["data"]
del self._recv_on_events[listen_on]

fmt_len = int(data[:8])
fmt = data[8 : 8 + fmt_len].decode()
data = data[8 + fmt_len :]

fmt_ast = _typecast.read_fmt(fmt)
typecasted_data = _typecast.typecast_data(fmt_ast, data)

# Return
return _type_cast(type_cast=recv_as, content_to_type_cast=data, func_name="<recv function>")
return typecasted_data
Loading

0 comments on commit eff4209

Please sign in to comment.