Skip to content

feat(API): Add descendant find for parent db objects.#248

Merged
jashnani merged 2 commits intoni:masterfrom
d-bohls:dbFindObject
Apr 30, 2018
Merged

feat(API): Add descendant find for parent db objects.#248
jashnani merged 2 commits intoni:masterfrom
d-bohls:dbFindObject

Conversation

@d-bohls
Copy link
Contributor

@d-bohls d-bohls commented Apr 11, 2018

Fixes #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 #238.

  • This contribution adheres to CONTRIBUTING.md.
  • New tests have been created for any new features or regression tests for bugfixes.
  • tox successfully runs, including unit tests and style checks (see CONTRIBUTING.md).

@d-bohls d-bohls self-assigned this Apr 11, 2018
@d-bohls d-bohls requested review from epage and jashnani April 11, 2018 20:57
@d-bohls
Copy link
Contributor Author

d-bohls commented Apr 11, 2018

A couple of notes on this review.

After #237 goes in, I plan to add docstrings to this review, because the docstrings for this review will reference docstrings in the other review.

I am deferring unit tests for this review to be done as part of #238.

@coveralls
Copy link

coveralls commented Apr 11, 2018

Coverage Status

Coverage increased (+0.1%) to 67.679% when pulling 263b1ae on d-bohls:dbFindObject into fba4880 on ni:master.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.3%) to 67.914% when pulling f3d3ceb on d-bohls:dbFindObject into 82aad3b on ni:master.

2 similar comments
@coveralls
Copy link

Coverage Status

Coverage decreased (-0.3%) to 67.914% when pulling f3d3ceb on d-bohls:dbFindObject into 82aad3b on ni:master.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.3%) to 67.914% when pulling f3d3ceb on d-bohls:dbFindObject into 82aad3b on ni:master.

@d-bohls
Copy link
Contributor Author

d-bohls commented Apr 11, 2018

Why you spamming me @coveralls!

@epage
Copy link
Contributor

epage commented Apr 13, 2018

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 :)

_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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like you could make the return type a union of the regular and NoReturn



def find_object(parent_handle, object_class, object_name):
# type: (int, constants.ObjectClass, typing.Text) -> object
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Instead of an enum, should people pass Cluster etc into here?
  2. Wish mypy had dependent types. That'd be cool to say that the return type is the type of object_class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. Providing a common base class for all database objects
  2. Change the annotations from type: (int, constants.ObjectClass, typing.Text) -> object to type: (int, typing.Type[nixnet.database.SomeBaseClass], typing.Text) -> nixnet.database.SomeBaseClass

This is similar to the approach we took elsewhere in the API

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 a Cluster vs having to deal with an enum that represents Cluster.
  • 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 System be an option.

@d-bohls
Copy link
Contributor Author

d-bohls commented Apr 13, 2018

@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)

@epage
Copy link
Contributor

epage commented Apr 13, 2018

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. nixnet.database only exposes Database and not the other types
https://github.com/ni/nixnet-python/blob/master/nixnet/database/__init__.py

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 handle a keyword-only arg that is prefixed with _. That isn't officially supported in python2 but I recently learned about a backport of the feature: https://pypi.python.org/pypi/kwonly-args

Copy link

@jashnani jashnani left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@jashnani jashnani self-requested a review April 16, 2018 19:31
@d-bohls
Copy link
Contributor Author

d-bohls commented Apr 19, 2018

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.

@epage
Copy link
Contributor

epage commented Apr 19, 2018

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 **kwargs, then that is effectively another way of defining a keyword-only argument and fits the spirit of what I'm suggesting. The literal keyword-only argument feature just requires less boiler plate / is more clear on intent.

@d-bohls d-bohls force-pushed the dbFindObject branch 4 times, most recently from fed7115 to d381e6e Compare April 25, 2018 06:44
@d-bohls
Copy link
Contributor Author

d-bohls commented Apr 25, 2018

