Overhaul converter system

- Keyword argument support
- New Greedy, Union and Optional converters
- Optionally passing extra data as raw list
This commit is contained in:
Ben 2019-04-11 01:00:14 +01:00
parent d292407dc7
commit 5a01be8c68
2 changed files with 185 additions and 105 deletions

View File

@ -1,155 +1,221 @@
import zope.interface from abc import ABC
from zope.interface import implementer from abc import abstractmethod
import asyncio
class IConverter(zope.interface.Interface): class IConverter(ABC):
description = zope.interface.Attribute("""A short description of the purpose of the converter""") @property
@abstractmethod
def description(self):
"""A short description of the purpose of the converter"""
async def convert(self): @abstractmethod
raise NotImplementedError('Converter must derive this class!') async def convert(self, ctx):
"""The actual converter implementation"""
class Converter: class CredentialsConverter(IConverter):
__slots__ = ['p', 'argument']
def __init__(self, p, argument):
self.p = p
self.argument = argument
@implementer(IConverter)
class CredentialsConverter(Converter):
description = """Used for obtaining login credentials from XML login data""" description = """Used for obtaining login credentials from XML login data"""
async def convert(self): async def convert(self, ctx):
username = self.argument[0][0].text username = ctx.argument[0][0].text
password = self.argument[0][1].text password = ctx.argument[0][1].text
return username, password return username, password
@implementer(IConverter) class VersionChkConverter(IConverter):
class VersionChkConverter(Converter):
description = """Used for checking the verChk version number""" description = """Used for checking the verChk version number"""
async def convert(self): async def convert(self, ctx):
return self.argument[0].get('v') return ctx.argument[0].get('v')
@implementer(IConverter) class ConnectedPenguinConverter(IConverter):
class ConnectedPenguinConverter(Converter):
description = """Converts a penguin ID into a live penguin instance description = """Converts a penguin ID into a live penguin instance
or none if the player is offline""" or none if the player is offline"""
async def convert(self): async def convert(self, ctx):
penguin_id = int(self.argument) penguin_id = int(ctx.argument)
if penguin_id in self.p.server.penguins_by_id: if penguin_id in ctx.p.server.penguins_by_id:
return self.p.server.penguins_by_id[penguin_id] return ctx.p.server.penguins_by_id[penguin_id]
return None return None
@implementer(IConverter) class ConnectedIglooConverter(IConverter):
class ConnectedIglooConverter(Converter):
description = """Converts a penguin ID into a live igloo instance or description = """Converts a penguin ID into a live igloo instance or
none if it's not available""" none if it's not available"""
async def convert(self): async def convert(self, ctx):
igloo_id = int(self.argument) igloo_id = int(ctx.argument)
if igloo_id in self.p.server.igloo_map: if igloo_id in ctx.p.server.igloo_map:
return self.p.server.igloo_map[igloo_id] return ctx.p.server.igloo_map[igloo_id]
return None return None
@implementer(IConverter) class RoomConverter(IConverter):
class RoomConverter(Converter):
description = """Converts a room ID into a Houdini.Data.Room instance""" description = """Converts a room ID into a Houdini.Data.Room instance"""
async def convert(self): async def convert(self, ctx):
room_id = int(self.argument) room_id = int(ctx.argument)
if room_id in self.p.server.rooms: if room_id in ctx.p.server.rooms:
return self.p.server.rooms[room_id] return ctx.p.server.rooms[room_id]
return None return None
@implementer(IConverter) class ItemConverter(IConverter):
class ItemConverter(Converter):
description = """Converts an item ID into a Houdini.Data.Item instance""" description = """Converts an item ID into a Houdini.Data.Item instance"""
async def convert(self): async def convert(self, ctx):
item_id = int(self.argument) item_id = int(ctx.argument)
if item_id in self.p.server.items: if item_id in ctx.p.server.items:
return self.p.server.items[item_id] return ctx.p.server.items[item_id]
return None return None
@implementer(IConverter) class IglooConverter(IConverter):
class IglooConverter(Converter):
description = """Converts an igloo ID into a Houdini.Data.Igloo instance""" description = """Converts an igloo ID into a Houdini.Data.Igloo instance"""
async def convert(self): async def convert(self, ctx):
igloo_id = int(self.argument) igloo_id = int(ctx.argument)
if igloo_id in self.p.server.igloos: if igloo_id in ctx.p.server.igloos:
return self.p.server.igloos[igloo_id] return ctx.p.server.igloos[igloo_id]
return None return None
@implementer(IConverter) class FurnitureConverter(IConverter):
class FurnitureConverter(Converter):
description = """Converts a furniture ID into a Houdini.Data.Furniture instance""" description = """Converts a furniture ID into a Houdini.Data.Furniture instance"""
async def convert(self): async def convert(self, ctx):
furniture_id = int(self.argument) furniture_id = int(ctx.argument)
if furniture_id in self.p.server.furniture: if furniture_id in ctx.p.server.furniture:
return self.p.server.furniture[furniture_id] return ctx.p.server.furniture[furniture_id]
return None return None
@implementer(IConverter) class FlooringConverter(IConverter):
class FlooringConverter(Converter):
description = """Converts a flooring ID into a Houdini.Data.Flooring instance""" description = """Converts a flooring ID into a Houdini.Data.Flooring instance"""
async def convert(self): async def convert(self, ctx):
flooring_id = int(self.argument) flooring_id = int(ctx.argument)
if flooring_id in self.p.server.flooring: if flooring_id in ctx.p.server.flooring:
return self.p.server.flooring[flooring_id] return ctx.p.server.flooring[flooring_id]
return None return None
@implementer(IConverter) class StampConverter(IConverter):
class StampConverter(Converter):
description = """Converts a stamp ID into a Houdini.Data.Stamp instance""" description = """Converts a stamp ID into a Houdini.Data.Stamp instance"""
async def convert(self): async def convert(self, ctx):
stamp_id = int(self.argument) stamp_id = int(ctx.argument)
if stamp_id in self.p.server.stamps: if stamp_id in ctx.p.server.stamps:
return self.p.server.stamps[stamp_id] return ctx.p.server.stamps[stamp_id]
return None return None
@implementer(IConverter) class SeparatorConverter(IConverter):
class VerticalConverter(Converter):
description = """Converts vertically separated values into an int list""" __slots__ = ['separator', 'mapper']
async def convert(self): description = """Converts strings separated by char into a list of type"""
return map(int, self.argument.split('|'))
def __init__(self, separator='|', mapper=int):
self.separator = separator
self.mapper = mapper
async def convert(self, ctx):
return map(self.mapper, ctx.argument.split(self.separator))
@implementer(IConverter) class UnionConverter(IConverter):
class CommaConverter(Converter):
description = """Converts comma separated values into an int list""" __slots__ = ['types']
async def convert(self): description = """Converts union type into argument"""
return map(int, self.argument.split(','))
def __init__(self, *types, skip_none=False):
self.types = types
self.skip_none = skip_none
async def convert(self, ctx):
for converter in self.types:
try:
result = await do_conversion(converter, ctx)
if not self.skip_none or result is not None:
return result
except ValueError:
continue
class GreedyConverter(IConverter):
__slots__ = ['target']
description = """Converts until it can't any longer"""
def __init__(self, target=int):
self.target = target
async def convert(self, ctx):
converted = []
try:
converted.append(await do_conversion(self.target, ctx))
for ctx.argument in ctx.arguments:
converted.append(await do_conversion(self.target, ctx))
except ValueError:
return converted
return converted
class OptionalConverter(IConverter):
__slots__ = ['target']
description = """Tries to convert but ignores if it can't"""
def __init__(self, target=int):
self.target = target
async def convert(self, ctx):
try:
return await do_conversion(self.target, ctx)
except ValueError:
return ctx.component.default
class _ConverterContext:
__slots__ = ['component', 'arguments', 'argument', 'p']
def __init__(self, component, arguments, argument, p):
self.component = component
self.arguments = arguments
self.argument = argument
self.p = p
def get_converter(component):
if component.annotation is component.empty:
return str
return component.annotation
async def do_conversion(converter, ctx):
if issubclass(type(converter), IConverter) and not isinstance(converter, IConverter):
converter = converter()
if isinstance(converter, IConverter):
if asyncio.iscoroutinefunction(converter.convert):
return await converter.convert(ctx)
return converter.convert(ctx)
return converter(ctx.argument)

