-
Notifications
You must be signed in to change notification settings - Fork 1k
Fix typing for reactive server #1371
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
Conversation
Co-Authored-By: jan iversen <[email protected]>
Co-Authored-By: jan iversen <[email protected]>
janiversen
left a comment
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.
Some comments.
pymodbus/datastore/store.py
Outdated
| # Datablock Storage | ||
| # ---------------------------------------------------------------------------# | ||
| class BaseModbusDataBlock: | ||
| class BaseModbusDataBlock(dict): |
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.
Why does it inherit from a dict, it really have nothing to do with a dict.
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.
And in most datastores the values are not stored in a dict.
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.
Inhering from dict would mean the following is a legal use:
block = BaseModbusDataBlock()
response = block["address"]
But that is not the use case, and never will be, since we never just get address, we always request address, count (using get/set/validate).
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.
The block dict is passed to the constructor for ReactiveModbusslaveContext using ** unpacking.
pymodbus/pymodbus/server/reactive/main.py
Lines 440 to 446 in 031c457
| slave_context = ReactiveModbusSlaveContext( | |
| **block, | |
| randomize=randomize, | |
| change_rate=change_rate, | |
| zero_mode=True, | |
| **data_block_settings, | |
| ) |
That constructor takes several
BaseModbusDataBlocks as keyword arguments.pymodbus/pymodbus/server/reactive/main.py
Lines 103 to 116 in 031c457
| class ReactiveModbusSlaveContext(ModbusSlaveContext): | |
| """Reactive Modbus slave context""" | |
| def __init__( | |
| self, | |
| discrete_inputs: BaseModbusDataBlock = None, | |
| coils: BaseModbusDataBlock = None, | |
| input_registers: BaseModbusDataBlock = None, | |
| holding_registers: BaseModbusDataBlock = None, | |
| zero_mode: bool = False, | |
| randomize: int = 0, | |
| change_rate: int = 0, | |
| **kwargs, | |
| ): |
This means that block must be something like Dict[str, BaseModbusDataBlock]
So any block[item] must also be BaseModbusDataBlocks, which means the two ways of creating an item must follow this:
pymodbus/pymodbus/server/reactive/main.py
Lines 432 to 438 in 031c457
| block[modbus_entity] = { | |
| add: val | |
| for add in sorted(address_map) | |
| for val in default_values | |
| } | |
| else: | |
| block[modbus_entity] = db(start_address, default_values) |
(1)
block[modbus_entity] = {... } fails because it's too broad (a dict).(2)
block[modbus_entity] = db(...) fails for because db is a too broad (also a dict). If I type db to BaseModbusDataBlock, it cannot take the two arguments, anyways.
....somewhere around here I gave up, and made BaseModbusDataBlock(dict). :)
Typing is very difficult; I will try again.
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.
You are again mixing issues....you talk about how the datastore is defined (init) and then by adding dict, you redefine the usage to be as a dict, which is wrong.
Please accept the 3 views of the datastore, that might help you quite a bit.
pymodbus/datastore/store.py
Outdated
| self.values = [ # pylint: disable=attribute-defined-outside-init | ||
| self.default_value | ||
| ] * count | ||
| self.values = [self.default_value] * count |
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 does not seem right, and you missed the previous disable !
The problem is that the base class misses an init where the variables should be defined.
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 this case I followed pylint blindly. It warned me the disable was unnecessary. I will revisit after rethinking BaseModbusDataBlock(dict)
pymodbus/server/reactive/main.py
Outdated
| slaves = {} | ||
| for i in unit: | ||
| block = {} | ||
| block = BaseModbusDataBlock() |
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.
I think this is wrong, but I have to look deeper into it.
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.
On further analysis (see above), yes it's wrong. It should be Dict[str, BaseModbusDataBlock]
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 it should be "{}" or "dict", it is NOT a BaseModbusDataBlock, it is the setup to define a BaseModbusDataBlock, unless I have misunderstood the code.
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.
You are again mixing issues....you talk about how the datastore is defined (init) and then by adding dict, you redefine the usage to be as a dict, which is wrong.
No it should be "{}" or "dict", it is NOT a BaseModbusDataBlock, it is the setup to define a BaseModbusDataBlock, unless I have misunderstood the code.
pymodbus/server/reactive/main.py:441: error: Argument 1 to "ReactiveModbusSlaveContext" has incompatible type "**Dict[Any, Dict[Any, Any]]"; expected "BaseModbusDataBlock" [arg-type]
mypy is very strict, please try running it and you will see why I think in these ways. It does not care about the intended use of the code, only about how broad/narrow types are. If a variable is defined narrowly in some downstream code, then all previous users of that variable must be as narrow. They cannot be broader (which is what the error is here). This includes the initialization; there is nothing special about it.
block: Dict[str, BaseModbusDataBlock] != block: BaseModbusDataBlock. It means a dict with keys that are str and values that are BaseModbusDataBlock.
If block is permitted to be any arbitrary dict, then (I think) mypy is concerned that some code may do something stupid. For instance, block['coils'] = 3.
BaseModbusDataBlock(dict) was a mistake because it was too broad. Instead I need to find a way to make the other definitions more narrow.
===
Please accept the 3 views of the datastore, that might help you quite a bit.
Yes, it's true. Thank you for the explanations, I am slowly learning the intent of the code. It's difficult to serve two masters, you and mypy. :)
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.
[Argument 1 to React](error: Argument 1 to "ReactiveModbusSlaveContext") does not mean that ModbusSlaveContext should be a dict !
I am aware that mypy likes strict definition and I have NOTHING against that, but block is NOT a BaseModbusDataBlock, it is used to define one.
BaseModbusDataBlock do not inherit from anything ! that is a wrong idea. You need to add more strict definitions where data are defined for the DataBlock, which is in Init.
Be aware ReactiveModbusSlaveContext is yet another context version, look in directory datastore and you will find more. Being inherited means that it:
- can expand the base init to allow new type of definitions
- has its own internal way of storing things.
BUT get/set/validate must remain unchanged, or it would break the server.
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.
[Argument 1 to React](error: Argument 1 to "ReactiveModbusSlaveContext") does not mean that ModbusSlaveContext should be a dict !
When did I say this? I explained how block, which is a dict, is unpacked using ** into (a subclass of) ModbusSlaveContext.
block is NOT a BaseModbusDataBlock, it is used to define one.
I said that block is currently a dict, and the members of block (such as coils) must be BaseModbusDataBlock by the time they are used to initialize ReactiveModbusSlaveContext, which is typed to require e.g. coils: BaseModbusDataBlock (30f7166).
BaseModbusDataBlock do not inherit from anything ! that is a wrong idea.
I am not arguing for BaseModbusDataBlock(dict), and have agreed many times it's wrong. It's why I changed the PR to draft status. I'm also not arguing for ModbusSlaveContext(dict). I will revert the commit so you can stop focusing on telling me this, and perhaps try to work on a proper solution.
pymodbus/pymodbus/server/reactive/main.py
Lines 440 to 446 in 031c457
| slave_context = ReactiveModbusSlaveContext( | |
| **block, | |
| randomize=randomize, | |
| change_rate=change_rate, | |
| zero_mode=True, | |
| **data_block_settings, | |
| ) |
I'm arguing that the unpacking of
block into the the arguments for [Reactive]ModbusSlaveContext means that block should probably be Dict[str, BaseModbusDataBlock].
block: Dict[str, BaseModbusDataBlock] = {
"coils": BaseModbusDataBlock(),
"discrete_inputs" : BaseModbusDataBlock(),
[...]
}
slave_context = ReactiveModbusSlaveContext( **block, [...])
i.e.
slave_context = ReactiveModbusSlaveContext(
coils = BaseModbusDataBlock(), discrete_inputs=BaseModbusDataBlock()
)
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.
I think it is better I let you work, and will review when it is ready.
May I polity recommend you look at our examples to see how we use Context. BaseModbusDataBlock is a Base not a type.
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.
/s/BaseModbusDataBlock/Type[BaseModbusDatablock]
Perhaps that was our confusion. It gets annoying when sometimes you need Type[] (for arguments), and sometimes you need class instances (in return values).
https://adamj.eu/tech/2021/05/16/python-type-hints-return-class-not-instance/.
Thank you for your patience with me. Typing is difficult to learn in complex cases.
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 problem, we both pursue the same goal, a better library.
Thinking about Type[], but without knowing it, could this be a solution to mixin.py ?
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.
Thinking about Type[], but without knowing it, could this be a solution to mixin.py ?
Sadly, no. The "abstract" execute has a broader type, and the individual callers expect a narrower one.
This reverts commit d07b8bc.
|
closing in favor of #1381 |
Solves the following mypy errors:
pymodbus/server/reactive/main.py:441: error: Argument 1 to "ReactiveModbusSlaveContext" has incompatible type "**Dict[Any, Dict[Any, Any]]"; expected "BaseModbusDataBlock" [arg-type]pymodbus/server/reactive/main.py:450: error: Incompatible types in assignment (expression has type "ReactiveModbusSlaveContext", variable has type "Dict[int, ReactiveModbusSlaveContext]") [assignment]