diff --git a/dvrip/__init__.py b/dvrip/__init__.py index 1ffdc7d..58935c0 100644 --- a/dvrip/__init__.py +++ b/dvrip/__init__.py @@ -44,11 +44,10 @@ def request(self, request): class Client(Connection): - __slots__ = ('username', '_logininfo') + __slots__ = ('_logininfo',) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.username = None self._logininfo = None def login(self, username, password, hash=Hash.XMMD5, # pylint: disable=redefined-builtin @@ -63,13 +62,11 @@ def login(self, username, password, hash=Hash.XMMD5, # pylint: disable=redefine reply = self.request(request) DVRIPRequestError.signal(request, reply) self.session = reply.session - self.username = username self._logininfo = reply def logout(self): assert self.session is not None - request = ClientLogout(username=self.username, - session=self.session) + request = ClientLogout(session=self.session) self.request(request) self.session = None @@ -78,7 +75,7 @@ def connect(self, address, *args, **named): return self.login(*args, **named) def systeminfo(self): - reply = self.request(GetInfo(category=Info.SYSTEM, + reply = self.request(GetInfo(command=Info.SYSTEM, session=self.session)) if reply.system is NotImplemented: raise DVRIPDecodeError('invalid system info reply') @@ -86,14 +83,14 @@ def systeminfo(self): return reply.system def storageinfo(self): - reply = self.request(GetInfo(category=Info.STORAGE, + reply = self.request(GetInfo(command=Info.STORAGE, session=self.session)) if reply.storage is NotImplemented: raise DVRIPDecodeError('invalid system info reply') return reply.storage def activityinfo(self): - reply = self.request(GetInfo(category=Info.ACTIVITY, + reply = self.request(GetInfo(command=Info.ACTIVITY, session=self.session)) if reply.activity is NotImplemented: raise DVRIPDecodeError('invalid system info reply') diff --git a/dvrip/info.py b/dvrip/info.py index 7ee372d..e33996b 100644 --- a/dvrip/info.py +++ b/dvrip/info.py @@ -40,7 +40,7 @@ def json_to(cls, datum): try: return cls(json_to(str)(datum)) except ValueError: - raise DVRIPDecodeError('not a known info category') + raise DVRIPDecodeError('not a known info command') SYSTEM = 'SystemInfo' STORAGE = 'StorageInfo' @@ -114,7 +114,7 @@ class GetInfoReply(Object, ControlMessage): type = 1021 status: member[Status] = member('Ret') - category: member[Info] = member('Name') + command: member[Info] = member('Name') session: member[Session] = member('SessionID') system: optionalmember[SystemInfo] = optionalmember('SystemInfo') storage: optionalmember[StorageInfo] = optionalmember('StorageInfo') @@ -127,5 +127,5 @@ class GetInfo(Object, ControlRequest): type = 1020 reply = GetInfoReply - category: member[Info] = member('Name') - session: member[Session] = member('SessionID') + command: member[Info] = member('Name') + session: member[Session] = member('SessionID') diff --git a/dvrip/login.py b/dvrip/login.py index 09b9849..d064667 100644 --- a/dvrip/login.py +++ b/dvrip/login.py @@ -3,7 +3,8 @@ from string import ascii_lowercase, ascii_uppercase, digits from .message import ControlMessage, ControlRequest, Session, Status from .errors import DVRIPDecodeError -from .typing import Object, for_json, json_to, member, optionalmember +from .typing import Object, fixedmember, for_json, json_to, member, \ + optionalmember __all__ = ('xmmd5', 'Hash', 'ClientLoginReply', 'ClientLogin', 'ClientLogoutReply', 'ClientLogout') @@ -72,15 +73,14 @@ class ClientLogin(Object, ControlRequest): class ClientLogoutReply(Object, ControlMessage): type = 1003 - status: member[Status] = member('Ret') - username: member[str] = member('Name') - session: member[Session] = member('SessionID') + status: member[Status] = member('Ret') + command: fixedmember = fixedmember('Name', '') + session: member[Session] = member('SessionID') class ClientLogout(Object, ControlRequest): type = 1002 reply = ClientLogoutReply - # FIXME 'username' unused? - username: member[str] = member('Name') - session: member[Session] = member('SessionID') + command: fixedmember = fixedmember('Name', '') + session: member[Session] = member('SessionID') diff --git a/dvrip/typing.py b/dvrip/typing.py index 6cfd2e6..d9dba2a 100644 --- a/dvrip/typing.py +++ b/dvrip/typing.py @@ -214,9 +214,36 @@ def _compose(*args: Callable[[Any], Any]) -> Callable[[Any], Any]: return env['composition'] -class absentmember(Member[Union['NotImplemented', T]]): # see python/mypy#4791 - default = NotImplemented - key = NotImplemented +class fixedmember(Member[object]): + __slots__ = ('key', 'default') + + def __init__(self, key: str, datum: object) -> None: + self.key = key + self.default = datum + + def __get__(self, + obj: 'Object', + cls: type + ) -> Union['fixedmember', object]: + if obj is None: + return self + return self.default + + def __set__(self, obj: 'Object', value: object) -> None: + if value != self.default: + raise ValueError('not the fixed value') + + def push(self, push: Callable[[str, object], None], value: object) -> None: + push(self.key, self.default) + + def pop(self, pop: Callable[[str], object]) -> object: + if pop(self.key) != self.default: + raise DVRIPDecodeError('not the fixed value') + return self.default + + +class AttributeMember(Member[T]): + __slots__ = () def __get__(self, obj: 'Object', _type: type) -> Union['member[T]', T]: if obj is None: @@ -226,17 +253,24 @@ def __get__(self, obj: 'Object', _type: type) -> Union['member[T]', T]: def __set__(self, obj: 'Object', value: T) -> None: return setattr(obj._values_, self.name, value) # pylint: disable=protected-access - def push(self, push, value): + +# NotImplemented is not allowed as a type, see python/mypy#4791 +class absentmember(AttributeMember[Union['NotImplemented', T]]): + __slots__ = () + default = NotImplemented + + def push(self, push, value): # pylint: disable=no-self-use if value is not NotImplemented: raise ValueError('value provided for absent member {!r}' .format(self.name)) - def pop(self, pop): + def pop(self, pop): # pylint: disable=no-self-use return NotImplemented -class member(Member[T]): - __slots__ = ('key', 'pipe', 'default', 'json_to', 'for_json') +class member(AttributeMember[T]): + __slots__ = ('key', 'pipe', 'json_to', 'for_json') + # Pylint incorrectly reports assignments below due to PyCQA/pylint#2807 def __init__(self, key: str, @@ -246,7 +280,7 @@ def __init__(self, *args: Tuple[Callable[[Any], Any], Callable[[Any], Any]], ) -> None: - self.key = key + self.key = key if conv is not None: self.pipe = (conv, *args) else: @@ -269,14 +303,6 @@ def __set_name__(self, cls: 'ObjectMeta', name: str) -> None: self.json_to = _compose(*(p[0] for p in self.pipe)) self.for_json = _compose(*(p[1] for p in reversed(self.pipe))) - def __get__(self, obj: 'Object', _type: type) -> Union['member[T]', T]: - if obj is None: - return self - return getattr(obj._values_, self.name) # pylint: disable=protected-access - - def __set__(self, obj: 'Object', value: T) -> None: - return setattr(obj._values_, self.name, value) # pylint: disable=protected-access - def push(self, push, value): super().push(push, value) push(self.key, self.for_json(value)) @@ -314,9 +340,7 @@ class ObjectMeta(ABCMeta): def __new__(cls, name, bases, namespace, **kwargs) -> 'ObjectMeta': names: MutableMapping[str, Member] = OrderedDict() for mname, value in namespace.items(): - if (_isunder(mname) or - not isinstance(value, Member) or - not hasattr(value, 'key')): + if _isunder(mname) or not isinstance(value, Member): # Pytest-cov mistakenly thinks this branch is # not taken. Place a print statement here to # verify. diff --git a/dvrip_test b/dvrip_test index fd30290..2bc0b11 100755 --- a/dvrip_test +++ b/dvrip_test @@ -24,6 +24,6 @@ dvrip.packet._write = _write # pylint: disable=protected-access conn = Client(Socket(AF_INET, SOCK_STREAM)) conn.connect((argv[1], 34567), argv[2], argv[3]) print(conn.systeminfo()) -print(conn.request(GetInfo(session=conn.session, category=Info.STORAGE)) +print(conn.request(GetInfo(session=conn.session, command=Info.STORAGE)) .storage) conn.logout() diff --git a/test_dvrip.py b/test_dvrip.py index 45fa614..dc9a6b0 100644 --- a/test_dvrip.py +++ b/test_dvrip.py @@ -359,41 +359,37 @@ def test_ControlFilter_accept_invalid_overlap(): replies.accept(q) def test_ClientLogout_topackets(session): - p, = (ClientLogout(username='admin', session=session) - .topackets(session, 0)) + p, = (ClientLogout(session=session).topackets(session, 0)) assert p.encode() == (b'\xFF\x01\x00\x00\x57\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\xEA\x03\x2E\x00\x00\x00' - b'{"Name": "admin", "SessionID": "0x00000057"}' + b'\x00\x00\x00\x00\xEA\x03\x29\x00\x00\x00' + b'{"Name": "", "SessionID": "0x00000057"}' b'\x0A\x00') def test_ClientLogoutReply_accept(): data = (b'\xFF\x01\x00\x00\x57\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\xeb\x03\x3A\x00\x00\x00' b'{ "Name" : "", "Ret" : 100, ' - b'"SessionID" : "0x00000057" }\x0A\x00') + b'"SessionID" : "0x00000057" }' + b'\x0A\x00') replies = ClientLogout.replies(0) (n, m), = replies.accept(Packet.decode(data)) assert n == 0 - assert (m.username == "" and m.status == Status(100) and # pylint: disable=no-value-for-parameter - m.session == Session(0x57)) + assert m.status == Status(100) and m.session == Session(0x57) def test_Client_logout(capsys, session, clinoconn, clitosrv, srvtocli): p, = (ClientLogoutReply(status=Status.OK, - username='admin', session=session) .topackets(session, 2)) p.dump(srvtocli) p, = (ClientLogoutReply(status=Status.OK, - username='admin', session=session) .topackets(session, 1)) p.dump(srvtocli) srvtocli.seek(0) - clinoconn.username = 'admin' clinoconn.logout() clitosrv.seek(0); m = ClientLogout.frompackets([Packet.load(clitosrv)]) - assert m == ClientLogout(username='admin', session=session) + assert m == ClientLogout(session=session) out1, out2 = capsys.readouterr().out.split('\n') assert out1.startswith('unrecognized packet: ') and out2 == '' @@ -448,19 +444,19 @@ def test_version(): assert _versiontype == (_json_to_version, _version_for_json) @given(sampled_from(list(Info.__members__.values()))) -def test_Info_repr(cat): - assert repr(cat) == 'Info.{}'.format(cat.name) +def test_Info_repr(cmd): + assert repr(cmd) == 'Info.{}'.format(cmd.name) @given(sampled_from(list(Info.__members__.values()))) -def test_Info_str(cat): - assert str(cat) == cat.value +def test_Info_str(cmd): + assert str(cmd) == cmd.value @given(sampled_from(list(Info.__members__.values()))) -def test_Info_forjson(cat): - assert cat.for_json() == cat.value +def test_Info_forjson(cmd): + assert cmd.for_json() == cmd.value @given(sampled_from(list(Info.__members__.values()))) -def test_info_jsonto(cat): - assert Info.json_to(cat.value) == cat - with raises(DVRIPDecodeError, match='not a known info category'): +def test_info_jsonto(cmd): + assert Info.json_to(cmd.value) == cmd + with raises(DVRIPDecodeError, match='not a known info command'): Info.json_to('SPAM') diff --git a/test_typing.py b/test_typing.py index 2c1448a..e5d7dad 100644 --- a/test_typing.py +++ b/test_typing.py @@ -9,8 +9,8 @@ from dvrip.errors import DVRIPDecodeError from dvrip.typing import EnumValue, Member, Object, Value, absentmember, \ - _compose, for_json, json_to, jsontype, member, \ - optionalmember + _compose, fixedmember, for_json, json_to, jsontype, \ + member, optionalmember def test_forjson(): @@ -197,6 +197,27 @@ def hextext(): return (text(sampled_from('0123456789abcdef')) .filter(lambda s: len(s) % 2 == 0)) +class FixedExample(Object): + mint: fixedmember = fixedmember('Int', 57) + +def test_fixedmember_get(): + assert FixedExample().mint == 57 + +def test_fixedmember_set(): + obj = FixedExample() + obj.mint = 57 + with raises(ValueError, match='not the fixed value'): + obj.mint = 58 + assert obj.mint == 57 + +def test_fixedmember_forjson(): + assert FixedExample().for_json() == {'Int': 57} + +def test_fixedmember_jsonto(): + assert FixedExample.json_to({'Int': 57}) == FixedExample() + with raises(DVRIPDecodeError, match='not the fixed value'): + FixedExample.json_to({'Int': 58}) + class AbsentExample(Object): mint: absentmember[int] = absentmember()