-
Notifications
You must be signed in to change notification settings - Fork 950
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
eliminates implicit Optional #1817
Conversation
… on Python 3.8 happy
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something to get you going.
A couple of general comments:
- do not mix "Union", "Optional" and "|", decide what to use and use it
- do not use "cast" unless there is a very good reason to do so
Once these parts are done, please ask for a new review.
@@ -22,7 +22,7 @@ class ModbusBaseClient(ModbusClientMixin, ModbusProtocol): | |||
|
|||
**Parameters common to all clients**: | |||
|
|||
:param framer: (optional) Modbus Framer class. | |||
:param framer: Modbus Framer class. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but framer is an optional parameter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you read my comment 3? If framer is optional and defaults to None then it should not be called unprotected in __init__
:
self.framer = framer(ClientDecoder(), self)
I don't think None
can be called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No I do not read comments, I review code. All optional parameters are described as optional, and framer is and should be optional.
None is a legal value for framer at API level.
|
||
def __init__( # pylint: disable=too-many-arguments | ||
self, | ||
framer: type[ModbusFramer] = None, | ||
framer: type[ModbusFramer], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This changes the API, that is not something we do lightly !
I do not see the benefit of this change, please revert or explain why it is an advantage for the users.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, there is a type error in __init__
if this stays optional which should then be resolved in another way. How to initialize self.framer
if the argument to framer
not given and thus framer
defaults to None?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is another problem, which I think is handled at lower levels in the code.
Please do not change API, because this will cause every app to be changed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apart from that the type error is quite easy to solve without changing the parameter itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I did not see how to resolve that easily. Here I need your support.
@@ -122,10 +122,10 @@ def __init__( # pylint: disable=too-many-arguments | |||
self.transaction = DictTransactionManager( | |||
self, retries=retries, retry_on_empty=retry_on_empty, **kwargs | |||
) | |||
self.reconnect_delay_current = self.params.reconnect_delay | |||
self.reconnect_delay_current = cast(float, self.params.reconnect_delay) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
both delay_current and reconnect_delay are (or should be float) so why the cast.
cast is an ugly beast, that preferable never should be used, it typically hides a fundamental problem, likely maybe in this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self.params.reconnect_delay
defaults to None
. Maybe it shouldn't.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes it should, that is the way to tell the lower system that reconnect is not used. That could of course be done differently, but please make 1 pull request for 1 problem, in this case "typing":
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, but if self.params.reconnect_delay
can be None
it should not be assigned to self.reconnect_delay_current
which is declared to be a float
in
self.reconnect_delay_current = self.params.reconnect_delay
"""Handle received data | ||
|
||
returns number of bytes consumed | ||
""" | ||
self.framer.processIncomingPacket(data, self._handle_response, slave=0) | ||
return len(data) | ||
|
||
def callback_disconnected(self, _reason: Exception) -> None: | ||
def callback_disconnected(self, _reason: Exception | None) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can that really be None ? where is it called with None ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my comment 2
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again I review your code...All call I can see have an exception.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In transport/transport.py:transport_close
there is a value.callback_disconnected(None)
as I wrote in the PR comment.
@@ -502,7 +502,7 @@ def read_fifo_queue(self, address: int = 0x0000, **kwargs: Any) -> ModbusRespons | |||
# code 0x2B sub 0x0D: CANopen General Reference Request and Response, NOT IMPLEMENTED | |||
|
|||
def read_device_information( | |||
self, read_code: int = None, object_id: int = 0x00, **kwargs: Any | |||
self, read_code: Union[int, None] = None, object_id: int = 0x00, **kwargs: Any |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Union ?? I think we should either use "Union" or "|" not both.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use from __future__ import annotations
to allow |
to be used.
I will mark the other redundant comments Resolved.
return | ||
host, port = host_serial, port_serial |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just complicating things ! please use host, port directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my comment 9
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is a not acceptable complication, that brings nothing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well it makes this type correct
def init_setup_serial(self, host: str, _port: int) -> tuple[str, int]: | ||
def init_setup_serial( | ||
self, host: str, _port: int | ||
) -> tuple[str, int] | tuple[None, None]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this really return None, None ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, also in comment 9.
@@ -211,19 +214,25 @@ def init_setup_connect_listen(self, host: str, port: int) -> None: | |||
"""Handle connect/listen handler.""" | |||
if self.comm_params.comm_type == CommType.UDP: | |||
if self.is_server: | |||
self.call_create = lambda: self.loop.create_datagram_endpoint( | |||
self.call_create = lambda: cast( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is wrong, call_create is NOT loop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, self.loop
has to be a loop to call create_datagram_endpoint
. See comment 10.
@@ -289,7 +304,7 @@ def connection_made(self, transport: asyncio.BaseTransport): | |||
self.reset_delay() | |||
self.callback_connected() | |||
|
|||
def connection_lost(self, reason: Exception): | |||
def connection_lost(self, reason: Exception | None) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can that be called with None ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comment 11.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As MAKOMO explained, the default is in fact calling with None
(via a lambda)
pymodbus/pymodbus/transport/transport.py
Line 162 in 2adff1f
self.call_create: Callable[[], Coroutine[Any, Any, Any]] = lambda: None |
I wrote "choose", but thinking a second time, the preference is to use "|" because that is the future. |
Yes |
I will wait to comment/review until you have made the requested changes, otherwise we keep commenting on the same things. Please add me a reviewer when it is ready to be reviewed. |
I think the problem is that you try to solve the whole world in one go, that is difficult, even though I know the code quite good, I would have problems doing that. Instead of giving up, submit a pull request with the simple changes, basically "| None", easy to review and easy to get merged...then you can focus on the more difficult items in smaller chunks. But of course its all up to you. (not sure why we started talking about this PR in my PR). |
I just replaced all Unions in client/mixing.py by | bars. Fine for mypy, but pylint complaints. Not supported under Python 3.8? Strange as transport/transport.py the | bars are accepted. Somehow the CI seems to treat different files in this project differently. I seem not to be the right person to bring this forward. Sorry. |
CI are not treating the files differently, but the files causing you problems might miss an import at the very top, to allow future changes. |
If I may be blunt, I do not think it's you as a person that's wrong....I actually find it refreshing to see your PR with interesting changes. It seems you lack som python experience. The difference in the files are this "from future import annotations" and not CI or other things, A similar problem was with the import variable "web", where it is correct that we want optional import, but there are different ways of doing it. transport_serial.py have a way that do not make mypy complain. So back to what I started saying do not try to solve the whole world in one pull request, that is extremely difficult, use the "salami tactic", and make smaller easier pull requests. Just a recommendation. Ps. I still think your goal is correct, and I hope you noted that I have not spoken against your changes just tried to guide you in the direction where we all benefit. |
This situation is unfortunate. A well-meaning contributor and a well-meaning maintainer became frustrated at each other over some misunderstandings. I hope it may be possible to try again. :) @janiversen I recommend you follow Marko's advice and read the pull request comments! He offers good analysis of the problems @MAKOMO I recommend you follow Jan's advice and split this PR into smaller ones, starting with one change per PR. This makes it much easier to keep the comments near the code and review them. EDIT: (Sorry typo with MAKOMO) |
As a first example, there was much discussion regarding Using I presume Marko is using Python > 3.10 and so pymodbus/.github/workflows/ci.yml Lines 31 to 36 in 2adff1f
The answer is to use below for the cases where it fails. from __future__ import annotations |
Yes it is very unfortunate! and I apologize but the pull request was big and I saw the same problems again and again, so I basically gave up on making a normal review which is also the reason for my not too polite (my apologies again) comment. what especially concerned me was the inconsistent use of | and Option which was caused by not importing future (which I explained) I hope he will come back, because his work was not bad, just needed finishing touches...which I hope I explained directly earlier. |
When passionate Spain and methodical Germany can work peacefully together, the world will be amazed 😄 . I am going to try a few smaller PRs. |
heh, do not forget I am a real Viking (dane), living in Spain...I did live 5 years in Vienna if that counts. Anyhow you are maintainer and THE typing specialist in this project, so your word on these subjects are what matters....I am just the old code guy 😀 |
See #1825 as an example |
Yes and see #1827, to see why not just accept everything. Everything that makes the code more stable or faster are super, but changes that makes life harder for users of the library or complicates the code just to please mypy are at the very least something that should be discussed. |
For an update on progress: When the PR was created:
Today:
|
Seems this PR is no longer updated, and have been replaced by a number of PR´s by @alexrudd2. I am closing this, but the base of this is not bad !!! it just needed a bit of hand holding which have happened thanks to @alexrudd2. I am sorry for not being of more help, but mypy is to me working a lot against the nature of python. To be fair I am a C/C++ programmer so I am used to very strict typing etc, and python offered an interesting alternative. |
PEP 484 prohibits implicit Optional for good reasons. See for Any advantages of enabled mypy `strict_optional? some arguments.
Current mypy configuration ignores implicit Optional
with
strict_optional
set mypy raises 87 errors in 13 files. This commit resolves all of them.While this is mostly about adding/changing type annotations I had to apply few code changes too as well as one (minor) API change (see the marks below). Here are non-trivial issues I encountered. Maybe you find better resolutions than the ones proposed by this PR for those.
resolved by widening
resolved by widening
resolved by a cast
which make pylint to fail under Python 3.8:
so I removed the cast and made the framer argument mandatory
This is an API change!
base.py:ModbusBaseClient(ModbusClientMixin, ModbusProtocol)
resolved by extending the type declaration of `self.last_frame_end
server/simulator/http_server.py
mypy reports
resolved by using a cast
see python/mypy#10512
mypy reports
We have
but
transport/transport.py:ModbusProtocol
How can self.transport is of type asyncio.BaseTransport and thus cannot hold a tuple.
client/serial.py
but from
transport/transport.py
we see thatself.comm_params.baudrate
could beNone
:resolved by adding an assertion
transport/transport.py
The return type of
init_setup_serial
declaration does not match its implementation.Resolved by extending this type declaration and taking care that in the assignment of
host
andport
in__init__
does not violate its declaration:transport/transport.py:ModbusProtocol
resolved by adding a cast
transport/transport.py:ModbusProtocol
None is not a Coroutine
Resolved by using None instead of the dummy lambda
and adding an assertion to
here and in some related occurrences