feat(API): Add descendant find for parent db objects.#248
feat(API): Add descendant find for parent db objects.#248
Conversation
2 similar comments
|
Why you spamming me @coveralls! |
|
Would it be possible to post a comment of what your modified example looks like, so we can see what the API is like to use? Or more ideally, tests :) |
nixnet/database/_find_object.py
Outdated
| _errors.check_for_error(_cconsts.NX_ERR_INVALID_PROPERTY_VALUE) | ||
| # The above line will raise an exception. | ||
| # However, mypy doesn't know that and will complain if the following return is missing. | ||
| return None |
There was a problem hiding this comment.
Looks like you could make the return type a union of the regular and NoReturn
nixnet/database/_find_object.py
Outdated
|
|
||
|
|
||
| def find_object(parent_handle, object_class, object_name): | ||
| # type: (int, constants.ObjectClass, typing.Text) -> object |
There was a problem hiding this comment.
- Instead of an enum, should people pass
Clusteretc into here? - Wish mypy had dependent types. That'd be cool to say that the return type is the type of
object_class.
There was a problem hiding this comment.
Regarding 1, I had thought about doing it that way. I guess on the plus side, it eliminates an enum the user has to deal with, and encourages familiarity with the actual objects. On the con side, those of us who use IDEs lose the benefit of intellisense for the enum values. I think my preference would be to continue using the enum as I don't really see much benefit to passing in an object instead of an enum. I'm open to either way though.
There was a problem hiding this comment.
On the con side, those of us who use IDEs lose the benefit of intellisense for the enum values.
Completion for nixnet.constants.ObjectClass is not much different than completion for nixnet.database.
As for other intellisense features, you can improve that by
- Providing a common base class for all database objects
- Change the annotations from
type: (int, constants.ObjectClass, typing.Text) -> objecttotype: (int, typing.Type[nixnet.database.SomeBaseClass], typing.Text) -> nixnet.database.SomeBaseClass
This is similar to the approach we took elsewhere in the API
- Define
FrameFactoryhave have each frame implement it - Accept a frame object when reading and converting
I think my preference would be to continue using the enum as I don't really see much benefit to passing in an object instead of an enum.
Advantages
- Minimize vocabulary terms the user of the API has to deal with. I need a
Cluster, so let me pass aClustervs having to deal with an enum that representsCluster. - Avoid C-isms. Part of the role of the enum is a form of RTTI in C. Changing it to the actual python class is exposing the same concept in the python way
- Controlled access. The doc string and intellisense will help guide people to the correct values to pass in rather than having things like
Systembe an option.
|
@epage As requested, here is some code to show usage of find. import nixnet
from nixnet.constants import ObjectClass
from nixnet import database
def main():
with database.Database("NIXNET_example") as db:
cluster = db.find(ObjectClass.CLUSTER, "CAN_Cluster")
print(cluster.name)
frame1 = db.find(ObjectClass.FRAME, "CANCyclicFrame2")
frame2 = cluster.find(ObjectClass.FRAME, "CANCyclicFrame2")
print(frame1.name)
print(frame2.name)
assert (frame1 == frame2)
signal = db.find(ObjectClass.SIGNAL, "ClutchPressure")
print(signal.name) |
|
For reference, with using classes instead of enums import nixnet
from nixnet import database
def main():
with database.Database("NIXNET_example") as db:
cluster = db.find(database.Cluster, "CAN_Cluster")
print(cluster.name)
frame1 = db.find(database.Frame, "CANCyclicFrame2")
frame2 = cluster.find(database.Frame, "CANCyclicFrame2")
print(frame1.name)
print(frame1.name)
assert (frame1 == frame2)
signal = db.find(database.Signal, "ClutchPressure")
print(signal.name)Oh, fun, that won't work as of this moment. Interesting. I know the concern was about exposing the useless private constructors to users but this doesn't allow our users to use mypy. A workaround for exposing the useless private constructors is to make |
jashnani
left a comment
There was a problem hiding this comment.
I have a preference for using classes instead of enums, mainly because I don't have to remember the enum name. Other reasons mentioned by @epage are also compelling.
Exposing useless private constructors can be tricky and I'm curious what that would look like to the user. If the type/class is completely useless and that's apparent to the user, I would be okay with this approach.
|
I'm curious about your reasoning for suggesting kwonly-args and not kwargs? Won't both work fine for this purpose? kwonly-args looks like it might be a cleaner implementation. |
If you are taking about usage, any keyword argument can also be a positional argument. If you are talking about |
fed7115 to
d381e6e
Compare
|
I've done the following:
However, I'm still not seeing a useful namespacing experience. Not sure yet what I'm missing. I'm holding off on producing docstrings and making the kwargs constructor changes until I can fix the namespacing experience. |
nixnet/database/_find_object.py
Outdated
| class_enum = constants.ObjectClass.SUBFRAME | ||
| else: | ||
| # arbitrary value for class_enum so mypy sees a value in each condition | ||
| class_enum = constants.ObjectClass.CLUSTER |
There was a problem hiding this comment.
I'd actually recommend a raise ValueError("Unsupported database object")
There was a problem hiding this comment.
Oh, missed the raise below :)
|
|
||
|
|
||
| __all__ = ["Database"] | ||
| __all__ = [ |
There was a problem hiding this comment.
DatabaseObjectBase should be in here so users can do mypy typing
| import typing # NOQA: F401 | ||
|
|
||
|
|
||
| class DatabaseObjectBase(object): |
There was a problem hiding this comment.
Since this should be public, we should ensure we have a name we like. I personally find -Base suffixes a little off, at least in python code.
There was a problem hiding this comment.
I was following what I saw with SessionBase. So your suggestion is DatabaseObject?
There was a problem hiding this comment.
iirc SessionBase was me doing what I said not to do, concrete inheritance for code sharing. Its not actually exposed in the API.
Something like DatabaseObject might work.
Looks slick
What do you mean? |
Apologies for not being clear there. I was talking about the intellisense issue where I don't see anything useful in the database namespace after typing "database.". Turns out, this seems to be a PyCharm quirk where my nixnet-python package was defined in my PyCharm project (rather than being installed traditionally) and using a loose python script outside of the project to test with. It's strange because the script would run just fine, but I wasn't getting any intellisense for things in nixnet. Once I include my test files as a source root in the project, the intellisense automagically works. |
|
Added docstrings and modified database objects to init with kwargs. @jashnani and I chose to not include docs for the new DatabaseObject base class. Thanks to @jashnani and @epage for not settling on "good enough". I really like the final design. I'll add tests for this and all database objects next. |
|
@jashnani Just in case, let me squash before merge. |
db473e2 to
5e664b9
Compare
nixnet/database/_cluster.py
Outdated
| * :any:`SubFrame` | ||
| * :any:`Ecu` | ||
| * :any:`LinSched` | ||
| * :any:`LinSchedEntry` |
There was a problem hiding this comment.
Would it be possible to have fully qualified names of accepted values while keeping links to the classes? For ex:
nixnet.database.Cluster
nixnet.database.Frame
nixnet.database.Signal
etc.
But have the links go to the appropriate class RTD page.
nixnet/database/_database_object.py
Outdated
| ): | ||
| # type: (...) -> None | ||
| if not kwargs or '_handle' not in kwargs: | ||
| _errors.raise_xnet_error(_cconsts.NX_ERR_INVALID_PROPERTY_VALUE) |
There was a problem hiding this comment.
We should raise a TypeError similar to keyword-only saying more arguments were passed in than expected.
For ex: TypeError: () takes exactly 1 argument (2 given)
An alternative would be to switch to using keyword-only args which will automatically throw this error us.
There was a problem hiding this comment.
You mentioned this PEP for keyword-only arguments:
https://www.python.org/dev/peps/pep-3102/
"In accordance with the current Python implementation, any errors encountered will be signaled by raising TypeError."
Changed to raise TypeError(),
nixnet/database/_signal.py
Outdated
| def pdu(self): | ||
| # actually returns _pdu.Pdu, but avoiding a circular import | ||
| # type: () -> typing.Any | ||
| # type: () -> _database_object.DatabaseObject |
There was a problem hiding this comment.
Should the return type just be PDU? Ditto as above
nixnet/database/_find_object.py
Outdated
| class_enum = constants.ObjectClass.SIGNAL | ||
| elif object_class is SubFrame: | ||
| class_enum = constants.ObjectClass.SUBFRAME | ||
| else: |
There was a problem hiding this comment.
If you're wishing there was a switch-statement in Python, there's a convention for using a dictionaries in situations like this. Where keys would be object _classes and values would be enum_classes.
Here's an example: https://github.com/ni/nixnet-python/blob/master/nixnet/types.py#L1022
There was a problem hiding this comment.
I had thought about using a dictionary but had no strong preference.
It's a dictionary now. If the key isn't found, I'm raising a ValueError for the object_class argument.
nixnet/database/_signal.py
Outdated
| def frame(self): | ||
| # actually returns _frame.Frame, but avoiding a circular import | ||
| # type: () -> typing.Any | ||
| # type: () -> _database_object.DatabaseObject |
There was a problem hiding this comment.
Shouldn't the return type just be Frame? Could we workaround the circular import by scoping it in this property?
There was a problem hiding this comment.
Shouldn't the return type just be Frame?
Yes, that's what the comment is saying. I couldn't find a way to import before the type check, and not for lack of trying.
There was a problem hiding this comment.
There's a way to do it by setting MYPY = false. See details here: https://mypy.readthedocs.io/en/latest/common_issues.html#import-cycles
I'm not going to hold the review for this, so don't feel like you have to go out of way to do this.
There was a problem hiding this comment.
I was able to get this to work using the MYPY = False technique. Cool.
nixnet/database/_subframe.py
Outdated
| def pdu(self): | ||
| # actually returns _pdu.Pdu, but avoiding a circular import | ||
| # type: () -> typing.Any | ||
| # type: () -> _database_object.DatabaseObject |
There was a problem hiding this comment.
Should the return type just be PDU? Ditto as above.
There was a problem hiding this comment.
I was able to get this to work using the MYPY = False technique.
| if isinstance(index, six.string_types): | ||
| ref = _funcs.nxdb_find_object(self._handle, self._type, index) | ||
| return self._factory(ref) | ||
| return self._factory(_handle=ref) |
There was a problem hiding this comment.
DBCollection init function's factory parameter type can now be changed to DatabaseObject
# type: (int, constants.ObjectClass, int, typing.Type[_database_object.DatabaseObject]) -> None
nixnet/database/_database_object.py
Outdated
| return hash(self._handle) | ||
|
|
||
| def __repr__(self): | ||
| return '{}(handle={})'.format(type(self).__name__, self._handle) |
There was a problem hiding this comment.
Our use of DatabaseObject is meant for interface only. I would've expected these implementations to continue living in child classes and evolve as necessary:
__eq__
__ne__
__hash__
__repr__
There was a problem hiding this comment.
I read the information you sent me on this. I do understand some of the benefits mentioned to not DNR (like code locality). In this particular situation, I think I'd still prefer not to duplicate theses four functions in all eight subclasses. Regardless, I've moved them back as they were and I'll add unit tests as appropriate in my upcoming PR.
nixnet/database/_signal.py
Outdated
| def mux_subfrm(self): | ||
| # actually returns _subframe.SubFrame, but avoiding a circular import | ||
| # type: () -> typing.Any | ||
| # type: () -> _database_object.DatabaseObject |
There was a problem hiding this comment.
Should the return type just be SubFrame? Ditto as above.
There was a problem hiding this comment.
I was able to get this to work using the MYPY = False technique.
fc76719 to
6b3ebde
Compare
Fixes ni#239 The following parent database objects now have a find method: - database - cluster - lin schedule - pdu - frame - subframe All database object are now subclasses of DatabaseObject, which is used as an interface. Database objects are now exposed through the nixnet.database namespace, and their constructors now take kwargs to signal to users that these objects should not be instantiated by users. I tested on the shipping example database "NIXNET_example" to ensure I can indeed find children and grandchildren, and that sane errors are thrown when the object name not found or the object class is wrong. Unit tests will be added in ni#238.

Fixes #239
The following parent database objects now have a find method:
All database object are now subclasses of
DatabaseObject, which is used as an interface.
Database objects are now exposed through the
nixnet.database namespace, and their constructors
now take kwargs to signal to users that these
objects should not be instantiated by users.
I tested on the shipping example database "NIXNET_example"
to ensure I can indeed find children and grandchildren,
and that sane errors are thrown when the object name
not found or the object class is wrong.
Unit tests will be added in #238.
New tests have been created for any new features or regression tests for bugfixes.toxsuccessfully runs, including unit tests and style checks (see CONTRIBUTING.md).