Skip to content
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

ENH: Containers via entrypoints #126

Merged
merged 8 commits into from
Apr 16, 2020
Merged

ENH: Containers via entrypoints #126

merged 8 commits into from
Apr 16, 2020

Conversation

hhslepicka
Copy link
Contributor

Description

As of now in order to add a new container we need to modify Happi source code and add it at the happi.containers module so it is found by the client.
Via discussion at #106 and many talks with @ZLLentz and @klauer we decided to experiment with the usage of entry_points.
The LCLS specific containers and item were moved to pcdsdevices under the following PR: pcdshub/pcdsdevices#402

Motivation and Context

Merging this will contribute to close #106

How Has This Been Tested?

Tested locally

Where Has This Been Documented?

Still need to be done. Will work on that after code comments

Screenshots (if appropriate):

In [1]: import happi
Questionnaire backend unavailable: No module named 'psdm_qs_cli'

In [2]: happi.containers.registry
Out[2]:
{pcdsdevices.happi.containers.Acromag,
 pcdsdevices.happi.containers.AreaDetector,
 pcdsdevices.happi.containers.Attenuator,
 pcdsdevices.happi.containers.BeamControl,
 pcdsdevices.happi.containers.Diagnostic,
 pcdsdevices.happi.containers.GateValve,
 pcdsdevices.happi.containers.IPM,
 pcdsdevices.happi.containers.LCLSItem,
 pcdsdevices.happi.containers.LODCM,
 pcdsdevices.happi.containers.Motor,
 pcdsdevices.happi.containers.MovableStand,
 pcdsdevices.happi.containers.OffsetMirror,
 pcdsdevices.happi.containers.PIM,
 pcdsdevices.happi.containers.PulsePicker,
 pcdsdevices.happi.containers.Slits,
 pcdsdevices.happi.containers.Stopper,
 pcdsdevices.happi.containers.Trigger,
 pcdsdevices.happi.containers.Vacuum}

In [3]: happi.containers.Acromag
Out[3]: pcdsdevices.happi.containers.Acromag

In [4]: dir(happi.containers)
Out[4]:
['Acromag',
 'AreaDetector',
 'Attenuator',
 'BeamControl',
 'Diagnostic',
 'GateValve',
 'HappiItem',
 'IPM',
 'LCLSItem',
 'LODCM',
 'Motor',
 'MovableStand',
 'OffsetMirror',
 'PIM',
 'PulsePicker',
 'Slits',
 'Stopper',
 'Trigger',
 'Vacuum',
 '__builtins__',
 '__cached__',
 '__doc__',
 '__file__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 '_entries',
 'entry',
 'entrypoints',
 'inspect',
 'logger',
 'logging',
 'obj',
 'registry']

Hugo Slepicka added 2 commits March 20, 2020 15:27
ENH: Allow containers via entrypoint happi.containers.
Copy link
Contributor

@klauer klauer left a comment

Choose a reason for hiding this comment

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

Exciting to see this happening... a few thoughts below

happi/containers.py Outdated Show resolved Hide resolved
happi/containers.py Outdated Show resolved Hide resolved
# Avoid happi internal classes due to imports
and not var.__module__.startswith('happi.')])

locals().update({c.__name__: c for c in registry})
Copy link
Contributor

Choose a reason for hiding this comment

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

Indentation right here (seems like this should happen at the end after the registry is built)?

I think you're trying to get happi.containers.<classname> working: globals() seems more correct but may do the same here. We're also risking name collisions in the namespace as this strips off the __module__ prefix, hmm...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indentation solved with 5c39ffb. Sorry that I forgot to push before opening the PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

About the globals()... I'm not sure.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok, now I understand... that is correct, we inject the class name into the happi.containers namespace it would fail if we have 2 containers with the same name but different full qualified name.


_entries = entrypoints.get_group_all('happi.containers')

