From c98e82197e41e2e354a59ccfb3dddbfd3a0ce4a7 Mon Sep 17 00:00:00 2001 From: David Shepherd Date: Sun, 29 Aug 2021 09:06:55 +0000 Subject: [PATCH 01/11] Remove end-of-life'd python versions from circleCI python 3.4 is causing the tests to fail, and it's EOL so I guess it's ok to remove it? python 2 - I'd like to try out adding type checking support and I think that's a lot harder with python 2, so if it's ok I'd like to remove support. --- .circleci/config.yml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d7d80ca..c164b12 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,10 +8,7 @@ workflows: - test-3.7 - test-3.6 - test-3.5 - - test-3.4 - - test-2.7 - test-pypy3 - - test-pypy2 jobs: test-3.9: &test-template docker: @@ -52,19 +49,7 @@ jobs: <<: *test-template docker: - image: python:3.5-alpine - test-3.4: - <<: *test-template - docker: - - image: python:3.4-alpine - test-2.7: - <<: *test-template - docker: - - image: python:2.7-alpine test-pypy3: <<: *test-template docker: - image: pypy:3-slim - test-pypy2: - <<: *test-template - docker: - - image: pypy:2-slim From bcee943933404cfd6b464ead7824d8c930f0175b Mon Sep 17 00:00:00 2001 From: David Shepherd Date: Sun, 29 Aug 2021 09:31:32 +0000 Subject: [PATCH 02/11] Fix invalid emacs coding value --- smpplib/gsm.py | 2 +- smpplib/tests/test_gsm.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/smpplib/gsm.py b/smpplib/gsm.py index c3336a4..d33f168 100644 --- a/smpplib/gsm.py +++ b/smpplib/gsm.py @@ -1,4 +1,4 @@ -# -*- coding: utf8 -*- +# -*- coding: utf-8 -*- import random import six diff --git a/smpplib/tests/test_gsm.py b/smpplib/tests/test_gsm.py index b6641fd..44b8c66 100644 --- a/smpplib/tests/test_gsm.py +++ b/smpplib/tests/test_gsm.py @@ -1,4 +1,4 @@ -# -*- coding: utf8 -*- +# -*- coding: utf-8 -*- import mock from pytest import mark, raises From 12e6baefa16def9b5247266b567dcc47c72898ca Mon Sep 17 00:00:00 2001 From: David Shepherd Date: Sun, 29 Aug 2021 09:33:50 +0000 Subject: [PATCH 03/11] Remove six dependency --- setup.py | 1 - smpplib/command.py | 8 +++----- smpplib/command_codes.py | 4 +--- smpplib/gsm.py | 8 +++----- 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/setup.py b/setup.py index 06ba978..8f9b5c4 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,6 @@ url='https://github.com/podshumok/python-smpplib', description='SMPP library for python', packages=find_packages(), - install_requires=['six'], extras_require=dict( tests=('pytest', 'mock'), ), diff --git a/smpplib/command.py b/smpplib/command.py index 5bc6379..8dab1fa 100644 --- a/smpplib/command.py +++ b/smpplib/command.py @@ -22,8 +22,6 @@ import logging import struct -import six - from smpplib import consts, exceptions, pdu from smpplib.ptypes import flag, ostr @@ -64,7 +62,7 @@ def get_optional_name(code): """Return optional_params name by given code. If code is unknown, raise UnkownCommandError exception""" - for key, value in six.iteritems(consts.OPTIONAL_PARAMS): + for key, value in consts.OPTIONAL_PARAMS.items(): if value == code: return key @@ -106,7 +104,7 @@ def __init__(self, command, need_sequence=True, allow_unknown_opt_params=False, def _set_vars(self, **kwargs): """set attributes accordingly to kwargs""" - for key, value in six.iteritems(kwargs): + for key, value in kwargs.items(): if not hasattr(self, key) or getattr(self, key) is None: setattr(self, key, value) @@ -179,7 +177,7 @@ def _generate_string(self, field): value = chr(0) setattr(self, field, field_value) - return six.b(value) + return value def _generate_ostring(self, field): """Generate octet string value (no null terminator)""" diff --git a/smpplib/command_codes.py b/smpplib/command_codes.py index eab6868..a0fc53e 100644 --- a/smpplib/command_codes.py +++ b/smpplib/command_codes.py @@ -1,5 +1,3 @@ -import six - from smpplib import exceptions # @@ -42,7 +40,7 @@ def get_command_name(code): If code is unknown, raise UnknownCommandError exception. """ - for key, value in six.iteritems(commands): + for key, value in commands.items(): if value == code: return key diff --git a/smpplib/gsm.py b/smpplib/gsm.py index d33f168..137ac82 100644 --- a/smpplib/gsm.py +++ b/smpplib/gsm.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- import random -import six - from smpplib import consts, exceptions @@ -55,7 +53,7 @@ def gsm_encode(plaintext): """Performs default GSM 7-bit encoding. Beware it's vendor-specific and not recommended for use.""" try: return b''.join( - six.int2byte(index) if index < 0x80 else b'\x1B' + six.int2byte(index - 0x80) + bytes((index, )) if index < 0x80 else b'\x1B' + bytes((index - 0x80, )) for index in map(GSM_CHARACTER_TABLE.index, plaintext) ) except ValueError: @@ -78,9 +76,9 @@ def make_parts_encoded(encoded_text, part_size): raise exceptions.MessageTooLong() uid = random.randint(0, 255) - header = b''.join((b'\x05\x00\x03', six.int2byte(uid), six.int2byte(len(chunks)))) + header = b''.join((b'\x05\x00\x03', bytes((uid, )), bytes((len(chunks), )))) - return [b''.join((header, six.int2byte(i), chunk)) for i, chunk in enumerate(chunks, start=1)] + return [b''.join((header, bytes((i, )), chunk)) for i, chunk in enumerate(chunks, start=1)] def split_sequence(sequence, part_size): From 3f4ca133b3334a93306921b7574e53f27eb599e3 Mon Sep 17 00:00:00 2001 From: David Shepherd Date: Sun, 29 Aug 2021 10:04:28 +0000 Subject: [PATCH 04/11] Fix a test depending on the configured warning filter --- smpplib/tests/test_client.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/smpplib/tests/test_client.py b/smpplib/tests/test_client.py index a10666f..a5e3ba6 100644 --- a/smpplib/tests/test_client.py +++ b/smpplib/tests/test_client.py @@ -10,6 +10,9 @@ def test_client_construction_allow_unknown_opt_params_warning(): with warnings.catch_warnings(record=True) as w: + # TODO: we should probably switch to assertWarns if we drop python 2 + # support entirely + warnings.simplefilter("always") client = Client("localhost", 5679) assert len(w) == 1 From c71141cc386788bf0697f0ebd7b77350c2b741f4 Mon Sep 17 00:00:00 2001 From: David Shepherd Date: Sun, 29 Aug 2021 10:21:54 +0000 Subject: [PATCH 05/11] MVP type checking with minimal code changes The only change to runtime code introduced here should be skipping the initialisation of Client.port to None. I'm pretty confident that this is fine since __init__ always initialises it to an int. --- .circleci/config.yml | 7 +++++- Makefile | 14 +++++++++++ mypy.ini | 15 ++++++++++++ setup.py | 2 +- smpplib/client.py | 18 ++++++++------ smpplib/command.py | 44 +++++++++++++++++++---------------- smpplib/pdu.py | 15 +++++++++++- smpplib/tests/test_client.py | 4 ++-- smpplib/tests/test_command.py | 8 +++---- 9 files changed, 91 insertions(+), 36 deletions(-) create mode 100644 Makefile create mode 100644 mypy.ini diff --git a/.circleci/config.yml b/.circleci/config.yml index c164b12..6f502d2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -28,11 +28,16 @@ jobs: command: | . venv/bin/activate pip install -e .[tests] + - run: + name: Typecheck + command: | + . venv/bin/activate + venv/bin/mypy -p smpplib - run: name: Run tests command: | . venv/bin/activate - pytest -v + pytest -v smpplib test-3.8: <<: *test-template docker: diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a058f13 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ + +venv: + python -m venv venv + +deps: + pip install -e .[tests] + +typecheck: + . venv/bin/activate + venv/bin/mypy -p smpplib + +test: + . venv/bin/activate + pytest -v smpplib diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..e3dbc07 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,15 @@ +[mypy] + +strict_optional = True +no_implicit_optional = True +warn_unused_configs = True +strict_equality = True +warn_unused_ignores = True + +check_untyped_defs = True + +# disallow_untyped_calls = True +# disallow_untyped_defs = True +# disallow_incomplete_defs = True +# disallow_untyped_decorators = True +# disallow_any_generics = True diff --git a/setup.py b/setup.py index 8f9b5c4..20e2e7c 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ description='SMPP library for python', packages=find_packages(), extras_require=dict( - tests=('pytest', 'mock'), + tests=('pytest', 'mock', 'mypy', 'types-mock'), ), zip_safe=True, classifiers=( diff --git a/smpplib/client.py b/smpplib/client.py index b89af50..1897478 100644 --- a/smpplib/client.py +++ b/smpplib/client.py @@ -24,6 +24,7 @@ import socket import struct import warnings +from typing import Optional from smpplib import consts, exceptions, smpp @@ -54,11 +55,11 @@ class Client(object): state = consts.SMPP_CLIENT_STATE_CLOSED host = None - port = None + port: int vendor = None - _socket = None + _socket: Optional[socket.socket] = None _ssl_context = None - sequence_generator = None + sequence_generator: SimpleSequenceGenerator def __init__( self, @@ -216,6 +217,7 @@ def send_pdu(self, p): while sent < len(generated): try: + assert self._socket is not None sent_last = self._socket.send(generated[sent:]) except socket.error as e: self.logger.warning(e) @@ -232,6 +234,7 @@ def read_pdu(self): self.logger.debug('Waiting for PDU...') try: + assert self._socket is not None raw_len = self._socket.recv(4) except socket.timeout: raise @@ -249,6 +252,7 @@ def read_pdu(self): raw_pdu = raw_len while len(raw_pdu) < length: + assert self._socket is not None raw_pdu += self._socket.recv(length - len(raw_pdu)) self.logger.debug('<<%s (%d bytes)', binascii.b2a_hex(raw_pdu), len(raw_pdu)) @@ -294,19 +298,19 @@ def _alert_notification(self, pdu): def set_message_received_handler(self, func): """Set new function to handle message receive event""" - self.message_received_handler = func + self.message_received_handler = func # type: ignore def set_message_sent_handler(self, func): """Set new function to handle message sent event""" - self.message_sent_handler = func + self.message_sent_handler = func # type: ignore def set_query_resp_handler(self, func): """Set new function to handle query resp event""" - self.query_resp_handler = func + self.query_resp_handler = func # type: ignore def set_error_pdu_handler(self, func): """Set new function to handle PDUs with an error status""" - self.error_pdu_handler = func + self.error_pdu_handler = func # type: ignore def message_received_handler(self, pdu, **kwargs): """Custom handler to process received message. May be overridden""" diff --git a/smpplib/command.py b/smpplib/command.py index 8dab1fa..8a763b5 100644 --- a/smpplib/command.py +++ b/smpplib/command.py @@ -21,6 +21,7 @@ import logging import struct +from typing import Dict, Tuple, Any, Optional, Callable from smpplib import consts, exceptions, pdu from smpplib.ptypes import flag, ostr @@ -86,7 +87,8 @@ def unpack_short(data, pos): class Command(pdu.PDU): """SMPP PDU Command class""" - params = {} + params: Dict[str, "Param"] = {} + params_order: Tuple[str, ...] def __init__(self, command, need_sequence=True, allow_unknown_opt_params=False, **kwargs): super(Command, self).__init__(**kwargs) @@ -111,8 +113,8 @@ def _set_vars(self, **kwargs): def generate_params(self): """Generate binary data from the object""" - if hasattr(self, 'prep') and callable(self.prep): - self.prep() + if hasattr(self, 'prep') and callable(self.prep): # type: ignore + self.prep() # type: ignore body = consts.EMPTY_STRING @@ -165,11 +167,11 @@ def _generate_string(self, field): field_value = getattr(self, field) if hasattr(self.params[field], 'size'): - size = self.params[field].size + size = self.params[field].size # type: ignore value = field_value.ljust(size, chr(0)) elif hasattr(self.params[field], 'max'): - if len(field_value or '') >= self.params[field].max: - field_value = field_value[0:self.params[field].max - 1] + if len(field_value or '') >= self.params[field].max: # type: ignore + field_value = field_value[0:self.params[field].max - 1] # type: ignore if field_value: value = field_value + chr(0) @@ -193,7 +195,7 @@ def _generate_int_tlv(self, field): fmt = self._int_pack_format(field) data = getattr(self, field) field_code = get_optional_code(field) - field_length = self.params[field].size + field_length = self.params[field].size # type: ignore value = None if data is not None: value = struct.pack(">HH" + fmt, field_code, field_length, data) @@ -206,12 +208,12 @@ def _generate_string_tlv(self, field): field_code = get_optional_code(field) if hasattr(self.params[field], 'size'): - size = self.params[field].size + size = self.params[field].size # type: ignore fvalue = field_value.ljust(size, chr(0)) value = struct.pack(">HH", field_code, size) + fvalue elif hasattr(self.params[field], 'max'): - if len(field_value or '') > self.params[field].max: - field_value = field_value[0:self.params[field].max - 1] + if len(field_value or '') > self.params[field].max: # type: ignore + field_value = field_value[0:self.params[field].max - 1] # type: ignore if field_value: fvalue = field_value + chr(0) @@ -237,7 +239,7 @@ def _generate_ostring_tlv(self, field): def _int_pack_format(self, field): """Return format type""" - return consts.INT_PACK_FORMATS[self.params[field].size] + return consts.INT_PACK_FORMATS[self.params[field].size] # type: ignore def _parse_int(self, field, data, pos): """ @@ -245,7 +247,7 @@ def _parse_int(self, field, data, pos): Return (data, pos) tuple. """ - size = self.params[field].size + size = self.params[field].size # type: ignore fmt = self._int_pack_format(field) field_value, = struct.unpack(">" + fmt, data[pos:pos + size]) setattr(self, field, field_value) @@ -277,7 +279,7 @@ def _parse_ostring(self, field, data, pos, length=None): """ if length is None: - length_field = self.params[field].len_field + length_field = self.params[field].len_field # type: ignore length = int(getattr(self, length_field)) setattr(self, field, data[pos:pos + length]) @@ -351,7 +353,7 @@ def field_exists(self, field): def field_is_optional(self, field): """Return True if field is optional, False otherwise""" - if hasattr(self, 'mandatory_fields') and field in self.mandatory_fields: + if hasattr(self, 'mandatory_fields') and field in self.mandatory_fields: # type: ignore return False elif field in consts.OPTIONAL_PARAMS: return True @@ -565,7 +567,8 @@ def __init__(self, command, **kwargs): class GenericNAck(Command): """General Negative Acknowledgement class""" - _defs = [] + # TODO: seems unused + _defs: Any = [] def __init__(self, command, **kwargs): super(GenericNAck, self).__init__(command, need_sequence=False, **kwargs) @@ -695,7 +698,7 @@ class SubmitSM(Command): 'ussd_service_op': Param(type=int, size=1), } - params_order = ( + params_order: Tuple[str, ...] = ( 'service_type', 'source_addr_ton', 'source_addr_npi', 'source_addr', 'dest_addr_ton', 'dest_addr_npi', 'destination_addr', 'esm_class', 'protocol_id', 'priority_flag', @@ -870,6 +873,7 @@ def prep(self): class QuerySMResp(Command): """Response command for query_sm""" + # TODO: this seems like a bug, misssing a , to make it a tuple mandatory_fields = ('message_state') params = { @@ -892,7 +896,7 @@ def __init__(self, command, **kwargs): class Unbind(Command): """Unbind command""" - params = {} + params: Dict[str, Param] = {} params_order = () def __init__(self, command, **kwargs): @@ -902,7 +906,7 @@ def __init__(self, command, **kwargs): class UnbindResp(Command): """Unbind response command""" - params = {} + params: Dict[str, Param] = {} params_order = () def __init__(self, command, **kwargs): @@ -911,7 +915,7 @@ def __init__(self, command, **kwargs): class EnquireLink(Command): """Enquire link command""" - params = {} + params: Dict[str, Param] = {} params_order = () def __init__(self, command, **kwargs): @@ -920,7 +924,7 @@ def __init__(self, command, **kwargs): class EnquireLinkResp(Command): """Enquire link command response""" - params = {} + params: Dict[str, Param] = {} params_order = () def __init__(self, command, **kwargs): diff --git a/smpplib/pdu.py b/smpplib/pdu.py index 4557210..1f512ba 100644 --- a/smpplib/pdu.py +++ b/smpplib/pdu.py @@ -19,10 +19,14 @@ """PDU module""" import struct +from typing import TYPE_CHECKING from smpplib import command_codes, consts from smpplib.consts import SMPP_ESME_ROK +if TYPE_CHECKING: + from smpplib.client import Client + def extract_command(pdu): """Extract command from a PDU""" @@ -44,11 +48,14 @@ class PDU(object): command = None status = None _sequence = None + _client: "Client" def __init__(self, client=default_client(), **kwargs): """Singleton dummy client will be used if omitted""" if client is None: - self._client = default_client() + # TODO: this is probably a bug, default client doesn't have the + # right methods. + self._client = default_client() # type:ignore else: self._client = client @@ -100,6 +107,12 @@ def get_status_desc(self, status=None): return desc + def parse_params(self, data): + raise NotImplementedError() + + def generate_params(self): + raise NotImplementedError() + def parse(self, data): """Parse raw PDU""" diff --git a/smpplib/tests/test_client.py b/smpplib/tests/test_client.py index a5e3ba6..1051a3c 100644 --- a/smpplib/tests/test_client.py +++ b/smpplib/tests/test_client.py @@ -24,7 +24,7 @@ def test_client_error_pdu_default(): client = Client("localhost", 5679) error_pdu = make_pdu("submit_sm_resp") error_pdu.status = consts.SMPP_ESME_RINVMSGLEN - client.read_pdu = Mock(return_value=error_pdu) + client.read_pdu = Mock(return_value=error_pdu) # type: ignore with pytest.raises(exceptions.PDUError) as exec_info: client.read_once() @@ -39,7 +39,7 @@ def test_client_error_pdu_custom_handler(): client = Client("localhost", 5679) error_pdu = make_pdu("submit_sm_resp") error_pdu.status = consts.SMPP_ESME_RINVMSGLEN - client.read_pdu = Mock(return_value=error_pdu) + client.read_pdu = Mock(return_value=error_pdu) # type: ignore mock_error_pdu_handler = Mock() client.set_error_pdu_handler(mock_error_pdu_handler) diff --git a/smpplib/tests/test_command.py b/smpplib/tests/test_command.py index a0e2433..84fec3b 100644 --- a/smpplib/tests/test_command.py +++ b/smpplib/tests/test_command.py @@ -19,10 +19,10 @@ def test_parse_deliver_sm(): assert pdu.source_addr_npi == consts.SMPP_NPI_ISDN assert pdu.source_addr == b'31600000000' assert pdu.destination_addr == b'XXX YYYY' - assert pdu.receipted_message_id == b'1d305b4c' - assert pdu.source_network_type == consts.SMPP_NETWORK_TYPE_GSM - assert pdu.message_state == consts.SMPP_MESSAGE_STATE_DELIVERED - assert pdu.user_message_reference is None + assert pdu.receipted_message_id == b'1d305b4c' # type: ignore + assert pdu.source_network_type == consts.SMPP_NETWORK_TYPE_GSM # type: ignore + assert pdu.message_state == consts.SMPP_MESSAGE_STATE_DELIVERED # type: ignore + assert pdu.user_message_reference is None # type: ignore def test_unrecognised_optional_parameters(): From 9a4464f1c0acf0ff8fe0e8335025d5bee1f142cc Mon Sep 17 00:00:00 2001 From: David Shepherd Date: Sun, 29 Aug 2021 10:33:52 +0000 Subject: [PATCH 06/11] Fix test mock client having a missing required method --- smpplib/pdu.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/smpplib/pdu.py b/smpplib/pdu.py index 1f512ba..33e37f2 100644 --- a/smpplib/pdu.py +++ b/smpplib/pdu.py @@ -19,7 +19,7 @@ """PDU module""" import struct -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Union from smpplib import command_codes, consts from smpplib.consts import SMPP_ESME_ROK @@ -40,6 +40,9 @@ class default_client(object): """Dummy client""" sequence = 0 + def next_sequence(self): + raise NotImplementedError() + class PDU(object): """PDU class""" @@ -48,14 +51,12 @@ class PDU(object): command = None status = None _sequence = None - _client: "Client" + _client: Union["Client", default_client] def __init__(self, client=default_client(), **kwargs): """Singleton dummy client will be used if omitted""" if client is None: - # TODO: this is probably a bug, default client doesn't have the - # right methods. - self._client = default_client() # type:ignore + self._client = default_client() else: self._client = client From 17008489d3e6b579dfd5c4259818ba73edf187d0 Mon Sep 17 00:00:00 2001 From: David Shepherd Date: Sun, 29 Aug 2021 10:34:14 +0000 Subject: [PATCH 07/11] Explicitly fail when a Param has no max or size This would crash anyway because the `value` variable doesn't exist, but with the explict assertion it typechecks (and is easier to debug). --- smpplib/command.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/smpplib/command.py b/smpplib/command.py index 8a763b5..337c838 100644 --- a/smpplib/command.py +++ b/smpplib/command.py @@ -177,6 +177,8 @@ def _generate_string(self, field): value = field_value + chr(0) else: value = chr(0) + else: + assert False, "Param must have either size or max." setattr(self, field, field_value) return value @@ -221,6 +223,9 @@ def _generate_string_tlv(self, field): value = struct.pack(">HH", field_code, field_length) + fvalue.encode() else: value = None # chr(0) + else: + assert False, "Param must have either size or max." + return value def _generate_ostring_tlv(self, field): From 3a1b17b445d6ed44f1f74204afefa198ffd8ded1 Mon Sep 17 00:00:00 2001 From: David Shepherd Date: Sun, 29 Aug 2021 10:51:33 +0000 Subject: [PATCH 08/11] Add easy types --- mypy.ini | 16 +++++-- smpplib/client.py | 100 ++++++++++++++++++++------------------- smpplib/command.py | 95 +++++++++++++++++++------------------ smpplib/command_codes.py | 4 +- smpplib/gsm.py | 9 ++-- smpplib/pdu.py | 38 ++++++++------- smpplib/smpp.py | 5 +- 7 files changed, 141 insertions(+), 126 deletions(-) diff --git a/mypy.ini b/mypy.ini index e3dbc07..274bbc2 100644 --- a/mypy.ini +++ b/mypy.ini @@ -8,8 +8,14 @@ warn_unused_ignores = True check_untyped_defs = True -# disallow_untyped_calls = True -# disallow_untyped_defs = True -# disallow_incomplete_defs = True -# disallow_untyped_decorators = True -# disallow_any_generics = True +disallow_untyped_calls = True +disallow_untyped_defs = True +disallow_incomplete_defs = True +disallow_untyped_decorators = True +disallow_any_generics = True + +[mypy-smpplib/tests/*] + +disallow_untyped_calls = False +disallow_untyped_defs = False +disallow_incomplete_defs = False diff --git a/smpplib/client.py b/smpplib/client.py index 1897478..9a4ff1d 100644 --- a/smpplib/client.py +++ b/smpplib/client.py @@ -24,9 +24,11 @@ import socket import struct import warnings -from typing import Optional +from typing import Optional, Callable +from ssl import SSLContext -from smpplib import consts, exceptions, smpp +from smpplib import consts, exceptions, smpp, pdu +from typing import Any, Dict, NoReturn, List class SimpleSequenceGenerator(object): @@ -34,14 +36,14 @@ class SimpleSequenceGenerator(object): MIN_SEQUENCE = 0x00000001 MAX_SEQUENCE = 0x7FFFFFFF - def __init__(self): + def __init__(self) -> None: self._sequence = self.MIN_SEQUENCE @property - def sequence(self): + def sequence(self) -> int: return self._sequence - def next_sequence(self): + def next_sequence(self) -> int: if self._sequence == self.MAX_SEQUENCE: self._sequence = self.MIN_SEQUENCE else: @@ -54,23 +56,23 @@ class Client(object): state = consts.SMPP_CLIENT_STATE_CLOSED - host = None + host: str port: int vendor = None _socket: Optional[socket.socket] = None _ssl_context = None - sequence_generator: SimpleSequenceGenerator + sequence_generator: Any def __init__( self, - host, - port, - timeout=5, - sequence_generator=None, - logger_name=None, - ssl_context=None, - allow_unknown_opt_params=None, - ): + host: str, + port: int, + timeout: float =5, + sequence_generator: Optional[Any]=None, + logger_name: Optional[str]=None, + ssl_context: Optional[SSLContext]=None, + allow_unknown_opt_params: Optional[bool]=None, + ) -> None: self.host = host self.port = int(port) self._ssl_context = ssl_context @@ -95,10 +97,10 @@ def __init__( self._socket = self._create_socket() - def __enter__(self): + def __enter__(self) -> "Client": return self - def __exit__(self, exc_type, exc_value, traceback): + def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: if self._socket is not None: try: self.unbind() @@ -109,18 +111,18 @@ def __exit__(self, exc_type, exc_value, traceback): self.logger.warning('%s. Ignored', e) self.disconnect() - def __del__(self): + def __del__(self) -> None: if self._socket is not None: self.logger.warning('%s was not closed', self) @property - def sequence(self): + def sequence(self) -> int: return self.sequence_generator.sequence - def next_sequence(self): + def next_sequence(self) -> int: return self.sequence_generator.next_sequence() - def _create_socket(self): + def _create_socket(self) -> socket.socket: raw_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) raw_socket.settimeout(self.timeout) @@ -129,7 +131,7 @@ def _create_socket(self): return self._ssl_context.wrap_socket(raw_socket) - def connect(self): + def connect(self) -> None: """Connect to SMSC""" self.logger.info('Connecting to %s:%s...', self.host, self.port) @@ -142,7 +144,7 @@ def connect(self): except socket.error: raise exceptions.ConnectionError("Connection refused") - def disconnect(self): + def disconnect(self) -> None: """Disconnect from the SMSC""" self.logger.info('Disconnecting...') @@ -153,7 +155,7 @@ def disconnect(self): self._socket = None self.state = consts.SMPP_CLIENT_STATE_CLOSED - def _bind(self, command_name, **kwargs): + def _bind(self, command_name: str, **kwargs: Any) -> Any: """Send bind_transmitter command to the SMSC""" if command_name in ('bind_receiver', 'bind_transceiver'): @@ -175,19 +177,19 @@ def _bind(self, command_name, **kwargs): ) return resp - def bind_transmitter(self, **kwargs): + def bind_transmitter(self, **kwargs: Any) -> Any: """Bind as a transmitter""" return self._bind('bind_transmitter', **kwargs) - def bind_receiver(self, **kwargs): + def bind_receiver(self, **kwargs: Any) -> Any: """Bind as a receiver""" return self._bind('bind_receiver', **kwargs) - def bind_transceiver(self, **kwargs): + def bind_transceiver(self, **kwargs: Any) -> Any: """Bind as a transmitter and receiver at once""" return self._bind('bind_transceiver', **kwargs) - def unbind(self): + def unbind(self) -> Any: """Unbind from the SMSC""" p = smpp.make_pdu('unbind', client=self) @@ -198,7 +200,7 @@ def unbind(self): except socket.timeout: raise exceptions.ConnectionError() - def send_pdu(self, p): + def send_pdu(self, p: pdu.PDU) -> bool: """Send PDU to the SMSC""" if self.state not in consts.COMMAND_STATES[p.command]: @@ -228,7 +230,7 @@ def send_pdu(self, p): return True - def read_pdu(self): + def read_pdu(self) -> Any: """Read PDU from the SMSC""" self.logger.debug('Waiting for PDU...') @@ -273,11 +275,11 @@ def read_pdu(self): return pdu - def accept(self, obj): + def accept(self, obj: Any) -> NoReturn: """Accept an object""" raise NotImplementedError('not implemented') - def _message_received(self, pdu): + def _message_received(self, pdu: pdu.PDU) -> None: """Handler for received message event""" status = self.message_received_handler(pdu=pdu) if status is None: @@ -286,56 +288,58 @@ def _message_received(self, pdu): dsmr.sequence = pdu.sequence self.send_pdu(dsmr) - def _enquire_link_received(self, pdu): + def _enquire_link_received(self, pdu: pdu.PDU) -> None: """Response to enquire_link""" ler = smpp.make_pdu('enquire_link_resp', client=self) ler.sequence = pdu.sequence self.send_pdu(ler) - def _alert_notification(self, pdu): + def _alert_notification(self, pdu: pdu.PDU) -> None: """Handler for alert notification event""" self.message_received_handler(pdu=pdu) - def set_message_received_handler(self, func): + def set_message_received_handler(self, func: Callable[..., Optional[int]]) -> None: """Set new function to handle message receive event""" self.message_received_handler = func # type: ignore - def set_message_sent_handler(self, func): + def set_message_sent_handler(self, func: Callable[..., None]) -> None: """Set new function to handle message sent event""" self.message_sent_handler = func # type: ignore - def set_query_resp_handler(self, func): + def set_query_resp_handler(self, func: Callable[..., None]) -> None: """Set new function to handle query resp event""" self.query_resp_handler = func # type: ignore - def set_error_pdu_handler(self, func): + def set_error_pdu_handler(self, func: Callable[..., None]) -> None: """Set new function to handle PDUs with an error status""" self.error_pdu_handler = func # type: ignore - def message_received_handler(self, pdu, **kwargs): + def message_received_handler(self, pdu: pdu.PDU, **kwargs: Any) -> Optional[int]: """Custom handler to process received message. May be overridden""" self.logger.warning('Message received handler (Override me)') + return None - def message_sent_handler(self, pdu, **kwargs): + def message_sent_handler(self, pdu: pdu.PDU, **kwargs: Any) -> None: """ Called when SMPP server accept message (SUBMIT_SM_RESP). May be overridden """ self.logger.warning('Message sent handler (Override me)') - def query_resp_handler(self, pdu, **kwargs): + def query_resp_handler(self, pdu: pdu.PDU, **kwargs: Any) -> None: """Custom handler to process response to queries. May be overridden""" self.logger.warning('Query resp handler (Override me)') - def error_pdu_handler(self, pdu): + def error_pdu_handler(self, pdu: pdu.PDU) -> NoReturn: raise exceptions.PDUError('({}) {}: {}'.format( pdu.status, pdu.command, + 'Unknown status' if pdu.status is None else consts.DESCRIPTIONS.get(pdu.status, 'Unknown status')), - int(pdu.status), + pdu.status, ) - def read_once(self, ignore_error_codes=None, auto_send_enquire_link=True): + def read_once(self, ignore_error_codes: Optional[List[int]]=None, auto_send_enquire_link: bool=True) -> None: """Read a PDU and act""" if ignore_error_codes is not None: @@ -382,7 +386,7 @@ def read_once(self, ignore_error_codes=None, auto_send_enquire_link=True): else: raise - def poll(self, ignore_error_codes=None, auto_send_enquire_link=True): + def poll(self, ignore_error_codes: Optional[List[int]]=None, auto_send_enquire_link: bool=True) -> None: """Act on available PDUs and return""" while True: readable, _writable, _exceptional = select.select([self._socket], [], [], 0) @@ -390,12 +394,12 @@ def poll(self, ignore_error_codes=None, auto_send_enquire_link=True): break self.read_once(ignore_error_codes, auto_send_enquire_link) - def listen(self, ignore_error_codes=None, auto_send_enquire_link=True): + def listen(self, ignore_error_codes: Optional[List[int]]=None, auto_send_enquire_link: bool=True) -> None: """Listen for PDUs and act""" while True: self.read_once(ignore_error_codes, auto_send_enquire_link) - def send_message(self, **kwargs): + def send_message(self, **kwargs: Any) -> Any: """Send message Required Arguments: @@ -410,7 +414,7 @@ def send_message(self, **kwargs): self.send_pdu(ssm) return ssm - def query_message(self, **kwargs): + def query_message(self, **kwargs: Any) -> Any: """Query message state Required Arguments: diff --git a/smpplib/command.py b/smpplib/command.py index 337c838..17c358d 100644 --- a/smpplib/command.py +++ b/smpplib/command.py @@ -25,11 +25,12 @@ from smpplib import consts, exceptions, pdu from smpplib.ptypes import flag, ostr +from typing import NoReturn, TypeVar logger = logging.getLogger('smpplib.command') -def factory(command_name, **kwargs): +def factory(command_name: str, **kwargs: Any) -> pdu.PDU: """Return instance of a specific command class""" try: @@ -59,7 +60,7 @@ def factory(command_name, **kwargs): raise exceptions.UnknownCommandError('Command "%s" is not supported' % command_name) -def get_optional_name(code): +def get_optional_name(code: int) -> str: """Return optional_params name by given code. If code is unknown, raise UnkownCommandError exception""" @@ -70,7 +71,7 @@ def get_optional_name(code): raise exceptions.UnknownCommandError('Unknown SMPP command code "0x%x"' % code) -def get_optional_code(name): +def get_optional_code(name: str) -> int: """Return optional_params code by given command name. If name is unknown, raise UnknownCommandError exception""" @@ -80,7 +81,7 @@ def get_optional_code(name): raise exceptions.UnknownCommandError('Unknown SMPP command name "%s"' % name) -def unpack_short(data, pos): +def unpack_short(data: bytes, pos: int) -> Tuple[int, int]: return struct.unpack('>H', data[pos:pos+2])[0], pos + 2 @@ -90,7 +91,7 @@ class Command(pdu.PDU): params: Dict[str, "Param"] = {} params_order: Tuple[str, ...] - def __init__(self, command, need_sequence=True, allow_unknown_opt_params=False, **kwargs): + def __init__(self, command: str, need_sequence: bool=True, allow_unknown_opt_params: bool=False, **kwargs: Any) -> None: super(Command, self).__init__(**kwargs) self.allow_unknown_opt_params = allow_unknown_opt_params @@ -104,13 +105,13 @@ def __init__(self, command, need_sequence=True, allow_unknown_opt_params=False, self._set_vars(**kwargs) - def _set_vars(self, **kwargs): + def _set_vars(self, **kwargs: Any) -> None: """set attributes accordingly to kwargs""" for key, value in kwargs.items(): if not hasattr(self, key) or getattr(self, key) is None: setattr(self, key, value) - def generate_params(self): + def generate_params(self) -> bytes: """Generate binary data from the object""" if hasattr(self, 'prep') and callable(self.prep): # type: ignore @@ -146,12 +147,12 @@ def generate_params(self): body += value return body - def _generate_opt_header(self, field): + def _generate_opt_header(self, field: str) -> NoReturn: """Generate a header for an optional parameter""" raise NotImplementedError('Vendors not supported') - def _generate_int(self, field): + def _generate_int(self, field: str) -> bytes: """Generate integer value""" fmt = self._int_pack_format(field) @@ -161,7 +162,7 @@ def _generate_int(self, field): else: return consts.NULL_STRING - def _generate_string(self, field): + def _generate_string(self, field: str) -> bytes: """Generate string value""" field_value = getattr(self, field) @@ -183,7 +184,7 @@ def _generate_string(self, field): setattr(self, field, field_value) return value - def _generate_ostring(self, field): + def _generate_ostring(self, field: str) -> Optional[bytes]: """Generate octet string value (no null terminator)""" value = getattr(self, field) @@ -192,7 +193,7 @@ def _generate_ostring(self, field): else: return None # chr(0) - def _generate_int_tlv(self, field): + def _generate_int_tlv(self, field: str) -> Optional[bytes]: """Generate integer value""" fmt = self._int_pack_format(field) data = getattr(self, field) @@ -203,7 +204,7 @@ def _generate_int_tlv(self, field): value = struct.pack(">HH" + fmt, field_code, field_length, data) return value - def _generate_string_tlv(self, field): + def _generate_string_tlv(self, field: str) -> Optional[bytes]: """Generate string value""" field_value = getattr(self, field) @@ -228,7 +229,7 @@ def _generate_string_tlv(self, field): return value - def _generate_ostring_tlv(self, field): + def _generate_ostring_tlv(self, field: str) -> Optional[bytes]: """Generate octet string value (no null terminator)""" try: field_value = getattr(self, field) @@ -242,11 +243,11 @@ def _generate_ostring_tlv(self, field): value = struct.pack(">HH", field_code, field_length) + field_value return value - def _int_pack_format(self, field): + def _int_pack_format(self, field: str) -> str: """Return format type""" return consts.INT_PACK_FORMATS[self.params[field].size] # type: ignore - def _parse_int(self, field, data, pos): + def _parse_int(self, field: str, data: bytes, pos: int) -> Tuple[bytes, int]: """ Parse fixed-length chunk from a PDU. Return (data, pos) tuple. @@ -260,7 +261,7 @@ def _parse_int(self, field, data, pos): return data, pos - def _parse_string(self, field, data, pos, length=None): + def _parse_string(self, field: str, data: bytes, pos: int, length: Optional[int]=None) -> Tuple[bytes, int]: """ Parse variable-length string from a PDU. Return (data, pos) tuple. @@ -277,7 +278,7 @@ def _parse_string(self, field, data, pos, length=None): return data, pos - def _parse_ostring(self, field, data, pos, length=None): + def _parse_ostring(self, field: str, data: bytes, pos: int, length: Optional[int]=None) -> Tuple[bytes, int]: """ Parse an octet string from a PDU. Return (data, pos) tuple. @@ -292,14 +293,14 @@ def _parse_ostring(self, field, data, pos, length=None): return data, pos - def is_fixed(self, field): + def is_fixed(self, field: str) -> bool: """Return True if field has fixed length, False otherwise""" if hasattr(self.params[field], 'size'): return True return False - def parse_params(self, data): + def parse_params(self, data: bytes) -> None: """Parse data into the object structure""" pos = 0 @@ -319,7 +320,7 @@ def parse_params(self, data): if pos < dlen: self.parse_optional_params(data[pos:]) - def parse_optional_params(self, data): + def parse_optional_params(self, data: bytes) -> None: """Parse optional parameters. Optional parameters have the following format: @@ -351,11 +352,11 @@ def parse_optional_params(self, data): elif param.type is ostr: data, pos = self._parse_ostring(field, data, pos, length) - def field_exists(self, field): + def field_exists(self, field: str) -> bool: """Return True if field exists, False otherwise""" return hasattr(self.params, field) - def field_is_optional(self, field): + def field_is_optional(self, field: str) -> bool: """Return True if field is optional, False otherwise""" if hasattr(self, 'mandatory_fields') and field in self.mandatory_fields: # type: ignore @@ -372,7 +373,7 @@ def field_is_optional(self, field): class Param(object): """Command parameter info class""" - def __init__(self, **kwargs): + def __init__(self, **kwargs: Any) -> None: if 'type' not in kwargs: raise KeyError('Parameter Type not defined') @@ -390,7 +391,7 @@ def __init__(self, **kwargs): if param in kwargs: setattr(self, param, kwargs[param]) - def __repr__(self): + def __repr__(self) -> str: """Shows type of Param in console""" return ''.join(('')) @@ -414,7 +415,7 @@ class BindTransmitter(Command): 'interface_version', 'addr_ton', 'addr_npi', 'address_range', ) - def __init__(self, command, **kwargs): + def __init__(self, command: str, **kwargs: Any) -> None: super(BindTransmitter, self).__init__(command, need_sequence=False, **kwargs) self._set_vars(**(dict.fromkeys(self.params))) @@ -423,13 +424,13 @@ def __init__(self, command, **kwargs): class BindReceiver(BindTransmitter): """Bind as a receiver command""" - def __init__(self, command, **kwargs): + def __init__(self, command: str, **kwargs: Any) -> None: super(BindReceiver, self).__init__(command, **kwargs) class BindTransceiver(BindTransmitter): """Bind as receiver and transmitter command""" - def __init__(self, command, **kwargs): + def __init__(self, command: str, **kwargs: Any) -> None: super(BindTransceiver, self).__init__(command, **kwargs) @@ -443,7 +444,7 @@ class BindTransmitterResp(Command): params_order = ('system_id', 'sc_interface_version') - def __init__(self, command, **kwargs): + def __init__(self, command: str, **kwargs: Any) -> None: super(BindTransmitterResp, self).__init__(command, need_sequence=False, **kwargs) @@ -452,13 +453,13 @@ def __init__(self, command, **kwargs): class BindReceiverResp(BindTransmitterResp): """Response for bind as a reciever command""" - def __init__(self, command, **kwargs): + def __init__(self, command: str, **kwargs: Any) -> None: super(BindReceiverResp, self).__init__(command, **kwargs) class BindTransceiverResp(BindTransmitterResp): """Response for bind as a transceiver command""" - def __init__(self, command, **kwargs): + def __init__(self, command: str, **kwargs: Any) -> None: super(BindTransceiverResp, self).__init__(command, **kwargs) @@ -538,7 +539,7 @@ class DataSM(Command): 'its_session_info', ) - def __init__(self, command, **kwargs): + def __init__(self, command: str, **kwargs: Any) -> None: super(DataSM, self).__init__(command, **kwargs) self._set_vars(**(dict.fromkeys(self.params))) @@ -564,7 +565,7 @@ class DataSMResp(Command): 'dpf_result', ) - def __init__(self, command, **kwargs): + def __init__(self, command: str, **kwargs: Any) -> None: super(DataSMResp, self).__init__(command, **kwargs) self._set_vars(**(dict.fromkeys(self.params))) @@ -575,7 +576,7 @@ class GenericNAck(Command): # TODO: seems unused _defs: Any = [] - def __init__(self, command, **kwargs): + def __init__(self, command: str, **kwargs: Any) -> None: super(GenericNAck, self).__init__(command, need_sequence=False, **kwargs) @@ -724,11 +725,11 @@ class SubmitSM(Command): 'ussd_service_op', ) - def __init__(self, command, **kwargs): + def __init__(self, command: str, **kwargs: Any) -> None: super(SubmitSM, self).__init__(command, **kwargs) self._set_vars(**(dict.fromkeys(self.params))) - def prep(self): + def prep(self) -> None: """Prepare to generate binary data""" if self.short_message: @@ -748,7 +749,7 @@ class SubmitSMResp(Command): params_order = ('message_id',) - def __init__(self, command, **kwargs): + def __init__(self, command: str, **kwargs: Any) -> None: super(SubmitSMResp, self).__init__(command, need_sequence=False, **kwargs) self._set_vars(**(dict.fromkeys(self.params))) @@ -820,7 +821,7 @@ class DeliverSM(SubmitSM): 'source_network_type', 'dest_network_type', 'more_messages_to_send', ) - def __init__(self, command, **kwargs): + def __init__(self, command: str, **kwargs: Any) -> None: super(DeliverSM, self).__init__(command, need_sequence=False, **kwargs) self._set_vars(**(dict.fromkeys(self.params))) @@ -829,7 +830,7 @@ class DeliverSMResp(SubmitSMResp): """deliver_sm_response response class, same as submit_sm""" message_id = None - def __init__(self, command, **kwargs): + def __init__(self, command: str, **kwargs: Any) -> None: super(DeliverSMResp, self).__init__(command, **kwargs) class QuerySM(Command): @@ -864,11 +865,11 @@ class QuerySM(Command): 'source_addr', ) - def __init__(self, command, **kwargs): + def __init__(self, command: str, **kwargs: Any) -> None: super(QuerySM, self).__init__(command, **kwargs) self._set_vars(**(dict.fromkeys(self.params))) - def prep(self): + def prep(self) -> None: """Prepare to generate binary data""" if not self.message_id: @@ -893,7 +894,7 @@ class QuerySMResp(Command): 'error_code', ) - def __init__(self, command, **kwargs): + def __init__(self, command: str, **kwargs: Any) -> None: super(QuerySMResp, self).__init__(command, need_sequence=False, **kwargs) self._set_vars(**(dict.fromkeys(self.params))) @@ -904,7 +905,7 @@ class Unbind(Command): params: Dict[str, Param] = {} params_order = () - def __init__(self, command, **kwargs): + def __init__(self, command: str, **kwargs: Any) -> None: super(Unbind, self).__init__(command, need_sequence=False, **kwargs) @@ -914,7 +915,7 @@ class UnbindResp(Command): params: Dict[str, Param] = {} params_order = () - def __init__(self, command, **kwargs): + def __init__(self, command: str, **kwargs: Any) -> None: super(UnbindResp, self).__init__(command, need_sequence=False, **kwargs) @@ -923,7 +924,7 @@ class EnquireLink(Command): params: Dict[str, Param] = {} params_order = () - def __init__(self, command, **kwargs): + def __init__(self, command: str, **kwargs: Any) -> None: super(EnquireLink, self).__init__(command, need_sequence=False, **kwargs) @@ -932,7 +933,7 @@ class EnquireLinkResp(Command): params: Dict[str, Param] = {} params_order = () - def __init__(self, command, **kwargs): + def __init__(self, command: str, **kwargs: Any) -> None: super(EnquireLinkResp, self).__init__(command, need_sequence=False, **kwargs) @@ -980,6 +981,6 @@ class AlertNotification(Command): 'ms_availability_status', ) - def __init__(self, command, **kwargs): + def __init__(self, command: str, **kwargs: Any) -> None: super(AlertNotification, self).__init__(command, **kwargs) self._set_vars(**(dict.fromkeys(self.params))) diff --git a/smpplib/command_codes.py b/smpplib/command_codes.py index a0fc53e..677f01b 100644 --- a/smpplib/command_codes.py +++ b/smpplib/command_codes.py @@ -34,7 +34,7 @@ } -def get_command_name(code): +def get_command_name(code: int) -> str: """ Return command name by given code. If code is unknown, raise UnknownCommandError exception. @@ -47,7 +47,7 @@ def get_command_name(code): raise exceptions.UnknownCommandError("Unknown SMPP command code '0x%x'" % code) -def get_command_code(name): +def get_command_code(name: str) -> int: """ Return command code by given command name. If name is unknown, raise UnknownCommandError exception. diff --git a/smpplib/gsm.py b/smpplib/gsm.py index 137ac82..37e3057 100644 --- a/smpplib/gsm.py +++ b/smpplib/gsm.py @@ -2,9 +2,10 @@ import random from smpplib import consts, exceptions +from typing import Any, List, Tuple, TypeVar, Union -def make_parts(text, encoding=consts.SMPP_ENCODING_DEFAULT, use_udhi=True): +def make_parts(text: str, encoding: int=consts.SMPP_ENCODING_DEFAULT, use_udhi: bool=True) -> Tuple[Any, int, int]: """Returns tuple(parts, encoding, esm_class)""" try: # Try to encode with the user-defined encoding first. @@ -49,7 +50,7 @@ def make_parts(text, encoding=consts.SMPP_ENCODING_DEFAULT, use_udhi=True): ) -def gsm_encode(plaintext): +def gsm_encode(plaintext: str) -> bytes: """Performs default GSM 7-bit encoding. Beware it's vendor-specific and not recommended for use.""" try: return b''.join( @@ -69,7 +70,7 @@ def gsm_encode(plaintext): } -def make_parts_encoded(encoded_text, part_size): +def make_parts_encoded(encoded_text: bytes, part_size: int) -> List[bytes]: """Splits encoded text into SMS parts""" chunks = split_sequence(encoded_text, part_size) if len(chunks) > 255: @@ -81,6 +82,6 @@ def make_parts_encoded(encoded_text, part_size): return [b''.join((header, bytes((i, )), chunk)) for i, chunk in enumerate(chunks, start=1)] -def split_sequence(sequence, part_size): +def split_sequence(sequence: bytes, part_size: int) -> List[bytes]: """Splits the sequence into equal parts""" return [sequence[i:i + part_size] for i in range(0, len(sequence), part_size)] diff --git a/smpplib/pdu.py b/smpplib/pdu.py index 33e37f2..b3db084 100644 --- a/smpplib/pdu.py +++ b/smpplib/pdu.py @@ -23,12 +23,13 @@ from smpplib import command_codes, consts from smpplib.consts import SMPP_ESME_ROK +from typing import Any, Optional if TYPE_CHECKING: from smpplib.client import Client -def extract_command(pdu): +def extract_command(pdu: Any) -> str: """Extract command from a PDU""" code = struct.unpack('>L', pdu[4:8])[0] @@ -40,7 +41,7 @@ class default_client(object): """Dummy client""" sequence = 0 - def next_sequence(self): + def next_sequence(self) -> int: raise NotImplementedError() @@ -48,58 +49,59 @@ class PDU(object): """PDU class""" length = 0 - command = None - status = None - _sequence = None + command: str + status: Optional[int] = None + _sequence: Optional[int]= None _client: Union["Client", default_client] - def __init__(self, client=default_client(), **kwargs): + def __init__(self, client: Union["Client", default_client]=default_client(), **kwargs: Any) -> None: """Singleton dummy client will be used if omitted""" if client is None: self._client = default_client() else: self._client = client - def _get_sequence(self): + def _get_sequence(self) -> int: """Return global sequence number""" return self._sequence if self._sequence is not None else \ self._client.sequence - def _set_sequence(self, sequence): + def _set_sequence(self, sequence: int) -> None: """Setter for sequence""" self._sequence = sequence sequence = property(_get_sequence, _set_sequence) - def _next_seq(self): + def _next_seq(self) -> int: """Return next sequence number""" return self._client.next_sequence() - def is_vendor(self): + def is_vendor(self) -> bool: """Return True if this is a vendor PDU, False otherwise""" return hasattr(self, 'vendor') - def is_request(self): + def is_request(self) -> bool: """Return True if this is a request PDU, False otherwise""" return not self.is_response() - def is_response(self): + def is_response(self) -> bool: """Return True if this is a response PDU, False otherwise""" if command_codes.get_command_code(self.command) & 0x80000000: return True return False - def is_error(self): + def is_error(self) -> bool: """Return True if this is an error response, False otherwise""" if self.status != SMPP_ESME_ROK: return True return False - def get_status_desc(self, status=None): + def get_status_desc(self, status: Optional[int]=None) -> str: """Return status description""" if status is None: status = self.status + assert status is not None try: desc = consts.DESCRIPTIONS[status] @@ -108,13 +110,13 @@ def get_status_desc(self, status=None): return desc - def parse_params(self, data): + def parse_params(self, data: bytes) -> None: raise NotImplementedError() - def generate_params(self): + def generate_params(self) -> bytes: raise NotImplementedError() - def parse(self, data): + def parse(self, data: bytes) -> None: """Parse raw PDU""" # @@ -140,7 +142,7 @@ def parse(self, data): if len(data) > 16: self.parse_params(data[16:]) - def generate(self): + def generate(self) -> bytes: """Generate raw PDU""" body = self.generate_params() diff --git a/smpplib/smpp.py b/smpplib/smpp.py index c93b3f5..0ff81e3 100644 --- a/smpplib/smpp.py +++ b/smpplib/smpp.py @@ -19,9 +19,10 @@ """SMPP module""" from smpplib import command, pdu +from typing import Any -def make_pdu(command_name, **kwargs): +def make_pdu(command_name: str, **kwargs: Any) -> pdu.PDU: """Return PDU instance""" f = command.factory(command_name, **kwargs) @@ -29,7 +30,7 @@ def make_pdu(command_name, **kwargs): return f -def parse_pdu(data, **kwargs): +def parse_pdu(data: bytes, **kwargs: Any) -> pdu.PDU: """Parse binary PDU""" command = pdu.extract_command(data) From daabdbe5b294cc38ef4c3ad67cceb2770d13acb0 Mon Sep 17 00:00:00 2001 From: David Shepherd Date: Sun, 29 Aug 2021 12:32:44 +0000 Subject: [PATCH 09/11] Add a simpler repr for command objects --- smpplib/command.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/smpplib/command.py b/smpplib/command.py index 17c358d..16d1a70 100644 --- a/smpplib/command.py +++ b/smpplib/command.py @@ -369,6 +369,11 @@ def field_is_optional(self, field: str) -> bool: return False + def __repr__(self) -> str: + args = ', '.join(p + ":" + str(getattr(self, p)) for p in self.params_order) + return f'<{self.command} {args}>' + + class Param(object): """Command parameter info class""" From 7bc5c07c2dedfbd2c7e143bb6e13a3120807b747 Mon Sep 17 00:00:00 2001 From: David Shepherd Date: Sun, 29 Aug 2021 12:13:48 +0000 Subject: [PATCH 10/11] Fix inconsistent use of null unicode vs null bytes when generating some output Writing tests and adding types flagged up this issue where sometimes the generate functions would use "\0" instead of b"\0". --- smpplib/command.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/smpplib/command.py b/smpplib/command.py index 16d1a70..5478bcf 100644 --- a/smpplib/command.py +++ b/smpplib/command.py @@ -156,7 +156,7 @@ def _generate_int(self, field: str) -> bytes: """Generate integer value""" fmt = self._int_pack_format(field) - data = getattr(self, field) + data: int = getattr(self, field) if data: return struct.pack(">" + fmt, data) else: @@ -165,19 +165,19 @@ def _generate_int(self, field: str) -> bytes: def _generate_string(self, field: str) -> bytes: """Generate string value""" - field_value = getattr(self, field) + field_value: bytes = getattr(self, field) if hasattr(self.params[field], 'size'): size = self.params[field].size # type: ignore - value = field_value.ljust(size, chr(0)) + value = field_value.ljust(size, consts.NULL_STRING) elif hasattr(self.params[field], 'max'): if len(field_value or '') >= self.params[field].max: # type: ignore field_value = field_value[0:self.params[field].max - 1] # type: ignore if field_value: - value = field_value + chr(0) + value = field_value + consts.NULL_STRING else: - value = chr(0) + value = consts.NULL_STRING else: assert False, "Param must have either size or max." @@ -187,16 +187,16 @@ def _generate_string(self, field: str) -> bytes: def _generate_ostring(self, field: str) -> Optional[bytes]: """Generate octet string value (no null terminator)""" - value = getattr(self, field) + value: bytes = getattr(self, field) if value: return value else: - return None # chr(0) + return None # consts.NULL_STRING def _generate_int_tlv(self, field: str) -> Optional[bytes]: """Generate integer value""" fmt = self._int_pack_format(field) - data = getattr(self, field) + data: int = getattr(self, field) field_code = get_optional_code(field) field_length = self.params[field].size # type: ignore value = None @@ -207,23 +207,24 @@ def _generate_int_tlv(self, field: str) -> Optional[bytes]: def _generate_string_tlv(self, field: str) -> Optional[bytes]: """Generate string value""" - field_value = getattr(self, field) + field_value: bytes = getattr(self, field) field_code = get_optional_code(field) + value: Optional[bytes] if hasattr(self.params[field], 'size'): size = self.params[field].size # type: ignore - fvalue = field_value.ljust(size, chr(0)) + fvalue = field_value.ljust(size, consts.NULL_STRING) value = struct.pack(">HH", field_code, size) + fvalue elif hasattr(self.params[field], 'max'): if len(field_value or '') > self.params[field].max: # type: ignore field_value = field_value[0:self.params[field].max - 1] # type: ignore if field_value: - fvalue = field_value + chr(0) + fvalue = field_value + consts.NULL_STRING field_length = len(fvalue) - value = struct.pack(">HH", field_code, field_length) + fvalue.encode() + value = struct.pack(">HH", field_code, field_length) + fvalue else: - value = None # chr(0) + value = None # consts.NULL_STRING else: assert False, "Param must have either size or max." @@ -232,7 +233,7 @@ def _generate_string_tlv(self, field: str) -> Optional[bytes]: def _generate_ostring_tlv(self, field: str) -> Optional[bytes]: """Generate octet string value (no null terminator)""" try: - field_value = getattr(self, field) + field_value: bytes = getattr(self, field) except: return None field_code = get_optional_code(field) From 882a784783c93e0d093b4fadfeb23065e1b05796 Mon Sep 17 00:00:00 2001 From: David Shepherd Date: Sun, 29 Aug 2021 12:45:56 +0000 Subject: [PATCH 11/11] Add more tests for PDU parsing/generating --- smpplib/tests/test_command.py | 42 ++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/smpplib/tests/test_command.py b/smpplib/tests/test_command.py index 84fec3b..1cf2ae6 100644 --- a/smpplib/tests/test_command.py +++ b/smpplib/tests/test_command.py @@ -1,12 +1,43 @@ from smpplib import consts, exceptions -from smpplib.command import DeliverSM +from smpplib.command import DeliverSM, SubmitSM, SubmitSMResp import pytest +from mock import Mock +def test_parse_submit_sm(): + # Example from smpp.org + raw = bytes.fromhex( + "000000480000000400000000000000020005004d656c726f73654c61627300" + "01013434373731323334353637380000000000000100000010" + "48656c6c6f20576f726c64201b650201" + ) + pdu = SubmitSM('submit_sm', client=Mock()) + pdu.parse(raw) + + assert pdu.source_addr == b'MelroseLabs' + assert pdu.destination_addr == b'447712345678' + assert pdu.data_coding == consts.SMPP_ENCODING_DEFAULT + assert pdu.short_message == b'Hello World \x1be\x02\x01' + + assert pdu.generate() == raw + + +def test_parse_submit_sm_resp(): + # Another example from smpp.org + raw = bytes.fromhex( + "00000051800000040000000000000002" + "30393537326130613039626337336632653930653933386263366561386361326463663" + "06364343562343039383165343632396638343035353534376561333100" + ) + pdu = SubmitSMResp('submit_sm_resp', client=Mock()) + pdu.parse(raw) + + assert pdu.message_id == b'09572a0a09bc73f2e90e938bc6ea8ca2dcf0cd45b40981e4629f84055547ea31' # type: ignore + + assert pdu.generate() == raw def test_parse_deliver_sm(): - pdu = DeliverSM('deliver_sm') - pdu.parse( + raw = ( b"\x00\x00\x00\xcb\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x01\x00" b"\x01\x0131600000000\x00\x05\x00XXX YYYY\x00\x04\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x0e\x00\x01\x01\x00\x06\x00\x01\x01\x00\x1e\x00" @@ -14,6 +45,8 @@ def test_parse_deliver_sm(): b" dlvrd:001 submit date:1810151907 done date:1810151907 stat:DELIVRD" b" err:000 text:\x04\x1f\x04@\x048\x042\x045\x04B\x04&\x00\x01\x01" ) + pdu = DeliverSM('deliver_sm') + pdu.parse(raw) assert pdu.source_addr_ton == consts.SMPP_TON_INTL assert pdu.source_addr_npi == consts.SMPP_NPI_ISDN @@ -24,6 +57,9 @@ def test_parse_deliver_sm(): assert pdu.message_state == consts.SMPP_MESSAGE_STATE_DELIVERED # type: ignore assert pdu.user_message_reference is None # type: ignore + # TODO: not sure why this doesn't re-generate the raw input, but it seems + # worth having this test anyway. + assert pdu.generate() == b"\x00\x00\x00\xcb\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01\x0131600000000\x00\x05\x00XXX YYYY\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04$\x00rid:0489708364 sub:001 dlvrd:001 submit date:1810151907 done date:1810151907 stat:DELIVRD err:000 text:\x04\x1f\x04@\x048\x042\x045\x04B\x04'\x00\x01\x02\x00\x1e\x00\t1d305b4c\x00\x00\x0e\x00\x01\x01\x00\x06\x00\x01\x01\x04&\x00\x01\x01" def test_unrecognised_optional_parameters(): pdu = DeliverSM("deliver_sm", allow_unknown_opt_params=True)