From 074e8fd6ed11e6312f4e2584b1c03e238761c10c Mon Sep 17 00:00:00 2001 From: DarkCat09 Date: Fri, 1 Jul 2022 14:28:39 +0400 Subject: [PATCH] Module docstrings, pylint, pep8 --- pylintrc | 212 ++++++++++++++++++++++++++++++++++++ python_aternos/__init__.py | 12 +- python_aternos/atclient.py | 7 +- python_aternos/atconf.py | 21 +++- python_aternos/atconnect.py | 40 ++++--- python_aternos/aterrors.py | 6 +- python_aternos/atfile.py | 51 ++++++++- python_aternos/atfm.py | 60 ++++++---- python_aternos/atjsparse.py | 13 ++- python_aternos/atplayers.py | 12 +- python_aternos/atserver.py | 131 +++++++++++++++++++++- python_aternos/atwss.py | 55 +++++++--- setup.cfg | 5 + test.sh | 49 ++++++++- tests/test_js2py.py | 12 +- 15 files changed, 603 insertions(+), 83 deletions(-) create mode 100644 pylintrc create mode 100644 setup.cfg diff --git a/pylintrc b/pylintrc new file mode 100644 index 0000000..303a4c7 --- /dev/null +++ b/pylintrc @@ -0,0 +1,212 @@ +[MAIN] +analyse-fallback-blocks=no +extension-pkg-allow-list= +extension-pkg-whitelist= +fail-on= +fail-under=10.0 +ignore=CVS +ignore-paths= +ignore-patterns=^\.# +ignored-modules= +jobs=4 +limit-inference-results=100 +load-plugins= +persistent=yes +py-version=3.10 +recursive=no +suggestion-mode=yes +unsafe-load-any-extension=no + + +[REPORTS] +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) +msg-template= +reports=no +score=yes + + +[MESSAGES CONTROL] +confidence=HIGH, + CONTROL_FLOW, + INFERENCE, + INFERENCE_FAILURE, + UNDEFINED +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + wrong-import-order, + unspecified-encoding, + logging-not-lazy, + logging-fstring-interpolation, + no-member, + too-many-branches, + too-many-arguments, + too-many-public-methods, + too-many-instance-attributes +enable=c-extension-no-member + + +[SIMILARITIES] +ignore-comments=yes +ignore-docstrings=yes +ignore-imports=yes +ignore-signatures=yes +min-similarity-lines=4 + + +[MISCELLANEOUS] +notes=FIXME, + XXX, + TODO +notes-rgx= + + +[DESIGN] +exclude-too-few-public-methods= +ignored-parents= +max-args=5 +max-attributes=7 +max-bool-expr=5 +max-branches=12 +max-locals=15 +max-parents=7 +max-public-methods=20 +max-returns=6 +max-statements=50 +min-public-methods=2 + + +[STRING] +check-quote-consistency=no +check-str-concat-over-line-jumps=no + + +[CLASSES] +check-protected-access-in-special-methods=no +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make +valid-classmethod-first-arg=cls +valid-metaclass-classmethod-first-arg=cls + + +[FORMAT] +expected-line-ending-format= +ignore-long-lines=^\s*(# )??$ +indent-after-paren=4 +indent-string=' ' +max-line-length=100 +max-module-lines=1000 +single-line-class-stmt=no +single-line-if-stmt=no + + +[IMPORTS] +allow-any-import-level= +allow-wildcard-with-all=no +deprecated-modules= +ext-import-graph= +import-graph= +int-import-graph= +known-standard-library= +known-third-party=enchant +preferred-modules= + + +[VARIABLES] +additional-builtins= +allow-global-unused-variables=yes +allowed-redefined-builtins= +callbacks=cb_, + _cb +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ +ignored-argument-names=_.*|^ignored_|^unused_ +init-import=no +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[LOGGING] +logging-format-style=new +logging-modules=logging + + +[EXCEPTIONS] +overgeneral-exceptions=BaseException, + Exception + + +[BASIC] +argument-naming-style=snake_case +attr-naming-style=snake_case +bad-names=foo, + bar, + baz, + toto, + tutu, + tata +bad-names-rgxs= +class-attribute-naming-style=any +class-const-naming-style=any +class-naming-style=PascalCase +const-naming-style=UPPER_CASE +docstring-min-length=-1 +function-naming-style=snake_case +good-names=i, + j, + k, + f, + s, + ex, + Run, + _ +good-names-rgxs= +include-naming-hint=no +inlinevar-naming-style=any +method-naming-style=snake_case +module-naming-style=snake_case +name-group= +no-docstring-rgx=^_ +property-classes=abc.abstractproperty +variable-naming-style=snake_case + + +[SPELLING] +max-spelling-suggestions=4 +spelling-dict= +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: +spelling-ignore-words= +spelling-private-dict-file= +spelling-store-unknown-words=no + + +[TYPECHECK] +contextmanager-decorators=contextlib.contextmanager +generated-members= +ignore-none=yes +ignore-on-opaque-inference=yes +ignored-checks-for-mixins=no-member, + not-async-context-manager, + not-context-manager, + attribute-defined-outside-init +ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace +missing-member-hint=yes +missing-member-hint-distance=1 +missing-member-max-choices=1 +mixin-class-rgx=.*[Mm]ixin +signature-mutators= + + +[REFACTORING] +max-nested-blocks=5 +never-returning-functions=sys.exit,argparse.parse_error diff --git a/python_aternos/__init__.py b/python_aternos/__init__.py index 70196cb..92dca58 100644 --- a/python_aternos/__init__.py +++ b/python_aternos/__init__.py @@ -1,3 +1,7 @@ +""" +Unofficial Aternos API module written in Python. +It uses Aternos' private API and html parsing""" + from .atclient import Client from .atserver import AternosServer from .atserver import Edition @@ -23,8 +27,8 @@ from .aterrors import TokenError from .aterrors import ServerError from .aterrors import ServerStartError from .aterrors import FileError -from .aterrors import PermissionError -from .atjsparse import exec, atob +from .aterrors import AternosPermissionError +from .atjsparse import exec_js, atob from .atjsparse import to_ecma5_function __all__ = [ @@ -39,8 +43,8 @@ __all__ = [ 'FileManager', 'AternosFile', 'AternosError', 'CloudflareError', 'CredentialsError', 'TokenError', 'ServerError', 'ServerStartError', 'FileError', - 'PermissionError', - 'exec', 'atob', 'to_ecma5_function', + 'AternosPermissionError', + 'exec_js', 'atob', 'to_ecma5_function', 'Edition', 'Status', 'Lists', 'ServerOpts', 'WorldOpts', 'WorldRules', diff --git a/python_aternos/atclient.py b/python_aternos/atclient.py index d7d0612..b8edf80 100644 --- a/python_aternos/atclient.py +++ b/python_aternos/atclient.py @@ -1,3 +1,6 @@ +"""Entry point. Authorizes on Aternos +and allows to manage your account""" + import os import re import hashlib @@ -11,7 +14,7 @@ from .aterrors import CredentialsError class Client: - """Aternos API Client class whose object contains user's auth data + """Aternos API Client class object of which contains user's auth data :param atconn: :class:`python_aternos.atconnect.AternosConnect` instance with initialized Aternos session @@ -47,7 +50,7 @@ class Client: } loginreq = atconn.request_cloudflare( - f'https://aternos.org/panel/ajax/account/login.php', + 'https://aternos.org/panel/ajax/account/login.php', 'POST', data=credentials, sendtoken=True ) diff --git a/python_aternos/atconf.py b/python_aternos/atconf.py index 3eb0fec..dafa16f 100644 --- a/python_aternos/atconf.py +++ b/python_aternos/atconf.py @@ -1,3 +1,5 @@ +"""Modifying server and world options""" + import enum import re import lxml.html @@ -302,10 +304,25 @@ class AternosConfig: def set_world_props( self, - props: Dict[Union[WorldOpts, WorldRules], Any]) -> None: + props: Dict[Union[WorldOpts, WorldRules], Any], + world: str = 'world') -> None: + + """Sets level.dat options from + the dictionary for the specified world + + :param props: Level.dat options + :type props: Dict[Union[WorldOpts, WorldRules], Any] + :param world: name of the world which + level.dat must be edited, defaults to 'world' + :type world: str + """ for key in props: - self.set_world_prop(key, props[key]) + self.set_world_prop( + option=key, + value=props[key], + world=world + ) # # helpers diff --git a/python_aternos/atconnect.py b/python_aternos/atconnect.py index 61bd4e3..c4f2d55 100644 --- a/python_aternos/atconnect.py +++ b/python_aternos/atconnect.py @@ -1,3 +1,5 @@ +"""Stores API connection session and sends requests""" + import re import random import logging @@ -6,9 +8,13 @@ from cloudscraper import CloudScraper from typing import Optional, Union from . import atjsparse -from .aterrors import TokenError, CloudflareError +from .aterrors import TokenError +from .aterrors import CloudflareError +from .aterrors import AternosPermissionError -REQUA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36 OPR/85.0.4341.47' +REQUA = \ + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ' \ + '(KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36 OPR/85.0.4341.47' class AternosConnect: @@ -21,6 +27,8 @@ class AternosConnect: self.session = CloudScraper() self.atsession = '' + self.sec = '' + self.token = '' def parse_token(self) -> str: @@ -36,7 +44,7 @@ class AternosConnect: """ loginpage = self.request_cloudflare( - f'https://aternos.org/go/', 'GET' + 'https://aternos.org/go/', 'GET' ).content # Using the standard string methods @@ -61,13 +69,13 @@ class AternosConnect: js_code = re.findall(r'\(\(\)(.*?)\)\(\);', text) token_func = js_code[1] if len(js_code) > 1 else js_code[0] - ctx = atjsparse.exec(token_func) + ctx = atjsparse.exec_js(token_func) self.token = ctx.window['AJAX_TOKEN'] - except (IndexError, TypeError): + except (IndexError, TypeError) as err: raise TokenError( 'Unable to parse TOKEN from the page' - ) + ) from err return self.token @@ -104,12 +112,12 @@ class AternosConnect: # a list with randlen+1 empty strings: # generate a string with spaces, # then split it by space - rand_arr = (' ' * (randlen+1)).split(' ') + rand_arr = (' ' * (randlen + 1)).split(' ') rand = random.random() rand_alphanum = self.convert_num(rand, 36) + ('0' * 17) - return (rand_alphanum[:18].join(rand_arr)[:randlen]) + return rand_alphanum[:18].join(rand_arr)[:randlen] def convert_num( self, num: Union[int, float, str], @@ -214,12 +222,12 @@ class AternosConnect: reqcookies['ATERNOS_SESSION'] = self.atsession del self.session.cookies['ATERNOS_SESSION'] - logging.debug(f'Requesting({method})' + url) - logging.debug('headers=' + str(headers)) - logging.debug('params=' + str(params)) - logging.debug('data=' + str(data)) - logging.debug('req-cookies=' + str(reqcookies)) - logging.debug('session-cookies=' + str(self.session.cookies)) + logging.debug(f'Requesting({method}){url}') + logging.debug(f'headers={headers}') + logging.debug(f'params={params}') + logging.debug(f'data={data}') + logging.debug(f'req-cookies={reqcookies}') + logging.debug(f'session-cookies={self.session.cookies}') if method == 'POST': req = self.session.post( @@ -249,6 +257,8 @@ class AternosConnect: f'{method} completed with {req.status_code} status' ) - req.raise_for_status() + if req.status_code == 402: + raise AternosPermissionError + req.raise_for_status() return req diff --git a/python_aternos/aterrors.py b/python_aternos/aterrors.py index 7dc6a93..882e09b 100644 --- a/python_aternos/aterrors.py +++ b/python_aternos/aterrors.py @@ -1,3 +1,5 @@ +"""Exceptions classes""" + from typing import Final @@ -81,7 +83,9 @@ class FileError(AternosError): by Aternos file operation""" -class PermissionError(AternosError): +# PermissionError is a built-in, +# so this exception called AternosPermissionError +class AternosPermissionError(AternosError): """Raised when trying to execute a disallowed command, usually because of shared access rights""" diff --git a/python_aternos/atfile.py b/python_aternos/atfile.py index 059a29b..82b8392 100644 --- a/python_aternos/atfile.py +++ b/python_aternos/atfile.py @@ -1,3 +1,5 @@ +"""File info object used by `python_aternos.atfm`""" + import enum import lxml.html from typing import Union @@ -86,7 +88,7 @@ class AternosFile: """ self.atserv.atserver_request( - f'https://aternos.org/panel/ajax/save.php', + 'https://aternos.org/panel/ajax/save.php', 'POST', data={ 'file': self._full, 'content': value @@ -123,29 +125,74 @@ class AternosFile: self.set_content(value.encode('utf-8')) @property - def path(self): + def path(self) -> str: + + """Path to a directory which + contains the file, without leading slash + + :return: Full path to directory + :rtype: str + """ + return self._path @property def name(self) -> str: + + """Filename including extension + + :return: Filename + :rtype: str + """ + return self._name @property def full(self) -> str: + + """Absolute path to the file, + without leading slash + + :return: Full path + :rtype: str + """ + return self._full @property def is_dir(self) -> bool: + + """Check if the file object is a directory + + :return: `True` if the file + is a directory, otherwise `False` + :rtype: bool + """ + if self._ftype == FileType.directory: return True return False @property def is_file(self) -> bool: + + """Check if the file object is not a directory + + :return: `True` if it is a file, otherwise `False` + :rtype: bool + """ + if self._ftype == FileType.file: return True return False @property def size(self) -> float: + + """File size in bytes + + :return: File size + :rtype: float + """ + return self._size diff --git a/python_aternos/atfm.py b/python_aternos/atfm.py index 114f4cf..de6d217 100644 --- a/python_aternos/atfm.py +++ b/python_aternos/atfm.py @@ -1,5 +1,7 @@ +"""Exploring files in your server directory""" + import lxml.html -from typing import Union, Optional, List +from typing import Union, Optional, Any, List from typing import TYPE_CHECKING from .atfile import AternosFile, FileType @@ -32,10 +34,12 @@ class FileManager: """ path = path.lstrip('/') + filesreq = self.atserv.atserver_request( f'https://aternos.org/files/{path}', 'GET' ) filestree = lxml.html.fromstring(filesreq.content) + fileslist = filestree.xpath( '//div[contains(concat(" ",normalize-space(@class)," ")," file ")]' ) @@ -48,18 +52,9 @@ class FileManager: if ftype_raw == 'file' \ else FileType.directory - fsize_raw = f.xpath('./div[@class="filesize"]') - fsize = 0.0 - if len(fsize_raw) > 0: - - fsize_text = fsize_raw[0].text.strip() - fsize_num = fsize_text[:fsize_text.rfind(' ')] - fsize_msr = fsize_text[fsize_text.rfind(' ')+1:] - - try: - fsize = self.convert_size(float(fsize_num), fsize_msr) - except ValueError: - fsize = -1 + fsize = self.extract_size( + f.xpath('./div[@class="filesize"]') + ) fullpath = f.xpath('@data-path')[0] filepath = fullpath[:fullpath.rfind('/')] @@ -74,7 +69,33 @@ class FileManager: return files - def convert_size(self, num: Union[int, float], measure: str) -> float: + def extract_size(self, fsize_raw: List[Any]) -> float: + + """Parses file size from the LXML tree + + :param fsize_raw: XPath method result + :type fsize_raw: List[Any] + :return: File size in bytes + :rtype: float + """ + + if len(fsize_raw) > 0: + + fsize_text = fsize_raw[0].text.strip() + fsize_num = fsize_text[:fsize_text.rfind(' ')] + fsize_msr = fsize_text[fsize_text.rfind(' ') + 1:] + + try: + return self.convert_size(float(fsize_num), fsize_msr) + except ValueError: + return -1.0 + + return 0.0 + + def convert_size( + self, + num: Union[int, float], + measure: str) -> float: """Converts "human" file size to size in bytes @@ -92,10 +113,7 @@ class FileManager: 'MB': 1000000, 'GB': 1000000000 } - try: - return num * measure_match[measure] - except KeyError: - return -1 + return measure_match.get(measure, -1) * num def get_file(self, path: str) -> Optional[AternosFile]: @@ -128,7 +146,7 @@ class FileManager: :rtype: bytes """ - file = self.atserv.atserver_request( + file = self.atserv.atserver_request( # type: ignore 'https://aternos.org/panel/ajax/files/download.php' 'GET', params={ 'file': path.replace('/', '%2F') @@ -148,11 +166,11 @@ class FileManager: :rtype: bytes """ - world = self.atserv.atserver_request( + resp = self.atserv.atserver_request( # type: ignore 'https://aternos.org/panel/ajax/worlds/download.php' 'GET', params={ 'world': world.replace('/', '%2F') } ) - return world.content + return resp.content diff --git a/python_aternos/atjsparse.py b/python_aternos/atjsparse.py index 00d0cb4..d5a50aa 100644 --- a/python_aternos/atjsparse.py +++ b/python_aternos/atjsparse.py @@ -1,3 +1,5 @@ +"""Parsing and executing JavaScript code""" + import regex import base64 import js2py @@ -27,10 +29,19 @@ def to_ecma5_function(f: str) -> str: def atob(s: str) -> str: + + """Decodes base64 string + + :param s: Encoded data + :type s: str + :return: Decoded string + :rtype: str + """ + return base64.standard_b64decode(str(s)).decode('utf-8') -def exec(f: str) -> Any: +def exec_js(f: str) -> Any: """Executes a JavaScript function diff --git a/python_aternos/atplayers.py b/python_aternos/atplayers.py index e174224..169c922 100644 --- a/python_aternos/atplayers.py +++ b/python_aternos/atplayers.py @@ -1,9 +1,10 @@ +"""Operators, whitelist and banned players lists""" + import enum import lxml.html from typing import List, Union from typing import TYPE_CHECKING -from .atserver import Edition if TYPE_CHECKING: from .atserver import AternosServer @@ -31,13 +32,18 @@ class PlayersList: :type atserv: python_aternos.atserver.AternosServer """ - def __init__(self, lst: Union[str, Lists], atserv: 'AternosServer') -> None: + def __init__( + self, + lst: Union[str, Lists], + atserv: 'AternosServer') -> None: self.atserv = atserv self.lst = Lists(lst) common_whl = (self.lst == Lists.whl) - bedrock = (atserv.edition == Edition.bedrock) + # 1 is atserver.Edition.bedrock + bedrock = (atserv.edition == 1) + if common_whl and bedrock: self.lst = Lists.whl_be diff --git a/python_aternos/atserver.py b/python_aternos/atserver.py index 7175fbe..21da81e 100644 --- a/python_aternos/atserver.py +++ b/python_aternos/atserver.py @@ -1,3 +1,5 @@ +"""Aternos Minecraft server""" + import enum import json from requests import Response @@ -85,7 +87,10 @@ class AternosServer: return AternosWss(self, autoconfirm) - def start(self, headstart: bool = False, accepteula: bool = True) -> None: + def start( + self, + headstart: bool = False, + accepteula: bool = True) -> None: """Starts a server @@ -113,7 +118,8 @@ class AternosServer: if error == 'eula' and accepteula: self.eula() - return self.start(accepteula=False) + self.start(accepteula=False) + return raise ServerStartError(error) @@ -238,11 +244,25 @@ class AternosServer: @property def subdomain(self) -> str: + + """Server subdomain (part of domain before `.aternos.me`) + + :return: Subdomain + :rtype: str + """ + atdomain = self.domain return atdomain[:atdomain.find('.')] @subdomain.setter def subdomain(self, value: str) -> None: + + """Set new subdomain for your server + + :param value: Subdomain + :type value: str + """ + self.atserver_request( 'https://aternos.org/panel/ajax/options/subdomain.php', 'GET', params={'subdomain': value}, @@ -251,10 +271,25 @@ class AternosServer: @property def motd(self) -> str: + + """Server message of the day, + which is shown below its name in the servers list + + :return: MOTD + :rtype: str + """ + return self._info['motd'] @motd.setter def motd(self, value: str) -> None: + + """Set new message of the day + + :param value: MOTD + :type value: str + """ + self.atserver_request( 'https://aternos.org/panel/ajax/options/motd.php', 'POST', data={'motd': value}, @@ -263,49 +298,135 @@ class AternosServer: @property def address(self) -> str: + + """Full server address including domain and port + + :return: Server address + :rtype: str + """ + return self._info['displayAddress'] @property def domain(self) -> str: + + """Server domain (test.aternos.me), + address without port number + + :return: Domain + :rtype: str + """ + return self._info['ip'] @property def port(self) -> int: + + """Server port number + + :return: Port + :rtype: int + """ + return self._info['port'] @property - def edition(self) -> int: + def edition(self) -> Edition: + + """Server software edition: Java or Bedrock + + :return: Software edition + :rtype: Edition + """ + soft_type = self._info['bedrock'] - return int(soft_type) + return Edition(soft_type) @property def software(self) -> str: + + """Server software name (e.g. `Vanilla`) + + :return: Software name + :rtype: str + """ + return self._info['software'] @property def version(self) -> str: + + """Server software version (e.g. `1.16.5`) + + :return: Software version + :rtype: str + """ + return self._info['version'] @property def status(self) -> str: + + """Server status string (offline, loading) + + :return: Status string + :rtype: str + """ + return self._info['class'] @property def status_num(self) -> int: - return int(self._info['status']) + + """Server numeric status. It is highly recommended + to use status string instead of a number. + + :return: Status code + :rtype: Status + """ + + return Status(self._info['status']) @property def players_list(self) -> List[str]: + + """List of connected players nicknames + + :return: Connected players + :rtype: List[str] + """ + return self._info['playerlist'] @property def players_count(self) -> int: + + """How many connected players + + :return: Connected players count + :rtype: int + """ + return int(self._info['players']) @property def slots(self) -> int: + + """Server slots, how many players can connect + + :return: Slots count + :rtype: int + """ + return int(self._info['slots']) @property def ram(self) -> int: + + """Server used RAM in MB + + :return: Used RAM + :rtype: int + """ + return int(self._info['ram']) diff --git a/python_aternos/atwss.py b/python_aternos/atwss.py index 159899e..48eef9f 100644 --- a/python_aternos/atwss.py +++ b/python_aternos/atwss.py @@ -1,16 +1,24 @@ +"""Connects to Aternos API websocket +for real-time information""" + import enum import json import asyncio import logging import websockets -from typing import Union, Any, Dict, Callable, Coroutine, Tuple +from typing import Union, Any +from typing import Dict, Tuple +from typing import Callable, Coroutine from typing import TYPE_CHECKING from .atconnect import REQUA if TYPE_CHECKING: from .atserver import AternosServer -FunctionT = Callable[[Any], Coroutine[Any, Any, None]] +OneArgT = Callable[[Any], Coroutine[Any, Any, None]] +TwoArgT = Callable[[Any, Tuple[Any, ...]], Coroutine[Any, Any, None]] +FunctionT = Union[OneArgT, TwoArgT] +ArgsTuple = Tuple[FunctionT, Tuple[Any, ...]] class Streams(enum.Enum): @@ -22,6 +30,7 @@ class Streams(enum.Enum): console = (2, 'console') ram = (3, 'heap') tps = (4, 'tick') + none = (-1, None) def __init__(self, num: int, stream: str) -> None: self.num = num @@ -40,24 +49,35 @@ class AternosWss: :type autoconfirm: bool, optional """ - def __init__(self, atserv: 'AternosServer', autoconfirm: bool = False) -> None: + def __init__( + self, + atserv: 'AternosServer', + autoconfirm: bool = False) -> None: self.atserv = atserv self.cookies = atserv.atconn.session.cookies self.session = self.cookies['ATERNOS_SESSION'] self.servid = atserv.servid - recvtype = Dict[Streams, Tuple[FunctionT, Tuple[Any]]] + + recvtype = Dict[Streams, ArgsTuple] self.recv: recvtype = {} self.autoconfirm = autoconfirm self.confirmed = False + self.socket: Any + self.keep: asyncio.Task + self.msgs: asyncio.Task + async def confirm(self) -> None: """Simple way to call AternosServer.confirm from this class""" self.atserv.confirm() - def wssreceiver(self, stream: Streams, *args: Any) -> Callable[[FunctionT], Any]: + def wssreceiver( + self, + stream: Streams, + *args: Any) -> Callable[[FunctionT], Any]: """Decorator that marks your function as a stream receiver. When websocket receives message from the specified stream, @@ -88,7 +108,7 @@ class AternosWss: f'ATERNOS_SERVER={self.servid}' ) ] - self.socket = await websockets.connect( + self.socket = await websockets.connect( # type: ignore 'wss://aternos.org/hermes/', origin='https://aternos.org', extra_headers=headers @@ -192,11 +212,11 @@ class AternosWss: """Receives messages from websocket servers and calls user's streams listeners""" - try: - while True: + while True: + try: data = await self.socket.recv() obj = json.loads(data) - msgtype = -1 + msgtype = Streams.none if obj['type'] == 'line': msgtype = Streams.console @@ -217,18 +237,19 @@ class AternosWss: if msgtype in self.recv: - # function info tuple: - # (function, arguments) - func = self.recv[msgtype] + # function info tuple + func: ArgsTuple = self.recv[msgtype] # if arguments is not empty if func[1]: # call the function with args - coro = func[0](msg, func[1]) + coro = func[0](msg, func[1]) # type: ignore else: - coro = func[0](msg) + # mypy error: Too few arguments + # looks like bug, so it is ignored + coro = func[0](msg) # type: ignore # run - await asyncio.create_task(coro) + asyncio.create_task(coro) - except asyncio.CancelledError: - pass + except asyncio.CancelledError: + break diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..136bc2f --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[mypy] +ignore_missing_imports = True + +[pycodestyle] +ignore = E501 diff --git a/test.sh b/test.sh index e26d7aa..c8b7b47 100755 --- a/test.sh +++ b/test.sh @@ -1,9 +1,40 @@ +failed=() + title () { + + RESET='\033[0m' + COLOR='\033[1;36m' + echo - echo "***" - echo "$1" - echo "***" - echo + echo -e "$COLOR[#] $1$RESET" +} + +error_msg () { + + RESET='\033[0m' + OK='\033[1;32m' + ERR='\033[1;31m' + + if (( $1 )); then + failed+=$2 + echo -e "$ERR[X] Found errors$RESET" + else + echo -e "$OK[V] Passed successfully$RESET" + fi +} + +display_failed() { + + RESET='\033[0m' + FAILED='\033[1;33m' + SUCCESS='\033[1;32m' + + local IFS=', ' + if [[ ${#failed[@]} > 0 ]]; then + echo -e "$FAILED[!] View output for: ${failed[*]}$RESET" + else + echo -e "$SUCCESS[V] All tests are passed successfully$RESET" + fi } title 'Checking needed modules...' @@ -11,9 +42,19 @@ pip install pycodestyle mypy pylint title 'Running unit tests...' python -m unittest discover -v ./tests +error_msg $? 'unittest' title 'Running pep8 checker...' python -m pycodestyle . +error_msg $? 'pep8' title 'Running mypy checker...' python -m mypy . +error_msg $? 'mypy' + +title 'Running pylint checker...' +python -m pylint ./python_aternos +error_msg $? 'pylint' + +display_failed +echo diff --git a/tests/test_js2py.py b/tests/test_js2py.py index 1c8d25b..8aee903 100644 --- a/tests/test_js2py.py +++ b/tests/test_js2py.py @@ -5,7 +5,7 @@ import unittest from python_aternos import atjsparse CONV_TOKEN_ARROW = '''(() => {window["AJAX_TOKEN"]=("2r" + "KO" + "A1" + "IFdBcHhEM" + "61" + "6cb");})();''' -CONV_TOKEN_FUNC = '(function(){window["AJAX_TOKEN"]=("2r" + "KO" + "A1" + "IFdBcHhEM" + "61" + "6cb");})()' +CONV_TOKEN_FUNC = '''(function(){window["AJAX_TOKEN"]=("2r" + "KO" + "A1" + "IFdBcHhEM" + "61" + "6cb");})()''' class TestJs2Py(unittest.TestCase): @@ -53,10 +53,10 @@ class TestJs2Py(unittest.TestCase): part2 = '''window.t2 = Boolean(!window[["p","Ma"].reverse().join('')]);''' part3 = '''window.t3 = Boolean(!window[["ut","meo","i","etT","s"].reverse().join('')]);''' - ctx0 = atjsparse.exec(code) - ctx1 = atjsparse.exec(part1) - ctx2 = atjsparse.exec(part2) - ctx3 = atjsparse.exec(part3) + ctx0 = atjsparse.exec_js(code) + ctx1 = atjsparse.exec_js(part1) + ctx2 = atjsparse.exec_js(part2) + ctx3 = atjsparse.exec_js(part3) self.assertEqual(ctx0.window['t0'], False) self.assertEqual(ctx1.window['t1'], True) @@ -66,7 +66,7 @@ class TestJs2Py(unittest.TestCase): def test_exec(self) -> None: for i, f in enumerate(self.tests): - ctx = atjsparse.exec(f) + ctx = atjsparse.exec_js(f) res = ctx.window['AJAX_TOKEN'] self.assertEqual(res, self.results[i])