View File

@ -110,39 +110,53 @@ class _Listener:
class _XTListener(_Listener): class _XTListener(_Listener):
__slots__ = ['pre_login'] __slots__ = ['pre_login', 'rest_raw', 'keywords']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.pre_login = kwargs.get('pre_login') self.pre_login = kwargs.get('pre_login')
self.rest_raw = kwargs.get('rest_raw', False)
self.keywords = len(inspect.getfullargspec(self.handler).kwonlyargs)
if self.rest_raw:
self.components = self.components[:-1]
async def __call__(self, p, packet_data): async def __call__(self, p, packet_data):
if not self.pre_login and not p.joined_world: await super().__call__(p, packet_data)
p.logger.warn('{} tried sending XT packet before authentication!'.format(p.peer_name))
await p.close()
return
super().__call__(p, packet_data) handler_call_arguments = [self.plugin] if self.plugin is not None else []
handler_call_arguments = [self.instance] if self.instance is not None else []
handler_call_arguments += [self.packet, p] if self.pass_packet else [p] handler_call_arguments += [self.packet, p] if self.pass_packet else [p]
handler_call_keywords = {}
arguments = iter(packet_data) arguments = iter(packet_data[:-self.keywords])
for index, component in enumerate(self.components): ctx = _ConverterContext(None, arguments, None, p)
if component.default is not component.empty: for ctx.component in self.components:
handler_call_arguments.append(component.default) if ctx.component.annotation is ctx.component.empty and ctx.component.default is not ctx.component.empty:
next(arguments) handler_call_arguments.append(ctx.component.default)
elif component.kind == component.POSITIONAL_OR_KEYWORD: next(ctx.arguments)
component_data = next(arguments) elif ctx.component.kind == ctx.component.POSITIONAL_OR_KEYWORD:
converter = get_converter(component) ctx.argument = next(ctx.arguments)
handler_call_arguments.append(await do_conversion(converter, p, component_data)) converter = get_converter(ctx.component)
elif component.kind == component.VAR_POSITIONAL:
for component_data in arguments: handler_call_arguments.append(await do_conversion(converter, ctx))
converter = get_converter(component) elif ctx.component.kind == ctx.component.VAR_POSITIONAL:
handler_call_arguments.append(await do_conversion(converter, p, component_data)) for argument in ctx.arguments:
break ctx.argument = argument
return await self.handler(*handler_call_arguments) converter = get_converter(ctx.component)
handler_call_arguments.append(await do_conversion(converter, ctx))
elif ctx.component.kind == ctx.component.KEYWORD_ONLY:
ctx.argument = packet_data[-self.keywords:][len(handler_call_keywords)]
converter = get_converter(ctx.component)
handler_call_keywords[ctx.component.name] = await do_conversion(converter, ctx)
if self.rest_raw:
handler_call_arguments.append(list(ctx.arguments))
return await self.handler(*handler_call_arguments, **handler_call_keywords)
elif not len(list(ctx.arguments)):
return await self.handler(*handler_call_arguments, **handler_call_keywords)
class _XMLListener(_Listener): class _XMLListener(_Listener):