registry = set()
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if the registry should be keyed as follows: registry['(module).(classname)'] = obj?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What about the name from the entry_points? This way we can make it uniform to always be the package that originated it and we guarantee unique names per package?
E.g.:
pcdsdevices.Acromag as the container and key at registry.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, maybe... but what about multi-level imports?

Would (e.g.) pcdsdevices.motors.Acromag become pcdsdevices.Acromag?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The second one will be an internal key for Happi at the registry and it will point to the proper class pcdsdevices.happi.containers.Acromag and queried. I decided to make it shorter as a convenience for users when selecting a class at the CLI but we can also make the namespace shorter at the pcdsdevices side and improve the display of the possibilities at the CLI.

Copy link
Contributor

Choose a reason for hiding this comment

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

In our example, the name you'd put in a happi entry to get the Acromag instantiated would be pcdsdevices.Acromag, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That depends but I can see the confusion. Would it be okay if I make the namespace be pcdsdevices.happi.Acromag and I switch to use the full class at the registry like you suggested before?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm trying to get at the heart of the problem and don't really know the "right" answer just yet to say yes or no...

full_module.classname is unambiguous. Entry_points name gives more flexibility, but adds indirection to get to the class itself.

Copy link
Contributor

Choose a reason for hiding this comment

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

If we populate the registry with both entry_point_name.classname and module.classname, I can imagine that'd get confusing as well. I think it has to be one or the other.

The entrypoint name would allow for us to move around classes and refactor codebases without updating our device databases. Maybe that's preferable?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As of now we have:

In [1]: import happi
Questionnaire backend unavailable: No module named 'psdm_qs_cli'

In [2]: happi.containers.registry
Out[2]:
{'pcdsdevices.Acromag': pcdsdevices.happi.containers.Acromag,
 'pcdsdevices.AreaDetector': pcdsdevices.happi.containers.AreaDetector,
 'pcdsdevices.Attenuator': pcdsdevices.happi.containers.Attenuator,
 'pcdsdevices.BeamControl': pcdsdevices.happi.containers.BeamControl,
 'pcdsdevices.Diagnostic': pcdsdevices.happi.containers.Diagnostic,
 'pcdsdevices.GateValve': pcdsdevices.happi.containers.GateValve,
 'pcdsdevices.IPM': pcdsdevices.happi.containers.IPM,
 'pcdsdevices.LCLSItem': pcdsdevices.happi.containers.LCLSItem,
 'pcdsdevices.LODCM': pcdsdevices.happi.containers.LODCM,
 'pcdsdevices.Motor': pcdsdevices.happi.containers.Motor,
 'pcdsdevices.MovableStand': pcdsdevices.happi.containers.MovableStand,
 'pcdsdevices.OffsetMirror': pcdsdevices.happi.containers.OffsetMirror,
 'pcdsdevices.PIM': pcdsdevices.happi.containers.PIM,
 'pcdsdevices.PulsePicker': pcdsdevices.happi.containers.PulsePicker,
 'pcdsdevices.Slits': pcdsdevices.happi.containers.Slits,
 'pcdsdevices.Stopper': pcdsdevices.happi.containers.Stopper,
 'pcdsdevices.Trigger': pcdsdevices.happi.containers.Trigger,
 'pcdsdevices.Vacuum': pcdsdevices.happi.containers.Vacuum}

So technically we are already doing that, right? Since you have the container class as the value of the dict you can get the module.classname ?
I agree, the entrypoint will allow for flexibility on this sense

Hugo Slepicka added 2 commits March 20, 2020 17:40
FIX: Switch client to use registry instead of search at happi.containers.
FIX: Address issues found during tests of the cli.
@hhslepicka
Copy link
Contributor Author

I need to fix the tests and I am finding some more issues... it will take a while, folks! 😞

for _, var in inspect.getmembers(obj, inspect.isclass)
if issubclass(var, HappiItem)
# Avoid happi internal classes due to imports
and not var.__module__.startswith('happi.')})
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
and not var.__module__.startswith('happi.')})
and not var.__module__.startswith('happi.')} and var not in registry.values())