I've done the following:

  1. Introduced DatabaseObjectBase for all database object to inherit from
  2. Exposed them in database package's init
  3. Modified find methods to use DatabaseObjectBase objects

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.

class_enum = constants.ObjectClass.SUBFRAME
else:
# arbitrary value for class_enum so mypy sees a value in each condition
class_enum = constants.ObjectClass.CLUSTER
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd actually recommend a raise ValueError("Unsupported database object")

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, missed the raise below :)



__all__ = ["Database"]
__all__ = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DatabaseObjectBase should be in here so users can do mypy typing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

import typing # NOQA: F401


class DatabaseObjectBase(object):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was following what I saw with SessionBase. So your suggestion is DatabaseObject?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@epage
Copy link
Contributor

epage commented Apr 25, 2018

I've done the following:

Looks slick

However, I'm still not seeing a useful namespacing experience. Not sure yet what I'm missing.

What do you mean?

@d-bohls
Copy link
Contributor Author

d-bohls commented Apr 25, 2018

@epage

However, I'm still not seeing a useful namespacing experience. Not sure yet what I'm missing.

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.

@d-bohls
Copy link
Contributor Author

d-bohls commented Apr 26, 2018

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.

@d-bohls
Copy link
Contributor Author

d-bohls commented Apr 26, 2018

@jashnani Just in case, let me squash before merge.

@d-bohls d-bohls force-pushed the dbFindObject branch 2 times, most recently from db473e2 to 5e664b9 Compare April 26, 2018 05:44
@d-bohls
Copy link
Contributor Author

d-bohls commented Apr 26, 2018

The only "less than ideal" issue I might mention is that the DatabaseObject base class shows up in intellisense, though this is better than the enum alone since other things like Session showed up in that.

image

Copy link

@jashnani jashnani left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall! Keyword args seem to do the trick and intellisense is working.

* :any:`SubFrame`
* :any:`Ecu`
* :any:`LinSched`
* :any:`LinSchedEntry`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

):
# type: (...) -> None
if not kwargs or '_handle' not in kwargs:
_errors.raise_xnet_error(_cconsts.NX_ERR_INVALID_PROPERTY_VALUE)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(),

def pdu(self):
# actually returns _pdu.Pdu, but avoiding a circular import
# type: () -> typing.Any
# type: () -> _database_object.DatabaseObject

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the return type just be PDU? Ditto as above

class_enum = constants.ObjectClass.SIGNAL
elif object_class is SubFrame:
class_enum = constants.ObjectClass.SUBFRAME
else:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

def frame(self):
# actually returns _frame.Frame, but avoiding a circular import
# type: () -> typing.Any
# type: () -> _database_object.DatabaseObject

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the return type just be Frame? Could we workaround the circular import by scoping it in this property?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

@d-bohls d-bohls Apr 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was able to get this to work using the MYPY = False technique. Cool.

def pdu(self):
# actually returns _pdu.Pdu, but avoiding a circular import
# type: () -> typing.Any
# type: () -> _database_object.DatabaseObject

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the return type just be PDU? Ditto as above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DBCollection init function's factory parameter type can now be changed to DatabaseObject
# type: (int, constants.ObjectClass, int, typing.Type[_database_object.DatabaseObject]) -> None

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

return hash(self._handle)

def __repr__(self):
return '{}(handle={})'.format(type(self).__name__, self._handle)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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__

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

def mux_subfrm(self):
# actually returns _subframe.SubFrame, but avoiding a circular import
# type: () -> typing.Any
# type: () -> _database_object.DatabaseObject

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the return type just be SubFrame? Ditto as above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was able to get this to work using the MYPY = False technique.

@d-bohls d-bohls force-pushed the dbFindObject branch 3 times, most recently from fc76719 to 6b3ebde Compare April 30, 2018 18:53
d-bohls added 2 commits April 30, 2018 16:11
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.
@jashnani jashnani merged commit 0132e8c into ni:master Apr 30, 2018
@d-bohls d-bohls deleted the dbFindObject branch April 30, 2018 21:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Exposing descendant find

4 participants