Copy link
Contributor

Choose a reason for hiding this comment

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

Also this check of "can we add it to the registry" is a repeated condition that could be made a function: i.e., it's duplicated on line 20 and in this dict comprehension


_entries = entrypoints.get_group_all('happi.containers')

registry = {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's move the registry generation code inside a function, otherwise things like name, entry, obj, etc may leak such that from happi.containers import entry would work.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will address that.

continue
if inspect.isclass(obj) and issubclass(obj, HappiItem):
registry[f"{name}.{obj.__name__}"] = obj
elif inspect.ismodule(obj):
Copy link
Contributor

Choose a reason for hiding this comment

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

To re-cap we have 2 options now:

  1. Specify a class
  2. Specify a (sub-)module to find classes in

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correct! I ended up adding support for both.

@hhslepicka
Copy link
Contributor Author

Sorry for the delay... I reworked the registry after talking with Ken.
Also, at the client we no longer have the device_types. It now points to the registry always and for the cli.py we filter-out the deprecated Device. It is included at the registry to allow us to properly load old devices that we have.

Most likely the changes here will break the existing db.json at devices-config repo if the type field does not match a key at the registry.

Registry keys are now in the following format:

entry_point_name + class_full_qualified_name[1:]

This change will allow us to move/rename packages and still be able to find the entries with no changes to the database.

What is not done:

  • Aggressive search when a package is passed. E.g.: Look at all python files inside of a package directory.

@codecov-io
Copy link

codecov-io commented Apr 16, 2020

Codecov Report

Merging #126 into master will decrease coverage by 2.98%.
The diff coverage is 54.28%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #126      +/-   ##
==========================================
- Coverage   75.57%   72.58%   -2.99%     
==========================================
  Files          15       15              
  Lines        1134     1120      -14     
==========================================
- Hits          857      813      -44     
- Misses        277      307      +30     
Impacted Files Coverage Δ
happi/item.py 97.76% <ø> (+0.59%) ⬆️
happi/cli.py 45.45% <14.28%> (-0.31%) ⬇️
happi/containers.py 58.06% <58.06%> (-41.94%) ⬇️
happi/client.py 89.31% <100.00%> (+0.22%) ⬆️
happi/device.py 91.17% <0.00%> (-2.95%) ⬇️
happi/backends/qs_db.py 65.21% <0.00%> (-1.45%) ⬇️
happi/loader.py 93.10% <0.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 4978127...d71fabf. Read the comment docs.

Copy link
Contributor

@klauer klauer left a comment

Choose a reason for hiding this comment

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

I think this looks really good!

happi/client.py Outdated Show resolved Hide resolved
f"and class: {klass}")
if klass in self._reverse_registry:
dup_key = self._reverse_registry.get(klass)
raise RuntimeError(f"Duplicated entry found. Keys: {key} "
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice and helpful 👍

@klauer
Copy link
Contributor

klauer commented Apr 16, 2020

(Nudging @ZLLentz)

Copy link
Member

@ZLLentz ZLLentz left a comment

Choose a reason for hiding this comment

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

LGTM. Was confused about including pcdsdevices in the tests, but it makes sense.

@hhslepicka
Copy link
Contributor Author

@ZLLentz I would gladly nuke that test out but it tests the Questionnaire backend and that directly depends on pcdsdevices to test the type of devices. Sorry about the confusion.

@klauer klauer merged commit 7bb9fd3 into pcdshub:master Apr 16, 2020
@ZLLentz
Copy link
Member

ZLLentz commented Apr 16, 2020

@hhslepicka that backend should eventually migrate to a different repo... Surely there's a questionnaire-centered repo somewhere? But it's not an issue so there is no reason to prioritize it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Discussion about Device and Mandatory Fields
4 participants